Skip to content

Commit a0e2aec

Browse files
committed
sqlite: quote identifiers that would not survive a reparse
fmt dropped double quotes from identifiers: the sqlite dialect's QuoteIdent returned every name bare, and the round-trip verification could not see the loss because it compares case-insensitively while the parser folds unquoted identifiers to lower case — so "EmailAddress" reprinted as EmailAddress, which sqlc's catalog then failed to resolve. Quote any identifier that is not a plain lower-case name, or that collides with a keyword (meyer's table, case-insensitive), escaping embedded quotes. The fmt endtoend case now pins quoted mixed-case and keyword identifiers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018MTvpHqNMadH12pTtsgUq2
1 parent 23b3957 commit a0e2aec

4 files changed

Lines changed: 45 additions & 5 deletions

File tree

internal/endtoend/testdata/fmt/sqlite/query.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ SELECT /* inline note */ id, name FROM authors ORDER BY name;
1818
-- name: ListAuthors :many
1919
SELECT id, name, bio FROM authors
2020
ORDER BY name;
21+
22+
-- name: GetEvent :one
23+
SELECT "EventName", "order"
24+
FROM "Events" WHERE id = ? LIMIT 1;

internal/endtoend/testdata/fmt/sqlite/schema.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,9 @@ CREATE TABLE authors (
33
name text NOT NULL,
44
bio text
55
);
6+
7+
CREATE TABLE "Events" (
8+
id integer PRIMARY KEY,
9+
"EventName" text NOT NULL,
10+
"order" integer NOT NULL
11+
);

internal/endtoend/testdata/fmt/sqlite/stdout.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,19 @@
2222
FROM authors
2323
-- soft-deleted rows are filtered
2424
WHERE bio IS NOT NULL
25-
@@ -16,5 +19,6 @@
25+
@@ -16,7 +19,8 @@
2626
SELECT /* inline note */ id, name FROM authors ORDER BY name;
2727

2828
-- name: ListAuthors :many
2929
+SELECT id, name, bio
3030
+FROM authors
3131
-SELECT id, name, bio FROM authors
3232
ORDER BY name;
33+
34+
-- name: GetEvent :one
35+
@@ -23,2 +27,4 @@
36+
SELECT "EventName", "order"
37+
+FROM "Events"
38+
+WHERE id = ?
39+
+LIMIT 1;
40+
-FROM "Events" WHERE id = ? LIMIT 1;

internal/engine/sqlite/format.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,35 @@
11
package sqlite
22

3-
// QuoteIdent returns a quoted identifier if it needs quoting.
4-
// SQLite uses double quotes for quoting identifiers (SQL standard),
5-
// though backticks are also supported for MySQL compatibility.
3+
import "strings"
4+
5+
// QuoteIdent quotes an identifier when printing it bare would change what it
6+
// names: the parser folds unquoted identifiers to lower case, so any name
7+
// that is not already a plain lower-case identifier — or that collides with
8+
// a keyword — only survives a parse round-trip inside double quotes.
69
func (p *Parser) QuoteIdent(s string) string {
7-
// For now, don't quote - return as-is
10+
if s == "" || p.IsReservedKeyword(s) || !plainIdent(s) {
11+
return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
12+
}
813
return s
914
}
1015

16+
// plainIdent reports whether s parses back as itself unquoted: ASCII
17+
// lower-case letters, digits and underscores, not starting with a digit.
18+
func plainIdent(s string) bool {
19+
for i, r := range s {
20+
switch {
21+
case r >= 'a' && r <= 'z', r == '_':
22+
case r >= '0' && r <= '9':
23+
if i == 0 {
24+
return false
25+
}
26+
default:
27+
return false
28+
}
29+
}
30+
return true
31+
}
32+
1133
// TypeName returns the SQL type name for the given namespace and name.
1234
func (p *Parser) TypeName(ns, name string) string {
1335
if ns != "" {

0 commit comments

Comments
 (0)