Skip to content

fix(dialect): complete real-world SQL audit and preserve round trips - #858

Closed
ajitpratap0 wants to merge 103 commits into
mainfrom
test/dialect-audit-2026-10
Closed

ajitpratap0 wants to merge 103 commits into
mainfrom
test/dialect-audit-2026-10

Conversation

@ajitpratap0

@ajitpratap0 ajitpratap0 commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

Audit 2,788 production-style SQL statements across eight dialects and preserve their parse/format round trips with zero listed corpus failures. Retain SQL Server GO batch boundaries and repeat counts, so formatting a procedure, view or trigger followed by another batch does not absorb that batch into the object definition.

Complete the render and performance reviews: fix raw JSON_TABLE/XMLTABLE literal quoting and dialect-sensitive nested hint placement; verify scratch-buffer ownership with concurrent retained ASTs; align the audit harness with the corpus detector. Keep SQL Server transpilation batch-aware, reject GO scripts targeting other dialects, and prevent CLI validation from accepting invalid GO counts through its splitter fallback. Update the unreleased changelog and dialect compatibility results.

Test Plan

  • task check: PASS, including fmt, vet, lint (0 issues) and race tests.
  • task test:cbinding: PASS with CGO and race detection.
  • All eight dialect harness runs: 2,788/2,788 pass; known_failures.txt unchanged.
  • Before/after perf output snapshot: all 2,788 FormatAST strings byte-identical between b609a11 and 78362f5.
  • Prefix truncation: 266,281 byte prefixes, zero parse/render panics, both immutable merged HEAD and final source.
  • Independent source review: no remaining actionable findings.
  • Integrate main 326fdd1: preserve BigQuery PIVOT/UNPIVOT alongside Oracle XML, dynamic IN lists and source aliases; keep one owner per aggregate expression. Both parent test suites and mixed-feature round trips pass under race detection.
  • Re-run task check, task test:cbinding, documentation drift checks and the full 266,281-prefix sweep after base integration: all pass, zero panics. Three interleaved before/after benchmark rounds show unchanged synthetic allocation counts and effectively stable corpus allocations.
  • TestPerformanceRegression: PASS; performance baseline thresholds unchanged (metadata matches main).
  • Five interleaved benchmark rounds: fixed 2,725-statement corpus parse bytes -62.2% and parse+format bytes -56.6% versus 6c81d7c; median times -9.2% and -7.9%.
  • Synthetic parser latency versus origin/main 5502fc3 regresses 24.4% (SimpleSelect) and 29.5% (ComplexSelect). AST growth explains the extra allocation bytes; this change does not claim a general latency improvement.
  • UNION scaling guard: native and race-plus-atomic-coverage runs pass; a recursive prefix-copying negative control fails the allocation budget at 1,024 UNIONs. The full 80,000-UNION exact-output assertion is retained, with a separate size-scaled benchmark.
  • Remote CI at 0db001a: 43 checks PASS, including Vercel; two package-publishing jobs skipped as expected for a PR.

Breaking Changes

  • With no dialect selected, unknown backslash escapes now return an error instead of silently changing the string value.
  • SQL Server AST.Statements and recovery results now include BatchSeparator nodes for GO; clients enumerating statements must account for them.
  • Transpiling a GO script to a target other than SQL Server now returns an error because its batch scopes and repeat counts cannot be represented there.
  • Rare SELECT dialect fields move behind optional SelectStatement.Extras. Those fields were introduced only on this unreleased branch; handle nil on reads and allocate before writes.
  • Additive AST literal prefix/escape and identifier quote metadata preserves source meaning and target-dialect transpilation.

Known low-priority follow-ups remain: deep (>100-branch) T-SQL IF/ELSE-IF chains, malformed-block parser/splitter fallback differences, and possible further AST layout optimization. No database execution-equivalence claim is made from the syntax/round-trip corpus.

Ajit Pratap Singh and others added 30 commits October 22, 2026 17:46
EXPLAIN SELECT ... discarded the query and formatted as DESCRIBE "SELECT";
EXPLAIN ANALYZE|VERBOSE <stmt> split into DESCRIBE ANALYZE; <stmt>, so
formatting turned a non-executing EXPLAIN into the executable statement.

Add ast.ExplainStatement {Keyword, Options, Statement} covering EXPLAIN,
DESCRIBE and DESC with options (ANALYZE, (FORMAT JSON), QUERY PLAN,
FORMAT=JSON, PIPELINE, PLAN FOR), render it in the formatter, and let the
CLI format it instead of refusing it.

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

Adds testdata/realworld/<dialect>/ corpora (PostgreSQL, MySQL, MariaDB,
SQL Server, Oracle, SQLite, Snowflake, ClickHouse) of production-style app,
reporting, ETL, DDL and dialect-specific SQL, plus:

- TestRealWorldCorpus: split -> parse -> format -> reparse -> idempotency
  -> dropped-word check for every statement, ratcheted against
  testdata/realworld/known_failures.txt (-update-known-failures regenerates)
- scripts/dialect-audit: CLI harness for the same pipeline

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
…IME ZONE, COLLATE, bitwise, ESCAPE)

Close the operator and predicate gaps found by the real-world corpus.

Parser
- IS [NOT] TRUE/FALSE/UNKNOWN, IS [NOT] DISTINCT FROM, SQLite "a IS [NOT] expr",
  PostgreSQL/SQLite ISNULL/NOTNULL (normalised to IS [NOT] NULL).
- = ANY/ALL/SOME over arrays and placeholders (new AnyAllExpression); the
  subquery forms keep AnyExpression/AllExpression.
- Postfix AT TIME ZONE and COLLATE (new AtTimeZoneExpression and
  CollateExpression), binding tighter than arithmetic; COLLATE on CREATE
  INDEX columns fills IndexColumn.Collate.
- Binary operators @@, &&, &, |, #, <<, >>, <->, <=>, == via a precedence
  climbing level between comparison and ||, unary ~ and MySQL !, MySQL
  DIV/MOD/XOR/SOUNDS LIKE/&&/:=, SQLite GLOB/MATCH, SIMILAR TO, NOT REGEXP/
  RLIKE. Word operators are gated to the dialect that defines them. <=>, <<,
  >>, <->, == are read from adjacent tokens until the tokenizer emits them.
