Skip to content

Commit 6992036

Browse files
authored
fix: WouldMatch does not properly memoize. (deephaven#6313)
Fixes deephaven#6312.
1 parent 7c999c3 commit 6992036

File tree

6 files changed

+109
-23
lines changed

6 files changed

+109
-23
lines changed

engine/table/src/main/java/io/deephaven/engine/table/impl/MemoizedOperationKey.java

+12-8
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import io.deephaven.api.RangeJoinMatch;
99
import io.deephaven.api.agg.Aggregation;
1010
import io.deephaven.engine.table.Table;
11-
import io.deephaven.engine.table.WouldMatchPair;
1211
import io.deephaven.engine.table.impl.select.*;
1312
import io.deephaven.engine.table.impl.sources.regioned.SymbolTableSource;
1413
import org.jetbrains.annotations.NotNull;
@@ -525,10 +524,12 @@ public int hashCode() {
525524
}
526525

527526
private static final class WouldMatch extends AttributeAgnosticMemoizedOperationKey {
528-
private final WouldMatchPair[] pairs;
527+
private final String[] names;
528+
private final WhereFilter[] filters;
529529

530-
private WouldMatch(WouldMatchPair[] pairs) {
531-
this.pairs = pairs;
530+
private WouldMatch(String[] names, WhereFilter[] filters) {
531+
this.names = names;
532+
this.filters = filters;
532533
}
533534

534535
@Override
@@ -540,12 +541,12 @@ public boolean equals(final Object other) {
540541
return false;
541542
}
542543
final WouldMatch wouldMatch = (WouldMatch) other;
543-
return Arrays.equals(pairs, wouldMatch.pairs);
544+
return Arrays.equals(names, wouldMatch.names) && Arrays.equals(filters, wouldMatch.filters);
544545
}
545546

546547
@Override
547548
public int hashCode() {
548-
return Arrays.hashCode(pairs);
549+
return Arrays.hashCode(names) ^ Arrays.hashCode(filters);
549550
}
550551

551552
@Override
@@ -554,8 +555,11 @@ BaseTable.CopyAttributeOperation copyType() {
554555
}
555556
}
556557

557-
public static MemoizedOperationKey wouldMatch(WouldMatchPair... pairs) {
558-
return new WouldMatch(pairs);
558+
public static MemoizedOperationKey wouldMatch(String[] names, WhereFilter... filters) {
559+
if (Arrays.stream(filters).allMatch(WhereFilter::canMemoize)) {
560+
return new WouldMatch(names, filters);
561+
}
562+
return null;
559563
}
560564

561565
private static class CrossJoin extends AttributeAgnosticMemoizedOperationKey {

engine/table/src/main/java/io/deephaven/engine/table/impl/QueryTable.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -3841,7 +3841,9 @@ public <R> R apply(@NotNull final Function<Table, R> function) {
38413841
public Table wouldMatch(WouldMatchPair... matchers) {
38423842
final UpdateGraph updateGraph = getUpdateGraph();
38433843
try (final SafeCloseable ignored = ExecutionContext.getContext().withUpdateGraph(updateGraph).open()) {
3844-
return getResult(new WouldMatchOperation(this, matchers));
3844+
final WouldMatchOperation operation = new WouldMatchOperation(this, matchers);
3845+
operation.initializeFilters(this);
3846+
return getResult(operation);
38453847
}
38463848
}
38473849

engine/table/src/main/java/io/deephaven/engine/table/impl/WouldMatchOperation.java

+19-5
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,30 @@ public String getLogPrefix() {
9292

9393
@Override
9494
public SafeCloseable beginOperation(@NotNull final QueryTable parent) {
95-
final QueryCompilerRequestProcessor.BatchProcessor compilationProcessor = QueryCompilerRequestProcessor.batch();
96-
Arrays.stream(whereFilters).forEach(filter -> filter.init(parent.getDefinition(), compilationProcessor));
97-
compilationProcessor.compile();
98-
9995
return Arrays.stream(whereFilters)
10096
.map((final WhereFilter filter) -> {
10197
// Ensure we gather the correct dependencies when building a snapshot control.
10298
return filter.beginOperation(parent);
10399
}).collect(SafeCloseableList.COLLECTOR);
104100
}
105101

102+
/**
103+
* Initialize the filters.
104+
*
105+
* <p>
106+
* We must initialize our filters before the wouldMatch operation's call to QueryTable's getResult method, so that
107+
* memoization processing can correctly compare them. MatchFilters do not properly implement memoization before
108+
* initialization, and they are the most common filter to memoize.
109+
* </p>
110+
*
111+
* @param parent the parent table to have wouldMatch applied
112+
*/
113+
void initializeFilters(@NotNull QueryTable parent) {
114+
final QueryCompilerRequestProcessor.BatchProcessor compilationProcessor = QueryCompilerRequestProcessor.batch();
115+
Arrays.stream(whereFilters).forEach(filter -> filter.init(parent.getDefinition(), compilationProcessor));
116+
compilationProcessor.compile();
117+
}
118+
106119
@Override
107120
public OperationSnapshotControl newSnapshotControl(@NotNull final QueryTable queryTable) {
108121
final List<NotificationQueue.Dependency> dependencies = WhereListener.extractDependencies(whereFilters);
@@ -180,7 +193,8 @@ public Result<QueryTable> initialize(boolean usePrev, long beforeClock) {
180193

181194
@Override
182195
public MemoizedOperationKey getMemoizedOperationKey() {
183-
return MemoizedOperationKey.wouldMatch();
196+
return MemoizedOperationKey.wouldMatch(
197+
matchColumns.stream().map(ColumnHolder::getColumnName).toArray(String[]::new), whereFilters);
184198
}
185199

186200
/**

engine/table/src/main/java/io/deephaven/engine/table/impl/select/MatchFilter.java

+30-9
Original file line numberDiff line numberDiff line change
@@ -854,30 +854,51 @@ private String toString(Object[] x) {
854854

855855
@Override
856856
public boolean equals(Object o) {
857-
if (this == o)
857+
if (this == o) {
858858
return true;
859-
if (o == null || getClass() != o.getClass())
859+
}
860+
if (o == null || getClass() != o.getClass()) {
860861
return false;
862+
}
863+
861864
final MatchFilter that = (MatchFilter) o;
862-
return invertMatch == that.invertMatch &&
863-
caseInsensitive == that.caseInsensitive &&
864-
Objects.equals(columnName, that.columnName) &&
865-
Arrays.equals(values, that.values) &&
866-
Arrays.equals(strValues, that.strValues);
865+
866+
// The equality check is used for memoization, and we cannot actually determine equality of an uninitialized
867+
// filter, because there is too much state that has not been realized.
868+
if (!initialized && !that.initialized) {
869+
throw new UnsupportedOperationException("MatchFilter has not been initialized");
870+
}
871+
872+
// start off with the simple things
873+
if (invertMatch != that.invertMatch ||
874+
caseInsensitive != that.caseInsensitive ||
875+
!Objects.equals(columnName, that.columnName)) {
876+
return false;
877+
}
878+
879+
if (!Arrays.equals(values, that.values)) {
880+
return false;
881+
}
882+
883+
return Objects.equals(getFailoverFilterIfCached(), that.getFailoverFilterIfCached());
867884
}
868885

869886
@Override
870887
public int hashCode() {
888+
if (!initialized) {
889+
throw new UnsupportedOperationException("MatchFilter has not been initialized");
890+
}
871891
int result = Objects.hash(columnName, invertMatch, caseInsensitive);
892+
// we can use values because we know the filter has been initialized; the hash code should be stable and it
893+
// cannot be stable before we convert the values
872894
result = 31 * result + Arrays.hashCode(values);
873-
result = 31 * result + Arrays.hashCode(strValues);
874895
return result;
875896
}
876897

877898
@Override
878899
public boolean canMemoize() {
879900
// we can be memoized once our values have been initialized; but not before
880-
return initialized;
901+
return initialized && (getFailoverFilterIfCached() == null || getFailoverFilterIfCached().canMemoize());
881902
}
882903

883904
@Override

engine/table/src/test/java/io/deephaven/engine/table/impl/QueryTableTest.java

+5
Original file line numberDiff line numberDiff line change
@@ -3280,6 +3280,11 @@ public void testMemoize() {
32803280
testNoMemoize(source, t -> t.where("Sym in `aa`, `bb`"), t -> t.where("Sym in `aa`, `cc`"));
32813281
testNoMemoize(source, t -> t.where("Sym.startsWith(`a`)"));
32823282

3283+
testMemoize(source, t -> t.wouldMatch("A=intCol == 7"), t -> t.wouldMatch("A=intCol == 7"));
3284+
testNoMemoize(source, t -> t.wouldMatch("A=intCol == 7"), t -> t.wouldMatch("A=intCol == 6"));
3285+
testNoMemoize(source, t -> t.wouldMatch("A=intCol == 7"), t -> t.wouldMatch("B=intCol == 7"));
3286+
testNoMemoize(source, t -> t.wouldMatch("A=intCol < 7"), t -> t.wouldMatch("A=intCol < 7"));
3287+
32833288
testMemoize(source, t -> t.countBy("Count", "Sym"));
32843289
testMemoize(source, t -> t.countBy("Sym"));
32853290
testMemoize(source, t -> t.sumBy("Sym"));

engine/table/src/test/java/io/deephaven/engine/table/impl/QueryTableWhereTest.java

+40
Original file line numberDiff line numberDiff line change
@@ -1677,4 +1677,44 @@ public void testAddAndRemoveRefilter() {
16771677

16781678
assertTableEquals(source.where("FV in `A`, `B`"), result);
16791679
}
1680+
1681+
@Test
1682+
public void testWhereFilterEquality() {
1683+
final Table x = TableTools.newTable(intCol("A", 1, 2, 3), intCol("B", 4, 2, 1), stringCol("S", "A", "B", "C"));
1684+
1685+
final WhereFilter f1 = WhereFilterFactory.getExpression("A in 7");
1686+
final WhereFilter f2 = WhereFilterFactory.getExpression("A in 8");
1687+
final WhereFilter f3 = WhereFilterFactory.getExpression("A in 7");
1688+
1689+
final Table ignored = x.where(Filter.and(f1, f2, f3));
1690+
1691+
assertEquals(f1, f3);
1692+
assertNotEquals(f1, f2);
1693+
assertNotEquals(f2, f3);
1694+
1695+
final WhereFilter fa = WhereFilterFactory.getExpression("A in 7");
1696+
final WhereFilter fb = WhereFilterFactory.getExpression("B in 7");
1697+
final WhereFilter fap = WhereFilterFactory.getExpression("A not in 7");
1698+
1699+
final Table ignored2 = x.where(Filter.and(fa, fb, fap));
1700+
1701+
assertNotEquals(fa, fb);
1702+
assertNotEquals(fa, fap);
1703+
assertNotEquals(fb, fap);
1704+
1705+
final WhereFilter fs = WhereFilterFactory.getExpression("S icase in `A`");
1706+
final WhereFilter fs2 = WhereFilterFactory.getExpression("S icase in `A`, `B`, `C`");
1707+
final WhereFilter fs3 = WhereFilterFactory.getExpression("S icase in `A`, `B`, `C`");
1708+
final Table ignored3 = x.where(Filter.and(fs, fs2, fs3));
1709+
assertNotEquals(fs, fs2);
1710+
assertNotEquals(fs, fs3);
1711+
assertEquals(fs2, fs3);
1712+
1713+
final WhereFilter fof1 = WhereFilterFactory.getExpression("A = B");
1714+
final WhereFilter fof2 = WhereFilterFactory.getExpression("A = B");
1715+
final Table ignored4 = x.where(fof1);
1716+
final Table ignored5 = x.where(fof2);
1717+
// the ConditionFilters do not compare as equal, so this is unfortunate, but expected behavior
1718+
assertNotEquals(fof1, fof2);
1719+
}
16801720
}

0 commit comments

Comments
 (0)