Skip to content

fix(sql): report empty LIMIT in DECLARE subquery - #6979

Merged
bluestreak01 merged 2 commits into
masterfrom
jh_children_dont_eat_parents
Apr 16, 2026
Merged

fix(sql): report empty LIMIT in DECLARE subquery#6979
bluestreak01 merged 2 commits into
masterfrom
jh_children_dont_eat_parents

Conversation

@jerrinot

@jerrinot jerrinot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Malformed SQL with inner queries such as: (notice the LIMIT has no value)

DECLARE @pair := (SELECT symbol FROM fx_trades LIMIT ) SELECT ... 

could trigger an internal error. This change improves error reporting for such queries.

Implementation details for reviewers
The parser was allowing a nested expression parse to consume operands from the
outer declaration expression, corrupting the assignment AST before parseDeclare() inspected it.

The fix:
Isolate ExpressionTreeBuilder operand stack frames across reentrant
parseExpr() calls, so nested parses cannot consume outer operands. Also
reject an empty LIMIT clause with a clear parser error: 'limit expression expected'

Malformed SQL such as: DECLARE @pair := (SELECT symbol FROM fx_trades LIMIT ) SELECT ...
(notice the LIMIT has no value)
could trigger an internal NullPointerException while validating the
DECLARE assignment. The parser was allowing a nested expression parse to
consume operands from the outer declaration expression, corrupting the
assignment AST before parseDeclare() inspected it.

Isolate ExpressionTreeBuilder operand stack frames across reentrant
parseExpr() calls, so nested parses cannot consume outer operands. Also
reject an empty LIMIT clause with a clear parser error: 'limit expression expected'

This turns the validation failure into a user-facing SQL error instead of
an internal server error, while preserving the existing specific
"unexpected token [)]" diagnostics for unbalanced top-level SELECT lists.
@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 150a5d92-d6af-4351-8253-bcca6cc45a6a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jh_children_dont_eat_parents

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@jerrinot jerrinot changed the title Jh children dont eat parents fix(sql): report empty LIMIT in DECLARE subquery Apr 16, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
core/src/test/java/io/questdb/test/griffin/DeclareTest.java (1)

801-811: Add companion coverage for the still-valid open-ended LIMIT forms.

This locks in the new failure path, but the parser change also explicitly preserves LIMIT 5, and LIMIT ,5. A couple of success assertions here would make that compatibility guarantee much harder to regress.

As per coding guidelines "Always think carefully about all possible code paths, especially error paths, and write tests that ensure correct resource cleanup on each path."

🤖 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/DeclareTest.java` around lines 801
- 811, The current test in
DeclareTest.testDeclareVariableAsSubQueryWithEmptyLimit asserts the parser error
for a completely empty LIMIT, but we need companion tests to ensure the parser
still accepts open-ended LIMIT forms `LIMIT 5,` and `LIMIT ,5`; add two new test
cases (or extend this test) that run queries similar to the existing one but use
"limit 5," and "limit ,5" in the subquery assigned to `@pair` and assert
successful execution (no exception) and/or expected results, ensuring these
paths in the parser (LIMIT with trailing comma and LIMIT with leading comma)
remain supported.
🤖 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/SqlParser.java`:
- Around line 937-939: The helper isUnexpectedRightParenInTopLevelSelect
currently treats ')' as unexpected unless subQueryMode, createTableMode,
copyMode, or createViewMode are set, which causes parenthesized SELECTs used by
parseViewSql() and parseCreateMatView() (which call parseDml()) to be rejected;
fix by adding a new boolean flag (e.g., parenthesizedSelectMode or parenDmlMode)
and set it true around the parseDml() calls in parseViewSql() and
parseCreateMatView(), then include that flag in the negation inside
isUnexpectedRightParenInTopLevelSelect so ')' is considered balanced when
parenthesizedSelectMode is active; update all references to use the new flag
(isUnexpectedRightParenInTopLevelSelect, parseViewSql, parseCreateMatView,
parseDml, parseSelectClause).
- Around line 3101-3105: The empty-LIMIT error currently uses
lexer.lastTokenPosition() which points at the LIMIT keyword for an EOF case;
change the position passed to SqlException.$(...) to use lexer.hasUnparsed() ?
lexer.lastTokenPosition() : lexer.getPosition() so the diagnostic points at EOF
when the clause ends unexpectedly. Update the throw in SqlParser where lo ==
null && hi == null to compute the position with that conditional (referencing
lexer.hasUnparsed(), lexer.lastTokenPosition(), lexer.getPosition(), and the
existing lo/hi check) and pass it into SqlException.$(...).

---