- LIKE/ILIKE/SIMILAR TO ... ESCAPE (BinaryExpression.Escape).
- Predicates and comparisons chain left to right (a IN (1, 2) = TRUE); SQLite
  IN () and IN tbl; ClickHouse GLOBAL IN keeps the GLOBAL flag; ClickHouse
  cond ? a : b (TernaryExpression).
- Data types: array dimensions (int[3][4]), schema-qualified, multi-word
  (DOUBLE PRECISION, CHARACTER VARYING, TIMESTAMP WITH TIME ZONE, SIGNED
  INTEGER) and CHAR(n) CHARACTER SET x, shared by CAST and ::.
- AND and OR are stored in canonical upper case.

Formatter and AST
- Render the new nodes and fields; extend the operator and data-type
  allow-lists so they are written verbatim.
- format_sql (MCP) now upper-cases AND/OR by default; tests and docs updated.

Real-world corpus: 59 statements now pass, 6 move to a later failure stage.

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

UPDATE and DELETE read a single table name and then expected SET/WHERE, and
the MySQL row limit was parsed but had no AST field, so the formatter
silently dropped LIMIT and the reformatted statement touched every row.

UPDATE / DELETE:
- targets with AS / bare aliases, PostgreSQL ONLY, Oracle derived-table
  targets and FROM-less DELETE, SQL Server TOP (n) [PERCENT]
- MySQL/MariaDB multi-table forms (UPDATE t JOIN u / UPDATE t1, t2,
  DELETE t1[, t2] FROM ..., DELETE FROM t1, t2 USING ...), SQL Server
  DELETE t FROM ... and UPDATE ... FROM t JOIN u
- DELETE ... USING routed into DeleteStatement.Using
- statement modifiers (LOW_PRIORITY, QUICK, IGNORE; SQLite OR ...)
- ORDER BY ... LIMIT n stored on the AST and rendered
- SET targets: qualified columns, (a, b) tuples, @variables, DEFAULT values
- SQL Server OUTPUT [INTO], RETURNING aliases and Oracle RETURNING ... INTO

INSERT: MySQL/SQLite/Snowflake modifiers, INTO-less SQL Server/MySQL form,
VALUES () and DEFAULT items, MySQL row alias, OVERRIDING SYSTEM|USER VALUE,
ON CONFLICT expression targets and partial-index WHERE, OUTPUT ... INTO,
ClickHouse SETTINGS / FUNCTION / FORMAT (FORMAT was dropped before), Oracle
INSERT ALL / INSERT FIRST. REPLACE gains SELECT, SET and RETURNING forms.

MERGE: derived-table sources, DO NOTHING, NOT MATCHED BY TARGET, RETURNING,
Oracle UPDATE ... WHERE / DELETE WHERE and INSERT ... WHERE; the formatter
now renders the OUTPUT clause it used to drop.

New dialect fields live in embedded structs (ast.DMLTargetFields,
ast.InsertDialectFields) so single-table code that reads TableName/Alias is
unchanged; keyword fields are allow-listed on render and reset by the pool
release functions. 91 real-world corpus statements now pass (one moves from
parse to the pre-existing INTERVAL drop).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
parseStatement was a closed switch, so BEGIN, COMMIT, SET, USE, CALL,
GRANT, SHOW ... LIKE, T-SQL DECLARE/EXEC/IF and the rest of the utility
family failed with "expected statement". Backtick- and bracket-quoted
keywords lost their quoting (`key` became the KEY token, [Group] formatted
as a bare GROUP), and keywords such as share, filter and owner were
rejected as aliases or columns.

- Add ast.TransactionStatement, SetStatement, UseStatement, CallStatement
  and a verbatim UtilityStatement whose SQL() reproduces the statement,
  rebuilt from its tokens with delimiters and escapes restored. Dispatch
  them by value after the typed statements, only at a statement boundary
  so the tail of a half-parsed statement is never split off. BEGIN/END
  are gated per dialect (T-SQL and PL/SQL blocks, END only in PostgreSQL
  and SQLite).
- SHOW forms ShowStatement cannot model, Snowflake USE and stage
  operations are now verbatim statements that re-parse (they formatted
  as an "-- UNSUPPORTED" comment); MariaDB ANALYZE <stmt> is an
  ExplainStatement.
- DescribeStatement keeps DESC vs DESCRIBE and the object kind; PRAGMA
  accepts a schema-qualified name.
- Delimited identifiers are never remapped to keywords; Identifier gains
  QuoteStyle so SQL() re-quotes with the source delimiters (`from`,
  [Group]); @/@@ variables are never quoted.
- Accept keyword words as aliases after AS and the role-option keywords
  (OWNER, URL, ...) as column names; allow a delimited implicit alias.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
String literals no longer corrupt on format. The tokenizer and the renderer
disagreed about backslashes: PostgreSQL, Oracle, SQLite, SQL Server and generic
treat a backslash as an ordinary character, MySQL/MariaDB decode escapes
(keeping \% and \_), and ClickHouse/Snowflake decode escapes but keep unknown
ones. The tokenizer now follows dialect.StringEscape, and LiteralValue records
RawBackslash so the renderer writes a literal back under the same rules instead
of doubling every backslash on each format pass (non-idempotent, data
corrupting for regexes and paths). Unknown escapes no longer fail tokenizing.

Lexical coverage:
- operators ^ ^@ <=> <-> << >> == := and { }
- 0x hex numbers, leading-dot (.5) and trailing-dot (5.) numbers
- prefixed strings N'' X'' B'' E'' q'[]' and MySQL _charset'' (kept on the AST
  as LiteralValue.Prefix and written back)
- $ inside identifiers (Oracle/Snowflake/PostgreSQL), # in Oracle identifiers
- SQLite :name, ?NNN and [bracket] identifiers, ClickHouse {p:Type} parameters
- a leading UTF-8 byte order mark is skipped

The real-world corpus ratchet drops 64 known failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
FIRST, LAST, NEXT, FILTER, SHARE, SOURCE, TARGET, MATCHED, FINAL,
TEMPORARY and the role-option words were rejected where a column
reference starts (SELECT first, key FROM t), although no clause gives
them a keyword meaning there. Accept them as column names and qualified
name parts; KEY, INDEX and VIEW stay rejected in MySQL, MariaDB, SQL
Server and Oracle, where they are reserved, and none of the words
becomes a function name.

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

