Skip to content

fix(parsers): stop dialect parsers dropping findings the fallback reports - #90

Merged
KARTIKrocks merged 4 commits into
mainfrom
fix/parser-structural-fields
Sep 26, 2026
Merged

KARTIKrocks merged 4 commits into
mainfrom
fix/parser-structural-fields

Conversation

@KARTIKrocks

@KARTIKrocks KARTIKrocks commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

User description

Closes #81. Closes #82.

What was wrong

Opting into pgparser / mysqlparser silently dropped findings the default parser reports — the inverse of the trade-off website/docs/parsers.md promises. The existing parity test (TestParser_NeverAddsFindingTheFallbackDoesNot) only catches findings a parser adds, so none of this was visible.

What changed

  • Unmodelled statements keep the fallback's Statement with Exact = false. Resets now happen only inside a handled case. This settles the design question left open on [Bug]: dialect parsers blank every structural field, then mark non-DML statements Exact #81; no rule reads Exact, so the flag change alters no findings.
  • HasLimit requires a row count (pgparser). LIMIT ALL still counts as a limit — the fallback reads it that way, and treating it as unbounded would make the parser add findings.
  • INSERT … SELECT * row sources are read by both parsers (plain and CTE-prefixed).
  • Set operations read every operand (UNION / INTERSECT / EXCEPT): FROM, star, DISTINCT and OFFSET are merged across operands; only the result-level ORDER BY counts. WHERE/LIMIT presence for a set operation is taken from the fallback, which counts them anywhere — reading them per operand made SELECT a FROM t UNION SELECT b FROM (SELECT b FROM u LIMIT 3) s report a finding the fallback does not. mysqlparser previously treated a UNION as StmtOther; pgparser left set operations blank deliberately.
  • Fallback: a fully parenthesised statement's ORDER BY is the statement's own. (SELECT … ORDER BY a) was read as a subquery, a pre-existing parity break where pgparser already reported orderby-without-limit and the fallback did not. Fixed in the fallback, per the "when a parser learns it, the fallback learns it too" rule.

Tests

Docs

AGENTS.md and all three reviewer configs (.greptile/config.json, .codeant/review.json, .coderabbit.yaml) now carry the converse invariant: a parser must not drop a finding it has no reason to drop. Also Statement.Exact's doc comment, website/docs/parsers.md (_Changed in 0.6._ — adjust if this ships as a 0.5.x patch) and the CHANGELOG.

Known, out of scope

Already on main, not introduced here: for a plain SELECT the parsers read LIMIT/WHERE only at the top level while the fallback counts them anywhere, so e.g. SELECT a FROM (SELECT a FROM t LIMIT 3) s gets select-without-limit under a parser but not the fallback. Resolving it means deciding what a subquery LIMIT bounds; it deserves its own issue.


CodeAnt-AI Description

Preserve SQL findings across dialect parsers and correctly handle limits and set operations

What Changed

  • Unmodeled statements such as CREATE VIEW, EXPLAIN, and DDL retain the fallback parser’s findings instead of losing structural facts.
  • INSERT ... SELECT * and every operand of UNION, INTERSECT, and EXCEPT now contribute relevant findings.
  • A bare OFFSET is no longer treated as a row limit; LIMIT ALL remains recognized as a limit.
  • Parenthesized statements now correctly detect result-level ORDER BY clauses.
  • Added regression coverage and documented the parser behavior.

Impact

✅ Fewer missed select-star findings
✅ Correct warnings for queries with OFFSET but no LIMIT
✅ Consistent findings across dialect and fallback parsers

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

  • Bug Fixes

    • Improved SQL analysis for INSERT … SELECT queries and set operations, preserving findings when statements use forms the parser doesn’t model.
    • Corrected ORDER BY checks for parenthesized statements and subqueries.
    • Updated limit detection so a bare OFFSET is not treated as a row limit; LIMIT ALL remains recognized.
  • Documentation

    • Clarified how analysis handles supported and unmodeled SQL statement forms.

…sLimit by row count

