Skip to content

fix(parser): support three-part identifiers and parenthesized join groups (#521) - #585

Merged
ajitpratap0 merged 3 commits into
mainfrom
fix/mysql-three-part-ident-paren-joins
Oct 13, 2026
Merged

ajitpratap0 merged 3 commits into
mainfrom
fix/mysql-three-part-ident-paren-joins

Conversation

@ajitpratap0

Copy link
Copy Markdown
Owner

Summary

#521 [BUG] Valid MySQL query fails on three-part identifiers and parenthesized joins

Fixes #521. Column references with more than two parts now parse in every dialect: db.table.col, db.t.* and catalog.schema.t.col. The extra qualifiers are folded into Identifier.Table as a dotted string, mirroring TableReference.Name, so SQL() reproduces the reference without any AST change. A parenthesized join group at the start of FROM, as in MySQL SHOW CREATE VIEW output, is now parsed recursively with a depth guard and its joins are flattened into the statement's join chain, so the AST and SQL() are identical to the flat query. Group shapes that flattening would get wrong (comma lists after or inside a group) are rejected with E4001. Verification: the new regression tests fail on the parent commit and pass on this branch. Differential testing of about 3,866 corpus statements in 9 dialects showed no changed output for any input that parsed before. In an alternating benchstat run, parser allocs/op and B/op are unchanged and ns/op is flat (geomean -0.86%). The race suite, task check and golangci-lint all pass.

Commits (1), one per issue:

5 files changed, 509 insertions(+), 12 deletions(-)

Test Plan

  • Independent validator ran the repository race gate (CGO_ENABLED=1 go test -race -timeout 120s $(go list ./... | grep -v /cbinding)), go vet, and golangci-lint on this branch.
  • Each change was reviewed by an independent reviewer with access to the code and benchmarks before validation.
  • Benchmarks compared with benchstat -count=6 before/after on the same machine; allocs/op and B/op are the deterministic signal, ns/op is directional on a loaded machine.

#521 benchstat

I built test binaries from a git-archive of the parent commit (142bff2) and from HEAD (18c73f2), then ran them alternately (6 rounds, count=1 each, Apple M4) and compared with benchstat. allocs/op and B/op are identical in every sample: SimpleSelect 9 / 1.656Ki, ComplexSelect 22 / 5.408Ki, Insert 16 / 2.039Ki, Update 13 / 1.625Ki, Delete 9 / 1.359Ki. ns/op shows no regression: SimpleSelect 407.8n→414.9n (~, p=0.093), ComplexSelect 1.381µ→1.379µ (~), Insert 516.5n→504.6n (~), Update 404.0n→396.3n (-1.91%, p=0.015), Delete 307.7n→302.8n (~), geomean -0.86%. The +34% to +65% ns/op in the implementer's report was machine noise.
Bug reproduction: I copied the new mysql_issue521_test.go and the updated qualified_identifier_test.go onto the parent archive. TestIssue521_ExactReport (mysql, mariadb), ParenthesizedJoinEquivalentToFlat (all dialects), RoundTrip, ExtractTables, Rejections (comma/nesting/trailing-dot) and the new QualifiedIdentifier_Shape rows all FAIL there and pass on HEAD.
CLI check: I built cmd/gosqlx from the worktree. `gosqlx validate --dialect mysql` on the verbatim issue query (taken from the issue body via gh) prints "Valid SQL". `gosqlx format` renders `sakila.city.city AS city` and a flat `FROM sakila.customer AS cu INNER JOIN sakila.address AS a ... INNER JOIN sakila.city ... INNER JOIN sakila.country ...`.
Differential: I extracted 3,837 SQL strings from the repo's tests and .sql files, added 29 hand-written edge cases, and ran all 3,866 through the parent and HEAD in 9 dialects (34,794 parses). SQL() and FormatAST(Compact) were compared. Result: 0 OK→OK output changes, 0 OK→ERR, 0 panics. There were 155 ERR→OK, all multi-part identifiers or parenthesized join groups, and 88 ERR→ERR where only the error message changed.

#521 race gate: PASS. CGO_ENABLED=1 go test -race -timeout 120s $(go list ./... | grep -v /cbinding) exited 0: 44 packages ok, no FAIL, no DATA RACE. Many results were cached, so I re-ran -race -count=1 on pkg/sql/parser (3.9s), pkg/gosqlx and pkg/formatter, and all were ok. task check (fmt, vet, lint, test:race) also exited 0, and git status --porcelain was still empty afterwards.

Breaking Changes

  • [BUG] Valid MySQL query fails on three-part identifiers and parenthesized joins #521: None to exported signatures or AST types. ast.Identifier gets no new field; the alternative of adding a Schema field was rejected because it would break unkeyed literals and every consumer. Three or more qualifiers are stored in the existing Table field as a dotted string. That matches TableReference.Name ("db.table") and the existing Identifier.SQL() handling, since identifierNeedsQuoting allows '.'. The only signature change is to the unexported parser method parseJoinClauses, which gains a joins []ast.JoinClause argument (one caller). Observable behaviour: SQL that used to fail (multi-part column references; left-nested parenthesized joins in FROM) now parses in all dialects. A parenthesized join group renders flattened, and its AST is identical to the flat join chain. Some invalid inputs get more specific error messages, including new E4001 unsupported-feature errors for comma lists combined with parenthesized joins. Nothing that parsed before parses differently.

Notes

Fixes #521
Part of #566

🤖 Generated with Claude Code

https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve

@vercel

vercel Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
gosqlx-website Ready Ready Preview Oct 13, 2026 8:18am UTC

@ajitpratap0 ajitpratap0 added bug Something isn't working parser labels Oct 11, 2026
@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Claude finished @ajitpratap0's task in 22s —— View job


Review of PR #585: three-part identifiers and parenthesized join groups (#521)

I reviewed the diff by reading it. I did not build the branch or run the tests, so I'm relying on the test results reported in the PR description.

Verdict: looks good. I found nothing blocking.

What I checked

  • Three-part identifiers (expressions_literal.go): The two-part path is unchanged, and the extra loop runs only when a third . follows. Folding the qualifiers into Identifier.Table as a dotted string matches TableReference.Name, so SQL() round-trips without an AST change. The db.t.* case correctly rejects a trailing . after *. Pulling isQualifiedNamePart() out as a helper removes duplicated logic.
  • Parenthesized joins (select_clauses.go): Flattening is semantically correct because joins associate to the left, so ((a JOIN b) JOIN c) JOIN d is the same as the flat chain. The depth guard uses the existing MaxRecursionDepth with a deferred decrement, so it can't leak on error paths. Rejecting comma lists after or inside a group with E4001 is the right call, since hoisting the joins would silently change which table they bind to. isParenthesizedJoinStart peeks only at identifier, double-quoted string, or (, so (SELECT ...) derived tables keep their existing path and error messages.
  • Performance: No new allocations on the hot path, which agrees with the benchstat numbers in the description. The unexported parseJoinClauses signature change has a single caller.

Minor suggestions (non-blocking)

  1. Backtick-quoted first table. isParenthesizedJoinStart doesn't list a dedicated backtick-quoted token type. The MySQL test using `sakila`.`customer` passes, so these tokens presumably lex as identifiers. A short comment saying so would help, and so would a test with a quoted name directly after (, such as FROM (`a` JOIN `b` ON ...).
  2. Alias after a group. FROM (a JOIN b ON ...) AS x is not valid MySQL for a joined table, but I'd add a negative test pinning the resulting error so it doesn't change by accident. The same goes for FROM (a) followed directly by a comma, which should still parse.
  3. Dotted Table value. Consumers that read Identifier.Table as a single qualifier, such as the linter, LSP and ExtractTables, will now see "db.t". The PR notes this is intentional and the ExtractTables test covers it. A one-line doc comment on the Identifier.Table field would help future readers.
  4. Changelog. CHANGELOG.md is deliberately left for a consolidated follow-up PR, per the description. Please make sure that follow-up covers [BUG] Valid MySQL query fails on three-part identifiers and parenthesized joins #521.

Test coverage: The new tests are thorough. They cover the exact issue query on MySQL and MariaDB, equivalence with the flat query across all dialects, round-trip, ExtractTables, and the rejection cases. The description also reports differential testing with no change to any input that parsed before.
| Branch: fix/mysql-three-part-ident-paren-joins

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Oct 11, 2026
@ajitpratap0
ajitpratap0 force-pushed the fix/mysql-three-part-ident-paren-joins branch from 18c73f2 to d76351f Compare October 13, 2026 07:13
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Claude finished @ajitpratap0's task in 39s —— View job


Review of PR #585: three-part identifiers and parenthesized join groups (#521)

I reviewed the diff by reading it, including the newer hardening commit. I did not build the branch or run the tests, so I'm relying on the results reported in the PR description.

Verdict: looks good. I found nothing blocking, and one item below is worth checking before merge.

Addressed since the earlier review

  • Identifier.Table now has a doc comment that explains the dotted form and the quoted-dot ambiguity (pkg/sql/ast/ast.go).
  • An alias or ClickHouse FINAL after a group is rejected with E4001. Silently attaching either one to the first table would have been wrong.
  • parseMultiPartColumnRef caps references at 5 parts, collects them in a fixed array, and joins them once. Cost stays linear and the two-part path is unchanged.
  • ExtractColumnsQualified splits the qualifier at the last dot, so db.t.col agrees with ExtractTablesQualified.
  • SQL() quotes each dotted part separately through writeDottedIdentifier and qualifierNeedsQuoting. Leading, trailing and doubled dots are handled.

Worth checking

  1. Possible regression for (( in FROM. isParenthesizedJoinStart returns true for ( followed by (. A query like FROM ((SELECT 1) UNION (SELECT 2)) t, or any other parenthesized set operation used as a derived table, now goes through parseParenthesizedJoin. The inner (SELECT ...) parses as a derived table, but the next token is UNION rather than a join keyword or ). That would produce "expected ) or JOIN", where it previously took the derived-table path, if that path accepted this input. I couldn't run it here. The differential corpus may not contain this shape. Please run it against main and add a test either way. If it parsed before, one fix is to fall back (for example by backtracking or by peeking past the nested parens) when the group isn't followed by a join or ).
  2. Test the quoted first table. Add FROM (`a` JOIN `b` ON ...) to confirm that backtick names lex as TokenTypeIdentifier or DoubleQuotedString after (. Also add one comment on the switch in isParenthesizedJoinStart saying so.

Minor notes (non-blocking)

Performance and security: No new allocations on the hot path. Recursion is bounded by MaxRecursionDepth with a deferred decrement, and the part count is capped. I saw no security concerns.

Test coverage: Thorough. It covers the exact issue query on MySQL and MariaDB, equivalence with the flat query in all dialects, round-trip, extraction, and the rejection cases. Item 1 above is the main gap.
| Branch: fix/mysql-three-part-ident-paren-joins

Ajit Pratap Singh and others added 2 commits October 13, 2026 13:13
…n groups (#521)

MySQL view definitions (SHOW CREATE VIEW output such as sakila.customer_list)
failed to parse for two reasons, in every dialect:

- A column reference with three or more parts (`sakila`.`city`.`city`,
  db.public.users.id) stopped after the second part with
  "E2002 expected FROM, semicolon, or end of statement, got PERIOD".
  parsePrimaryExpression now folds every extra qualifier into
  Identifier.Table as a dotted string (Table "sakila.city", Name "city"),
  mirroring TableReference.Name. Identifier.SQL() already prints dots
  unquoted, so no AST change is needed. The two-part path is untouched and
  still makes a single allocation.
- A leading parenthesized join group (FROM ((a JOIN b ON ..) JOIN c ON ..)
  JOIN d ON ..) was always treated as a derived table and failed with
  "expected SELECT in derived table, got LPAREN". parseFromClause now
  recognises "(" followed by an identifier or "(" as a joined table, parses
  it recursively (depth-guarded) and flattens its joins into the statement's
  join chain. Joins associate to the left, so the AST is identical to the
  equivalent flat query and SQL()/formatter output round-trips. Cases that
  flattening would get wrong (comma list after or inside a group) are
  rejected with E4001, and a parenthesized join on the right side of JOIN
  keeps its current error.

Parser benchmarks show no allocs/op or B/op change. docs/SQL_COMPATIBILITY.md
gains rows for both constructs.

Fixes #521

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
… follow-up)

- Identifier.SQL() quotes each dot-separated part of Table on its own, so
  `my-db`.`t`.`c` renders as "my-db".t.c instead of the single identifier
  "my-db.t".c. A dot in Table is always a part separator (documented on
  Identifier.Table); the SQL() fast path agrees with the buffered path for
  empty parts.
- Multi-part column references collect their parts and join once, and more
  than 5 parts (SQL Server server.db.schema.table.column) is rejected with
  E2004. A 50k-part chain now fails in milliseconds instead of running
  quadratically.
- ExtractColumnsQualified splits a dotted qualifier at its last dot, so
  db.t.col gives {Schema: db, Table: t, Name: col}, matching
  ExtractTablesQualified's {Schema: db, Name: t}.
- An alias or ClickHouse FINAL after a parenthesized join group is rejected
  with E4001 instead of a misleading "expected statement" error or FINAL
  silently moving onto the group's first table.
- Qualifier errors no longer pass hint text as the SQL source, which printed
  a bogus context snippet (new branch and pre-existing two-part branch).
- Tests for backtick (MySQL) and bracket (SQL Server) names directly after
  "(" in a join group.

Part of #521

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
@ajitpratap0

Copy link
Copy Markdown
Owner Author

Pre-merge review follow-ups (merge train), in the latest commits:

  • Quoted multi-part names now round-trip: my-db`.`t`.`c renders as "my-db".t.c instead of the single identifier "my-db.t".c. A quoted qualifier containing both a dot and an unsafe character is inherently ambiguous in the dotted Identifier.Table form; documented on the field.
  • Column references are capped at 5 parts and joined once (the 1 MB dotted chain went from ~10 s to ~10 ms rejection).
  • ExtractColumnsQualified splits db.t.col into Schema/Table/Name consistently with ExtractTablesQualified.
  • An alias or ClickHouse FINAL after a parenthesized join group is rejected with E4001 instead of a confusing error (FINAL was previously applied to the first table only).
  • Error snippets no longer print the hint text as source.
  • Checked the FROM ((SELECT ...) UNION (SELECT ...)) t concern: those shapes fail on main too ("expected SELECT in derived table"); this PR newly parses ((SELECT ...) t JOIN y ...) and ((a JOIN b ...)), and only changes error text for the rest. Parenthesized set operations as derived tables are tracked as a follow-up.
  • Rebased onto perf(tokenizer): Oracle bind variables, substring token values, zero-copy quoted literals, dialect pooling (#520, #546, #548, #550) #595 (shared files merged cleanly); race tests green.

@ajitpratap0
ajitpratap0 force-pushed the fix/mysql-three-part-ident-paren-joins branch from d76351f to ca7c41c Compare October 13, 2026 07:43
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Claude finished @ajitpratap0's task in 26s —— View job


Review of PR #585: three-part identifiers and parenthesized join groups (#521)

I reviewed this by reading the code. I tried to run probe tests for the open items below, but the sandbox denied go test, so nothing here has been run. I'm relying on the test results reported in the PR description.

Verdict: looks good. I found nothing blocking. One item is still open from the last review.

Open item: (( in FROM

  • isParenthesizedJoinStart (pkg/sql/parser/select_clauses.go:142) returns true for ( followed by (.
  • A parenthesized set operation used as a derived table, such as FROM ((SELECT 1) UNION (SELECT 2)) t, would now go to parseParenthesizedJoin. The inner (SELECT ...) parses as a derived table, and the next token is UNION, not a join keyword or ).
  • If main accepted that shape, this is a regression. The differential corpus may not contain it.
  • Please run this on main and on the branch, and add a test either way:
    • SELECT * FROM ((SELECT 1) UNION (SELECT 2)) t
    • If it parsed before, fall back to the derived-table path when the group isn't followed by a join or ).
    • If it never parsed, pin the current error message in a test.

Still to add

  • A test for a backtick-quoted first table directly after (, such as FROM (`a` JOIN `b` ON ...). The switch in isParenthesizedJoinStart lists only TokenTypeIdentifier, TokenTypeDoubleQuotedString and TokenTypeLParen. A one-line comment saying which type backtick names lex as would help.

Addressed since the earlier review

  • Identifier.Table now has a doc comment.
  • An alias or FINAL after a group is rejected with E4001.
  • Multi-part references are capped at 5 parts.
  • ExtractColumnsQualified and SQL() handle dotted qualifiers consistently.

Notes

Join groups and derived tables inside a group parse; a parenthesized set
operation used as a derived table was unsupported before #521 and still
returns an error rather than a wrong tree. Pin both so a later change to
either is deliberate.

Part of #521

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Claude finished @ajitpratap0's task in 15s —— View job


Review of PR #585: three-part identifiers and parenthesized join groups (#521)

I reviewed this by reading the code. I did not build the branch or run the tests, so I'm relying on the results in the PR description.

Verdict: looks good. I found nothing blocking.

Both open items from the earlier reviews are resolved

  • (( in FROM: Your 2026-10-13 comment says FROM ((SELECT ...) UNION (SELECT ...)) t already fails on main with "expected SELECT in derived table", so this isn't a regression. I couldn't confirm that by running it. Two small suggestions:
    • Pin the current error for that shape in a test, so a later follow-up that adds parenthesized set operations as derived tables changes it on purpose.
    • Link the follow-up issue from the PR.
  • Backtick-quoted first table: This is now covered. pkg/sql/parser/mysql_issue521_review_test.go:213 tests FROM (`a` JOIN `b` ON ...) JOIN `c` .... That is enough evidence that backtick names lex as TokenTypeIdentifier or TokenTypeDoubleQuotedString.

Optional

  • Add a one-line comment on the switch in isParenthesizedJoinStart (select_clauses.go:146) saying that backtick names arrive as one of those two token types. It's cheap and saves the next reader the same question.

Notes for the consolidated changelog PR (#566)

Performance and security

  • Recursion is bounded by MaxRecursionDepth with a deferred decrement.
  • Multi-part references are capped at 5 parts, so the long-dotted-chain rejection stays cheap.
  • I saw no new hot-path allocations and no security concerns.

Test coverage: Thorough. It covers the exact issue query on MySQL and MariaDB, flat-query equivalence, round-trip, extraction, quoted names, and the rejection cases.
| Branch: fix/mysql-three-part-ident-paren-joins

@ajitpratap0
ajitpratap0 merged commit 2304418 into main Oct 13, 2026
28 checks passed
ajitpratap0 added a commit that referenced this pull request Oct 14, 2026
#613)

parseQualifiedName built dotted names with `name = name + "." + part`, so
time and allocation grew quadratically with the number of parts: a ~100 KB
`a.a.a...` chain in FROM, JOIN, INSERT, UPDATE, DELETE, CREATE/DROP/TRUNCATE,
views or indexes cost hundreds of MB to GBs.

The parts are now collected in a fixed stack array (it does not escape) and
joined once with strings.Join, and names with more than
maxQualifiedNameParts (5, the same limit as column references from #585) are
rejected with E2004 "qualified name has more than 5 parts". Names with 1 to 5
parts produce identical strings and SQL() output. The FROM path surfaces the
E2004 directly; the JOIN, DML and DDL callers keep discarding or re-wrapping
the error as E2002 "expected table name", so those inputs are rejected fast
but with the callers' existing message (callers intentionally untouched to
keep the diff localized).

Benchmarks (benchstat, Apple M4, machine heavily loaded by concurrent runs so
ns/op is noisy; allocs and B/op are exact):
  ParserSimpleSelect/ComplexSelect/Insert/Update/Delete  allocs and B/op unchanged
  QualifiedTableName/1part     8 allocs, 2.128Ki  (unchanged)
  QualifiedTableName/3part     10 -> 9 allocs     (-10%)
  QualifiedTableName/5part     12 -> 9 allocs     (-25%)
  QualifiedTableName/chain4KB  2068 -> 30 allocs (-98.6%), 5032Ki -> 664Ki (-86.8%)

Fixes #613

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
ajitpratap0 added a commit that referenced this pull request Oct 14, 2026
…errors (#616)

Several parser error builders were handed a human-readable sentence as
their `sql` argument. That argument becomes Error.Context.SQL, so Error()
printed the sentence as "line 1" of the query with a caret at the real
column. The parser does not hold the source SQL, so these sites now pass
"" (no snippet, as in #585) and carry the sentence, verbatim, as the hint.
Code, message and location are unchanged.

ExpectedTokenError (E2002): FROM, SAMPLE (x5), PREWHERE, WHERE and the
SELECT column list guard.
InvalidSyntaxError (E2004), same defect: CAST type params, INTERVAL, empty
array subscript, ::TYPE(...) params, and the two strict-mode empty
statement checks.

Adds a table-driven regression test plus a go/ast guard that fails if a
non-test parser file passes a non-empty string literal as a pkg/errors
builder's `sql` parameter. Attaching the real source text to parser errors
is left as a follow-up.

Fixes #616

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
ajitpratap0 added a commit that referenced this pull request Oct 14, 2026
…ntext (#613, #616) (#627)

* perf(parser): join qualified object names once and cap them at 5 parts (#613)

parseQualifiedName built dotted names with `name = name + "." + part`, so
time and allocation grew quadratically with the number of parts: a ~100 KB
`a.a.a...` chain in FROM, JOIN, INSERT, UPDATE, DELETE, CREATE/DROP/TRUNCATE,
views or indexes cost hundreds of MB to GBs.

The parts are now collected in a fixed stack array (it does not escape) and
joined once with strings.Join, and names with more than
maxQualifiedNameParts (5, the same limit as column references from #585) are
rejected with E2004 "qualified name has more than 5 parts". Names with 1 to 5
parts produce identical strings and SQL() output. The FROM path surfaces the
E2004 directly; the JOIN, DML and DDL callers keep discarding or re-wrapping
the error as E2002 "expected table name", so those inputs are rejected fast
but with the callers' existing message (callers intentionally untouched to
keep the diff localized).

Benchmarks (benchstat, Apple M4, machine heavily loaded by concurrent runs so
ns/op is noisy; allocs and B/op are exact):
  ParserSimpleSelect/ComplexSelect/Insert/Update/Delete  allocs and B/op unchanged
  QualifiedTableName/1part     8 allocs, 2.128Ki  (unchanged)
  QualifiedTableName/3part     10 -> 9 allocs     (-10%)
  QualifiedTableName/5part     12 -> 9 allocs     (-25%)
  QualifiedTableName/chain4KB  2068 -> 30 allocs (-98.6%), 5032Ki -> 664Ki (-86.8%)

Fixes #613

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve

* fix(parser): stop passing hint text as the SQL source in E2002/E2004 errors (#616)

Several parser error builders were handed a human-readable sentence as
their `sql` argument. That argument becomes Error.Context.SQL, so Error()
printed the sentence as "line 1" of the query with a caret at the real
column. The parser does not hold the source SQL, so these sites now pass
"" (no snippet, as in #585) and carry the sentence, verbatim, as the hint.
Code, message and location are unchanged.

ExpectedTokenError (E2002): FROM, SAMPLE (x5), PREWHERE, WHERE and the
SELECT column list guard.
InvalidSyntaxError (E2004), same defect: CAST type params, INTERVAL, empty
array subscript, ::TYPE(...) params, and the two strict-mode empty
statement checks.

Adds a table-driven regression test plus a go/ast guard that fails if a
non-test parser file passes a non-empty string literal as a pkg/errors
builder's `sql` parameter. Attaching the real source text to parser errors
is left as a follow-up.

Fixes #616

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve

* fix(parser): address adversarial review follow-ups for #627

627-1: the Snowflake permissive CREATE <kind> stub (STAGE, SEQUENCE, ...)
discarded the parseQualifiedName error, so a 6+ part name parsed
successfully with only its tail in RawSQL ("CREATE SEQUENCE f" for
"CREATE SEQUENCE a.b.c.d.e.f"). The stub now returns the E2004 part-limit
error; other name errors are still tolerated as before.

627-2: every non-FROM caller of parseQualifiedName (JOIN, INSERT, UPDATE,
DELETE, CREATE TABLE/VIEW/MATERIALIZED VIEW/INDEX, ClickHouse MV TO,
REFRESH MATERIALIZED VIEW, DROP, TRUNCATE, MySQL SHOW/DESCRIBE/REPLACE,
Snowflake USE) replaced the E2004 with "expected table name, got
IDENTIFIER", pointing at a valid identifier. The part-limit error now
carries an errQualifiedNameTooLong cause, and callers pass it through
unchanged via isQualifiedNameTooLong/qualifiedNameError. Tests now assert
the E2004 message on every path.

627-3: the BETWEEN lower/upper, LIKE, REGEXP and IN-subquery wrappers
passed the current token's text as the error's sql argument, printing a
fake "1 | )" source line. They now pass "" and attach the operand error
with WithCause. The #616 static guard now flags any sql argument other
than the literal "" (allowlisting validate.go's real-source
string(input)), not just non-empty string literals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ajitpratap0 added a commit that referenced this pull request Oct 14, 2026
…resh performance baselines (#566)

CHANGELOG [Unreleased] now covers every PR merged since v1.14.0, with a
Behavior changes section for the observable changes in #597, #595, #584,
#585, #588, #581, #582/#570, #517/#518 and #511, plus Added, Changed,
Fixed, Performance, Security and Internal entries. The two existing
entries (#526 and the tokenizer-pool dialect fix) are kept verbatim.

performance_baselines.json is re-measured on an Apple M4 with
BenchmarkPerformanceBaseline -count=10 -benchmem (median). Baselines are
set at about 2.5x the M4 median rather than 1.5x: the CI Benchmark job on
ubuntu-latest measures these queries about 1.5-3.2x slower than M4 (last
9 Tests runs on main, 0715bb9..61ca6c8), so 1.5x with the unchanged
tolerances would fail CI's slowest runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
ajitpratap0 added a commit that referenced this pull request Oct 14, 2026
…xes; refresh performance baselines (#566) (#598)

* docs: consolidate CHANGELOG for the perf audit and batch-2 fixes; refresh performance baselines (#566)

CHANGELOG [Unreleased] now covers every PR merged since v1.14.0, with a
Behavior changes section for the observable changes in #597, #595, #584,
#585, #588, #581, #582/#570, #517/#518 and #511, plus Added, Changed,
Fixed, Performance, Security and Internal entries. The two existing
entries (#526 and the tokenizer-pool dialect fix) are kept verbatim.

performance_baselines.json is re-measured on an Apple M4 with
BenchmarkPerformanceBaseline -count=10 -benchmem (median). Baselines are
set at about 2.5x the M4 median rather than 1.5x: the CI Benchmark job on
ubuntu-latest measures these queries about 1.5-3.2x slower than M4 (last
9 Tests runs on main, 0715bb9..61ca6c8), so 1.5x with the unchanged
tolerances would fail CI's slowest runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve

* docs(changelog): add batch-3 follow-up fixes (#599-#620)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve

* docs(changelog): record #632 render-injection hardening and #624 quote guards

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve

* docs(changelog): correct batch-3 entries after review; record measured costs

- #628: SQLite still rejects count(*) [cnt]; only SQL Server keeps it.
- #631: bare FORMAT / FORMAT <name> rules apply after SETTINGS.
- Performance: record the measured costs of the batch-3 correctness fixes
  (Identifier and TableReference growth, quoting in SQL(), lineStarts
  reallocation, sorted Extract results).
- #632: note the lıke, WITH CHECK OPTION and MINUS rendering changes.
- Fix the [Unreleased] compare link (v1.14.0...HEAD) and add [1.14.0].
- Keep each section a single tight list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve

* docs(changelog): describe the release as source-compatible with runtime behaviour changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 9f90905e Deployed Oct 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ast AST node changes bug Something isn't working documentation Improvements or additions to documentation parser

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Valid MySQL query fails on three-part identifiers and parenthesized joins

1 participant