LIMIT/OFFSET/FETCH values are now expressions when they are not plain
integers ($1, ?, @n, -1, 1 + 1, subqueries), with LIMIT ALL, OFFSET before
LIMIT, OFFSET n ROWS, FETCH FIRST ROW ONLY and SQLite LIMIT a, b. The new
SelectStatement fields (LimitExpr, OffsetExpr, LimitAll, OffsetRows, ...)
sit beside the existing *int fields, which stay filled for integers.

The formatter and SelectStatement.SQL() now render every SELECT field:
QUALIFY, START WITH / CONNECT BY, PREWHERE, ARRAY JOIN, TOP inside derived
tables (keeping TOP (n) parentheses), OFFSET ... ROWS before FETCH, and
CROSS/OUTER APPLY without a trailing JOIN. A reflection test fails when a
new SelectStatement field is not rendered by both renderers.

New syntax, dialect-gated:
- MySQL select modifiers (SQL_CALC_FOUND_ROWS, STRAIGHT_JOIN, ...),
  STRAIGHT_JOIN joins, JOIN without ON, LOCK IN SHARE MODE, INTO
  @vars / OUTFILE / DUMPFILE; MariaDB/Oracle FOR UPDATE WAIT n
- SELECT ... INTO table (SQL Server, PostgreSQL) and WHERE / GROUP BY /
  HAVING without FROM
- GROUP BY ... WITH ROLLUP / CUBE / TOTALS kept as written (MySQL rejects
  ROLLUP(...)), GROUP BY ALL (Snowflake, ClickHouse, BigQuery)
- ClickHouse LIMIT n BY, LIMIT n WITH TIES, FORMAT, INTERPOLATE, GLOBAL and
  ANY/ALL/ASOF/SEMI/ANTI join strictness, * EXCEPT / REPLACE
- Snowflake TOP n, * EXCLUDE / REPLACE / RENAME / ILIKE, ASOF JOIN ...
  MATCH_CONDITION
- SQL Server FOR JSON / FOR XML / FOR BROWSE, OPTION (...) query hints,
  HASH/LOOP/MERGE/REMOTE join hints; Oracle CROSS/OUTER APPLY, ORDER
  SIBLINGS BY, START WITH after CONNECT BY; PostgreSQL ORDER BY ... USING op
- a comma-separated table after an explicit join

Non-integer LIMIT literals such as 10.5 are no longer truncated to an int;
they are kept verbatim in LimitExpr so formatting never changes the row
count. The real-world corpus ratchet drops 95 known failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
A bare GRANT, KILL or EXEC was accepted as a complete verbatim
statement. Require at least one argument token except for the
statements that are complete on their own (VACUUM, ANALYZE, REINDEX,
CLUSTER, CHECKPOINT, and T-SQL THROW, RETURN, REVERT).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
… calls and typed literals

Primary expressions and function-call forms that failed to parse or
silently changed meaning on format now round-trip:

- Keyword-named functions: LEFT/RIGHT, GROUPING, TRUNCATE, DEFAULT, ROW,
  INSERT, ClickHouse any/range, Snowflake HASH, MySQL INTERVAL(n, ...).
- Qualified function calls (pg_catalog.now(), auth.uid(), pkg.fn()) and
  the Oracle (+) outer-join marker (OuterJoinMarker).
- Typed literals DATE/TIME/TIMESTAMP '...' (any type name in PostgreSQL)
  as TypedLiteral; SQL Server N'...' strings (Type "nstring"); adjacent
  string literals concatenated in MySQL, MariaDB and PostgreSQL.
- SQL-standard special forms via KeywordArgument: SUBSTRING FROM/FOR,
  POSITION IN, TRIM BOTH/LEADING/TRAILING ... FROM, OVERLAY PLACING,
  CONVERT USING, TRY_PARSE/PARSE AS, COLUMN_GET AS, LISTAGG ON OVERFLOW.
- GROUP_CONCAT SEPARATOR is kept in FunctionCall.Separator instead of
  being rendered as an extra argument (which changed results).
- Oracle KEEP (DENSE_RANK FIRST|LAST ORDER BY ...) as FunctionCall.Keep;
  CONNECT_BY_ROOT and PRIOR in the select list.
- INTERVAL <expr> <unit> keeps the value unquoted (IntervalExpression
  ValueExpr/Unit), so MySQL INTERVAL 1 DAY no longer formats as the
  invalid INTERVAL '1 DAY'; legacy Value text is preserved.
- MATCH ... AGAINST is a MatchAgainstExpression (was rendered as
  AGAINST AGAINST(...)).
- NEXT VALUE FOR, MySQL @@[scope.]var and @v := expr.
- ROW(...), nested ARRAY[[...]], ARRAY(<set operation>), bare bracket
  array literals with subscripts (ClickHouse, Snowflake), ClickHouse
  tuple element access (t).1, CAST(x, 'Type') and extract(s, re).
- TRY_CAST is rendered as TRY_CAST, CAST target types accept array
  suffixes and multi-word types (DOUBLE PRECISION, SIGNED INTEGER).

The real-world corpus ratchet drops 123 known failures; three move to a
later stage.

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

Formatting for SQL Server or Oracle now turns LIMIT / OFFSET expression
values ($1, @n, :n) into TOP (expr) or OFFSET expr ROWS FETCH NEXT expr ROWS
ONLY, as it already did for integers, and drops LIMIT ALL. The right side of
CROSS APPLY / OUTER APPLY may be a table-valued function call such as
sys.dm_exec_sql_text(qs.sql_handle) in dialects whose FROM clause does not
otherwise take function calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
…k new operator nodes

- COLLATE "en_US.utf8" was written back as en_US."utf8" (a schema-qualified
  name): CollateExpression now carries the qualifier in a Schema field, so a
  dot inside a quoted name stays part of the name.
- CREATE INDEX ... (a COLLATE x): CreateIndexStatement.SQL() dropped the
  collation, and the formatter wrote COLLATE "C" as bare C (PostgreSQL folds
  it to c). IndexColumn gains CollateQuoted; both writers emit the collation.
