Skip to content

Commit 55cfba4

Browse files
kyleconroyclaude
andauthored
Upgrade to oliphant 0.2.0 and wire up sqlc fmt for PostgreSQL (#4587)
oliphant 0.2.0 adds parser.ParseFile, which returns the parse tree along with the comment tokens its scanner already produces. That is exactly what sqlc fmt needs from an engine, so the PostgreSQL parser now implements ParseFile — Parse delegates to it, keeping the statement-span conventions unchanged — and fmt formats postgresql query files. PostgreSQL also brings a proof the other engines cannot offer: oliphant's pg_query-compatible fingerprint. Where an engine implements Fingerprint, fmt only accepts a formatted statement whose fingerprint matches the original's, and falls back to the text as written otherwise. The fmt/postgresql end-to-end case now commits its formatted diff instead of an unsupported-engine notice. Claude-Session: https://claude.ai/code/session_01K8CZxjsrAjJsvGfZNrR7Gd Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5ab441a commit 55cfba4

7 files changed

Lines changed: 127 additions & 9 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ require (
2222
github.com/sqlc-dev/doubleclick v1.0.0
2323
github.com/sqlc-dev/marino v0.3.0
2424
github.com/sqlc-dev/meyer v0.1.2
25-
github.com/sqlc-dev/oliphant v0.1.0
25+
github.com/sqlc-dev/oliphant v0.2.0
2626
github.com/sqlc-dev/teesql v1.1.0
2727
github.com/sqlc-dev/zetajones v0.1.0
2828
github.com/tetratelabs/wazero v1.12.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ github.com/sqlc-dev/marino v0.3.0 h1:e9cinBXJFFa3yRpokYNNinWvkewgd6XVDgnlG0bOmTw
7373
github.com/sqlc-dev/marino v0.3.0/go.mod h1:mQxC2dgDE0DWHMb2B5jZNk7KToJuS6wnxnffBfYnq08=
7474
github.com/sqlc-dev/meyer v0.1.2 h1:40Ng9Glnx7CTf3yOYV7jfvDIN7voIFjrStkIULS+uws=
7575
github.com/sqlc-dev/meyer v0.1.2/go.mod h1:pS4USCRf/SLjWtaMcnTo4YrEEFKBj8CyyqlxcVUJQH8=
76-
github.com/sqlc-dev/oliphant v0.1.0 h1:RAsO6BMitIzB2+swx/qzUR5nf6w4cQ1abgHIu+Fgppo=
77-
github.com/sqlc-dev/oliphant v0.1.0/go.mod h1:fRM/t4FutRddTIq2YCuS4O9o+2rRwSwELRvLMqtPloo=
76+
github.com/sqlc-dev/oliphant v0.2.0 h1:jJ/s2fh4Plj3U1HsdqiY/clw/IHb4FCNaHfqirwxcsI=
77+
github.com/sqlc-dev/oliphant v0.2.0/go.mod h1:fRM/t4FutRddTIq2YCuS4O9o+2rRwSwELRvLMqtPloo=
7878
github.com/sqlc-dev/teesql v1.1.0 h1:3sVYQ9FGxQVcqrqQOQ27bk0aF4c4yN1H1zLL79uaSxQ=
7979
github.com/sqlc-dev/teesql v1.1.0/go.mod h1:WwOp9UtnxG17+eNFT5KXu/AGBQ8ucdGc8wIOpnj4XCI=
8080
github.com/sqlc-dev/zetajones v0.1.0 h1:VeG0atx6lNABr9V2bSI5vL9DvOKTHX0XjMqWUE/rv40=

internal/cmd/fmt.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/spf13/cobra"
1515

1616
"github.com/sqlc-dev/sqlc/internal/config"
17+
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
1718
"github.com/sqlc-dev/sqlc/internal/engine/sqlite"
1819
"github.com/sqlc-dev/sqlc/internal/sql/ast"
1920
"github.com/sqlc-dev/sqlc/internal/sql/format"
@@ -37,10 +38,13 @@ type queryFormatter interface {
3738
}
3839

3940
// newQueryFormatter returns the formatter for engines fmt supports —
40-
// SQLite today. An engine joins by teaching its parser to surface comments
41-
// (meyer's ParseFile is the template) and adding its case here.
41+
// SQLite and PostgreSQL today. An engine joins by teaching its parser to
42+
// surface comments (meyer's and oliphant's ParseFile are the templates) and
43+
// adding its case here.
4244
func newQueryFormatter(engine config.Engine) queryFormatter {
4345
switch engine {
46+
case config.EnginePostgreSQL:
47+
return postgresql.NewParser()
4448
case config.EngineSQLite:
4549
return sqlite.NewParser()
4650
default:
@@ -374,16 +378,37 @@ func isCommentLine(line string) bool {
374378
}
375379
}
376380

381+
// fingerprinter is implemented by engines that can reduce a query to a
382+
// fingerprint that survives changes in whitespace, case and layout —
383+
// PostgreSQL via oliphant's pg_query-compatible Fingerprint. Where it is
384+
// available, fmt gets a proof the other checks cannot give: the formatted
385+
// statement still parses to the same query as the original.
386+
type fingerprinter interface {
387+
Fingerprint(string) (string, error)
388+
}
389+
377390
// formatStmt returns the canonical form of a single statement, or the
378391
// original text when formatting cannot be proven to preserve the query.
379392
func formatStmt(f queryFormatter, raw *ast.RawStmt, orig string, interior []ast.Comment, src string) string {
380393
fallback := strings.TrimSuffix(strings.TrimSpace(orig), ";") + ";"
381394
if out, ok := formatWithComments(f, raw, interior, src); ok {
395+
if fp, ok := f.(fingerprinter); ok && !sameFingerprint(fp, orig, out) {
396+
return fallback
397+
}
382398
return out
383399
}
384400
return fallback
385401
}
386402

403+
// sameFingerprint reports that both texts fingerprint successfully to the
404+
// same value. Anything less is not a proof, so the caller keeps the
405+
// statement as written.
406+
func sameFingerprint(fp fingerprinter, orig, out string) bool {
407+
a, err1 := fp.Fingerprint(orig)
408+
b, err2 := fp.Fingerprint(out)
409+
return err1 == nil && err2 == nil && a == b
410+
}
411+
387412
// formatWithComments pretty-prints a statement with its interior comments
388413
// and proves the result faithful three ways before accepting it: every
389414
// comment survives (multiset equality after reparsing the output), the SQL

internal/endtoend/testdata/fmt/postgresql/stderr.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
--- a/query.sql
2+
+++ b/query.sql
3+
@@ -1,7 +1,8 @@
4+
-- name: GetAuthor :one
5+
-select id,name , bio
6+
-from authors
7+
-where id = $1 limit 1; -- the primary lookup
8+
+SELECT id, name, bio
9+
+FROM authors
10+
+WHERE id = $1
11+
+LIMIT 1; -- the primary lookup
12+
13+
/* This listing powers the admin page.
14+
Keep it ordered by name so the UI stays stable. */
15+
@@ -8,5 +9,6 @@
16+
-- name: ListAuthors :many
17+
-SELECT id, name, bio FROM authors
18+
+SELECT id, name, bio
19+
+FROM authors
20+
ORDER BY name;
21+
22+
-- name: CreateAuthor :one
23+
@@ -13,8 +15,5 @@
24+
-INSERT INTO authors (
25+
- name, bio
26+
-) VALUES (
27+
- $1, $2
28+
-)
29+
+INSERT INTO authors (name, bio)
30+
+VALUES ($1, $2)
31+
RETURNING *;
32+
33+
-- name: PickyQuery :many
34+
@@ -21,5 +20,6 @@
35+
-SELECT id, -- the primary key
36+
- name
37+
+SELECT
38+
+ id, -- the primary key
39+
+ name
40+
FROM authors
41+
WHERE id > $1;
42+
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;
45+
46+
-- name: DeleteAuthor :exec
47+
-DELETE FROM authors WHERE id = @id
48+
+DELETE FROM authors WHERE id = @id;

internal/engine/postgresql/parse.go

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,15 +150,28 @@ type Parser struct {
150150
var errSkip = errors.New("skip stmt")
151151

152152
func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) {
153-
contents, err := io.ReadAll(r)
153+
f, err := p.ParseFile(r)
154154
if err != nil {
155155
return nil, err
156156
}
157-
tree, err := Parse(string(contents))
157+
return f.Stmts, nil
158+
}
159+
160+
// ParseFile parses like Parse and also carries the file's comments, which
161+
// oliphant's parser collects from the same pass its scanner already makes
162+
// over the input.
163+
func (p *Parser) ParseFile(r io.Reader) (*ast.File, error) {
164+
blob, err := io.ReadAll(r)
165+
if err != nil {
166+
return nil, err
167+
}
168+
contents := string(blob)
169+
parsed, err := parser.ParseFile(contents)
158170
if err != nil {
159171
pErr := normalizeErr(err)
160172
return nil, pErr
161173
}
174+
tree := parsed.ParseResult
162175

163176
var stmts []ast.Statement
164177
// PostgreSQL 18 changed stmt_location to point at the statement's first
@@ -203,7 +216,33 @@ func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) {
203216
},
204217
})
205218
}
206-
return stmts, nil
219+
220+
var comments []ast.Comment
221+
for _, tok := range parsed.Comments {
222+
comments = append(comments, ast.Comment{
223+
Text: strings.TrimRight(contents[tok.Start:tok.End], " \t\r\n"),
224+
Start: int(tok.Start),
225+
End: int(tok.End),
226+
OwnLine: ownLine(contents, int(tok.Start)),
227+
})
228+
}
229+
return &ast.File{Stmts: stmts, Comments: comments}, nil
230+
}
231+
232+
// ownLine reports that only blank space sits between the preceding line
233+
// break and pos.
234+
func ownLine(src string, pos int) bool {
235+
for j := pos - 1; j >= 0; j-- {
236+
switch src[j] {
237+
case '\n':
238+
return true
239+
case ' ', '\t', '\r':
240+
continue
241+
default:
242+
return false
243+
}
244+
}
245+
return true
207246
}
208247

209248
func normalizeErr(err error) error {

internal/engine/postgresql/reserved.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ func (p *Parser) Cast(arg, typeName string) string {
7070
return arg + "::" + typeName
7171
}
7272

73+
// Fingerprint reduces a query to pg_query's fingerprint, which survives
74+
// changes in whitespace, case and layout. sqlc fmt uses it to prove a
75+
// formatted statement still parses to the same query.
76+
func (p *Parser) Fingerprint(sql string) (string, error) {
77+
return Fingerprint(sql)
78+
}
79+
7380
// https://www.postgresql.org/docs/current/sql-keywords-appendix.html
7481
func (p *Parser) IsReservedKeyword(s string) bool {
7582
switch strings.ToLower(s) {

0 commit comments

Comments
 (0)