Nitpick comments:
In `@core/src/test/java/io/questdb/test/griffin/DeclareTest.java`:
- Around line 801-811: The current test in
DeclareTest.testDeclareVariableAsSubQueryWithEmptyLimit asserts the parser error
for a completely empty LIMIT, but we need companion tests to ensure the parser
still accepts open-ended LIMIT forms `LIMIT 5,` and `LIMIT ,5`; add two new test
cases (or extend this test) that run queries similar to the existing one but use
"limit 5," and "limit ,5" in the subquery assigned to `@pair` and assert
successful execution (no exception) and/or expected results, ensuring these
paths in the parser (LIMIT with trailing comma and LIMIT with leading comma)
remain supported.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ac58a4a4-8b09-4772-9526-201704accf6b

📥 Commits

Reviewing files that changed from the base of the PR and between 5b045c6 and 929345c.

📒 Files selected for processing (3)
  • core/src/main/java/io/questdb/griffin/ExpressionTreeBuilder.java
  • core/src/main/java/io/questdb/griffin/SqlParser.java
  • core/src/test/java/io/questdb/test/griffin/DeclareTest.java

Comment on lines +937 to +939
private boolean isUnexpectedRightParenInTopLevelSelect(CharSequence tok) {
return Chars.equals(tok, ')') && !(subQueryMode || createTableMode || copyMode || createViewMode);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle parenthesized non-subquery SELECT contexts here too.

This helper only treats ) as balanced when subQueryMode, createTableMode, copyMode, or createViewMode is set. parseViewSql() and parseCreateMatView() both feed parenthesized queries through parseDml() without toggling any of those flags, so valid wrappers like (SELECT 1) now get rejected inside parseSelectClause as unexpected token [)] instead of returning control to the caller to consume the closing parenthesis.

🤖 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/SqlParser.java` around lines 937 - 939,
The helper isUnexpectedRightParenInTopLevelSelect currently treats ')' as
unexpected unless subQueryMode, createTableMode, copyMode, or createViewMode are
set, which causes parenthesized SELECTs used by parseViewSql() and
parseCreateMatView() (which call parseDml()) to be rejected; fix by adding a new
boolean flag (e.g., parenthesizedSelectMode or parenDmlMode) and set it true
around the parseDml() calls in parseViewSql() and parseCreateMatView(), then
include that flag in the negation inside isUnexpectedRightParenInTopLevelSelect
so ')' is considered balanced when parenthesizedSelectMode is active; update all
references to use the new flag (isUnexpectedRightParenInTopLevelSelect,
parseViewSql, parseCreateMatView, parseDml, parseSelectClause).

Comment on lines +3101 to +3105
// questdb accepts open-ended limits like 'LIMIT 5,' and 'LIMIT ,5'.
// so reject only when neither side of the LIMIT clause parsed.
if (lo == null && hi == null) {
throw SqlException.$(lexer.lastTokenPosition(), "limit expression expected");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Point the empty-LIMIT error at EOF when the clause just ends.

For ... LIMIT with no following token, lexer.lastTokenPosition() still points at the LIMIT keyword, so the new diagnostic highlights the wrong location. This file already uses lexer.hasUnparsed() ? lexer.lastTokenPosition() : lexer.getPosition() for that exact distinction.

Suggested fix
             // questdb accepts open-ended limits like 'LIMIT 5,' and 'LIMIT ,5'.
             // so reject only when neither side of the LIMIT clause parsed.
             if (lo == null && hi == null) {
-                throw SqlException.$(lexer.lastTokenPosition(), "limit expression expected");
+                throw SqlException.$(
+                        lexer.hasUnparsed() ? lexer.lastTokenPosition() : lexer.getPosition(),
+                        "limit expression expected"
+                );
             }

As per coding guidelines "When using SqlException.$(position, msg), the position should point at the specific offending character, not the start of the expression."

🤖 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/SqlParser.java` around lines 3101 -
3105, The empty-LIMIT error currently uses lexer.lastTokenPosition() which
points at the LIMIT keyword for an EOF case; change the position passed to
SqlException.$(...) to use lexer.hasUnparsed() ? lexer.lastTokenPosition() :
lexer.getPosition() so the diagnostic points at EOF when the clause ends
unexpectedly. Update the throw in SqlParser where lo == null && hi == null to
compute the position with that conditional (referencing lexer.hasUnparsed(),
lexer.lastTokenPosition(), lexer.getPosition(), and the existing lo/hi check)
and pass it into SqlException.$(...).

@mtopolnik

Copy link
Copy Markdown
Contributor

[PR Coverage check]

😍 pass : 20 / 20 (100.00%)

file detail

path covered line new line coverage
🔵 io/questdb/griffin/SqlParser.java 7 7 100.00%
🔵 io/questdb/griffin/ExpressionTreeBuilder.java 13 13 100.00%

@bluestreak01
bluestreak01 merged commit 0c180d8 into master Apr 16, 2026
53 checks passed
@bluestreak01
bluestreak01 deleted the jh_children_dont_eat_parents branch April 16, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants