Repository navigation
Conversation
MATCH (n:P) WITH DISTINCT n.id AS v WITH DISTINCT coalesce(v, 0) AS w RETURN w returned one row instead of one per node; CASE ... THEN v and [v] did the same. With runtime checks, these queries and v + 1 failed the !flat assertion in FactorizationGroup::setFlat instead. RemoveUnnecessaryDistinctOptimizer ran after FactorizationRewriter, so a removed DISTINCT left behind the FLATTEN placed for it, and the FLATTEN placed for the DISTINCT above then flattened the same chunk again. The pass and AggKeyDependencyOptimizer, whose dependent-key resolution it needs, now run before FactorizationRewriter, which places flattens only for the operators that remain. Plans that keep their DISTINCTs are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| // be incorrect here and would also throw on FLATTEN nodes). | ||
| result->computeFactorizedSchema(); | ||
| // This optimizer runs before FactorizationRewriter, so the plan is flat. | ||
| result->computeFlatSchema(); |
There was a problem hiding this comment.
Low (non-blocking, fail-closed today): computeFlatSchema() unconditionally throws on FLATTEN (logical_flatten.cpp:13-14). The invariant holds today (RemoveFactorizationRewriter strips FLATTENs first and this pass runs before FactorizationRewriter), but it is implicit. Consider a one-line DASSERT/comment at the pass entry, or defensively bypassing FLATTEN nodes, so a future pass-order change fails loudly at the right place.
There was a problem hiding this comment.
Done in 6efd2e0. With runtime checks, the pass now asserts at entry that the plan has no FLATTEN, next to a one-line comment saying why. RemoveUnnecessaryDistinctRequiresFlatPlan runs the pass on an optimized plan and expects the error from that assertion; without it, the test fails on LogicalFlatten::computeFlatSchema() should never be used. Release builds skip the walk and still throw in computeFlatSchema. I kept FLATTEN failing rather than bypassing it, since skipping it would hide a reorder.
|
Two notes from review (inline hardening suggestion posted separately):
|
LogicalFlatten::computeFlatSchema throws, so RemoveUnnecessaryDistinctOptimizer has to run before FactorizationRewriter. With runtime checks the pass now asserts that no FLATTEN is in the plan when it starts, so a later reorder fails at the pass entry instead of inside a FLATTEN's schema code. Release builds skip the walk. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ATTEN The e2e test catches wrong rows, but a FLATTEN left behind by a removed DISTINCT can also keep the rows right and only make the scan run row by row. Assert that MATCH (n:person) RETURN DISTINCT n.ID and a count(*) over WITH DISTINCT n.ID plan without DISTINCT and without FLATTEN. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
This work was produced with the help of language models.
Fixes #1293.
Over
P(id INT64 PRIMARY KEY, flag BOOLEAN)with ids 1, 2, 3,MATCH (p:P) WITH DISTINCT p.id AS id WITH DISTINCT coalesce(id, 0) AS v RETURN vreturned[1]instead of[1], [2], [3].CASE WHEN f THEN id ELSE 0 ENDand[id]collapsed the same way, also underRETURN DISTINCT, and aUNION ALLbranch returning[id]gave[1]on every row. With runtime checks on, as in CI, these queries failed withAssertion failed in file ".../src/include/planner/operator/schema.h" on line 26: !flatinstead, and so didid + 1, a join onidand anUNWINDover it.Fix.
RemoveUnnecessaryDistinctOptimizerran afterFactorizationRewriter, so a removed DISTINCT left its FLATTEN behind, and the FLATTEN placed for the operator above flattened the same chunk again. The pass andAggKeyDependencyOptimizer, whose key resolution it needs, now run just beforeFactorizationRewriter, which then places flattens only for the operators that remain. The removal pass now recomputes flat schemas and no longer walks through FLATTEN, which can't occur there. Resolving keys first doesn't change the set of flattened groups, since a DISTINCT or aggregate flattens its keys and payloads together. Across the e2e suite, 76,333 of 76,350 optimized plans print identically tomain's underLBUG_DUMP_LOGICAL; the rest are the new test, two other removed DISTINCTs that lose their leftover FLATTEN, and run-to-run noise.Speed. Without the leftover FLATTEN these plans stay vectorized. Per execution over 2M nodes, single-threaded:
main→ heremain→ hereMATCH (p:P) RETURN DISTINCT p.idMATCH (p:P) WITH DISTINCT p.id AS id RETURN count(*)MATCH (p:P) WITH DISTINCT p.id AS id RETURN sum(id)RETURN DISTINCT p.nameandRETURN p.name, count(*)over the same nodes stay within 0.01% ofmain.ORDER BY removal.
RemoveUnnecessaryOrderByOptimizeralso runs afterFactorizationRewriterand stays there. It has its own wrong-result bug (#1352), and before factorization it would find more ORDER BYs to remove.Tests.
optimizer/remove_unnecessary_distinct.testruns eight queries over four nodes, one with a null flag: the issue's cases,RETURN DISTINCT,v + 1, a join, anUNWINDand aUNION ALL. On the 0.21.2 and nightly wheels theCASE, bothcoalescestatements and the list return one row, and theUNION ALLreturns[1]five times. With runtime checks,mainfails all eight on the assertion. Full ctest with runtime checks: 3164/3164.