fix(sql): fix AssertionError that could be triggered by JOIN SQL#6824
Conversation
WalkthroughThe PR introduces a centralized filter-composition mechanism in SqlCodeGenerator via a new private wrapWithFilter() helper that safely combines filters using AndFunctionFactory. This replaces nested FilteredRecordCursorFactory instances across three code paths in filter and join handling. FilteredRecordCursorFactory is enhanced with public getFilter() and halfClose() methods for filter access and resource management. Two regression tests validate the fix for post-join filter scenarios. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@nwoolmer @bluestreak01 would you be able to take a look when you get a chance? Happy to make any changes needed. Thanks |
|
Comments from Claude Code: |
|
Thanks for the review , will address these comments Thanks |
24ae2e7 to
bb64f2d
Compare
When a JOIN query has a WHERE clause that the optimizer splits into both a postJoinWhereClause (column-referencing) and a constWhereClause (non-column-referencing, non-constant like NOW() = NOW()), the code generator in generateJoins() wraps the master factory in a FilteredRecordCursorFactory twice. The second wrapping violates the assertion at FilteredRecordCursorFactory:44 that forbids nesting, causing an internal server error. The fix extracts a wrapWithFilter() helper that detects when the factory is already a FilteredRecordCursorFactory and combines both filters into a single AND filter using AndFunctionFactory, following the established filter-stealing lifecycle pattern (getFilter + getBaseFactory + halfClose). Applied at all three FilteredRecordCursorFactory creation sites for comprehensive coverage. Fixes questdb#6762
bb64f2d to
45d5981
Compare
|
@glasstiger addressed your comments please take a look Thanks |
|
@CodeRabbit review the PR |
|
🧠 Learnings used✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
core/src/main/java/io/questdb/griffin/engine/table/FilteredRecordCursorFactory.java (1)
55-58: MovegetFilter()to preserve required member ordering.
getFilter()is currently placed beforegetCursor(). With alphabetical ordering for public instance members,getCursor()should come first, so this new method should be repositioned.As per coding guidelines "
**/*.java: Group Java class members by kind (static vs. instance) and visibility, sorted alphabetically. When adding new methods or fields, insert them in the correct alphabetical position among existing members of the same kind."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/src/main/java/io/questdb/griffin/engine/table/FilteredRecordCursorFactory.java` around lines 55 - 58, The public instance method getFilter() in class FilteredRecordCursorFactory is out of alphabetical order (it currently precedes getCursor()); move the entire getFilter() method so that getCursor() appears before getFilter() among the class's public instance members, preserving the method body and signature exactly and ensuring the class's public instance members remain alphabetically ordered.core/src/test/java/io/questdb/test/griffin/engine/join/JoinTest.java (1)
2866-2935: Reorder these new test methods to keep class-member alphabetical ordering consistent.
testJoinInnerPostJoinAndConstFiltershould be placed with thetestJoinInnerPostJoin...methods in strict alphabetical order (it currently appears aftertestJoinInnerPostJoinFilterFF).As per coding guidelines: Group Java class members by kind (static vs. instance) and visibility, sorted alphabetically. When adding new methods or fields, insert them in the correct alphabetical position among existing members of the same kind.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/src/test/java/io/questdb/test/griffin/engine/join/JoinTest.java` around lines 2866 - 2935, The two new test methods are out of alphabetical order; move testJoinInnerPostJoinAndConstFilter so it appears in strict alphabetical order among the other testJoinInnerPostJoin* methods (i.e., place it before testJoinInnerPostJoinFilterFF) inside the JoinTest class; ensure you only reorder the methods (preserve their bodies, annotations, and imports) so class-member grouping and alphabetical sorting rules are maintained.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@core/src/main/java/io/questdb/griffin/SqlCodeGenerator.java`:
- Around line 9008-9009: The call to existingFiltered.halfClose() currently
happens before constructing the wrapper, which can leak ownership if new
FilteredRecordCursorFactory(base, filter) throws; move or defer the halfClose()
call until after the FilteredRecordCursorFactory has been successfully created
(i.e., construct new FilteredRecordCursorFactory(base, filter) first, then call
existingFiltered.halfClose()), ensuring that ownership of base/filter is only
released after the wrapper exists; if necessary, create the wrapper into a local
variable first and then invoke existingFiltered.halfClose() before returning it
so exceptions do not leave resources unowned.
---
Nitpick comments:
In
`@core/src/main/java/io/questdb/griffin/engine/table/FilteredRecordCursorFactory.java`:
- Around line 55-58: The public instance method getFilter() in class
FilteredRecordCursorFactory is out of alphabetical order (it currently precedes
getCursor()); move the entire getFilter() method so that getCursor() appears
before getFilter() among the class's public instance members, preserving the
method body and signature exactly and ensuring the class's public instance
members remain alphabetically ordered.
In `@core/src/test/java/io/questdb/test/griffin/engine/join/JoinTest.java`:
- Around line 2866-2935: The two new test methods are out of alphabetical order;
move testJoinInnerPostJoinAndConstFilter so it appears in strict alphabetical
order among the other testJoinInnerPostJoin* methods (i.e., place it before
testJoinInnerPostJoinFilterFF) inside the JoinTest class; ensure you only
reorder the methods (preserve their bodies, annotations, and imports) so
class-member grouping and alphabetical sorting rules are maintained.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7cdcfff2-9bad-48bd-8858-6ddf27824e37
📒 Files selected for processing (3)
core/src/main/java/io/questdb/griffin/SqlCodeGenerator.javacore/src/main/java/io/questdb/griffin/engine/table/FilteredRecordCursorFactory.javacore/src/test/java/io/questdb/test/griffin/engine/join/JoinTest.java
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Fixes #6762
Summary
When a JOIN query has a WHERE clause that contains both a column-referencing condition and a non-column constant expression, QuestDB crashes with an internal
AssertionErrorinstead of returning results.Reproducer query:
Error:
Root Cause
The bug is in
SqlCodeGenerator.generateJoins(). During query compilation, the SQL optimizer splits the WHERE clause into two buckets:(cast(T1.origin as SYMBOL)) IS NULLpostJoinWhereClauseT1.originNOW() = NOW()constWhereClauseNOW()is runtime-evaluated)The code generator then applies each bucket as a separate
FilteredRecordCursorFactorywrapper:Step 1 (
SqlCodeGenerator.java:4195): Wraps the join result with the postJoin filter:Step 2 (
SqlCodeGenerator.java:4254): Tries to wrap again with the const filter:This violates the assertion at
FilteredRecordCursorFactory.java:44:The assertion exists by design — nesting
FilteredRecordCursorFactoryinstances is inefficient and the codebase expects filters to be combined, not stacked.The Fix
3 files changed, 61 lines added:
1.
FilteredRecordCursorFactory.java— AddedgetFilter()andhalfClose()overridesgetFilter()exposes the filter so it can be extracted and combined.halfClose()follows the established "filter stealing" lifecycle pattern — closes internal state (the cursor) while leavingbaseandfilteralive for reuse. This is the same pattern used byAsyncFilteredRecordCursorFactory.halfClose()and referenced at 7 other locations inSqlCodeGenerator.java.2.
SqlCodeGenerator.java:4254— Combine filters instead of nestingBefore creating a new
FilteredRecordCursorFactory, check ifmasteris already one. If so, extract the existing filter and base, combine both filters with AND usingAndFunctionFactory, and create a single factory.Instead of:
It now produces:
3.
JoinTest.java— Regression testAdded
testJoinInnerPostJoinAndConstFilterthat reproduces the exact scenario: a JOIN with both a column-referencing WHERE condition (T1.val > 0) and a non-constant expression (NOW() = NOW()).Test plan
JoinTest#testJoinInnerPostJoinAndConstFilter(new regression test) passesJoinTestsuite (171 tests) passes with 0 failuresAssertionErrorcrash confirmed