Repository navigation
fix(parser): INSERT ... SELECT RETURNING, DEFAULT VALUES, MySQL INSERT ... SET and parenthesized sources (#610) - #623
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude finished @ajitpratap0's task in 28s —— View job Review of PR #623 (INSERT grammar gaps, #610)I read the full diff of the parser, AST and formatter changes. I did not run the test suite, and I did not read the new test files in detail. Overall the change is well scoped, and the Correctness / edge cases
Code quality
Performance: The change adds a bool check in front of a string comparison, so hot-path cost is negligible. Security: I found no concerns. There is no new unbounded recursion, and the paren loop in Tests: Coverage looks thorough (parent-commit failures were verified, plus fixpoint and error cases, flag-restore and pool-reset tests). I suggest adding the cases from items 1–3 above and a deeply nested paren case. No blocking issues. The change looks good to merge once you've decided on item 1. |
…DEFAULT VALUES, MySQL INSERT ... SET, parenthesized INSERT sources and UPDATE ... FROM (#610) Several common INSERT forms were rejected: - RETURNING after an INSERT ... SELECT source: the implicit table-alias check took the unquoted RETURNING (tokenized as IDENTIFIER) as the alias of the last FROM/JOIN table, and the INSERT RETURNING check was case-sensitive. A parser context flag (dmlReturning), set only while parsing an INSERT source query or an UPDATE ... FROM list, now stops an unquoted RETURNING from becoming an implicit table or select-item alias and lets a FROM-less SELECT end before it. Outside those contexts `SELECT * FROM s returning` still yields alias "returning". The flag is restored before error checks and cleared by Reset. - INSERT INTO t DEFAULT VALUES: new InsertStatement.DefaultValues. - MySQL/MariaDB INSERT INTO t SET col = expr, ...: new InsertStatement.SetSyntax; assignments are stored as Columns plus one Values row so linter, extraction, security and span code are unchanged. SQL() and pkg/formatter emit SET when the shapes match and fall back to the VALUES form otherwise. - INSERT INTO t [(cols)] (SELECT ...): a '(' followed by SELECT, WITH or '(' is a query source, unwrapped iteratively and rendered without the parentheses. - PostgreSQL/SQL Server UPDATE t SET ... FROM s [, ...] now populates UpdateStatement.From; JOIN inside UPDATE ... FROM returns an unsupported-feature error because the AST has no join list. Both new InsertStatement fields are reset in releaseInsertStatement. All reported inputs failed with the issue's E2002 errors before the change; the new tests check AST shape and a SQL() re-parse fixpoint per dialect, plus FormatAST round-trips (readable style for nested query sources is left to #602). Fixes #610 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
F1 (parser): isReturningKeyword treated any identifier spelled RETURNING as the keyword, including backtick- and bracket-quoted ones. Inside an INSERT source or UPDATE ... FROM list, a quoted alias such as `returning` or [returning] was read as the start of the RETURNING clause, so valid SQL that parsed before #610 failed (E2002/E2004). The identifier branch now requires an unquoted token: Token.Quote must be zero (bracket identifiers set '['), and because the tokenizer leaves Quote unset for backtick identifiers, a source span wider than the value also counts as quoted. TestDMLReturningQuotedAlias covers table, join and nested-subquery aliases in INSERT ... SELECT (mysql, mariadb, sqlserver, postgresql) and UPDATE ... FROM (sqlserver, postgresql). F2 (cli): the gosqlx format renderer (cmd/gosqlx/cmd/sql_formatter.go) ignored the new UpdateStatement.From and InsertStatement.DefaultValues fields, so `format -i` rewrote UPDATE ... FROM without its source list (changing meaning) and INSERT ... DEFAULT VALUES as the invalid "INSERT INTO t", exiting 0. formatUpdate now renders the FROM list and formatInsert renders DEFAULT VALUES. TestFormatCommand_InsertUpdateForms checks exact output, readable layout, and that re-parsed output has the same SQL() as the input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
F3: the RETURNING stop used while parsing an INSERT source query or UPDATE ... FROM list leaked into nested scopes, so inputs such as "INSERT INTO t WITH c AS (SELECT * FROM s returning) SELECT * FROM c", "... WHERE a IN (SELECT b FROM r returning)" and "... FROM (SELECT a FROM r) returning" regressed from parsing to failing in dialects that do not reserve RETURNING. parseSubquery, derived-table parsing and CTE bodies now clear and restore p.dmlReturning, and the INSERT source calls a non-clearing parseQuerySource. RETURNING followed by a token that cannot start a RETURNING list (ON, USING, comma, dot, ")", join/clause keywords, end of statement) is treated as an alias, which fixes "FROM s JOIN r returning ON s.id = returning.id". F5: pkg/schema validateUpdate ignored UpdateStatement.From, producing false "not referenced in FROM clause" errors for valid UPDATE ... FROM and never checking that FROM tables exist. It now checks each named FROM table and adds its name/alias to the table map, mirroring validateSelect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
- val-0: isDMLReturningKeyword now treats a bare `returning` in an INSERT source FROM list or UPDATE ... FROM list as an alias when the next token is any join keyword (models.TokenType.IsJoinKeyword: LEFT, RIGHT, OUTER, ON, USING, ...), OFFSET, FETCH, FOR or WITH (SQL Server table hints), or starts a dialect-specific clause that may follow a table reference (WINDOW, Snowflake QUALIFY, PIVOT/UNPIVOT, Oracle/MariaDB START WITH and CONNECT BY, SAMPLE/TABLESAMPLE, MATCH_RECOGNIZE, time travel, MINUS, ClickHouse FINAL/PREWHERE/ARRAY JOIN/SETTINGS). The new nextStartsTableClause helper reuses the existing dialect-gated FROM-alias predicates one token ahead and restores the parser position. Inputs such as `INSERT INTO t SELECT * FROM s returning LEFT JOIN r ON ...` parse again as they did before RETURNING support; regression cases were added to TestDMLReturningNestedScopes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
f59b1a8 to
4273dbb
Compare
|
Claude finished @ajitpratap0's task in 26s —— View job Review of PR #623 (INSERT grammar gaps, #610)I read the parser diff in full: Overall: The change is well scoped and I found no blocking issues. Earlier review's item 1 is already handled.
Non-blocking observations
Performance: The hot-path cost is one bool check ahead of existing comparisons. Security: No new unbounded recursion, and pool reset covers the new AST fields. Tests: Coverage looks thorough. It includes parent-commit failure verification, fixpoint round-trips, flag-restore, pool-reset and error cases. The only gaps are the edge cases in items 1–4. This looks good to merge. I'd consider item 1, a one-line fix, before merging. |
|
Merge train (batch 3): rebased onto main (4273dbb). This PR got two rounds of adversarial fixes. Must-fix, fixed in 933c646 (Opus re-verified):
Should-fix, fixed in 5710b92 and 7e9e7e2 (Opus validated after a second round):
Bot review items, checked on HEAD:
|
…sts with #632/#623 Integration with #632 (now on main), which quotes reserved-word names on render: - ALTER TABLE ... ADD COLUMN "order" now formats correctly, so the guard test expects the quoted output instead of a refusal. - In MySQL/MariaDB a backtick keyword such as `select` would render as "select", which MySQL reads as a string unless ANSI_QUOTES is set. The new checkMySQLDoubleQuotes refuses output that adds double-quoted tokens the input did not have, keeping the CLI's previous safe refusal. Integration with #623: pkg/formatter writes table aliases without AS, so the UPDATE ... FROM list expectation becomes "u v". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
…ound-tripping output (#599) (#624) * fix(cli): render gosqlx format through pkg/formatter and refuse non-round-tripping output (#599) `gosqlx format` used its own 1,476-line renderer in cmd/gosqlx/cmd/sql_formatter.go. Any expression type it did not handle fell back to TokenLiteral(), and formatInsert dropped every clause after VALUES/SELECT. The command exited 0 with SQL of a different meaning, or with SQL that does not parse (CAST -> `CAST`, `a = ANY(...)` -> `ANY`, tuples -> `TUPLE`, `arr[1]` -> `[]`, ROLLUP/CUBE/GROUPING SETS, WITHIN GROUP, `EXISTS (SELECT 1 UNION SELECT 2)` -> `EXISTS ()`, ON CONFLICT/RETURNING dropped, PIVOT dropped, lossy ALTER TABLE). - sql_formatter.go is now a thin adapter. SQLFormatter, NewSQLFormatter, FormatterOptions and Format keep their signatures. Each statement is rendered with pkg/formatter.FormatStatement, with indent, compact and keyword case mapped to ast.FormatOptions. AddSemicolon is off and the CLI keeps its "; " / ";\n\n" separators. FormatterOptions gains an additive Dialect field. AlignColumns is retained but ignored. All parallel rendering code is deleted. - formatSQL now verifies its output. It re-parses the rendering with the same dialect and requires the same statement count and a case-insensitive equal SQL(). Otherwise it returns a "formatted output does not re-parse / changes the query" error, so stdout, -o and -i never receive corrupted SQL and --check counts the file as failed. Lowercase-keyword output is verified via its uppercase twin, because the parser rejects some lowercase keyword sequences. - pkg/formatter renders *ast.AlterStatement ALTER TABLE operations (ADD COLUMN, ADD CONSTRAINT, DROP COLUMN/CONSTRAINT [CASCADE], RENAME TO, RENAME COLUMN, ALTER COLUMN) in a new render_alter.go. Before, it rendered a bare "ALTER". Non-table ALTER keeps its previous output. render.go gains one switch case. Tests: - TestFormatSQL_Issue599 covers every reproduction in the issue (compact output must match exactly; readable output must round-trip and be idempotent). Rows that pkg/formatter itself still loses skip with the blocking issue and self-heal when the fix lands. - TestFormatSQL_CorpusRoundTrip formats all 134 parseable testdata/**/*.sql files in three modes, re-parses them, compares SQL() and checks idempotence. 8 documented known gaps may error but never emit changed SQL. - TestFormatStatement_AlterTable* cover the new ALTER rendering. Blocked on library fixes (these now match pkg/formatter and are fixed automatically when they land): - #600: named args, IGNORE NULLS, SETTINGS, ON DUPLICATE KEY UPDATE, OUTPUT - #603: JOIN USING - #604: WITH (NOLOCK) - #601: NOT EXISTS (now an explicit error) - #611: quoted aliases and identifiers (now an explicit error) Follow-ups to file: - CROSS/OUTER APPLY rendering - MATCH ... AGAINST rendering - Oracle CONNECT BY rendering - Snowflake QUALIFY rendering - backslash literals in dialects that keep them literal - ALTER ROLE/POLICY/CONNECTOR rendering - --no-uppercase flag has no effect - parser case-sensitivity for lowercase `returning` / `cross join lateral` CLI behaviour change: readable output layout now follows pkg/formatter, and output that would not round-trip is now an error with a non-zero exit instead of exit 0. Fixes #599 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve * fix(cli): drop duplicated "formatting failed" prefix in gosqlx format errors (#599) formatSQL wrapped its errors with "formatting failed:" and both format command paths wrapped them again, so users saw "formatting failed: formatting failed: ...". formatSQL now uses step-specific wrapping and returns the self-describing round-trip errors unchanged; the command keeps the single prefix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve * fix(cli): address adversarial review for #624 F1 (must-fix, regression): gosqlx format rewrote a non-executing EXPLAIN into executable SQL. The parser splits "EXPLAIN VERBOSE|ANALYZE <stmt>" into DescribeStatement{TableName: "VERBOSE"} plus <stmt>; the old CLI renderer refused DescribeStatement, but rendering through pkg/formatter accepted it, and the SQL() round-trip check is equally lossy, so `format -i` turned "EXPLAIN VERBOSE DELETE FROM users;" into "DESCRIBE VERBOSE; DELETE FROM users" with exit 0 (same for UPDATE, DROP, TRUNCATE, INSERT, EXPLAIN ANALYZE and MySQL EXPLAIN ANALYZE). formatSQL now refuses, before rendering, DescribeStatement (drops EXPLAIN options, the explained query and DESCRIBE object kinds) and UnsupportedStatement (rendered as a comment), restoring the base "unsupported statement type" error and leaving input unchanged. As a general guard it also refuses any parse with more statements than semicolon-separated segments in the input, since a statement split by the parser cannot be detected by re-parsing the output. The underlying parser bug (EXPLAIN VERBOSE/ANALYZE splitting into two statements, DescribeStatement losing options and object kind) still needs its own issue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve * fix(cli): address adversarial review follow-ups for #624 F2: the format round-trip check passed trivially for statements without a SQL() method (ALTER, sequences), since AST.SQL() skips them and "" equals "". verifyFormattedSQL now also compares a canonical pkg/formatter rendering of every statement, and compares the quoted tokens of the input and output: each quoted identifier must stay quoted or mean the same identifier bare (ASCII word, not a keyword for the dialect, and unchanged by the dialect's case folding), so `ADD COLUMN "order"`, "Mixed" and MySQL `select` are refused instead of written bare with exit 0. This also refuses SELECT "Mixed" and corpus files gorm_joins.sql (alias "CompanyName"), oracle/14_pivot.sql and 15_unpivot.sql (PIVOT IN 'North' rendered as the identifier North), which were previously written changed; they are listed as known gaps. F3 (guard only): every input string literal value must appear in the output, so backslash literals in dialects where backslash is not an escape are refused with an explicit "changes string literal" error. The renderer still re-escapes backslashes MySQL-style; making LiteralValue rendering dialect-aware needs the dialect threaded into pkg/sql/ast SQL() and is left as a follow-up. F5: UnaryExpression.SQL() wrote nested unary minus as "--", which starts a line comment; it now writes "- -1". Tests in pkg/sql/ast and pkg/formatter. F6: DESCRIBE/DESC were already refused by checkFormattable (previous commit); added Snowflake DESCRIBE STAGE / VIEW / DESC TABLE regression tests. F7: doc.go, formatter.go and sql_formatter.go comments now describe the check as a comparison of the input's parse with the output's parse, and state that what the parser drops (comments, skipped clauses) is not detected. F9: format --check no longer prints "All N files are properly formatted" when some files failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve * test(cli): expect pkg/formatter readable layout in JOIN USING format test TestFormatCommand_JoinUsing_Issue603 (#603) pinned the old CLI renderer's readable layout ("SELECT\n *\nFROM ..."). #599 routes `gosqlx format` through pkg/formatter, whose readable layout keeps a single-item select list on the SELECT line ("SELECT *\nFROM ..."). Only the readable expectation changes; the USING assertions (no "ON" rendering, re-parsed Using columns, nil Condition) are unchanged. Integration fix for #603 x #599; port into whichever of the two lands second. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve * test(cli): drop corpus known-gap entries fixed by #601 With #601 (NOT EXISTS renders as a prefix NOT) and #599 (gosqlx format via pkg/formatter) both applied, corpus/tpch/q21_suppliers_kept_waiting.sql and mysql/19_not_exists.sql round-trip in every mode, so they no longer belong in corpusFormatKnownGaps. Keeping them would let a regression of The ent_queries.sql reason now names what is still missing after #611: the quoted qualifier in "groups"."id" (Identifier.Table quoting was deferred by #611). Integration fix for #599 x #601/#611; port into whichever lands second. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve * fix(cli): refuse MySQL output with added ANSI double quotes; align tests with #632/#623 Integration with #632 (now on main), which quotes reserved-word names on render: - ALTER TABLE ... ADD COLUMN "order" now formats correctly, so the guard test expects the quoted output instead of a refusal. - In MySQL/MariaDB a backtick keyword such as `select` would render as "select", which MySQL reads as a string unless ANSI_QUOTES is set. The new checkMySQLDoubleQuotes refuses output that adds double-quoted tokens the input did not have, keeping the CLI's previous safe refusal. Integration with #623: pkg/formatter writes table aliases without AS, so the UPDATE ... FROM list expectation becomes "u v". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve * fix(cli): require MySQL output to keep the input's double-quoted tokens Review follow-ups for the rebase integration: - checkMySQLDoubleQuotes compared only counts, so a double-quoted MySQL string written bare could cancel out an identifier newly written in ANSI quotes. It now requires the output to keep exactly the input's double-quoted tokens, which also refuses WHERE name = "bob" -> name = bob (a string silently becoming a column reference). Input that uses "..." as a name where MySQL cannot accept a string ("users"."id", FROM "users") is ANSI_QUOTES-style and is not checked; no MySQL/MariaDB corpus file is refused. - TestSQLFormatter_NotExistsDoesNotPanic: the #601 Skipf is unreachable now that #632 is on main; fail instead. - The corpus round-trip test now fails when a known-gap file starts to format, so the list cannot go stale. 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>
Summary
#610 parser: INSERT gaps: RETURNING after INSERT ... SELECT, DEFAULT VALUES, MySQL INSERT ... SET, INSERT INTO t (SELECT ...)
Fixes #610.
INSERT ... SELECT now accepts a trailing RETURNING clause. While the parser is inside an INSERT source query or an UPDATE ... FROM list, an unquoted RETURNING is no longer consumed as an implicit table or select alias. The context flag is restored on every path, including errors, and is cleared by Reset().
The commit also adds:
SQL() and formatter.FormatAST render DEFAULT VALUES and the SET form.
Every issue case failed with E2002 on 61ca6c8, and the new regression tests fail on that parent. On HEAD they pass, and SQL() output is a parse fixpoint in pg, generic, sqlserver, sqlite, mysql and mariadb. The full race suite (excluding cbinding), go vet and golangci-lint are clean.
Deferred:
;inside CTE/IN/EXISTS subqueries and set operations, and;;after INSERT ... SELECT / CREATE VIEW #602).gosqlx formatsilently corrupts many expressions; switch the CLI to pkg/formatter #599).Commits (1), one implementation commit per issue plus test-only follow-ups:
12 files changed, 992 insertions(+), 25 deletions(-)Test Plan
CGO_ENABLED=1 go test -race -timeout 300s $(go list ./... | grep -v /cbinding)),go vet,golangci-lint, re-ran the reproduction cases on HEAD, and confirmed the new tests fail on the parent commit.benchstat -count=6before/after on the same machine.#610 evidence (validator)
#610 race gate: PASS. Ran
CGO_ENABLED=1 go test -race -timeout 300s $(go list ./... | grep -v /cbinding)in /Users/ajitpratapsingh/dev/GoSQLX-worktrees/fix-insert-grammar-gaps at HEAD f59b1a8. 44 packages reported ok, with no FAIL lines and no data races; exit status 0. The pool tests (pool_reset_test, pool_drawn_policy_test) and all new #610 tests passed in that run. Log: /private/tmp/claude-501/-Users-ajitpratapsingh-dev-GoSQLX/b99cd3cf-cb92-4294-a0db-d6a7c9a86253/scratchpad/b3/610/race.txtBreaking Changes
DefaultValuesis true forINSERT INTO t DEFAULT VALUES.SetSyntaxmarks the MySQL/MariaDBINSERT ... SETform, which is stored as Columns plus one Values row.Both are reset in the pool release path. No exported signatures change and no new exported functions are added.
Observable behaviour changes:
returningworks).Notes
gosqlx formatsilently corrupts many expressions; switch the CLI to pkg/formatter #599-schema/postgres: loader misreports index columns and key flags (column order, INCLUDE, expression and partitioned indexes, read-only roles) #620); later PRs in the train are rebased onto this one after it merges.main.Fixes #610
Follow-up to #566
🤖 Generated with Claude Code
https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve