I figured it out. I had to create my own History Node using decompiled versions of DefaultHistoryColumn and ProcessedHistoryColumn. ProcessedHistoryColumn does not work for MinMax and cannot be used.
I created the following column to be used for MinMax and the standard case of 1 value per column.
import com.inductiveautomation.ignition.common.TypeUtilities;
import com.inductiveautomation.ignition.common.model.values.BasicQualifiedValue;
import com.inductiveautomation.ignition.common.model.values.QualifiedValue;
import com.inductiveautomation.ignition.common.model.values.QualityCode;
import com.inductiveautomation.ignition.gateway.sqltags.history.query.AbstractHistoryNode;
import com.inductiveautomation.ignition.gateway.sqltags.history.query.HistoricalValue;
import com.inductiveautomation.ignition.gateway.sqltags.history.query.columns.HistoryColumn;
import java.util.*;
public class ProcessedIndexedHistoryColumn extends AbstractHistoryNode implements HistoryColumn {
private LinkedList<QV>[] values;
private QV[] lockedCurrentValues;
private QV[] carryThroughValues;
private int lastLevel;
public ProcessedIndexedHistoryColumn(String name) {
super(name);
setNextAvailableTime(Long.MAX_VALUE);
}
@Override
public void process(HistoricalValue historicalValue) {
put(List.of(new BasicQualifiedValue(historicalValue.getValue(), historicalValue.getQuality(), new Date(historicalValue.getTimestamp()))));
}
@Override
public void markCompleted(long time) {
this.find(time, true, lastLevel);
this.setNextAvailableTime(this.getNextTimestamp());
}
public long getNextTimestamp() {
return this.values[0].isEmpty() ? Long.MAX_VALUE : this.values[0].peek().timestamp;
}
@Override
public Object getValue(long timestamp, int index) {
lastLevel = index;
this.lockedCurrentValues[index] = this.find(timestamp, false, index);
return this.lockedCurrentValues[index] == null ? null : TypeUtilities.coerce(this.lockedCurrentValues[index].value, this.type.getJavaType());
}
public void put(Collection<Collection<QualifiedValue>> aggregateSet) {
if (!aggregateSet.isEmpty()) {
this.values = new LinkedList[aggregateSet.size()];
lockedCurrentValues = new QV[aggregateSet.size()];
carryThroughValues = new QV[aggregateSet.size()];
int i = 0;
long startTimestamp = Long.MAX_VALUE;
for (var aggregate : aggregateSet){
this.values[i] = new LinkedList<>();
for (var value : aggregate){
var qv = new QV(value);
if (qv.timestamp < startTimestamp){
startTimestamp = qv.timestamp;
}
this.values[i].add(qv);
}
i++;
}
this.setNextAvailableTime(startTimestamp);
}
}
public void put(List<QualifiedValue> values) {
if (!values.isEmpty()) {
this.values = new LinkedList[1];
this.values[0] = new LinkedList<>();
lockedCurrentValues = new QV[1];
carryThroughValues = new QV[1];
for(QualifiedValue qv : values) {
this.values[0].add(new QV(qv));
}
this.setNextAvailableTime((this.values[0].getFirst()).timestamp);
}
}
@Override
public QualityCode getQuality() {
if (lockedCurrentValues.length > 0){
return this.lockedCurrentValues[lastLevel].quality;
}
else{
return QualityCode.Bad_NotFound;
}
}
@Override
public boolean hasMore() {
return this.values != null && this.values.length - 1 > this.lastLevel;
}
@Override
public boolean wasValueInterpolated() {
return false;
}
protected QV find(long timestamp, boolean including, int index) {
while(true) {
label24: {
if (values.length > 0){
if (!this.values[index].isEmpty()) {
var value = this.values[index].peek();
if (including){
if (value != null && value.timestamp <= timestamp){
break label24;
}
}
else{
if (value != null && value.timestamp < timestamp) {
break label24;
}
}
}
}
return this.values[index].isEmpty() ? this.carryThroughValues[index] : this.values[index].peek();
}
this.carryThroughValues[index] = this.values[index].pop();
}
}
protected static class QV {
public Object value;
public QualityCode quality;
public long timestamp;
public QV(QualifiedValue qv) {
this.value = qv.getValue();
this.quality = qv.getQuality();
this.timestamp = qv.getTimestamp().getTime();
}
}
}