Skip to content

[Bug]: dialect parsers read WHERE/LIMIT at the top level only, the fallback anywhere — parsers add findings #91

Description

@KARTIKrocks

What happened

Split out of #90, where it was left out of scope (it predates that PR; output below is identical on main at e4f9d58).

For a plain SELECT / DELETE / UPDATE, both dialect parsers set HasWhere and HasLimit from the statement's top-level clause only. The FallbackParser matches WHERE / LIMIT anywhere in the sanitized text, including inside subqueries. Whenever the only WHERE or LIMIT sits in a subquery, the parser reports a finding the fallback does not — the direction the parity invariant forbids (TestParser_NeverAddsFindingTheFallbackDoesNot; no corpus row covers it).

SELECT a FROM (SELECT a FROM t LIMIT 3) s
  fallback: []    pgparser/mysqlparser: [select-without-limit]

SELECT a FROM (SELECT a FROM t WHERE x = 1) s
  fallback: []    pgparser: [select-without-limit]

SELECT a FROM t WHERE id IN (SELECT id FROM u LIMIT 1) ORDER BY a
  fallback: []    pgparser: [orderby-without-limit]

DELETE t FROM t JOIN (SELECT id FROM u WHERE x = 1) s ON s.id = t.id
  fallback: []    mysqlparser: [delete-without-where]

Set operations are already covered: #90 takes a UNION's HasWhere/HasLimit from the fallback for exactly this reason.

Also raised by CodeAnt on #90 (mysqlparser.go DELETE and fillSelect).

Expected behavior

A dialect parser never reports a finding the fallback does not.

Which side is right differs by case, so this needs a decision rather than a one-line fix:

  • A LIMIT / WHERE in a FROM-clause derived table does bound / filter the result — the parser findings are false positives, the fallback is right.
  • A LIMIT in an IN (...) or scalar subquery bounds nothing outer — the parser is right and the fallback has a false negative.

Options:

  1. Take HasWhere/HasLimit from the fallback everywhere (as set operations now do). Parity-safe, but the parser then adds nothing for those two fields.
  2. Have the parsers count a LIMIT/WHERE inside a derived table in FROM (correct semantics there), and teach the fallback the IN/scalar-subquery case so both agree — per the AGENTS.md rule that when a parser learns something, the fallback learns it too.

SQL

SELECT a FROM (SELECT a FROM t LIMIT 3) s;
SELECT a FROM (SELECT a FROM t WHERE x = 1) s;
SELECT a FROM t WHERE id IN (SELECT id FROM u LIMIT 1) ORDER BY a;
DELETE t FROM t JOIN (SELECT id FROM u WHERE x = 1) s ON s.id = t.id;

Minimal Go reproduction

a := analyzer.Default().WithParser(pgparser.New())
fmt.Println(analyzer.Default().Analyze("SELECT a FROM (SELECT a FROM t LIMIT 3) s")) // []
fmt.Println(a.Analyze("SELECT a FROM (SELECT a FROM t LIMIT 3) s"))                  // [select-without-limit]

Entry surface

Analyzer API (direct)

Parser in use

parsers/pgparser

sqlguard version

e4f9d58 (main, post-0.5.0); unchanged by #90

Go version

go1.27.1 linux/amd64

Database and dialect

No response

Additional context

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions