diff --git a/engine/table/src/main/java/io/deephaven/engine/util/TotalsTableBuilder.java b/engine/table/src/main/java/io/deephaven/engine/util/TotalsTableBuilder.java index 127ca5ef787..77e35040f2e 100644 --- a/engine/table/src/main/java/io/deephaven/engine/util/TotalsTableBuilder.java +++ b/engine/table/src/main/java/io/deephaven/engine/util/TotalsTableBuilder.java @@ -606,14 +606,14 @@ public static Collection makeAggregations(Table source, T final Set defaultOperations = EnumSet.of(builder.defaultOperation); final Map> columnsByType = new LinkedHashMap<>(); - for (final Map.Entry entry : source.getColumnSourceMap().entrySet()) { + for (final Map.Entry> entry : source.getColumnSourceMap().entrySet()) { final String columnName = entry.getKey(); if (ColumnFormatting.isFormattingColumn(columnName)) { continue; } final Set operations = builder.operationMap.getOrDefault(columnName, defaultOperations); - final Class type = entry.getValue().getType(); + final Class type = entry.getValue().getType(); for (final AggType op : operations) { if (operationApplies(type, op)) { diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/JoinableTable.java b/web/client-api/src/main/java/io/deephaven/web/client/api/JoinableTable.java new file mode 100644 index 00000000000..dafd829f865 --- /dev/null +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/JoinableTable.java @@ -0,0 +1,43 @@ +package io.deephaven.web.client.api; + +import elemental2.core.JsArray; +import elemental2.promise.Promise; +import io.deephaven.web.client.state.ClientTableState; +import jsinterop.annotations.JsIgnore; +import jsinterop.annotations.JsMethod; +import jsinterop.annotations.JsOptional; +import jsinterop.annotations.JsType; + +@JsType(namespace = "dh") +public interface JoinableTable { + @JsIgnore + ClientTableState state(); + + @JsMethod + Promise freeze(); + + @JsMethod + Promise snapshot(JsTable baseTable, @JsOptional Boolean doInitialSnapshot, + @JsOptional String[] stampColumns); + + @JsMethod + @Deprecated + Promise join(Object joinType, JoinableTable rightTable, JsArray columnsToMatch, + @JsOptional JsArray columnsToAdd, @JsOptional Object asOfMatchRule); + + @JsMethod + Promise asOfJoin(JoinableTable rightTable, JsArray columnsToMatch, + @JsOptional JsArray columnsToAdd, @JsOptional String asOfMatchRule); + + @JsMethod + Promise crossJoin(JoinableTable rightTable, JsArray columnsToMatch, + @JsOptional JsArray columnsToAdd, @JsOptional Double reserve_bits); + + @JsMethod + Promise exactJoin(JoinableTable rightTable, JsArray columnsToMatch, + @JsOptional JsArray columnsToAdd); + + @JsMethod + Promise naturalJoin(JoinableTable rightTable, JsArray columnsToMatch, + @JsOptional JsArray columnsToAdd); +} diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/JsTable.java b/web/client-api/src/main/java/io/deephaven/web/client/api/JsTable.java index 82351903e50..9e4dfe93981 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/JsTable.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/JsTable.java @@ -15,22 +15,30 @@ import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.object_pb.FetchObjectRequest; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.partitionedtable_pb.PartitionByRequest; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.partitionedtable_pb.PartitionByResponse; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.AggregateRequest; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.AsOfJoinTablesRequest; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.BatchTableRequest; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.CrossJoinTablesRequest; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.ExactJoinTablesRequest; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.ExportedTableCreationResponse; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.Literal; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.NaturalJoinTablesRequest; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.RunChartDownsampleRequest; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.SeekRowRequest; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.SeekRowResponse; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.SelectDistinctRequest; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.SelectOrUpdateRequest; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.SnapshotTableRequest; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.SnapshotWhenTableRequest; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.TableReference; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.batchtablerequest.Operation; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.runchartdownsamplerequest.ZoomRange; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb_service.ResponseStream; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.ticket_pb.Ticket; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.ticket_pb.TypedTicket; import io.deephaven.web.client.api.barrage.def.ColumnDefinition; import io.deephaven.web.client.api.barrage.def.TableAttributesDefinition; +import io.deephaven.web.client.api.barrage.stream.ResponseStreamWrapper; import io.deephaven.web.client.api.batch.RequestBatcher; import io.deephaven.web.client.api.console.JsVariableType; import io.deephaven.web.client.api.filter.FilterCondition; @@ -79,7 +87,7 @@ * handle/viewport. */ @TsName(namespace = "dh", name = "Table") -public class JsTable extends HasLifecycle implements HasTableBinding { +public class JsTable extends HasLifecycle implements HasTableBinding, JoinableTable { @JsProperty(namespace = "dh.Table") public static final String EVENT_SIZECHANGED = "sizechanged", EVENT_UPDATED = "updated", @@ -612,18 +620,16 @@ public Promise copy(boolean resolved) { return copy(); } - // TODO: #37: Need SmartKey support for this functionality - // @JsMethod + @JsMethod public Promise getTotalsTable( - /* @JsOptional @JsNullable */ @TsTypeRef(JsTotalsTableConfig.class) Object config) { + @JsOptional @JsNullable @TsTypeRef(JsTotalsTableConfig.class) Object config) { // fetch the handle and wrap it in a new jstable. listen for changes // on the parent table, and re-fetch each time. return fetchTotals(config, this::lastVisibleState); } - // TODO: #37: Need SmartKey support for this functionality - // @JsMethod + @JsMethod public JsTotalsTableConfig getTotalsTableConfig() { // we want to communicate to the JS dev that there is no default config, so we allow // returning null here, rather than a default config. They can then easily build a @@ -633,7 +639,6 @@ public JsTotalsTableConfig getTotalsTableConfig() { } private Promise fetchTotals(Object config, JsProvider state) { - JsTotalsTableConfig directive = getTotalsDirectiveFromOptionalConfig(config); ClientTableState[] lastGood = {null}; final JsTableFetch totalsFactory = (callback, newState, metadata) -> { @@ -655,14 +660,44 @@ private Promise fetchTotals(Object config, JsProvider updateViewExprs = directive.getCustomColumns(); + JsArray dropColumns = directive.getDropColumns(); + requestMessage.setSourceId(target.getHandle().makeTableReference()); + requestMessage.setResultId(newState.getHandle().makeTicket()); + if (updateViewExprs != null && updateViewExprs.length != 0) { + SelectOrUpdateRequest columnExpr = new SelectOrUpdateRequest(); + columnExpr.setResultId(requestMessage.getResultId()); + requestMessage.setResultId(); + columnExpr.setColumnSpecsList(updateViewExprs); + TableReference prev = new TableReference(); + prev.setBatchOffset(0); + columnExpr.setSourceId(prev); + BatchTableRequest batch = new BatchTableRequest(); + Operation aggOp = new Operation(); + aggOp.setAggregate(requestMessage); + Operation colsOp = new Operation(); + colsOp.setUpdateView(columnExpr); + batch.addOps(aggOp); + batch.addOps(colsOp); + ResponseStreamWrapper stream = ResponseStreamWrapper + .of(workerConnection.tableServiceClient().batch(batch, workerConnection.metadata())); + stream.onData(creationResponse -> { + if (creationResponse.getResultId().hasTicket()) { + // represents the final output + callback.apply(null, creationResponse); + } + }); + stream.onEnd(status -> { + if (!status.isOk()) { + callback.apply(status, null); + } + }); + } else { + workerConnection.tableServiceClient().aggregate(requestMessage, workerConnection.metadata(), + callback::apply); + } }; String summary = "totals table " + directive + ", " + directive.groupBy.join(","); final ClientTableState totals = workerConnection.newState(totalsFactory, summary); @@ -748,10 +783,9 @@ private JsTotalsTableConfig getTotalsDirectiveFromOptionalConfig(Object config) } } - // TODO: #37: Need SmartKey support for this functionality - // @JsMethod + @JsMethod public Promise getGrandTotalsTable( - /* @JsNullable @JsOptional */ @TsTypeRef(JsTotalsTableConfig.class) Object config) { + @JsOptional @JsNullable @TsTypeRef(JsTotalsTableConfig.class) Object config) { // As in getTotalsTable, but this time we want to skip any filters - this could mean use the // most-derived table which has no filter, or the least-derived table which has all custom columns. // Currently, these two mean the same thing. @@ -849,6 +883,7 @@ public Promise freeze() { .then(state -> Promise.resolve(new JsTable(workerConnection, state))); } + @Override @JsMethod public Promise snapshot(JsTable baseTable, @JsOptional Boolean doInitialSnapshot, @JsOptional String[] stampColumns) { @@ -859,12 +894,12 @@ public Promise snapshot(JsTable baseTable, @JsOptional Boolean doInitia } else { realDoInitialSnapshot = true; } - final String[] realStampColums; + final String[] realStampColumns; if (stampColumns == null) { - realStampColums = new String[0]; // server doesn't like null + realStampColumns = new String[0]; // server doesn't like null } else { // make sure we pass an actual string array - realStampColums = Arrays.stream(stampColumns).toArray(String[]::new); + realStampColumns = Arrays.stream(stampColumns).toArray(String[]::new); } final String fetchSummary = "snapshot(" + baseTable + ", " + doInitialSnapshot + ", " + Arrays.toString(stampColumns) + ")"; @@ -874,16 +909,17 @@ public Promise snapshot(JsTable baseTable, @JsOptional Boolean doInitia request.setTriggerId(state().getHandle().makeTableReference()); request.setResultId(state.getHandle().makeTicket()); request.setInitial(realDoInitialSnapshot); - request.setStampColumnsList(realStampColums); + request.setStampColumnsList(realStampColumns); workerConnection.tableServiceClient().snapshotWhen(request, metadata, c::apply); }, fetchSummary).refetch(this, workerConnection.metadata()) .then(state -> Promise.resolve(new JsTable(workerConnection, state))); } + @Override @JsMethod @Deprecated - public Promise join(Object joinType, JsTable rightTable, JsArray columnsToMatch, + public Promise join(Object joinType, JoinableTable rightTable, JsArray columnsToMatch, @JsOptional @JsNullable JsArray columnsToAdd, @JsOptional @JsNullable Object asOfMatchRule) { if (joinType.equals("AJ") || joinType.equals("RAJ")) { return asOfJoin(rightTable, columnsToMatch, columnsToAdd, (String) asOfMatchRule); @@ -898,10 +934,11 @@ public Promise join(Object joinType, JsTable rightTable, JsArray asOfJoin(JsTable rightTable, JsArray columnsToMatch, + public Promise asOfJoin(JoinableTable rightTable, JsArray columnsToMatch, @JsOptional @JsNullable JsArray columnsToAdd, @JsOptional @JsNullable String asOfMatchRule) { - if (rightTable.workerConnection != workerConnection) { + if (rightTable.state().getConnection() != workerConnection) { throw new IllegalStateException( "Table argument passed to join is not from the same worker as current table"); } @@ -922,10 +959,11 @@ public Promise asOfJoin(JsTable rightTable, JsArray columnsToMa .then(state -> Promise.resolve(new JsTable(workerConnection, state))); } + @Override @JsMethod - public Promise crossJoin(JsTable rightTable, JsArray columnsToMatch, + public Promise crossJoin(JoinableTable rightTable, JsArray columnsToMatch, @JsOptional JsArray columnsToAdd, @JsOptional Double reserve_bits) { - if (rightTable.workerConnection != workerConnection) { + if (rightTable.state().getConnection() != workerConnection) { throw new IllegalStateException( "Table argument passed to join is not from the same worker as current table"); } @@ -945,10 +983,11 @@ public Promise crossJoin(JsTable rightTable, JsArray columnsToM .then(state -> Promise.resolve(new JsTable(workerConnection, state))); } + @Override @JsMethod - public Promise exactJoin(JsTable rightTable, JsArray columnsToMatch, + public Promise exactJoin(JoinableTable rightTable, JsArray columnsToMatch, @JsOptional JsArray columnsToAdd) { - if (rightTable.workerConnection != workerConnection) { + if (rightTable.state().getConnection() != workerConnection) { throw new IllegalStateException( "Table argument passed to join is not from the same worker as current table"); } @@ -965,10 +1004,11 @@ public Promise exactJoin(JsTable rightTable, JsArray columnsToM .then(state -> Promise.resolve(new JsTable(workerConnection, state))); } + @Override @JsMethod - public Promise naturalJoin(JsTable rightTable, JsArray columnsToMatch, + public Promise naturalJoin(JoinableTable rightTable, JsArray columnsToMatch, @JsOptional JsArray columnsToAdd) { - if (rightTable.workerConnection != workerConnection) { + if (rightTable.state().getConnection() != workerConnection) { throw new IllegalStateException( "Table argument passed to join is not from the same worker as current table"); } diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/JsTotalsTable.java b/web/client-api/src/main/java/io/deephaven/web/client/api/JsTotalsTable.java index 9bae05e74eb..f46c7c0a293 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/JsTotalsTable.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/JsTotalsTable.java @@ -11,7 +11,9 @@ import elemental2.dom.Event; import elemental2.promise.Promise; import io.deephaven.web.client.api.filter.FilterCondition; +import io.deephaven.web.client.state.ClientTableState; import io.deephaven.web.shared.fu.RemoverFn; +import jsinterop.annotations.JsIgnore; import jsinterop.annotations.JsMethod; import jsinterop.annotations.JsOptional; import jsinterop.annotations.JsProperty; @@ -28,7 +30,7 @@ */ @TsInterface @TsName(namespace = "dh", name = "TotalsTable") -public class JsTotalsTable { +public class JsTotalsTable implements JoinableTable { private final JsTable wrappedTable; private final String directive; private final JsArray groupBy; @@ -57,6 +59,12 @@ public void refreshViewport() { } } + @JsIgnore + @Override + public ClientTableState state() { + return wrappedTable.state(); + } + @JsProperty public JsTotalsTableConfig getTotalsTableConfig() { JsTotalsTableConfig parsed = JsTotalsTableConfig.parse(directive); @@ -163,6 +171,52 @@ public JsArray getCustomColumns() { return wrappedTable.getCustomColumns(); } + @Override + @JsMethod + public Promise freeze() { + return wrappedTable.freeze(); + } + + @Override + @JsMethod + public Promise snapshot(JsTable baseTable, @JsOptional Boolean doInitialSnapshot, + @JsOptional String[] stampColumns) { + return wrappedTable.snapshot(baseTable, doInitialSnapshot, stampColumns); + } + + @Override + @Deprecated + @JsMethod + public Promise join(Object joinType, JoinableTable rightTable, JsArray columnsToMatch, + @JsOptional JsArray columnsToAdd, @JsOptional Object asOfMatchRule) { + return wrappedTable.join(joinType, rightTable, columnsToMatch, columnsToAdd, asOfMatchRule); + } + + @Override + @JsMethod + public Promise asOfJoin(JoinableTable rightTable, JsArray columnsToMatch, + @JsOptional JsArray columnsToAdd, @JsOptional String asOfMatchRule) { + return wrappedTable.asOfJoin(rightTable, columnsToMatch, columnsToAdd, asOfMatchRule); + } + + @Override + @JsMethod + public Promise crossJoin(JoinableTable rightTable, JsArray columnsToMatch, + @JsOptional JsArray columnsToAdd, @JsOptional Double reserve_bits) { + return wrappedTable.crossJoin(rightTable, columnsToMatch, columnsToAdd, reserve_bits); + } + @Override + @JsMethod + public Promise exactJoin(JoinableTable rightTable, JsArray columnsToMatch, + @JsOptional JsArray columnsToAdd) { + return wrappedTable.exactJoin(rightTable, columnsToMatch, columnsToAdd); + } + @Override + @JsMethod + public Promise naturalJoin(JoinableTable rightTable, JsArray columnsToMatch, + @JsOptional JsArray columnsToAdd) { + return wrappedTable.naturalJoin(rightTable, columnsToMatch, columnsToAdd); + } } diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/JsTotalsTableConfig.java b/web/client-api/src/main/java/io/deephaven/web/client/api/JsTotalsTableConfig.java index 447288dd2f6..ef4743cbd07 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/JsTotalsTableConfig.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/JsTotalsTableConfig.java @@ -7,16 +7,42 @@ import elemental2.core.Global; import elemental2.core.JsArray; import elemental2.core.JsObject; -import elemental2.core.JsString; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.Table_pb; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.AggSpec; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.AggregateRequest; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.Aggregation; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.aggregation.AggregationColumns; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.aggregation.AggregationCount; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.aggspec.AggSpecAbsSum; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.aggspec.AggSpecAvg; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.aggspec.AggSpecCountDistinct; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.aggspec.AggSpecDistinct; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.aggspec.AggSpecFirst; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.aggspec.AggSpecLast; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.aggspec.AggSpecMax; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.aggspec.AggSpecMin; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.aggspec.AggSpecNonUniqueSentinel; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.aggspec.AggSpecStd; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.aggspec.AggSpecSum; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.aggspec.AggSpecUnique; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.aggspec.AggSpecVar; import io.deephaven.web.client.api.tree.enums.JsAggregationOperation; -import jsinterop.annotations.JsConstructor; +import io.deephaven.web.client.fu.JsLog; import jsinterop.annotations.JsIgnore; import jsinterop.annotations.JsType; import jsinterop.base.Js; import jsinterop.base.JsPropertyMap; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collector; +import java.util.stream.Collectors; @JsType(name = "TotalsTableConfig", namespace = "dh") public class JsTotalsTableConfig { @@ -57,6 +83,10 @@ public class JsTotalsTableConfig { public JsArray groupBy = new JsArray<>(); + private AggregateRequest grpcRequest; + private JsArray customColumns; + private JsArray dropColumns; + public JsTotalsTableConfig() {} @JsIgnore @@ -110,7 +140,6 @@ public static JsTotalsTableConfig parse(String configString) { builder.defaultOperation = frontMatter[2]; checkOperation(builder.defaultOperation); - if (splitSemi.length > 1) { final String[] columnDirectives = splitSemi[1].split(","); for (final String columnDirective : columnDirectives) { @@ -141,7 +170,7 @@ private static void checkOperation(String op) { @Override public String toString() { - return "JsTotalsTableConfig{" + + return "TotalsTableConfig{" + "showTotalsByDefault=" + showTotalsByDefault + ", showGrandTotalsByDefault=" + showGrandTotalsByDefault + ", defaultOperation='" + defaultOperation + '\'' + @@ -159,20 +188,232 @@ public String toString() { @JsIgnore public String serialize() { final StringBuilder builder = new StringBuilder(); - builder.append(Boolean.toString(showTotalsByDefault)).append(",") - .append(Boolean.toString(showGrandTotalsByDefault)).append(",").append(defaultOperation).append(";"); + builder.append(showTotalsByDefault).append(",") + .append(showGrandTotalsByDefault).append(",").append(defaultOperation).append(";"); operationMap .forEach(key -> builder.append(key).append("=").append(operationMap.get(key).join(":")).append(",")); return builder.toString(); } - /** - * Expose groupBy as a plain Java array, so it can be serialized correctly to the server. - */ @JsIgnore - public String[] groupByArray() { - String[] strings = new String[groupBy.length]; - groupBy.forEach((str, index, array) -> strings[index] = Js.cast(str)); - return strings; + public AggregateRequest buildRequest(JsArray allColumns) { + AggregateRequest request = new AggregateRequest(); + customColumns = new JsArray<>(); + dropColumns = new JsArray<>(); + + request.setGroupByColumnsList(Js.>uncheckedCast(groupBy)); + JsArray aggregations = new JsArray<>(); + request.setAggregationsList(aggregations); + Map columnTypes = Arrays.stream(Js.uncheckedCast(allColumns)) + .collect(Collectors.toMap(Column::getName, Column::getType)); + Map> aggs = new HashMap<>(); + List colsNeedingCompoundNames = new ArrayList<>(); + Set seenColNames = new HashSet<>(); + groupBy.forEach((col, p1, p2) -> seenColNames.add(Js.cast(col))); + this.operationMap.forEach(colName -> { + this.operationMap.get(colName).forEach((agg, index, arr) -> { + if (!JsAggregationOperation.canAggregateType(agg, columnTypes.get(colName))) { + // skip this column. to follow DHE's behavior + return null; + } + aggs.computeIfAbsent(agg, ignore -> new LinkedHashSet<>()).add(colName); + if (seenColNames.contains(colName)) { + colsNeedingCompoundNames.add(colName); + } else { + seenColNames.add(colName); + } + return null; + }); + }); + Set unusedColumns = new HashSet<>(columnTypes.keySet()); + unusedColumns.removeAll(seenColNames); + // no unused column can collide, add to the default operation list + aggs.computeIfAbsent(defaultOperation, ignore -> new LinkedHashSet<>()) + .addAll(unusedColumns.stream().filter( + colName -> JsAggregationOperation.canAggregateType(defaultOperation, columnTypes.get(colName))) + .collect(Collectors.toList())); + + aggs.forEach((aggregationType, cols) -> { + Aggregation agg = new Aggregation(); + + JsArray aggColumns = dedup(cols, colsNeedingCompoundNames, aggregationType); + AggregationColumns columns = null; + + switch (aggregationType) { + case JsAggregationOperation.COUNT: { + AggregationCount count = new AggregationCount(); + count.setColumnName("Count"); + agg.setCount(count); + aggColumns.forEach((p0, p1, p2) -> { + String colName = p0.split("=")[0].trim(); + customColumns.push(colName + "__Count = Count"); + return null; + }); + dropColumns.push("Count"); + break; + } + case JsAggregationOperation.COUNT_DISTINCT: { + AggSpec spec = new AggSpec(); + spec.setCountDistinct(new AggSpecCountDistinct()); + columns = new AggregationColumns(); + columns.setSpec(spec); + columns.setMatchPairsList(aggColumns); + agg.setColumns(columns); + break; + } + case JsAggregationOperation.DISTINCT: { + AggSpec spec = new AggSpec(); + spec.setDistinct(new AggSpecDistinct()); + columns = new AggregationColumns(); + columns.setSpec(spec); + columns.setMatchPairsList(aggColumns); + agg.setColumns(columns); + aggColumns.forEach((p0, p1, p2) -> { + String colName = p0.split("=")[0].trim(); + customColumns.push(colName + "= `` + " + colName); + return null; + }); + break; + } + case JsAggregationOperation.MIN: { + AggSpec spec = new AggSpec(); + spec.setMin(new AggSpecMin()); + columns = new AggregationColumns(); + columns.setSpec(spec); + columns.setMatchPairsList(aggColumns); + agg.setColumns(columns); + break; + } + case JsAggregationOperation.MAX: { + AggSpec spec = new AggSpec(); + spec.setMax(new AggSpecMax()); + columns = new AggregationColumns(); + columns.setSpec(spec); + columns.setMatchPairsList(aggColumns); + agg.setColumns(columns); + break; + } + case JsAggregationOperation.SUM: { + AggSpec spec = new AggSpec(); + spec.setSum(new AggSpecSum()); + columns = new AggregationColumns(); + columns.setSpec(spec); + columns.setMatchPairsList(aggColumns); + agg.setColumns(columns); + break; + } + case JsAggregationOperation.ABS_SUM: { + AggSpec spec = new AggSpec(); + spec.setAbsSum(new AggSpecAbsSum()); + columns = new AggregationColumns(); + columns.setSpec(spec); + columns.setMatchPairsList(aggColumns); + agg.setColumns(columns); + break; + } + case JsAggregationOperation.VAR: { + AggSpec spec = new AggSpec(); + spec.setVar(new AggSpecVar()); + columns = new AggregationColumns(); + columns.setSpec(spec); + columns.setMatchPairsList(aggColumns); + agg.setColumns(columns); + break; + } + case JsAggregationOperation.AVG: { + AggSpec spec = new AggSpec(); + spec.setAvg(new AggSpecAvg()); + columns = new AggregationColumns(); + columns.setSpec(spec); + columns.setMatchPairsList(aggColumns); + agg.setColumns(columns); + break; + } + case JsAggregationOperation.STD: { + AggSpec spec = new AggSpec(); + spec.setStd(new AggSpecStd()); + columns = new AggregationColumns(); + columns.setSpec(spec); + columns.setMatchPairsList(aggColumns); + agg.setColumns(columns); + break; + } + case JsAggregationOperation.FIRST: { + AggSpec spec = new AggSpec(); + spec.setFirst(new AggSpecFirst()); + columns = new AggregationColumns(); + columns.setSpec(spec); + columns.setMatchPairsList(aggColumns); + agg.setColumns(columns); + break; + } + case JsAggregationOperation.LAST: { + AggSpec spec = new AggSpec(); + spec.setLast(new AggSpecLast()); + columns = new AggregationColumns(); + columns.setSpec(spec); + columns.setMatchPairsList(aggColumns); + agg.setColumns(columns); + break; + } + case JsAggregationOperation.UNIQUE: { + AggSpec spec = new AggSpec(); + AggSpecUnique unique = new AggSpecUnique(); + AggSpecNonUniqueSentinel sentinel = new AggSpecNonUniqueSentinel(); + sentinel.setNullValue(Table_pb.NullValue.getNULL_VALUE()); + unique.setNonUniqueSentinel(sentinel); + spec.setUnique(unique); + columns = new AggregationColumns(); + columns.setSpec(spec); + columns.setMatchPairsList(aggColumns); + agg.setColumns(columns); + break; + } + // case JsAggregationOperation.SORTED_FIRST: { + // // TODO #3302 support this + // } + // case JsAggregationOperation.SORTED_LAST: { + // // TODO #3302 support this + // } + // case JsAggregationOperation.WSUM: { + // // TODO #3302 support this + // } + default: + JsLog.warn("Aggregation " + aggregationType + " not supported, ignoring"); + } + + if (columns == null || columns.getMatchPairsList().length > 0) { + aggregations.push(agg); + } + }); + + if (aggregations.length != 0) { + request.setAggregationsList(aggregations); + } + + return request; + } + + private JsArray dedup(LinkedHashSet cols, List colsNeedingCompoundNames, + String aggregationType) { + return cols.stream().map(col -> { + if (colsNeedingCompoundNames.contains(col)) { + return col + "__" + aggregationType + " = " + col; + } + return col; + }).collect(Collector.of( + JsArray::new, + JsArray::push, + (arr1, arr2) -> arr1.concat(arr2.asArray(new String[0])))); + } + + @JsIgnore + public JsArray getCustomColumns() { + return customColumns; + } + + @JsIgnore + public JsArray getDropColumns() { + return dropColumns; } } diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/tree/JsTreeTable.java b/web/client-api/src/main/java/io/deephaven/web/client/api/tree/JsTreeTable.java index 84e61719df0..add460e5232 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/tree/JsTreeTable.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/tree/JsTreeTable.java @@ -1015,31 +1015,31 @@ public Promise selectDistinct(Column[] columns) { }); } - // @JsMethod - // public Promise getTotalsTableConfig() { - // // we want to communicate to the JS dev that there is no default config, so we allow - // // returning null here, rather than a default config. They can then easily build a - // // default config, but without this ability, there is no way to indicate that the - // // config omitted a totals table - // return sourceTable.get().then(t -> Promise.resolve(t.getTotalsTableConfig())); - // } - // - // @JsMethod - // public Promise getTotalsTable(@JsOptional Object config) { - // return sourceTable.get().then(t -> { - // // if this is the first time it is used, it might not be filtered correctly, so check that the filters match - // // up. - // if (!t.getFilter().asList().equals(getFilter().asList())) { - // t.applyFilter(getFilter().asArray(new FilterCondition[0])); - // } - // return Promise.resolve(t.getTotalsTable(config)); - // }); - // } - // - // @JsMethod - // public Promise getGrandTotalsTable(@JsOptional Object config) { - // return sourceTable.get().then(t -> Promise.resolve(t.getGrandTotalsTable(config))); - // } + @JsMethod + public Promise getTotalsTableConfig() { + // we want to communicate to the JS dev that there is no default config, so we allow + // returning null here, rather than a default config. They can then easily build a + // default config, but without this ability, there is no way to indicate that the + // config omitted a totals table + return sourceTable.get().then(t -> Promise.resolve(t.getTotalsTableConfig())); + } + + @JsMethod + public Promise getTotalsTable(@JsOptional Object config) { + return sourceTable.get().then(t -> { + // if this is the first time it is used, it might not be filtered correctly, so check that the filters match + // up. + if (!t.getFilter().asList().equals(getFilter().asList())) { + t.applyFilter(getFilter().asArray(new FilterCondition[0])); + } + return Promise.resolve(t.getTotalsTable(config)); + }); + } + + @JsMethod + public Promise getGrandTotalsTable(@JsOptional Object config) { + return sourceTable.get().then(t -> Promise.resolve(t.getGrandTotalsTable(config))); + } // TODO core#279 restore this with protobuf once smartkey has some pb-based analog // private static final int SERIALIZED_VERSION = 1; diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/tree/enums/JsAggregationOperation.java b/web/client-api/src/main/java/io/deephaven/web/client/api/tree/enums/JsAggregationOperation.java index 86fec6510c8..c2f0287d8e1 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/tree/enums/JsAggregationOperation.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/tree/enums/JsAggregationOperation.java @@ -4,6 +4,7 @@ package io.deephaven.web.client.api.tree.enums; import com.vertispan.tsdefs.annotations.TsTypeDef; +import jsinterop.annotations.JsIgnore; import jsinterop.annotations.JsType; @JsType(name = "AggregationOperation", namespace = "dh") @@ -31,4 +32,75 @@ public class JsAggregationOperation { // WSUM = "WeightedSum"; @Deprecated public static final String SKIP = "Skip"; + + @JsIgnore + public static boolean canAggregateType(String aggregationType, String columnType) { + switch (aggregationType) { + case COUNT: + case COUNT_DISTINCT: + case FIRST: + case LAST: + case UNIQUE: { + // These operations are always safe + return true; + } + case ABS_SUM: + case SUM: { + // Both sum operators will count "true" boolean values + return isNumericOrBoolean(columnType); + } + case AVG: + case VAR: + case STD: { + return isNumeric(columnType); + } + case MIN: + case MAX: { + // Can only apply to Comparables - JS can't work this out, so we'll stick to known types + return isComparable(columnType); + } + } + return false; + } + + private static boolean isNumeric(String columnType) { + switch (columnType) { + case "double": + case "float": + case "int": + case "long": + case "short": + case "char": + case "byte": { + return true; + } + } + return false; + } + + private static boolean isNumericOrBoolean(String columnType) { + if (isNumeric(columnType)) { + return true; + } + return columnType.equals("boolean") || columnType.equals("java.lang.Boolean"); + } + + private static boolean isComparable(String columnType) { + if (isNumericOrBoolean(columnType)) { + return true; + } + switch (columnType) { + case "java.lang.String": + case "java.time.Instant": + case "java.time.ZonedDateTime": + case "io.deephaven.time.DateTime": + case "java.time.LocalTime": + case "java.time.LocalDate": + case "java.math.BigDecimal": + case "java.math.BigInteger": + return true; + } + return false; + } + } diff --git a/web/client-api/src/main/java/io/deephaven/web/public/index.html b/web/client-api/src/main/java/io/deephaven/web/public/index.html index 97dfc6f4f0b..61e4eea90d8 100644 --- a/web/client-api/src/main/java/io/deephaven/web/public/index.html +++ b/web/client-api/src/main/java/io/deephaven/web/public/index.html @@ -20,6 +20,7 @@

Feature Demos

  • Create and manipulate an input table
  • Create a rollup from a simple table
  • Create a simple tree table
  • +
  • Create totals, grand totals from an existing table
  • Authentication samples
  • diff --git a/web/client-backplane/src/main/java/io/deephaven/javascript/proto/dhinternal/io/deephaven/proto/table_pb/aggspec/AggSpecNonUniqueSentinel.java b/web/client-backplane/src/main/java/io/deephaven/javascript/proto/dhinternal/io/deephaven/proto/table_pb/aggspec/AggSpecNonUniqueSentinel.java index 555ba476823..37723bf33af 100644 --- a/web/client-backplane/src/main/java/io/deephaven/javascript/proto/dhinternal/io/deephaven/proto/table_pb/aggspec/AggSpecNonUniqueSentinel.java +++ b/web/client-backplane/src/main/java/io/deephaven/javascript/proto/dhinternal/io/deephaven/proto/table_pb/aggspec/AggSpecNonUniqueSentinel.java @@ -197,7 +197,7 @@ public static native AggSpecNonUniqueSentinel.ToObjectReturnType toObject( public native String getLongValue(); - public native double getNullValue(); + public native int getNullValue(); public native double getShortValue(); @@ -241,7 +241,7 @@ public static native AggSpecNonUniqueSentinel.ToObjectReturnType toObject( public native void setLongValue(String value); - public native void setNullValue(double value); + public native void setNullValue(int value); public native void setShortValue(double value);