- transform's expression walker now descends into AT TIME ZONE, COLLATE,
  ANY/ALL/SOME over arrays, the ClickHouse conditional and LIKE ... ESCAPE, so
  QualifyColumns / ReplaceTable reach their operands (incl. = SOME (SELECT)).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
- ClickHouse: reject a second LIMIT ... BY, LIMIT n WITH TIES BY, and an
  OFFSET written before LIMIT n BY instead of silently dropping or moving
  a clause.
- Snowflake: ASOF JOIN ... MATCH_CONDITION (...) no longer requires ON.
- Render qualified EXCLUDE / EXCEPT names part by part so t.a does not
  become the single identifier "t.a".
- Release the pooled children of a StarExpression (pass-through case).

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

CALL with no procedure fell back to a verbatim UtilityStatement, SET = 1
recorded "=" as the variable name, and SAVEPOINT / RELEASE [SAVEPOINT] /
ROLLBACK TO [SAVEPOINT] / SAVE TRAN without a name parsed as valid. They
now report errors like GRANT and KILL with no arguments.

isKeywordColumnName now matches the token type before peeking at the next
token, so literal operands no longer pay for a token copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
Review follow-ups for the primary-expression and special-form work:

- Accept the ClickHouse NANOSECOND unit in INTERVAL <expr> <unit>; the
  stricter unit check had started rejecting it.
- Release pooled children under IntervalExpression.ValueExpr, FunctionCall
  KEEP ORDER BY, ARRAY(<set operation>) and the new unpooled nodes
  (KeywordArgument, MatchAgainstExpression, OuterJoinMarker,
  ConnectByRootExpression, AssignmentExpression).
- Have the AST security scanner descend into the new nodes. MATCH ...
  AGAINST (SLEEP(5)) was reported while it parsed as a BinaryExpression and
  was missed once it became a MatchAgainstExpression.
- Match schema- and package-qualified calls (pg_catalog.pg_sleep,
  DBMS_LOCK.SLEEP, UTL_HTTP.REQUEST, sys.xp_cmdshell) in the scanner, now
  that qualified calls parse in expressions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
# Conflicts:
#	testdata/realworld/known_failures.txt
# Conflicts:
#	cmd/gosqlx/cmd/format_corpus_test.go
#	testdata/realworld/known_failures.txt
# Conflicts:
#	testdata/realworld/known_failures.txt
# Conflicts:
#	testdata/realworld/known_failures.txt
# Conflicts:
#	cmd/gosqlx/cmd/format_corpus_test.go
#	pkg/sql/ast/ast.go
#	pkg/sql/ast/sql.go
#	pkg/sql/parser/dml_update.go
#	pkg/sql/parser/expressions_literal.go
#	testdata/realworld/known_failures.txt
BenchmarkRealWorldParse / BenchmarkRealWorldParseFormat run every
parseable statement of testdata/realworld through ParseWithDialect (and
FormatAST) in its own dialect, giving a production-shaped throughput and
allocation baseline across all eight dialects.

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

SplitStatements split on every top-level ';', so trigger and procedure
bodies were cut at the first inner ';' and multi-batch T-SQL scripts using
GO merged into one statement.

The splitter now tracks BEGIN ... END nesting (plus CASE, IF/LOOP/WHILE/
REPEAT inside a block, Oracle DECLARE and IS/AS sections, PostgreSQL BEGIN
ATOMIC) and keeps the whole compound statement together. Transaction
statements (BEGIN; BEGIN TRANSACTION; BEGIN IMMEDIATE; ...) are not blocks,
and a script whose blocks do not balance falls back to plain semicolon
splitting. Client batch separators are honoured and consumed: a GO [n]
line for SQL Server, the DELIMITER directive for MySQL/MariaDB and a lone
"/" line for Oracle. Plain scripts split byte-for-byte as before, and the
splitter still allocates only its result slice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
…eject truncated ones

SQL Server batches without semicolons ("SET NOCOUNT ON SELECT * FROM t")
swallowed every statement after a SET, USE, transaction, PRINT, EXEC,
DECLARE, THROW, RAISERROR, WAITFOR, DBCC or RETURN into its text, so the
linter, scanner and analyzer never saw them. Such a statement now ends at
a top-level word that starts a statement (SELECT, INSERT, UPDATE, DELETE,
MERGE, WITH CTE, CREATE, ALTER, DROP, SET, EXEC, IF, BEGIN, ...), outside
parentheses and not after FOR / AS (cursor declarations) or a "." Options
such as WITH MARK, WITH RECOMPILE and WITH NOWAIT still continue the
statement. Statements that own a body (IF, WHILE, BEGIN blocks, END) and
GRANT / REVOKE keep running to the semicolon. Other dialects are unchanged.

The verbatim fallback no longer accepts truncated input: SET with nothing
after the variable name (SET GLOBAL, SET @x), SHOW alone or with only a
modifier, and statements that end on LIKE, WHERE, FROM, IN, TO, a comma or
an assignment operator are now parse errors, so gosqlx validate reports them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
…LACE pool release

- SQL Server MERGE ... OUTPUT ... INTO @t now uses parseDMLOutputClause;
  MergeStatement gains OutputInto, rendered by the AST writer and formatter.
- MySQL and MariaDB reject DELETE t WHERE ... (FROM is required) and
  DELETE FROM t1, t2 WHERE ... (several targets need USING); both used to
  be accepted and rewritten or passed through. Oracle and SQL Server keep
  FROM-less DELETE.
- PutMergeStatement releases a derived-table source or target subquery and
  the OUTPUT INTO columns. Merge and Replace release now follow the #556
  releaseDrawn policy: ReleaseAST puts only the node types the parser draws
  (the parser allocates MergeStatement with a literal), while the exported
  Put functions still return everything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
…ATE and DELETE

The transforms only touched UpdateStatement/DeleteStatement.TableName and
WHERE, so MySQL multi-table forms, UPDATE ... FROM and DELETE ... FROM /
USING produced SQL that still named the old table, with no error.

ReplaceTable now renames the table in further targets, JOINs, FROM / USING
lists and in column qualifiers of SET, OUTPUT, ORDER BY and RETURNING.
AddTableAlias applies the alias wherever the statement names the table,
points the qualifiers at the alias (a table with an alias cannot be named
any more) and renames DELETE t FROM ... targets to the alias. A table that
appears twice (self join) returns an error instead of taking one alias for
both instances.

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

