Skip to content

Commit 250f19f

Browse files
kyleconroyclaude
andauthored
Fix sqlc fmt corruption in PostgreSQL and MySQL formatters (#4596)
Fix six root causes of corruption found by running sqlc fmt across the endtoend test corpus and verifying the results: PostgreSQL: a named parameter with a cast (@name::type) printed as '@ name::type' because the cast binds tighter than the @ operator, so the printer saw a prefix operator and added a space — silently dropping the parameter from generated code. Prefix sigils now glue to operands that start with an identifier. MySQL (dolphin): the formatter reprinted from the compiler's normalized AST, which loses information the compiler doesn't need but a formatter must keep. sqlc fmt now uses a format parser that preserves identifier case (table names are case-sensitive on most servers), and the converter keeps what it used to drop: - NULL, TRUE/FALSE, and decimal literals convert by datum kind, so NULL no longer prints as '', 1.0 as 0, or true as 1. This also fixes type inference: a bool literal column is now bool, not int32 (selectstatic golden regenerated). - SELECT DISTINCT keeps its DISTINCT. - The ORDER BY of a compound (UNION) statement survives. - GROUP_CONCAT(... ORDER BY ...) keeps its ordering. - Optimizer hints (/*+ ... */) are carried through to printing. - Multi-table UPDATE keeps its JOIN's ON condition and the table qualifiers in SET. - DECIMAL(p,s) precision and UNSIGNED survive in column definitions. Backstop: dolphin now implements Fingerprint (via marino's restore with identifier case preserved and redundant parens unwrapped), giving MySQL the same proof PostgreSQL has: fmt accepts a formatted statement only when it provably means what the author wrote, and otherwise leaves the statement exactly as written. That is what fixes SHOW WARNINGS, which the parser rewrites into a synthetic SELECT for analysis: it now falls back to the original text instead of printing the internal form. Claude-Session: https://claude.ai/code/session_01UZBvADuHMZEjoF551NccgP Co-authored-by: Claude <noreply@anthropic.com>
1 parent f9f610c commit 250f19f

15 files changed

Lines changed: 369 additions & 76 deletions

File tree

‎internal/cmd/fmt.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ func newQueryFormatter(engine config.Engine) queryFormatter {
4949
case config.EngineSQLite:
5050
return sqlite.NewParser()
5151
case config.EngineMySQL:
52-
return dolphin.NewParser()
52+
// The format parser preserves identifier case and proves every
53+
// statement via Fingerprint, the way oliphant does for PostgreSQL.
54+
return dolphin.NewFormatParser()
5355
default:
5456
return nil
5557
}

‎internal/endtoend/testdata/fmt/mysql/query.sql‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,34 @@ insert into authors (
3939
) values (
4040
?, ?
4141
);
42+
43+
-- name: CasePreserved :many
44+
SELECT ID, Name FROM Authors WHERE Name <> '' ORDER BY Name;
45+
46+
-- name: LiteralsSurvive :one
47+
SELECT true AS t, false AS f, 1.50 AS score, CASE WHEN name = '' THEN NULL ELSE name END AS n FROM authors LIMIT 1;
48+
49+
-- name: DistinctNames :many
50+
SELECT DISTINCT name FROM authors;
51+
52+
-- name: UnionOrdered :many
53+
SELECT name AS foo FROM authors
54+
UNION
55+
SELECT bio AS foo FROM authors
56+
ORDER BY foo;
57+
58+
-- name: ConcatNames :many
59+
SELECT group_concat(DISTINCT name ORDER BY name DESC SEPARATOR ' ') FROM authors GROUP BY bio;
60+
61+
-- name: Hinted :one
62+
SELECT /*+ MAX_EXECUTION_TIME(1000) */ id FROM authors LIMIT 1;
63+
64+
-- name: UpdateWithJoin :exec
65+
UPDATE authors AS a JOIN authors AS p ON p.id = a.id
66+
SET a.name = ?
67+
WHERE p.bio IS NOT NULL;
68+
69+
-- name: ShowWarnings :many
70+
SHOW WARNINGS;
71+
72+
CREATE TABLE scores (points decimal(10, 5), views bigint unsigned NOT NULL);

‎internal/endtoend/testdata/fmt/mysql/stdout.txt‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343

4444
-- name: CastUnsigned :one
4545
SELECT CAST(id AS UNSIGNED) FROM authors LIMIT 1;
46-
@@ -34,8 +39,5 @@
46+
@@ -34,14 +39,11 @@
4747
SELECT id FROM authors LIMIT 1;
4848

4949
-- name: CreateAuthor :execresult
@@ -54,3 +54,20 @@
5454
-) values (
5555
- ?, ?
5656
-);
57+
58+
-- name: CasePreserved :many
59+
+SELECT ID, Name FROM Authors WHERE Name != '' ORDER BY Name;
60+
-SELECT ID, Name FROM Authors WHERE Name <> '' ORDER BY Name;
61+
62+
-- name: LiteralsSurvive :one
63+
SELECT true AS t, false AS f, 1.50 AS score, CASE WHEN name = '' THEN NULL ELSE name END AS n FROM authors LIMIT 1;
64+
@@ -62,7 +64,8 @@
65+
SELECT /*+ MAX_EXECUTION_TIME(1000) */ id FROM authors LIMIT 1;
66+
67+
-- name: UpdateWithJoin :exec
68+
+UPDATE authors AS a
69+
+JOIN authors AS p ON p.id = a.id
70+
-UPDATE authors AS a JOIN authors AS p ON p.id = a.id
71+
SET a.name = ?
72+
WHERE p.bio IS NOT NULL;
73+

‎internal/endtoend/testdata/fmt/postgresql/query.sql‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,8 @@ WHERE id > $1;
2626
-- name: SearchAuthors :many
2727
SELECT id, name, bio, created_at FROM authors WHERE name LIKE $1 AND bio IS NOT NULL AND id > $2 AND name <> $3 ORDER BY name, id LIMIT $4;
2828

29+
-- name: AtParamsGlued :many
30+
SELECT name FROM authors WHERE name = @slug AND @filter::bool;
31+
2932
-- name: DeleteAuthor :exec
30-
DELETE FROM authors WHERE id = @id
33+
DELETE FROM authors WHERE id = @id

‎internal/endtoend/testdata/fmt/postgresql/stdout.txt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@
4040
FROM authors
4141
WHERE id > $1;
4242

43-
@@ -27,4 +27,4 @@
44-
SELECT id, name, bio, created_at FROM authors WHERE name LIKE $1 AND bio IS NOT NULL AND id > $2 AND name <> $3 ORDER BY name, id LIMIT $4;
43+
@@ -30,4 +30,4 @@
44+
SELECT name FROM authors WHERE name = @slug AND @filter::bool;
4545

4646
-- name: DeleteAuthor :exec
4747
-DELETE FROM authors WHERE id = @id

‎internal/endtoend/testdata/selectstatic/mysql/go/query.sql.go‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)