Both dialect parsers blanked every structural field before switching on
the AST and refilled them only for SELECT/INSERT/UPDATE/DELETE, so any
other statement the grammar accepted lost what the fallback had found
and was still marked Exact: CREATE VIEW ... AS SELECT *, CREATE TABLE
... AS SELECT * and EXPLAIN SELECT * all dropped select-star. Fields are
now reset only inside a handled case; anything else returns the
fallback's Statement untouched with Exact=false (#81). Both parsers also
read the row source of INSERT ... SELECT *, and mysqlparser reads a
UNION's ORDER BY/LIMIT as pgparser does instead of treating it as
StmtOther.

pgparser set HasLimit from the presence of a limit node, which the
grammar builds for a bare OFFSET too, silencing select-without-limit and
orderby-without-limit. HasLimit now requires a row count; LIMIT ALL still
counts, matching the fallback (#82).

That exposed a pre-existing parity gap: the fallback read the ORDER BY
of a fully parenthesised statement as a subquery's, so pgparser reported
orderby-without-limit where the fallback did not. The fallback now
unwraps statement-enclosing parentheses first.

Closes #81
Closes #82
A set operation was marked Exact with SelectStar and HasFrom false and
its operands never read, so SELECT * FROM t UNION SELECT * FROM u lost
select-star and select-without-limit under both parsers, and an
INSERT ... SELECT * ... UNION row source lost select-star. pgparser had
done this deliberately; the new mysqlparser Union case copied it.

Each operand is now folded in: a fact is true when any operand has it,
matching how the fallback reads the same text, and only the ORDER BY
that applies to the whole result counts.
Merging operands made HasFrom true for a set operation, but HasWhere and
HasLimit were read only at each operand's top level while the fallback
counts them anywhere. SELECT a FROM t UNION SELECT b FROM (SELECT b FROM
u LIMIT 3) s then reported select-without-limit that the fallback does
not. A set operation now takes both from the fallback; FROM, star,
DISTINCT and OFFSET are still merged from the operands, and the ORDER BY
and LIMIT on the whole result are still read from the AST.
@codeant-ai

codeant-ai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Reviewed your PR 16c4b2f Sep 26, 2026 · 00:03 00:05

@codeant-ai

codeant-ai Bot commented Sep 26, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai

coderabbitai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Repository: KARTIKrocks/sqlguard/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: abc70dbd-fd46-4e80-b92c-1fb5c2d659a5

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: KARTIKrocks/sqlguard/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 3f0ebab4-f7f8-4360-bf71-b586e08d5634

📥 Commits

Reviewing files that changed from the base of the PR and between e4f9d58 and 16c4b2f.

📒 Files selected for processing (13)
  • .codeant/review.json
  • .coderabbit.yaml
  • .greptile/config.json
  • AGENTS.md
  • CHANGELOG.md
  • analyzer/analyzer_test.go
  • analyzer/fallback.go
  • analyzer/statement.go
  • parsers/mysqlparser/mysqlparser.go
  • parsers/mysqlparser/mysqlparser_test.go
  • parsers/pgparser/pgparser.go
  • parsers/pgparser/pgparser_test.go
  • website/docs/parsers.md

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

The fallback analyzer now recognizes ORDER BY on fully parenthesized statements. The MySQL and PostgreSQL parsers preserve fallback facts for unmodeled statements and update structural facts for modeled queries, including INSERT SELECT and set operations. PostgreSQL limit detection excludes bare OFFSET.

Changes

Parser fact preservation

Layer / File(s) Summary
Fallback facts and parenthesized ORDER BY
.coderabbit.yaml, .greptile/config.json, .codeant/review.json, AGENTS.md, analyzer/fallback.go, analyzer/statement.go, analyzer/analyzer_test.go, CHANGELOG.md, website/docs/parsers.md
Parser guidance and documentation describe retaining fallback facts for unmodeled statements. The fallback analyzer unwraps fully enclosing parentheses when checking top-level ORDER BY. Tests cover parenthesized statements, subqueries, and UNION arms.
PostgreSQL structural facts
parsers/pgparser/*, CHANGELOG.md, website/docs/parsers.md
The parser retains fallback facts for unmodeled statements and derives facts for modeled statements, including set-operation operands and INSERT SELECT. HasLimit requires a row count or LIMIT ALL; bare OFFSET does not count. Tests cover parser facts and analyzer findings.
MySQL structural facts
parsers/mysqlparser/*, CHANGELOG.md
The parser retains fallback facts for unmodeled statements and derives facts from INSERT SELECT and set-operation operands. Tests cover structural facts, fallback parity, and analyzer findings.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 16c4b

No actionable risk introduced by this change remains; the PR is mergeable after normal checks.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing dialect parsers from dropping findings reported by the fallback parser.
Linked Issues check ✅ Passed The PR meets the coding requirements in [#81] and [#82]. Both dialect parsers keep the fallback Statement for parsed but unmodeled AST nodes and return with Exact=false. The modeled paths reset only f…
Out of Scope Changes check ✅ Passed The changes stay within the linked issue scope. The MySQL updates apply the same fallback-preservation fix required by [#81]. INSERT ... SELECT, set-operation handling, fallback parenthesis handling, …
Docstring Coverage ✅ Passed Docstring coverage is 81.48% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 27 functions across 7 files. (6 skipped: 6 …
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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

Parentheses open, then close,
ORDER BY finds its place.
UNION arms share their facts,
Bare OFFSET leaves no limit,
Fallback clues remain in sight.

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

@codeant-ai codeant-ai Bot added the size:XL This PR changes 500-999 lines, ignoring generated files label Sep 26, 2026
@codeant-ai

codeant-ai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

🏁 CodeAnt Quality Gate Results

Commit: 7c7772a9
Scan Time: 2026-09-26 00:23:28 UTC

✅ Overall Status: PASSED

Quality Gate Details

Quality Gate Status Details
Secrets ✅ PASSED 0 secrets found
SAST ✅ PASSED No security issues
SCA (Dependencies) ✅ PASSED Rating S: No vulnerabilities

View Full Results

// LIMIT ALL is unbounded as well, but it is an explicit statement that no limit
// is wanted, and the fallback reads it as a LIMIT, so it counts here too.
func hasRowLimit(lim *tree.Limit) bool {
return lim != nil && (lim.Count != nil || lim.LimitAll)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Only return true when the LIMIT expression represents a finite row count; LIMIT ALL is unbounded and must leave HasLimit false so limit-related rules still report it.

Severity Level: Major ⚠️ · 🏷️ Custom_rule

Rule source 📖

.codeant/review.json line 58 (rule "parser-never-breaks-query-path")

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** parsers/pgparser/pgparser.go
**Line:** 130:130
**Comment:**
	*Custom Rule: Only return true when the LIMIT expression represents a finite row count; LIMIT ALL is unbounded and must leave HasLimit false so limit-related rules still report it.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Declining this one. The FallbackParser reads LIMIT ALL as a LIMIT (fbLimitRe matches it), so leaving HasLimit false here would make pgparser report select-without-limit / orderby-without-limit on SELECT a FROM t ORDER BY a LIMIT ALL where the default parser reports nothing, which breaks the "a dialect parser may only remove findings" half of the same rule this suggestion cites. It is also an explicit statement that no limit is wanted. The behaviour is deliberate and pinned: the limit all counts as a limit row in TestParser_ExactStructuralFacts, the SELECT a FROM t ORDER BY a LIMIT ALL parity corpus row, and the hasRowLimit doc 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.

✅ Customized review instruction saved!

Instruction:

In the PostgreSQL parser, treat explicit LIMIT ALL as a limit for structural facts to preserve parity with the fallback parser and avoid reporting limit-related findings.

Applied to:

  • parsers/pgparser/pgparser.go

💡 To manage or update this instruction, visit: CodeAnt AI Settings

Comment on lines 73 to +74
st.HasWhere = n.Where != nil
st.HasLimit = n.Limit != nil
st.HasLimit = hasRowLimit(n.Limit)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Preserve the fallback WHERE result when the DELETE AST lacks a top-level WHERE, rather than overwriting a fallback-detected clause from a nested predicate.

Severity Level: Major ⚠️ · 🏷️ Custom_rule

Rule source 📖

.codeant/review.json line 58 (rule "parser-never-breaks-query-path")

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** parsers/mysqlparser/mysqlparser.go
**Line:** 73:74
**Comment:**
	*Custom Rule: Preserve the fallback WHERE result when the DELETE AST lacks a top-level WHERE, rather than overwriting a fallback-detected clause from a nested predicate.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Valid, but it predates this PR: the DELETE case here is unchanged apart from resetStructural and hasRowLimit, and on main (e4f9d58) mysqlparser already reports delete-without-where on DELETE t FROM t JOIN (SELECT id FROM u WHERE x = 1) s ON s.id = t.id while the fallback does not. It is one instance of a wider gap (the parsers read WHERE/LIMIT at the top level, the fallback anywhere) that needs a decision about which side is right per case, so it is tracked separately in #91 rather than patched here.

Comment on lines +141 to +142
st.HasLimit = hasRowLimit(s.Limit)
st.HasOrderBy = len(s.OrderBy) > 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Merge the AST result with the fallback value instead of replacing it, so LIMIT clauses inside nested queries are not lost when the modeled statement has no top-level LIMIT.

Severity Level: Major ⚠️ · 🏷️ Custom_rule

Rule source 📖

.codeant/review.json line 58 (rule "parser-never-breaks-query-path")

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** parsers/mysqlparser/mysqlparser.go
**Line:** 141:142
**Comment:**
	*Custom Rule: Merge the AST result with the fallback value instead of replacing it, so LIMIT clauses inside nested queries are not lost when the modeled statement has no top-level LIMIT.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Valid, but it predates this PR: this is a refactor of the old SELECT case, and the top-level read of LIMIT is unchanged, so on main (e4f9d58) mysqlparser (and pgparser) already report select-without-limit on SELECT a FROM (SELECT a FROM t LIMIT 3) s while the fallback does not. Merging the fallback value unconditionally would make the parser's HasLimit identical to the fallback's everywhere, and a subquery LIMIT inside IN (...) does not bound the outer result, so the right fix differs per case. Tracked in #91 together with the DELETE/WHERE instance. For set operations this PR already takes HasWhere/HasLimit from the fallback (keepFallbackBounds).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Customized review instruction saved!

Instruction:

Do not unconditionally merge AST-derived bounds with fallback values; preserve statement-level semantics for nested subqueries and set operations, and handle fallback bounds per statement context.

Applied to:

  • parsers/**

💡 To manage or update this instruction, visit: CodeAnt AI Settings

Comment on lines +64 to +69
case *sqlparser.Union:
fb := *st
resetStructural(st)
st.Kind = analyzer.StmtSelect
fillSelect(st, n, true)
keepFallbackBounds(st, &fb)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Exact is set true even though UNION HasWhere and HasLimit come from fallback text, which can include clauses inside nested subqueries.

Assessment: 🟠 Major · 🔁 Occurrence: Sometimes · 🏷️ Api mismatch

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** parsers/mysqlparser/mysqlparser.go
**Line:** 64:69
**Comment:**
	*Api Mismatch: `Exact` is set true even though UNION `HasWhere` and `HasLimit` come from fallback text, which can include clauses inside nested subqueries.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch, fixed in 7c7772a by documenting it rather than dropping Exact. Exact is a single flag and the set operation's Kind, HasFrom, SelectStar, SelectDistinct, OffsetValue and result-level HasOrderBy are still AST-derived, so it stays true. Statement.Exact's doc comment and website/docs/parsers.md now list set-operation HasWhere/HasLimit alongside the other fields that stay lexical under a parser (MaxInListLen, ImplicitCommaJoin, ...), with the reason: the fallback counts them anywhere, and reading them per operand would report findings the fallback does not. No rule reads Exact, so findings are unaffected either way.

… fallback

Statement.Exact promised HasWhere and HasLimit were AST-derived whenever
it was true, but for a set operation both come from the fallback so the
grammar never reports select-without-limit the default parser does not.
List that alongside the other fields that stay lexical under a parser.
@KARTIKrocks
KARTIKrocks merged commit 0edb9c5 into main Sep 26, 2026
33 checks passed
@KARTIKrocks
KARTIKrocks deleted the fix/parser-structural-fields branch September 26, 2026 00:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

1 participant