Follow-ups to the dialect string/escape work:

- Unconfigured tokenizer (no dialect) is strict again: \\ \' \" \` \n \r \t
  decode, any other backslash escape is an error (as before the escape
  rework), so gosqlx.Parse/Format and the CLI 'format' without --dialect
  refuse 'C:\dir' instead of rewriting it to a different PostgreSQL value.
  New zero-value mode dialect.StringEscapeStrict; unknown dialects map to it.
- NUL is never silently dropped: escaping literals write it as \0 (E-strings
  as \000). A raw NUL/Ctrl-Z inside a string, or an escape decoding to one,
  is rejected where the formatter cannot write it back (no-escape dialects,
  unconfigured, Snowflake/Redshift; Ctrl-Z in ClickHouse).
- Snowflake and Redshift get StringEscapeDropUnknown: the backslash before
  an unrecognised character is ignored (Snowflake docs: "the backslash is
  ignored"; Redshift's documented LIKE default escape is written '\\\\'),
  with complete octal/\xhh/\uhhhh escapes; dialect-dependent short forms
  are rejected.
- ClickHouse no longer decodes \Z (kept as written, like other unknown
  escapes) and decodes \a \v \f and ASCII \xHH.
- Triple-quoted strings are BigQuery-only, so '''a' tokenizes (value 'a)
  and '''abc''' keeps its quotes in every other dialect.
- Hex literals (0x1E) are typed int, not float.
- A delimited identifier containing '$' keeps its delimiters (standard SQL
  and BigQuery do not accept '$' in bare names); bare ones stay bare.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
Transpile wrote each string literal with the SOURCE dialect's escaping and
introducers. A PostgreSQL 'x\' followed by more text closed early in MySQL
(the rest of the statement ran as code), 'C:\dir' lost its backslash, and
E'..', q'[..]' and _utf8mb4'..' reached targets that reject them.

After the rewrite rules, every string literal (found by a reflective walk,
so a node with an incomplete Children list cannot hide one) gets the
target's backslash mode, and introducers the target lacks are dropped:
E'..' outside PostgreSQL, q'[..]' outside Oracle, a charset introducer
outside the MySQL family. The decoded value is unchanged and re-escaped
for the target. A NUL or Ctrl-Z the target cannot spell (NUL where
backslashes do not escape; \0/\Z outside MySQL where they read
differently) is an error instead of being dropped or changed.

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

ALTER TABLE previously accepted one unqualified table name and one of
ADD COLUMN / ADD CONSTRAINT / DROP COLUMN / DROP CONSTRAINT / RENAME / ALTER,
with ALTER COLUMN reading its sub-action as a column definition (so DROP
DEFAULT formatted as a column of type "DROP").

- Qualified names, IF EXISTS, ONLY, ON CLUSTER (ClickHouse) and the MariaDB
  ONLINE / IGNORE modifiers; comma-separated action lists in
  AlterStatement.Operations (Operation stays the first action).
- ADD [COLUMN] [IF NOT EXISTS] with the optional COLUMN keyword, Oracle and
  MySQL parenthesized lists, SQL Server column lists, FIRST / AFTER; ADD
  [CONSTRAINT] PRIMARY KEY / UNIQUE / FOREIGN KEY / CHECK with CLUSTERED /
  NONCLUSTERED, NOT VALID and other trailing attributes, WITH [NO]CHECK;
  ADD / DROP INDEX / KEY (MySQL, MariaDB, ClickHouse).
- DROP COLUMN [IF EXISTS] lists, CASCADE / RESTRICT, DROP PRIMARY KEY /
  FOREIGN KEY / CHECK; MODIFY, CHANGE, RENAME COLUMN / CONSTRAINT / TO / AS.
- ALTER COLUMN SET / DROP DEFAULT with any expression, SET / DROP NOT NULL,
  [SET DATA] TYPE t [USING e] and other sub-actions; full definitions for
  SQL Server / Oracle.
- Dialect-gated long tail (ENABLE / DISABLE / VALIDATE, OWNER TO, SWAP WITH,
  CLUSTER BY, SET props, MySQL table options, partition operations,
  ClickHouse MODIFY TTL / SETTING and partition actions) kept as a keyword
  phrase plus text as written; ClickHouse UPDATE / DELETE mutations parsed.
- ALTER INDEX / SESSION / SYSTEM / USER / DATABASE / SCHEMA / VIEW /
  WAREHOUSE / TASK / ... are kept as written in a UtilityStatement.
- The formatter renders every action; pkg/schema applies multi-action and
  multi-column ALTERs and CHANGE renames.

Removes 130 statements from the real-world known-failures list.

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

Set operations:
- INTERSECT binds tighter than UNION/EXCEPT/MINUS (precedence climbing);
  SQLite and Oracle keep equal-precedence left-to-right grouping
- record explicit DISTINCT quantifiers and MINUS spelling (rendered as
  EXCEPT only for a target dialect without MINUS)
- VALUES operands; right-nested INTERSECT renders without parentheses so a
  trailing ORDER BY/LIMIT keeps applying to the whole query

CTEs: render MATERIALIZED / NOT MATERIALIZED and ClickHouse scalar CTEs in
the formatter; accept VALUES and parenthesized query bodies.

Table factors:
- render ClickHouse FINAL, Snowflake AT/BEFORE/CHANGES, FOR SYSTEM_TIME and
  MATCH_RECOGNIZE in the formatter (previously dropped)
- store TABLESAMPLE / SAMPLE (method, args, PERCENT/ROWS, REPEATABLE/SEED)
- PostgreSQL ONLY, Oracle @dblink and AS OF / VERSIONS BETWEEN flashback,
  MySQL PARTITION (...) and index hints, SQLite INDEXED BY / NOT INDEXED,
  SQL Server bare (NOLOCK) hints, INDEX(ix) hints and FOR SYSTEM_TIME
  (incl. CONTAINED IN)
- VALUES derived tables, column alias lists on any derived table or table
  (typed lists for PostgreSQL record functions), table functions for every
  named dialect, keyword-named function arguments (FLATTEN outer =>)

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

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

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


Review of PR #858

Scope: The merge base now resolves (326fdd1), but at this size I spot-checked rather than read the whole diff. I read internal/batch/batch.go, internal/batch/batch_test.go and internal/tokscratch/tokscratch.go in full, and I looked at where batch.End is called. I did not run tests or lint, and I did not review the formatter, parser, CLI or splitter bodies. This is not a sign-off.

Resolved from earlier bot reviews

  • Trailing comments: batch_test.go has a "go 3 -- repeat\nSELECT 1" case that expects a separator. So GO -- comment is covered, and the tokenizer evidently drops comments from the token stream. That earlier concern needs no action.
  • Case: End uses EqualFold, so lowercase go is accepted, and the test covers it.

internal/batch/batch.go

  • Count, IsText and End are tight, and the validation is sound. Quoted, string, fractional, zero and trailing-word forms are all rejected.
  • Suggestion: add test cases for GO;, GO /* x */, GO 5 -- c, and GO at end of input with no newline. The existing table doesn't include them.
  • Minor: End requires End.Column-Start.Column == 2. If the tokenizer ever reports byte columns for lines with leading multibyte characters, that is still fine, because GO itself is ASCII. No action needed.
  • Minor: IsText uses strings.Fields. That means GO\t5 and GO 5 both match, which is the intended behavior.
  • Possible gap: End only checks that the previous token's end line is less than GO's line. A multi-line block comment ending on the same line as GO (/* c */ GO) is invisible if comments are dropped, so that form would be treated as a separator. SSMS also treats it as a separator only in some versions, so I'd add a test that pins the behavior you want.

internal/tokscratch/tokscratch.go

  • The registration pattern is reasonable and the ownership contract is clearly documented.
  • Robustness: impl is nil unless pkg/sql/tokenizer is linked in, and t any is asserted inside the registered function. Both fail with a panic rather than an error. All callers are internal, so this is low risk. A one-line nil guard that returns an error would give a clearer failure for test binaries that import it indirectly.
  • Register is unsynchronized, which is fine while it runs only from init.
  • Risk to keep an eye on: the "valid until next tokenize/Reset/Put" contract is the main hazard. scratch_tokens_test.go and concurrent_retained_ast_test.go target it directly. Make sure the AST never retains Word values that point into the scratch buffer. The PR notes the preprocess step copies them.

PR-level

  • Breaking changes: the five listed changes are documented. A short CHANGELOG.md migration snippet for the SelectStatement.Extras nil check (read: nil check, write: allocate first) would help consumers.
  • Performance: the 24–29% synthetic parse latency regression versus main is disclosed. Please file a tracking issue for the AST layout follow-up so it isn't lost. The same goes for deep T-SQL IF/ELSE chains and the splitter/parser fallback differences.
  • Size: about 50k added lines in 84 commits is hard to review. Splitting the corpus/harness, formatter and parser work would help next time.
  • Testing: the described coverage is strong (2,788-statement corpus, prefix truncation, race and cbinding runs). I did not verify those results. Remote CI, including Vercel, should be green before merging.

Ajit Pratap Singh and others added 14 commits October 25, 2026 22:57
The PostgreSQL absolute-value operator was written directly before its
operand, so "@ a - b", "@ t.a", "@ abs(a)" and "@ a::int" rendered as
"@A ...", which re-parses as a placeholder or variable and changes the
tree. Always write "@ " for the operator.

The parser now also refuses "@" directly followed by operator characters
("@-@", "@-5", "@~a"): PostgreSQL lexes those as one longer operator, so
reading them as abs of a signed operand produced output that no longer
re-parsed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
For "WITH c ... ((A UNION B) INTERSECT C) EXCEPT D" the parser marks the
outermost level whose left operand was parenthesized after the WITH
clause, but HoistLeadingWith walked the left spine from the innermost
level and gave up at the first parenthesized level without the mark, so
SQL(), FormatAST and Transpile wrote the WITH clause inside the
parentheses, hiding its CTEs from the other operands.

Walk from the outermost level and hoist at the marked level; levels
below it are inside the clause's scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
A hint after a nested SELECT was put back after the SELECT with the same
ordinal in the rendered statement. When the renderer writes the clauses
holding the subqueries in another order (Oracle START WITH before
CONNECT BY), the hints swapped subqueries.

Record a context signature (surrounding words) for every occurrence of
the hinted keyword in the source and in the rendered statement. Place
the hint by ordinal when the signatures line up; otherwise place it at
the only rendered occurrence with the hinted signature, and refuse with
ErrUnplaceableComment (input left unchanged) when that occurrence cannot
be identified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
sqlcmd splits a script at every line holding only GO [n], but the parser
recognized GO only at a statement start, so "SELECT a,\ngo\nFROM t"
parsed (and formatted) as one statement with a column named go, while
the comment layer split the source at the GO line.

In SQL Server, a statement that runs across a GO line is now a parse
error ("GO batch separator inside a statement"); the recovering parser
reports it and resumes at the GO line. GO with an invalid repeat count
alone on its line (GO 0, GO 1.5) gets a dedicated "invalid GO repeat
count" error instead of an unrelated FROM / end-of-statement error.

The comment layer now takes the parser's separator positions
(CommentLayout.Batches, from formatter.BatchPositions) instead of
re-detecting GO lines, and falls back to hoisting comments when the GO
items of the source and the rendered statements do not line up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
handleDocumentSymbol listed every BatchSeparator as "Statement #n" with
a range starting at the top of the file, and statementStartLocation had
no position for it, so the statement before a GO line ran to the end of
the file. Skip separators in the outline and use their position to bound
the preceding statement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
The CLI analyzer, the MCP parse_sql tool and the C binding counted (and
typed) every retained BatchSeparator as a statement, so a SQL Server
script with GO lines reported more statements than it has. Skip
separators in statement_count and statement types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
checkFormattable skipped its "parser split a statement" guard for SQL
Server because T-SQL statements need no ';', leaving the withBatches
parameter of statementSegments dead. Bound SQL Server parses by the
semicolon/GO runs plus GO lines plus the words a T-SQL statement can
start with, and drop the dead parameter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
releaseStatement had no case for *ast.Values or *ast.SetOperation, so
ReleaseAST never returned the pooled nodes under a standalone VALUES
statement or a top-level (or CTE-body) UNION/INTERSECT/EXCEPT to their
pools. Release set-operation operands (operand links are cleared, so a
node cannot be put twice) and VALUES rows through a shared releaseValues
also used for VALUES derived tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
The AST recorded only "^", so Transpile emitted it unchanged although it
is exponentiation in PostgreSQL and bitwise XOR (at two different
precedence levels) in MySQL/MariaDB and SQL Server. PostgreSQL prefix
operators, SQL Server money literals and Snowflake object constants were
also emitted unchanged for dialects that lack them.

Record XOR semantics on BinaryExpression.BitwiseXor (fits in existing
padding; the node size is unchanged) and rewrite for the target:
exponentiation becomes POWER(a, b) outside PostgreSQL; XOR keeps ^ with
explicit grouping between the MySQL family and SQL Server, becomes # in
PostgreSQL, BITXOR() in Snowflake, bitXor() in ClickHouse, and is an
error elsewhere. @, |/ and ||/ become ABS(), SQRT() and CBRT() (an error
without CBRT); !!, money literals and object constants are errors in
dialects without them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
scripts/dialect-audit and the real-world corpus test carried identical
copies of the dropped-word detector. Move it, with both test suites, to
internal/dropcheck and use it from both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
The dialect audit added precedence levels (XOR, bitwise/other operators,
^, postfix COLLATE / AT TIME ZONE) and many per-clause dialect probes, so
every operand, table reference and SELECT tail ran through several
non-inlined checks that cannot match. ParserComplexSelect was 27% and
ParserSimpleSelect 24% slower than v1.15.0.

- parseBitwiseExpression returns an operand directly when the next token
  is one no operator of any level or dialect consumes (",", ")", FROM,
  WHERE, JOIN, ON, AS, ...); the ^ * / + - || levels gain tail functions
  so the shortcut composes them exactly as before. The comparison loop
  and canBeAlias use the same screen.
- parseComparisonSuffix jumps straight to the comparison operators for
  a comparison token, past the predicate (LIKE, IN, IS, ...) probes.
- parsePrimaryExpression screens the PostgreSQL prefix operator,
  prefixed string, typed literal, MySQL BINARY and hierarchical
  operator probes with inlinable token checks.
- FROM / JOIN table references stop at a table-reference end, and
  parseSelectStatement stops at the end of the statement, instead of
  probing every modifier and trailing clause.
- parseJoinStrictness and isTSQLStatementStart test the dialect inline.
- TableReference.Only and JoinClause.Comma (both new since v1.15.0)
  move into existing padding: TableReference 160 -> 152 bytes,
  JoinClause 456 -> 432.

Parse output is unchanged: FormatAST, SQL() and error text for every
testdata statement (per dialect) and a full reflective AST dump are
byte-identical to b14f14c.

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

Further cuts to the per-operand, per-table and per-statement probes that
the dialect audit added:

- Table-reference and alias probes (OPENJSON WITH, FOR SYSTEM_TIME,
  time travel, SAMPLE, flashback, CONNECT BY / START WITH, PIVOT,
  QUALIFY, MATCH_RECOGNIZE, WINDOW, RETURNING, select extensions, index
  hints, INDEXED BY) get an inlinable head: a necessary condition (the
  dialect, the token type, or len(word) <= len(value) for a word match)
  ANDed with the unchanged original.
- equalsUpper screens on length inline; parseTopClause, isSomeQuantifier
  and the MySQL BINARY probe test the cheap condition first.
- parseBitwiseExpression also returns at once before =, <>, >=, ==, the
  regex operators, NOT, BETWEEN, LIKE, IN, IS, AND and OR, which no
  tighter level consumes.
- UPDATE / DELETE parse a plain target name straight into the statement
  instead of building, returning and unpacking a TableReference.

Parse output is unchanged: FormatAST, SQL() and error text for every
testdata statement (per dialect) and a full reflective AST dump are
byte-identical to b14f14c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
The dialect audit (#858) made BenchmarkPerformanceBaseline ~22% slower than
main (geomean; SimpleSelect +25%, ComplexQuery +28%) and +19% B/op. GOGC=off
showed the same gap, so it was parse-path CPU, accumulated across the
pkg-d1, pkg-c1, pkg-b, d2, fu-quoting and w3-boundaries merges.

- Move branch-new cold AST fields behind lazily allocated side structs
  (SelectStatement.Extras, LiteralValue extras, InsertStatement.Extras,
  TableReference.Modifiers, BinaryExpression/FunctionCall.Extras) with
  read-only DialectExtras/DialectModifiers accessors and EnsureExtras writers;
  pool release resets them. No field that exists on main moved.
- Remove per-token/per-expression overhead ordinary SQL paid for the new
  dialect features: comparison-suffix dispatch on clause-ending tokens, cold
  SELECT/table-modifier probes, prefix/quote metadata checks, CTE quote
  metadata built eagerly, string escape mode recomputed per literal (now a
  per-parser flag refreshed in WithDialect/resetDialectState), and helper
  calls for table references at EOF or ')'.
- TestRealWorldCorpus now asserts the shared read-only stand-ins are still
  empty after the whole corpus round-trips.

Interleaved benchstat vs origin/main (n=10, Apple M4): sec/op +2.2% geomean,
no sub-benchmark significant (SimpleSelect p=0.36, ComplexQuery p=0.09,
CTE p=0.14); allocs/op identical; B/op +7.3% (was +18.9%).

Behaviour: corpus 2,788/2,788 with known_failures.txt unchanged; an
independent A/B harness found byte-identical parse/SQL()/format/transpile
output across 9 dialects (64k + 26k + 721k cases). task check and
task test:cbinding pass.

Implemented with Codex (gpt-6.1-sol), two rounds; reviewed and verified here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9
Ajit Pratap Singh and others added 5 commits October 26, 2026 00:35
… paths

- parsePrimaryExpression handles string, number, boolean, placeholder
  and NULL tokens before the identifier case, whose condition never
  holds for them, so literals no longer run the keyword-name probes.
- The bitwise level also returns at once at <, > and <= when the next
  token cannot make them <<, >>, <-> or <=>.
- parseQualifiedNameQuoted returns a one-part name without the parts
  array; TRY_CAST / EXTRACT detection screens on length first.
- UPDATE / DELETE: the assignment column records name quotes only when
  they can be non-zero, the parenthesized-target path and the
  multi-table UPDATE target list get their own functions so their
  TableReference temporaries leave the common frames, and the SQL
  Server GO-batch checks run only in that dialect.

Parse output is unchanged: FormatAST, SQL() and error text for every
testdata statement (per dialect) and a full reflective AST dump are
byte-identical to b14f14c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
- isWord and isKeepClause screen on the word length inline, so a
  function call no longer makes two calls to look for SEPARATOR and KEEP.
- UPDATE / DELETE: the RETURNING, target-alias, OUTPUT and
  ORDER BY / LIMIT probes screen on the token type or dialect inline,
  and ONLY is looked for only when a table name follows.

Parse output is unchanged: FormatAST, SQL() and error text for every
testdata statement (per dialect) and a full reflective AST dump are
byte-identical to b14f14c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
Resolve extract.go: keep the branch's join-group and multi-table DML
table collection alongside main's SELECT-alias scoping (#849).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
UpdateStatement and DeleteStatement embedded DMLTargetFields by value,
which made every plain UPDATE/DELETE 352 B. The fields now live under an
optional Extras pointer (like SelectStatement.Extras) that the parser sets
only when one of them is used; TableNameQuote and AliasQuote stay on the
statement. Update is 160 B and Delete 176 B.

Rendered SQL/FormatAST output is byte-identical over all testdata
statements; FullPipeline/update and /delete gain about 1 point vs main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
Reconcile the two perf lines that both started at 0db001a. Keep this
branch's parser hot-path work (review fixes, operand/clause-end screens,
DML target parsing) where both sides changed the same code, and take the
other line's memory moves: LiteralValue, FunctionCall, BinaryExpression,
InsertStatement and SelectStatement side structs, TableReference.Values
under Modifiers, the rawStringLiterals flag and the dialect stand-in guard
in TestRealWorldCorpus. UPDATE/DELETE gain the matching DialectExtras()
accessor and are covered by the guard.

FormatAST/SQL() output is byte-identical to b14f14c over all 4,597
testdata statements; the AST dump matches apart from moved field paths.

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

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

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


Review of PR #858 (spot-check, not a sign-off)

The merge base resolves this time (286f2c9). The diff is 408 files and about 53k added lines, so I didn't read all of it. I read internal/batch/batch.go and internal/tokscratch/tokscratch.go in full. I did not run tests or lint. I did not read the formatter, parser, CLI or splitter bodies.

internal/batch/batch.go

  • Validation: Count and Line split "shape of a separator" from "valid count". That is what lets CLI validation reject GO 0 and GO 1.5 instead of falling back to a plain identifier. This is sound.
  • Same-line prefix: /* c */ GO is only rejected if comments appear in the token stream. The earlier bot reviews concluded the tokenizer drops comments, so /* c */ GO would likely be accepted as a separator. sqlcmd behavior here varies, so add a test that pins the behavior you want.
  • Missing test cases: I'd add tests for GO;, GO 5 -- c, GO at end of input with no newline, and GO immediately after a multi-line block comment.
  • Column check: End.Column-Start.Column != 2 assumes columns count the same units for both ends. That holds because GO is ASCII. The check is fine.
  • IsText: it is case-insensitive and uses strings.Fields, so GO\t5 matches. Make sure the formatter output goes through it consistently.

internal/tokscratch/tokscratch.go

  • Pattern: the registration pattern avoids the import cycle, and the borrowed-buffer contract is well documented.
  • Nil impl: any binary that links tokscratch without pkg/sql/tokenizer panics with a nil func call. A guard that returns an error is cheap.
  • t any: a wrong type panics inside the registered function. A comma-ok assertion there would return a clear error.
  • Missing context check: Bytes and String take no context, so no guard is needed. The StringContext pre-check is fine.
  • Main hazard: an AST retaining Word values that alias the scratch buffer. The doc says the parser's preprocess step copies them. concurrent_retained_ast_test.go targets this directly, which is the right test. If you haven't already, run it under -race with a pool size of 1 to maximize buffer reuse.

PR-level

  • Breaking changes: the five listed changes are documented. The SelectStatement.Extras change (nil-check on reads, allocate before writes) deserves a short migration snippet in CHANGELOG.md. So does the BatchSeparator nodes appearing in AST.Statements.
  • Performance: the 24–29% synthetic latency regression versus main is disclosed. Open a tracking issue for the AST layout work so it isn't lost. The deep T-SQL IF/ELSE chains and the splitter/parser fallback differences should get issues too.
  • Size: the PR is very hard to review as one unit. Splitting the corpus/harness, formatter and parser changes would help next time.
  • Testing: the described coverage is strong (2,788-statement corpus, 266k prefix truncation, race and cbinding runs). I didn't verify these results. Confirm remote CI is green after the latest sync before merging.

@ajitpratap0

Copy link
Copy Markdown
Owner Author

This PR is split into a stack of three for review:

  1. test(realworld): add multi-dialect real-world corpus with ratchet #866: real-world corpus, ratchet test and harness, based on main. 1,327 known failures on main.
  2. fix(dialect): parse, format and transpile the real-world corpus in all 8 dialects #867: all dialect, render, quoting and transpile fixes plus the perf work, stacked on test(realworld): add multi-dialect real-world corpus with ratchet #866. Known failures drop to 0.
  3. fix(parser)!: treat a GO line inside a SQL Server statement as an error #868: the one breaking parser rule (a GO line inside a SQL Server statement is an error), stacked on fix(dialect): parse, format and transpile the real-world corpus in all 8 dialects #867.

With all three merged, the code is identical to this PR's head c8bd83b7; only the CHANGELOG wording differs, and it corrects the escape note: main already rejected unknown backslash escapes. This PR can be closed once the stack is accepted.

🤖 Generated with Claude Code

@ajitpratap0

Copy link
Copy Markdown
Owner Author

Superseded by the stack #866 → #867 → #868, all merged into main (15ffeec, e3e01c5, 953138e). The main tree matches the reviewed stack.

This branch was successfully deployed

1 active deployment
Preview — c8bd83b7 Deployed Oct 26, 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 cli documentation Improvements or additions to documentation linter lsp parser performance python security tokenizer Tokenizer / lexer changes transform wasm WebAssembly playground

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant