From d9b243b9e0b3da89a1f157e16481ce321d79951d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 18:27:15 +0000 Subject: [PATCH 1/2] compiler: expand stars in the query text on the core path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The core analyzer already resolved a star to the columns it covers, but only for the result set — the query it handed to codegen still said "SELECT *", so the generated SQL asked the database for whatever the table happened to hold at run time rather than the columns sqlc scanned into. Every case in the corpus that selects a star generated different code through the core than through the legacy path. The analyzer now reports each star along with the columns it stands for, sharing one list with the analyzers of the queries nested in it so a statement reports the stars in its subqueries and CTEs too. The compiler turns those into edits on the query text, which is where the engine's quoting rules and SQLite's jsonb wrapping live and where the legacy path already does the same rewrite — the two now produce byte-identical SQL across the corpus. Rewriting means reparsing to check the edit produced valid SQL, and the core path analyzes statements concurrently. A parser holds too much state for two goroutines to share one, so the compiler keeps the constructor and a goroutine builds its own. 181 of the 527 cases failing under the core context now pass. --- internal/compiler/engine.go | 17 +- internal/compiler/expand.go | 62 +++--- internal/compiler/expand_core.go | 74 +++++++ internal/compiler/parse_core.go | 16 ++ internal/core/analysis.go | 36 +++- internal/core/analyzer/analyzer.go | 20 +- internal/core/analyzer/projection.go | 17 +- .../mysql/go/query.sql.go | 2 +- .../postgresql/stdlib/go/query.sql.go | 4 +- .../sqlite/go/query.sql.go | 4 +- .../star_expansion_core/mysql/exec.json | 6 + .../star_expansion_core/mysql/go/db.go | 31 +++ .../star_expansion_core/mysql/go/models.go | 20 ++ .../star_expansion_core/mysql/go/query.sql.go | 182 ++++++++++++++++ .../star_expansion_core/mysql/query.sql | 14 ++ .../star_expansion_core/mysql/schema.sql | 2 + .../star_expansion_core/mysql/sqlc.json | 12 ++ .../star_expansion_core/postgresql/exec.json | 6 + .../star_expansion_core/postgresql/go/db.go | 31 +++ .../postgresql/go/models.go | 20 ++ .../postgresql/go/query.sql.go | 198 ++++++++++++++++++ .../star_expansion_core/postgresql/query.sql | 17 ++ .../star_expansion_core/postgresql/schema.sql | 2 + .../star_expansion_core/postgresql/sqlc.json | 12 ++ .../star_expansion_core/sqlite/exec.json | 6 + .../star_expansion_core/sqlite/go/db.go | 31 +++ .../star_expansion_core/sqlite/go/models.go | 20 ++ .../sqlite/go/query.sql.go | 198 ++++++++++++++++++ .../star_expansion_core/sqlite/query.sql | 17 ++ .../star_expansion_core/sqlite/schema.sql | 2 + .../star_expansion_core/sqlite/sqlc.json | 12 ++ 31 files changed, 1046 insertions(+), 45 deletions(-) create mode 100644 internal/compiler/expand_core.go create mode 100644 internal/endtoend/testdata/star_expansion_core/mysql/exec.json create mode 100644 internal/endtoend/testdata/star_expansion_core/mysql/go/db.go create mode 100644 internal/endtoend/testdata/star_expansion_core/mysql/go/models.go create mode 100644 internal/endtoend/testdata/star_expansion_core/mysql/go/query.sql.go create mode 100644 internal/endtoend/testdata/star_expansion_core/mysql/query.sql create mode 100644 internal/endtoend/testdata/star_expansion_core/mysql/schema.sql create mode 100644 internal/endtoend/testdata/star_expansion_core/mysql/sqlc.json create mode 100644 internal/endtoend/testdata/star_expansion_core/postgresql/exec.json create mode 100644 internal/endtoend/testdata/star_expansion_core/postgresql/go/db.go create mode 100644 internal/endtoend/testdata/star_expansion_core/postgresql/go/models.go create mode 100644 internal/endtoend/testdata/star_expansion_core/postgresql/go/query.sql.go create mode 100644 internal/endtoend/testdata/star_expansion_core/postgresql/query.sql create mode 100644 internal/endtoend/testdata/star_expansion_core/postgresql/schema.sql create mode 100644 internal/endtoend/testdata/star_expansion_core/postgresql/sqlc.json create mode 100644 internal/endtoend/testdata/star_expansion_core/sqlite/exec.json create mode 100644 internal/endtoend/testdata/star_expansion_core/sqlite/go/db.go create mode 100644 internal/endtoend/testdata/star_expansion_core/sqlite/go/models.go create mode 100644 internal/endtoend/testdata/star_expansion_core/sqlite/go/query.sql.go create mode 100644 internal/endtoend/testdata/star_expansion_core/sqlite/query.sql create mode 100644 internal/endtoend/testdata/star_expansion_core/sqlite/schema.sql create mode 100644 internal/endtoend/testdata/star_expansion_core/sqlite/sqlc.json diff --git a/internal/compiler/engine.go b/internal/compiler/engine.go index b830faaa19..808f225bd3 100644 --- a/internal/compiler/engine.go +++ b/internal/compiler/engine.go @@ -38,6 +38,12 @@ type Compiler struct { coreAnalysis bool coreDialect core.Option + // newParser builds a parser for the configured engine. The core path + // analyzes statements concurrently, and a parser holds enough state that + // two goroutines cannot share one, so a goroutine that has to parse + // something of its own — the query text it just rewrote — builds its own. + newParser func() Parser + schema []string } @@ -119,28 +125,29 @@ func (c *Compiler) initCore() error { var dialect core.Option switch c.conf.Engine { case config.EngineSQLite: - c.parser = sqlite.NewParser() + c.newParser = func() Parser { return sqlite.NewParser() } c.selector = newSQLiteSelector() dialect = sqlite.Dialect() case config.EngineMySQL: - c.parser = dolphin.NewParser() + c.newParser = func() Parser { return dolphin.NewParser() } c.selector = newDefaultSelector() dialect = dolphin.Dialect() case config.EnginePostgreSQL: - c.parser = postgresql.NewParser() + c.newParser = func() Parser { return postgresql.NewParser() } c.selector = newDefaultSelector() dialect = postgresql.Dialect() case config.EngineClickHouse: - c.parser = clickhouse.NewParser() + c.newParser = func() Parser { return clickhouse.NewParser() } c.selector = newDefaultSelector() dialect = clickhouse.Dialect() case config.EngineGoogleSQL: - c.parser = googlesql.NewParser() + c.newParser = func() Parser { return googlesql.NewParser() } c.selector = newDefaultSelector() dialect = googlesql.Dialect() default: return fmt.Errorf("unknown engine: %s", c.conf.Engine) } + c.parser = c.newParser() c.coreDialect = dialect return nil } diff --git a/internal/compiler/expand.go b/internal/compiler/expand.go index 98dd82cbdc..e76853c5a3 100644 --- a/internal/compiler/expand.go +++ b/internal/compiler/expand.go @@ -78,6 +78,38 @@ func (c *Compiler) quote(x string) string { } } +// starOldFunc measures how much of the query text a star reference occupies, +// so an edit replaces the reference and nothing else. Each part is measured +// both bare and quoted: an embed was rewritten to "table.*" in the query text, +// preserving the way the user quoted the table, so it is measured the same way +// as a star reference the user wrote. +func (c *Compiler) starOldFunc(parts []string) func(string) int { + old := make([]string, 0, len(parts)) + for _, p := range parts { + if p == "*" { + old = append(old, p) + } else { + old = append(old, c.quoteIdent(p)) + } + } + return func(s string) int { + length := 0 + for i, o := range old { + if hasSeparator := i > 0; hasSeparator { + length++ + } + if strings.HasPrefix(s[length:], o) { + length += len(o) + } else if quoted := c.quote(o); strings.HasPrefix(s[length:], quoted) { + length += len(quoted) + } else { + length += len(o) + } + } + return length + } +} + func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node) ([]source.Edit, error) { tables, err := c.sourceTables(qc, node) if err != nil { @@ -157,37 +189,9 @@ func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node) cols = append(cols, cname) } } - var old []string - for _, p := range parts { - if p == "*" { - old = append(old, p) - } else { - old = append(old, c.quoteIdent(p)) - } - } - - // An embed was rewritten to "table.*" in the query text, so it is - // measured the same way as a star reference the user wrote. - oldFunc := func(s string) int { - length := 0 - for i, o := range old { - if hasSeparator := i > 0; hasSeparator { - length++ - } - if strings.HasPrefix(s[length:], o) { - length += len(o) - } else if quoted := c.quote(o); strings.HasPrefix(s[length:], quoted) { - length += len(quoted) - } else { - length += len(o) - } - } - return length - } - edits = append(edits, source.Edit{ Location: res.Location - raw.StmtLocation, - OldFunc: oldFunc, + OldFunc: c.starOldFunc(parts), New: strings.Join(cols, ", "), }) } diff --git a/internal/compiler/expand_core.go b/internal/compiler/expand_core.go new file mode 100644 index 0000000000..aa23166fca --- /dev/null +++ b/internal/compiler/expand_core.go @@ -0,0 +1,74 @@ +package compiler + +import ( + "strings" + + "github.com/sqlc-dev/sqlc/internal/core" + "github.com/sqlc-dev/sqlc/internal/source" + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// expandCore rewrites the stars in a query's text with the columns the core +// analyzer resolved them to. The analyzer has already walked the statement and +// its subqueries, so there is nothing left to look up here: what remains is +// deciding how each name is written, which is the engine's business and not +// the core's. +func (c *Compiler) expandCore(raw *ast.RawStmt, stars []core.StarExpansion) ([]source.Edit, error) { + if len(stars) == 0 { + return nil, nil + } + edits := make([]source.Edit, 0, len(stars)) + seen := make(map[int]bool, len(stars)) + for _, star := range stars { + // A statement analyzed more than once — the same CTE referenced twice, + // say — reports its stars once per pass. Editing one twice would + // overlap, so only the first is kept. + if seen[star.Location] { + continue + } + seen[star.Location] = true + + // Everything before the star qualifies it: "foo.*" is scoped to foo, + // while a bare "*" covers every relation in the FROM clause. + scope := strings.Join(star.Fields[:len(star.Fields)-1], ".") + + // An unqualified star that covers more than one relation may name the + // same column twice, so those are written with their relation. + counts := map[string]int{} + if scope == "" { + for _, col := range star.Columns { + counts[col.Name]++ + } + } + + cols := make([]string, 0, len(star.Columns)) + for _, col := range star.Columns { + cname := col.Name + if star.Alias != "" { + cname = star.Alias + } + cname = c.quoteIdent(cname) + if scope != "" { + cname = c.quoteIdent(scope) + "." + cname + } + if counts[cname] > 1 { + cname = c.quoteIdent(col.Relation) + "." + cname + } + + // This is important for SQLite in particular which needs to wrap + // jsonb column values with `json(colname)` so they're in a publicly + // usable format (i.e. not jsonb). + cols = append(cols, c.selector.ColumnExpr(cname, &Column{ + Name: col.Name, + DataType: col.DataType, + })) + } + + edits = append(edits, source.Edit{ + Location: star.Location - raw.StmtLocation, + OldFunc: c.starOldFunc(star.Fields), + New: strings.Join(cols, ", "), + }) + } + return edits, nil +} diff --git a/internal/compiler/parse_core.go b/internal/compiler/parse_core.go index 920d3140fa..b0c4519160 100644 --- a/internal/compiler/parse_core.go +++ b/internal/compiler/parse_core.go @@ -2,6 +2,7 @@ package compiler import ( "errors" + "fmt" "strings" "github.com/sqlc-dev/sqlc/internal/core" @@ -64,6 +65,21 @@ func (c *Compiler) parseQueryCore(raw *ast.RawStmt, src string, pre *preprocess. for _, p := range res.Parameters { params = append(params, Parameter{Number: p.Number, Column: coreParamColumn(p, namedParams)}) } + edits, err := c.expandCore(raw, res.Stars) + if err != nil { + return nil, err + } + expanded, err = source.Mutate(rawSQL, edits) + if err != nil { + return nil, err + } + } + + // If the query string was edited, make sure the syntax is valid + if expanded != rawSQL { + if _, err := c.newParser().Parse(strings.NewReader(expanded)); err != nil { + return nil, fmt.Errorf("edited query syntax is invalid: %w", err) + } } trimmed, comments, err := source.StripComments(expanded) diff --git a/internal/core/analysis.go b/internal/core/analysis.go index e8e2edf851..822c12c411 100644 --- a/internal/core/analysis.go +++ b/internal/core/analysis.go @@ -10,9 +10,39 @@ const ( ) type PrepareResult struct { - Command Command `json:"command,omitempty"` - Columns []Column `json:"columns"` - Parameters []Parameter `json:"parameters"` + Command Command `json:"command,omitempty"` + Columns []Column `json:"columns"` + Parameters []Parameter `json:"parameters"` + Stars []StarExpansion `json:"stars,omitempty"` +} + +// StarExpansion is what a star in a target list stands for. The analyzer +// resolves the reference against the query's scope and reports the columns it +// covers; rewriting the query text with them is the caller's to do, since only +// it knows how the engine quotes an identifier. +type StarExpansion struct { + // Location is where the target the star belongs to starts, measured the + // way the AST measures a node: from the beginning of the file the + // statement was parsed from. + Location int `json:"location"` + + // Fields is the reference as it was written, with the star as its last + // element: ["*"] for a bare star and ["foo", "*"] for a qualified one. + Fields []string `json:"fields"` + + // Alias is the output name the target was given, if any. + Alias string `json:"alias,omitempty"` + + Columns []StarColumn `json:"columns"` +} + +// StarColumn is a single column a star expanded to. +type StarColumn struct { + // Relation is the name the column's relation goes by in the query, which + // is its alias when it was given one. + Relation string `json:"relation,omitempty"` + Name string `json:"name"` + DataType string `json:"data_type,omitempty"` } type ColumnSource struct { diff --git a/internal/core/analyzer/analyzer.go b/internal/core/analyzer/analyzer.go index 7096a70bb7..4608ff1bdc 100644 --- a/internal/core/analyzer/analyzer.go +++ b/internal/core/analyzer/analyzer.go @@ -14,6 +14,7 @@ func Prepare(cat *core.Catalog, stmt ast.Node) (core.PrepareResult, error) { a := &analyzer{ cat: cat, params: map[int]core.Parameter{}, + stars: &[]core.StarExpansion{}, } switch s := stmt.(type) { case *ast.SelectStmt: @@ -63,6 +64,18 @@ type analyzer struct { // resolving guards against an alias that refers to itself. resolving map[string]bool + + // stars are the expansions every star in the statement asked for, shared + // with the analyzers of the queries nested in it so one statement reports + // all of them. + stars *[]core.StarExpansion +} + +func (a *analyzer) recordStar(s core.StarExpansion) { + if a.stars == nil { + return + } + *a.stars = append(*a.stars, s) } // subquery analyzes a nested SELECT. It shares the parameter set, so a @@ -74,6 +87,7 @@ func (a *analyzer) subquery(s *ast.SelectStmt) (*analyzer, error) { params: a.params, outer: a.scope, ctes: a.ctes, + stars: a.stars, } if err := sub.analyzeSelect(s); err != nil { return nil, err @@ -105,11 +119,15 @@ func derivedRel(alias string, cols []core.Column) scopeRel { } func (a *analyzer) result() core.PrepareResult { - return core.PrepareResult{ + res := core.PrepareResult{ Command: a.command, Columns: a.columns, Parameters: orderedParams(a.params), } + if a.stars != nil { + res.Stars = *a.stars + } + return res } func orderedParams(m map[int]core.Parameter) []core.Parameter { diff --git a/internal/core/analyzer/projection.go b/internal/core/analyzer/projection.go index 9547eed833..29f1f7c548 100644 --- a/internal/core/analyzer/projection.go +++ b/internal/core/analyzer/projection.go @@ -14,7 +14,7 @@ func (a *analyzer) projectTarget(rt *ast.ResTarget) error { if cr, ok := rt.Val.(*ast.ColumnRef); ok { fields = flattenFields(cr.Fields) if isStar(fields) { - a.emitStar(fields) + a.emitStar(rt, fields) return nil } } @@ -79,16 +79,23 @@ func isStar(fields []string) bool { return len(fields) > 0 && fields[len(fields)-1] == "*" } -func (a *analyzer) emitStar(fields []string) { +func (a *analyzer) emitStar(rt *ast.ResTarget, fields []string) { relName := "" if len(fields) > 1 { relName = fields[0] } + // The star is reported along with the columns it covers, so the query text + // can be rewritten to name them. + star := core.StarExpansion{Location: rt.Location, Fields: fields} + if rt.Name != nil { + star.Alias = *rt.Name + } for _, rel := range a.scope.rels { if relName != "" && rel.alias != relName { continue } a.columns = slices.Grow(a.columns, len(rel.cols)) + star.Columns = slices.Grow(star.Columns, len(rel.cols)) for _, c := range rel.cols { col := core.Column{ Name: c.Name, @@ -100,6 +107,12 @@ func (a *analyzer) emitStar(fields []string) { col.DataType, col.IsArray = a.typeNameOf(exprType{typeOID: c.TypeOID}) a.decorateSource(&col, c.AttOID, rel.alias) a.columns = append(a.columns, col) + star.Columns = append(star.Columns, core.StarColumn{ + Relation: rel.alias, + Name: c.Name, + DataType: col.DataType, + }) } } + a.recordStar(star) } diff --git a/internal/endtoend/testdata/experiment_coreanalyzer/mysql/go/query.sql.go b/internal/endtoend/testdata/experiment_coreanalyzer/mysql/go/query.sql.go index ea37ba27da..69c870213b 100644 --- a/internal/endtoend/testdata/experiment_coreanalyzer/mysql/go/query.sql.go +++ b/internal/endtoend/testdata/experiment_coreanalyzer/mysql/go/query.sql.go @@ -36,7 +36,7 @@ func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error { } const getAuthor = `-- name: GetAuthor :one -SELECT * FROM authors +SELECT id, name, bio FROM authors WHERE id = ? ` diff --git a/internal/endtoend/testdata/experiment_coreanalyzer/postgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/experiment_coreanalyzer/postgresql/stdlib/go/query.sql.go index 706ce0e529..2e2ced077f 100644 --- a/internal/endtoend/testdata/experiment_coreanalyzer/postgresql/stdlib/go/query.sql.go +++ b/internal/endtoend/testdata/experiment_coreanalyzer/postgresql/stdlib/go/query.sql.go @@ -15,7 +15,7 @@ import ( const createAuthor = `-- name: CreateAuthor :one INSERT INTO authors (id, name, bio) VALUES ($1, $2, $3) -RETURNING * +RETURNING id, name, bio, tags ` type CreateAuthorParams struct { @@ -47,7 +47,7 @@ func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error { } const getAuthor = `-- name: GetAuthor :one -SELECT * FROM authors +SELECT id, name, bio, tags FROM authors WHERE id = $1 ` diff --git a/internal/endtoend/testdata/experiment_coreanalyzer/sqlite/go/query.sql.go b/internal/endtoend/testdata/experiment_coreanalyzer/sqlite/go/query.sql.go index db27b7ced6..cc56c0a7d5 100644 --- a/internal/endtoend/testdata/experiment_coreanalyzer/sqlite/go/query.sql.go +++ b/internal/endtoend/testdata/experiment_coreanalyzer/sqlite/go/query.sql.go @@ -13,7 +13,7 @@ import ( const createAuthor = `-- name: CreateAuthor :one INSERT INTO authors (id, name, bio) VALUES (?, ?, ?) -RETURNING * +RETURNING id, name, bio ` type CreateAuthorParams struct { @@ -40,7 +40,7 @@ func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error { } const getAuthor = `-- name: GetAuthor :one -SELECT * FROM authors +SELECT id, name, bio FROM authors WHERE id = ? ` diff --git a/internal/endtoend/testdata/star_expansion_core/mysql/exec.json b/internal/endtoend/testdata/star_expansion_core/mysql/exec.json new file mode 100644 index 0000000000..8da46d61b5 --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/mysql/exec.json @@ -0,0 +1,6 @@ +{ + "command": "generate", + "env": { + "SQLCEXPERIMENT": "coreanalyzer" + } +} diff --git a/internal/endtoend/testdata/star_expansion_core/mysql/go/db.go b/internal/endtoend/testdata/star_expansion_core/mysql/go/db.go new file mode 100644 index 0000000000..80dd6ab1f6 --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/mysql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/star_expansion_core/mysql/go/models.go b/internal/endtoend/testdata/star_expansion_core/mysql/go/models.go new file mode 100644 index 0000000000..7d8b997fea --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/mysql/go/models.go @@ -0,0 +1,20 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Bar struct { + A sql.NullString + C sql.NullString +} + +type Foo struct { + A sql.NullString + B sql.NullString + Group sql.NullString +} diff --git a/internal/endtoend/testdata/star_expansion_core/mysql/go/query.sql.go b/internal/endtoend/testdata/star_expansion_core/mysql/go/query.sql.go new file mode 100644 index 0000000000..cbf431b0e3 --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/mysql/go/query.sql.go @@ -0,0 +1,182 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const starExpansion = `-- name: StarExpansion :many +SELECT a, b, ` + "`" + `group` + "`" + `, a, b, ` + "`" + `group` + "`" + `, foo.a, foo.b, foo.` + "`" + `group` + "`" + ` FROM foo +` + +type StarExpansionRow struct { + A sql.NullString + B sql.NullString + Group sql.NullString + A_2 sql.NullString + B_2 sql.NullString + Group_2 sql.NullString + A_3 sql.NullString + B_3 sql.NullString + Group_3 sql.NullString +} + +func (q *Queries) StarExpansion(ctx context.Context) ([]StarExpansionRow, error) { + rows, err := q.db.QueryContext(ctx, starExpansion) + if err != nil { + return nil, err + } + defer rows.Close() + var items []StarExpansionRow + for rows.Next() { + var i StarExpansionRow + if err := rows.Scan( + &i.A, + &i.B, + &i.Group, + &i.A_2, + &i.B_2, + &i.Group_2, + &i.A_3, + &i.B_3, + &i.Group_3, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const starExpansionCTE = `-- name: StarExpansionCTE :many +WITH t AS (SELECT a, c FROM bar) SELECT a, c FROM t +` + +func (q *Queries) StarExpansionCTE(ctx context.Context) ([]Bar, error) { + rows, err := q.db.QueryContext(ctx, starExpansionCTE) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Bar + for rows.Next() { + var i Bar + if err := rows.Scan(&i.A, &i.C); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const starExpansionJoin = `-- name: StarExpansionJoin :many +SELECT foo.a, b, ` + "`" + `group` + "`" + `, bar.a, c FROM foo, bar +` + +type StarExpansionJoinRow struct { + A sql.NullString + B sql.NullString + Group sql.NullString + A_2 sql.NullString + C sql.NullString +} + +func (q *Queries) StarExpansionJoin(ctx context.Context) ([]StarExpansionJoinRow, error) { + rows, err := q.db.QueryContext(ctx, starExpansionJoin) + if err != nil { + return nil, err + } + defer rows.Close() + var items []StarExpansionJoinRow + for rows.Next() { + var i StarExpansionJoinRow + if err := rows.Scan( + &i.A, + &i.B, + &i.Group, + &i.A_2, + &i.C, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const starExpansionSubquery = `-- name: StarExpansionSubquery :many +SELECT a, c FROM (SELECT a, c FROM bar) sub +` + +func (q *Queries) StarExpansionSubquery(ctx context.Context) ([]Bar, error) { + rows, err := q.db.QueryContext(ctx, starExpansionSubquery) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Bar + for rows.Next() { + var i Bar + if err := rows.Scan(&i.A, &i.C); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const starQuotedExpansion = `-- name: StarQuotedExpansion :many +SELECT t.a, t.b, t.` + "`" + `group` + "`" + ` FROM foo ` + "`" + `t` + "`" + ` +` + +func (q *Queries) StarQuotedExpansion(ctx context.Context) ([]Foo, error) { + rows, err := q.db.QueryContext(ctx, starQuotedExpansion) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Foo + for rows.Next() { + var i Foo + if err := rows.Scan(&i.A, &i.B, &i.Group); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/star_expansion_core/mysql/query.sql b/internal/endtoend/testdata/star_expansion_core/mysql/query.sql new file mode 100644 index 0000000000..036504e238 --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/mysql/query.sql @@ -0,0 +1,14 @@ +-- name: StarExpansion :many +SELECT *, *, foo.* FROM foo; + +-- name: StarQuotedExpansion :many +SELECT `t`.* FROM foo `t`; + +-- name: StarExpansionJoin :many +SELECT * FROM foo, bar; + +-- name: StarExpansionSubquery :many +SELECT * FROM (SELECT * FROM bar) sub; + +-- name: StarExpansionCTE :many +WITH t AS (SELECT * FROM bar) SELECT * FROM t; diff --git a/internal/endtoend/testdata/star_expansion_core/mysql/schema.sql b/internal/endtoend/testdata/star_expansion_core/mysql/schema.sql new file mode 100644 index 0000000000..fa1f85d923 --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/mysql/schema.sql @@ -0,0 +1,2 @@ +CREATE TABLE foo (a text, b text, `group` text); +CREATE TABLE bar (a text, c text); diff --git a/internal/endtoend/testdata/star_expansion_core/mysql/sqlc.json b/internal/endtoend/testdata/star_expansion_core/mysql/sqlc.json new file mode 100644 index 0000000000..974aa9ff9e --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/mysql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "engine": "mysql", + "path": "go", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/star_expansion_core/postgresql/exec.json b/internal/endtoend/testdata/star_expansion_core/postgresql/exec.json new file mode 100644 index 0000000000..8da46d61b5 --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/postgresql/exec.json @@ -0,0 +1,6 @@ +{ + "command": "generate", + "env": { + "SQLCEXPERIMENT": "coreanalyzer" + } +} diff --git a/internal/endtoend/testdata/star_expansion_core/postgresql/go/db.go b/internal/endtoend/testdata/star_expansion_core/postgresql/go/db.go new file mode 100644 index 0000000000..80dd6ab1f6 --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/postgresql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/star_expansion_core/postgresql/go/models.go b/internal/endtoend/testdata/star_expansion_core/postgresql/go/models.go new file mode 100644 index 0000000000..7d8b997fea --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/postgresql/go/models.go @@ -0,0 +1,20 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Bar struct { + A sql.NullString + C sql.NullString +} + +type Foo struct { + A sql.NullString + B sql.NullString + Group sql.NullString +} diff --git a/internal/endtoend/testdata/star_expansion_core/postgresql/go/query.sql.go b/internal/endtoend/testdata/star_expansion_core/postgresql/go/query.sql.go new file mode 100644 index 0000000000..cbdf5b51a8 --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/postgresql/go/query.sql.go @@ -0,0 +1,198 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const starExpansion = `-- name: StarExpansion :many +SELECT a, b, "group", a, b, "group", foo.a, foo.b, foo."group" FROM foo +` + +type StarExpansionRow struct { + A sql.NullString + B sql.NullString + Group sql.NullString + A_2 sql.NullString + B_2 sql.NullString + Group_2 sql.NullString + A_3 sql.NullString + B_3 sql.NullString + Group_3 sql.NullString +} + +func (q *Queries) StarExpansion(ctx context.Context) ([]StarExpansionRow, error) { + rows, err := q.db.QueryContext(ctx, starExpansion) + if err != nil { + return nil, err + } + defer rows.Close() + var items []StarExpansionRow + for rows.Next() { + var i StarExpansionRow + if err := rows.Scan( + &i.A, + &i.B, + &i.Group, + &i.A_2, + &i.B_2, + &i.Group_2, + &i.A_3, + &i.B_3, + &i.Group_3, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const starExpansionCTE = `-- name: StarExpansionCTE :many +WITH t AS (SELECT a, c FROM bar) SELECT a, c FROM t +` + +func (q *Queries) StarExpansionCTE(ctx context.Context) ([]Bar, error) { + rows, err := q.db.QueryContext(ctx, starExpansionCTE) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Bar + for rows.Next() { + var i Bar + if err := rows.Scan(&i.A, &i.C); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const starExpansionJoin = `-- name: StarExpansionJoin :many +SELECT foo.a, b, "group", bar.a, c FROM foo, bar +` + +type StarExpansionJoinRow struct { + A sql.NullString + B sql.NullString + Group sql.NullString + A_2 sql.NullString + C sql.NullString +} + +func (q *Queries) StarExpansionJoin(ctx context.Context) ([]StarExpansionJoinRow, error) { + rows, err := q.db.QueryContext(ctx, starExpansionJoin) + if err != nil { + return nil, err + } + defer rows.Close() + var items []StarExpansionJoinRow + for rows.Next() { + var i StarExpansionJoinRow + if err := rows.Scan( + &i.A, + &i.B, + &i.Group, + &i.A_2, + &i.C, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const starExpansionReturning = `-- name: StarExpansionReturning :one +INSERT INTO bar (a, c) VALUES ($1, $2) RETURNING a, c +` + +type StarExpansionReturningParams struct { + A sql.NullString + C sql.NullString +} + +func (q *Queries) StarExpansionReturning(ctx context.Context, arg StarExpansionReturningParams) (Bar, error) { + row := q.db.QueryRowContext(ctx, starExpansionReturning, arg.A, arg.C) + var i Bar + err := row.Scan(&i.A, &i.C) + return i, err +} + +const starExpansionSubquery = `-- name: StarExpansionSubquery :many +SELECT a, c FROM (SELECT a, c FROM bar) sub +` + +func (q *Queries) StarExpansionSubquery(ctx context.Context) ([]Bar, error) { + rows, err := q.db.QueryContext(ctx, starExpansionSubquery) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Bar + for rows.Next() { + var i Bar + if err := rows.Scan(&i.A, &i.C); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const starQuotedExpansion = `-- name: StarQuotedExpansion :many +SELECT t.a, t.b, t."group" FROM foo "t" +` + +func (q *Queries) StarQuotedExpansion(ctx context.Context) ([]Foo, error) { + rows, err := q.db.QueryContext(ctx, starQuotedExpansion) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Foo + for rows.Next() { + var i Foo + if err := rows.Scan(&i.A, &i.B, &i.Group); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/star_expansion_core/postgresql/query.sql b/internal/endtoend/testdata/star_expansion_core/postgresql/query.sql new file mode 100644 index 0000000000..a6e4b9352d --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/postgresql/query.sql @@ -0,0 +1,17 @@ +-- name: StarExpansion :many +SELECT *, *, foo.* FROM foo; + +-- name: StarQuotedExpansion :many +SELECT "t".* FROM foo "t"; + +-- name: StarExpansionJoin :many +SELECT * FROM foo, bar; + +-- name: StarExpansionSubquery :many +SELECT * FROM (SELECT * FROM bar) sub; + +-- name: StarExpansionCTE :many +WITH t AS (SELECT * FROM bar) SELECT * FROM t; + +-- name: StarExpansionReturning :one +INSERT INTO bar (a, c) VALUES ($1, $2) RETURNING *; diff --git a/internal/endtoend/testdata/star_expansion_core/postgresql/schema.sql b/internal/endtoend/testdata/star_expansion_core/postgresql/schema.sql new file mode 100644 index 0000000000..fb4a21842a --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/postgresql/schema.sql @@ -0,0 +1,2 @@ +CREATE TABLE foo (a text, b text, "group" text); +CREATE TABLE bar (a text, c text); diff --git a/internal/endtoend/testdata/star_expansion_core/postgresql/sqlc.json b/internal/endtoend/testdata/star_expansion_core/postgresql/sqlc.json new file mode 100644 index 0000000000..cd518671ac --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/postgresql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "engine": "postgresql", + "path": "go", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/star_expansion_core/sqlite/exec.json b/internal/endtoend/testdata/star_expansion_core/sqlite/exec.json new file mode 100644 index 0000000000..8da46d61b5 --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/sqlite/exec.json @@ -0,0 +1,6 @@ +{ + "command": "generate", + "env": { + "SQLCEXPERIMENT": "coreanalyzer" + } +} diff --git a/internal/endtoend/testdata/star_expansion_core/sqlite/go/db.go b/internal/endtoend/testdata/star_expansion_core/sqlite/go/db.go new file mode 100644 index 0000000000..80dd6ab1f6 --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/sqlite/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/star_expansion_core/sqlite/go/models.go b/internal/endtoend/testdata/star_expansion_core/sqlite/go/models.go new file mode 100644 index 0000000000..7d8b997fea --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/sqlite/go/models.go @@ -0,0 +1,20 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Bar struct { + A sql.NullString + C sql.NullString +} + +type Foo struct { + A sql.NullString + B sql.NullString + Group sql.NullString +} diff --git a/internal/endtoend/testdata/star_expansion_core/sqlite/go/query.sql.go b/internal/endtoend/testdata/star_expansion_core/sqlite/go/query.sql.go new file mode 100644 index 0000000000..7a97f1e9da --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/sqlite/go/query.sql.go @@ -0,0 +1,198 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const starExpansion = `-- name: StarExpansion :many +SELECT a, b, "group", a, b, "group", foo.a, foo.b, foo."group" FROM foo +` + +type StarExpansionRow struct { + A sql.NullString + B sql.NullString + Group sql.NullString + A_2 sql.NullString + B_2 sql.NullString + Group_2 sql.NullString + A_3 sql.NullString + B_3 sql.NullString + Group_3 sql.NullString +} + +func (q *Queries) StarExpansion(ctx context.Context) ([]StarExpansionRow, error) { + rows, err := q.db.QueryContext(ctx, starExpansion) + if err != nil { + return nil, err + } + defer rows.Close() + var items []StarExpansionRow + for rows.Next() { + var i StarExpansionRow + if err := rows.Scan( + &i.A, + &i.B, + &i.Group, + &i.A_2, + &i.B_2, + &i.Group_2, + &i.A_3, + &i.B_3, + &i.Group_3, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const starExpansionCTE = `-- name: StarExpansionCTE :many +WITH t AS (SELECT a, c FROM bar) SELECT a, c FROM t +` + +func (q *Queries) StarExpansionCTE(ctx context.Context) ([]Bar, error) { + rows, err := q.db.QueryContext(ctx, starExpansionCTE) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Bar + for rows.Next() { + var i Bar + if err := rows.Scan(&i.A, &i.C); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const starExpansionJoin = `-- name: StarExpansionJoin :many +SELECT foo.a, b, "group", bar.a, c FROM foo, bar +` + +type StarExpansionJoinRow struct { + A sql.NullString + B sql.NullString + Group sql.NullString + A_2 sql.NullString + C sql.NullString +} + +func (q *Queries) StarExpansionJoin(ctx context.Context) ([]StarExpansionJoinRow, error) { + rows, err := q.db.QueryContext(ctx, starExpansionJoin) + if err != nil { + return nil, err + } + defer rows.Close() + var items []StarExpansionJoinRow + for rows.Next() { + var i StarExpansionJoinRow + if err := rows.Scan( + &i.A, + &i.B, + &i.Group, + &i.A_2, + &i.C, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const starExpansionReturning = `-- name: StarExpansionReturning :one +INSERT INTO bar (a, c) VALUES (?, ?) RETURNING a, c +` + +type StarExpansionReturningParams struct { + A sql.NullString + C sql.NullString +} + +func (q *Queries) StarExpansionReturning(ctx context.Context, arg StarExpansionReturningParams) (Bar, error) { + row := q.db.QueryRowContext(ctx, starExpansionReturning, arg.A, arg.C) + var i Bar + err := row.Scan(&i.A, &i.C) + return i, err +} + +const starExpansionSubquery = `-- name: StarExpansionSubquery :many +SELECT a, c FROM (SELECT a, c FROM bar) sub +` + +func (q *Queries) StarExpansionSubquery(ctx context.Context) ([]Bar, error) { + rows, err := q.db.QueryContext(ctx, starExpansionSubquery) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Bar + for rows.Next() { + var i Bar + if err := rows.Scan(&i.A, &i.C); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const starQuotedExpansion = `-- name: StarQuotedExpansion :many +SELECT t.a, t.b, t."group" FROM foo "t" +` + +func (q *Queries) StarQuotedExpansion(ctx context.Context) ([]Foo, error) { + rows, err := q.db.QueryContext(ctx, starQuotedExpansion) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Foo + for rows.Next() { + var i Foo + if err := rows.Scan(&i.A, &i.B, &i.Group); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/star_expansion_core/sqlite/query.sql b/internal/endtoend/testdata/star_expansion_core/sqlite/query.sql new file mode 100644 index 0000000000..f58708c449 --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/sqlite/query.sql @@ -0,0 +1,17 @@ +-- name: StarExpansion :many +SELECT *, *, foo.* FROM foo; + +-- name: StarQuotedExpansion :many +SELECT "t".* FROM foo "t"; + +-- name: StarExpansionJoin :many +SELECT * FROM foo, bar; + +-- name: StarExpansionSubquery :many +SELECT * FROM (SELECT * FROM bar) sub; + +-- name: StarExpansionCTE :many +WITH t AS (SELECT * FROM bar) SELECT * FROM t; + +-- name: StarExpansionReturning :one +INSERT INTO bar (a, c) VALUES (?, ?) RETURNING *; diff --git a/internal/endtoend/testdata/star_expansion_core/sqlite/schema.sql b/internal/endtoend/testdata/star_expansion_core/sqlite/schema.sql new file mode 100644 index 0000000000..fb4a21842a --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/sqlite/schema.sql @@ -0,0 +1,2 @@ +CREATE TABLE foo (a text, b text, "group" text); +CREATE TABLE bar (a text, c text); diff --git a/internal/endtoend/testdata/star_expansion_core/sqlite/sqlc.json b/internal/endtoend/testdata/star_expansion_core/sqlite/sqlc.json new file mode 100644 index 0000000000..1f9d43df5d --- /dev/null +++ b/internal/endtoend/testdata/star_expansion_core/sqlite/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "engine": "sqlite", + "path": "go", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} From 2a8278c72b3047ba6defc86092a4492b17f08098 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 20:16:48 +0000 Subject: [PATCH 2/2] compiler: fix what the star dedupe claims, and prove it is needed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of the previous commit turned up three things. The dedupe in expandCore was justified by a CTE analyzed twice, which does not happen — a CTE is analyzed once and cached. Instrumenting it to panic on a duplicate showed the corpus never hits it at all. The real source is an expression typed a second time when a later clause refers to the output name it was given: "SELECT (SELECT * FROM baz LIMIT 1) AS x FROM foo GROUP BY x" reports the star in the subquery once for GROUP BY and once for the target list, and dropping the dedupe turns that query into an overlapping edit. The comment now says so and the query is part of the star_expansion_core case on every engine, where it generates what the legacy path generates. expandCore returned an error it never produced, so it returns only edits. newParser was set on the core path alone, leaving a nil func field on a Compiler built the other way. Nothing calls it there today, but a field that is only sometimes valid is one to get wrong later, so the legacy path sets it too and takes its own parser from it. --- internal/compiler/engine.go | 16 ++++++----- internal/compiler/expand_core.go | 14 +++++----- internal/compiler/parse_core.go | 6 +---- .../star_expansion_core/mysql/go/models.go | 4 +++ .../star_expansion_core/mysql/go/query.sql.go | 27 +++++++++++++++++++ .../star_expansion_core/mysql/query.sql | 3 +++ .../star_expansion_core/mysql/schema.sql | 1 + .../postgresql/go/models.go | 4 +++ .../postgresql/go/query.sql.go | 27 +++++++++++++++++++ .../star_expansion_core/postgresql/query.sql | 3 +++ .../star_expansion_core/postgresql/schema.sql | 1 + .../star_expansion_core/sqlite/go/models.go | 4 +++ .../sqlite/go/query.sql.go | 27 +++++++++++++++++++ .../star_expansion_core/sqlite/query.sql | 3 +++ .../star_expansion_core/sqlite/schema.sql | 1 + 15 files changed, 123 insertions(+), 18 deletions(-) diff --git a/internal/compiler/engine.go b/internal/compiler/engine.go index 808f225bd3..c1b7f7e5ec 100644 --- a/internal/compiler/engine.go +++ b/internal/compiler/engine.go @@ -38,10 +38,11 @@ type Compiler struct { coreAnalysis bool coreDialect core.Option - // newParser builds a parser for the configured engine. The core path - // analyzes statements concurrently, and a parser holds enough state that - // two goroutines cannot share one, so a goroutine that has to parse - // something of its own — the query text it just rewrote — builds its own. + // newParser builds a parser for the configured engine, and is set for + // every engine either path supports. The core path analyzes statements + // concurrently, and a parser holds enough state that two goroutines cannot + // share one, so a goroutine that has to parse something of its own — the + // query text it just rewrote — builds its own rather than taking c.parser. newParser func() Parser schema []string @@ -82,7 +83,7 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts switch conf.Engine { case config.EngineSQLite: - c.parser = sqlite.NewParser() + c.newParser = func() Parser { return sqlite.NewParser() } c.catalog = sqlite.NewCatalog() c.selector = newSQLiteSelector() @@ -96,11 +97,11 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts } } case config.EngineMySQL: - c.parser = dolphin.NewParser() + c.newParser = func() Parser { return dolphin.NewParser() } c.catalog = dolphin.NewCatalog() c.selector = newDefaultSelector() case config.EnginePostgreSQL: - c.parser = postgresql.NewParser() + c.newParser = func() Parser { return postgresql.NewParser() } c.catalog = postgresql.NewCatalog() c.selector = newDefaultSelector() @@ -116,6 +117,7 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts default: return nil, fmt.Errorf("unknown engine: %s", conf.Engine) } + c.parser = c.newParser() return c, nil } diff --git a/internal/compiler/expand_core.go b/internal/compiler/expand_core.go index aa23166fca..ab818c38e1 100644 --- a/internal/compiler/expand_core.go +++ b/internal/compiler/expand_core.go @@ -13,16 +13,18 @@ import ( // its subqueries, so there is nothing left to look up here: what remains is // deciding how each name is written, which is the engine's business and not // the core's. -func (c *Compiler) expandCore(raw *ast.RawStmt, stars []core.StarExpansion) ([]source.Edit, error) { +func (c *Compiler) expandCore(raw *ast.RawStmt, stars []core.StarExpansion) []source.Edit { if len(stars) == 0 { - return nil, nil + return nil } edits := make([]source.Edit, 0, len(stars)) seen := make(map[int]bool, len(stars)) for _, star := range stars { - // A statement analyzed more than once — the same CTE referenced twice, - // say — reports its stars once per pass. Editing one twice would - // overlap, so only the first is kept. + // The analyzer types an expression again when a later clause refers to + // the output name it was given, so "SELECT (SELECT * FROM t) AS x ... + // GROUP BY x" reports the star in the subquery once per pass. The + // passes agree on what the star covers, and editing the same reference + // twice would overlap, so only the first is kept. if seen[star.Location] { continue } @@ -70,5 +72,5 @@ func (c *Compiler) expandCore(raw *ast.RawStmt, stars []core.StarExpansion) ([]s New: strings.Join(cols, ", "), }) } - return edits, nil + return edits } diff --git a/internal/compiler/parse_core.go b/internal/compiler/parse_core.go index b0c4519160..6cae7586c3 100644 --- a/internal/compiler/parse_core.go +++ b/internal/compiler/parse_core.go @@ -65,11 +65,7 @@ func (c *Compiler) parseQueryCore(raw *ast.RawStmt, src string, pre *preprocess. for _, p := range res.Parameters { params = append(params, Parameter{Number: p.Number, Column: coreParamColumn(p, namedParams)}) } - edits, err := c.expandCore(raw, res.Stars) - if err != nil { - return nil, err - } - expanded, err = source.Mutate(rawSQL, edits) + expanded, err = source.Mutate(rawSQL, c.expandCore(raw, res.Stars)) if err != nil { return nil, err } diff --git a/internal/endtoend/testdata/star_expansion_core/mysql/go/models.go b/internal/endtoend/testdata/star_expansion_core/mysql/go/models.go index 7d8b997fea..3e874b1005 100644 --- a/internal/endtoend/testdata/star_expansion_core/mysql/go/models.go +++ b/internal/endtoend/testdata/star_expansion_core/mysql/go/models.go @@ -13,6 +13,10 @@ type Bar struct { C sql.NullString } +type Baz struct { + Z sql.NullString +} + type Foo struct { A sql.NullString B sql.NullString diff --git a/internal/endtoend/testdata/star_expansion_core/mysql/go/query.sql.go b/internal/endtoend/testdata/star_expansion_core/mysql/go/query.sql.go index cbf431b0e3..10c1e7be16 100644 --- a/internal/endtoend/testdata/star_expansion_core/mysql/go/query.sql.go +++ b/internal/endtoend/testdata/star_expansion_core/mysql/go/query.sql.go @@ -59,6 +59,33 @@ func (q *Queries) StarExpansion(ctx context.Context) ([]StarExpansionRow, error) return items, nil } +const starExpansionAliasedSubquery = `-- name: StarExpansionAliasedSubquery :many +SELECT (SELECT z FROM baz LIMIT 1) AS x FROM foo GROUP BY x +` + +func (q *Queries) StarExpansionAliasedSubquery(ctx context.Context) ([]sql.NullString, error) { + rows, err := q.db.QueryContext(ctx, starExpansionAliasedSubquery) + if err != nil { + return nil, err + } + defer rows.Close() + var items []sql.NullString + for rows.Next() { + var x sql.NullString + if err := rows.Scan(&x); err != nil { + return nil, err + } + items = append(items, x) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const starExpansionCTE = `-- name: StarExpansionCTE :many WITH t AS (SELECT a, c FROM bar) SELECT a, c FROM t ` diff --git a/internal/endtoend/testdata/star_expansion_core/mysql/query.sql b/internal/endtoend/testdata/star_expansion_core/mysql/query.sql index 036504e238..89c1b257b0 100644 --- a/internal/endtoend/testdata/star_expansion_core/mysql/query.sql +++ b/internal/endtoend/testdata/star_expansion_core/mysql/query.sql @@ -12,3 +12,6 @@ SELECT * FROM (SELECT * FROM bar) sub; -- name: StarExpansionCTE :many WITH t AS (SELECT * FROM bar) SELECT * FROM t; + +-- name: StarExpansionAliasedSubquery :many +SELECT (SELECT * FROM baz LIMIT 1) AS x FROM foo GROUP BY x; diff --git a/internal/endtoend/testdata/star_expansion_core/mysql/schema.sql b/internal/endtoend/testdata/star_expansion_core/mysql/schema.sql index fa1f85d923..1a6957d981 100644 --- a/internal/endtoend/testdata/star_expansion_core/mysql/schema.sql +++ b/internal/endtoend/testdata/star_expansion_core/mysql/schema.sql @@ -1,2 +1,3 @@ CREATE TABLE foo (a text, b text, `group` text); CREATE TABLE bar (a text, c text); +CREATE TABLE baz (z text); diff --git a/internal/endtoend/testdata/star_expansion_core/postgresql/go/models.go b/internal/endtoend/testdata/star_expansion_core/postgresql/go/models.go index 7d8b997fea..3e874b1005 100644 --- a/internal/endtoend/testdata/star_expansion_core/postgresql/go/models.go +++ b/internal/endtoend/testdata/star_expansion_core/postgresql/go/models.go @@ -13,6 +13,10 @@ type Bar struct { C sql.NullString } +type Baz struct { + Z sql.NullString +} + type Foo struct { A sql.NullString B sql.NullString diff --git a/internal/endtoend/testdata/star_expansion_core/postgresql/go/query.sql.go b/internal/endtoend/testdata/star_expansion_core/postgresql/go/query.sql.go index cbdf5b51a8..3a688f9404 100644 --- a/internal/endtoend/testdata/star_expansion_core/postgresql/go/query.sql.go +++ b/internal/endtoend/testdata/star_expansion_core/postgresql/go/query.sql.go @@ -59,6 +59,33 @@ func (q *Queries) StarExpansion(ctx context.Context) ([]StarExpansionRow, error) return items, nil } +const starExpansionAliasedSubquery = `-- name: StarExpansionAliasedSubquery :many +SELECT (SELECT z FROM baz LIMIT 1) AS x FROM foo GROUP BY x +` + +func (q *Queries) StarExpansionAliasedSubquery(ctx context.Context) ([]sql.NullString, error) { + rows, err := q.db.QueryContext(ctx, starExpansionAliasedSubquery) + if err != nil { + return nil, err + } + defer rows.Close() + var items []sql.NullString + for rows.Next() { + var x sql.NullString + if err := rows.Scan(&x); err != nil { + return nil, err + } + items = append(items, x) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const starExpansionCTE = `-- name: StarExpansionCTE :many WITH t AS (SELECT a, c FROM bar) SELECT a, c FROM t ` diff --git a/internal/endtoend/testdata/star_expansion_core/postgresql/query.sql b/internal/endtoend/testdata/star_expansion_core/postgresql/query.sql index a6e4b9352d..099f54dc6d 100644 --- a/internal/endtoend/testdata/star_expansion_core/postgresql/query.sql +++ b/internal/endtoend/testdata/star_expansion_core/postgresql/query.sql @@ -15,3 +15,6 @@ WITH t AS (SELECT * FROM bar) SELECT * FROM t; -- name: StarExpansionReturning :one INSERT INTO bar (a, c) VALUES ($1, $2) RETURNING *; + +-- name: StarExpansionAliasedSubquery :many +SELECT (SELECT * FROM baz LIMIT 1) AS x FROM foo GROUP BY x; diff --git a/internal/endtoend/testdata/star_expansion_core/postgresql/schema.sql b/internal/endtoend/testdata/star_expansion_core/postgresql/schema.sql index fb4a21842a..126783157b 100644 --- a/internal/endtoend/testdata/star_expansion_core/postgresql/schema.sql +++ b/internal/endtoend/testdata/star_expansion_core/postgresql/schema.sql @@ -1,2 +1,3 @@ CREATE TABLE foo (a text, b text, "group" text); CREATE TABLE bar (a text, c text); +CREATE TABLE baz (z text); diff --git a/internal/endtoend/testdata/star_expansion_core/sqlite/go/models.go b/internal/endtoend/testdata/star_expansion_core/sqlite/go/models.go index 7d8b997fea..3e874b1005 100644 --- a/internal/endtoend/testdata/star_expansion_core/sqlite/go/models.go +++ b/internal/endtoend/testdata/star_expansion_core/sqlite/go/models.go @@ -13,6 +13,10 @@ type Bar struct { C sql.NullString } +type Baz struct { + Z sql.NullString +} + type Foo struct { A sql.NullString B sql.NullString diff --git a/internal/endtoend/testdata/star_expansion_core/sqlite/go/query.sql.go b/internal/endtoend/testdata/star_expansion_core/sqlite/go/query.sql.go index 7a97f1e9da..1ad5fa7633 100644 --- a/internal/endtoend/testdata/star_expansion_core/sqlite/go/query.sql.go +++ b/internal/endtoend/testdata/star_expansion_core/sqlite/go/query.sql.go @@ -59,6 +59,33 @@ func (q *Queries) StarExpansion(ctx context.Context) ([]StarExpansionRow, error) return items, nil } +const starExpansionAliasedSubquery = `-- name: StarExpansionAliasedSubquery :many +SELECT (SELECT z FROM baz LIMIT 1) AS x FROM foo GROUP BY x +` + +func (q *Queries) StarExpansionAliasedSubquery(ctx context.Context) ([]sql.NullString, error) { + rows, err := q.db.QueryContext(ctx, starExpansionAliasedSubquery) + if err != nil { + return nil, err + } + defer rows.Close() + var items []sql.NullString + for rows.Next() { + var x sql.NullString + if err := rows.Scan(&x); err != nil { + return nil, err + } + items = append(items, x) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const starExpansionCTE = `-- name: StarExpansionCTE :many WITH t AS (SELECT a, c FROM bar) SELECT a, c FROM t ` diff --git a/internal/endtoend/testdata/star_expansion_core/sqlite/query.sql b/internal/endtoend/testdata/star_expansion_core/sqlite/query.sql index f58708c449..90351e9b99 100644 --- a/internal/endtoend/testdata/star_expansion_core/sqlite/query.sql +++ b/internal/endtoend/testdata/star_expansion_core/sqlite/query.sql @@ -15,3 +15,6 @@ WITH t AS (SELECT * FROM bar) SELECT * FROM t; -- name: StarExpansionReturning :one INSERT INTO bar (a, c) VALUES (?, ?) RETURNING *; + +-- name: StarExpansionAliasedSubquery :many +SELECT (SELECT * FROM baz LIMIT 1) AS x FROM foo GROUP BY x; diff --git a/internal/endtoend/testdata/star_expansion_core/sqlite/schema.sql b/internal/endtoend/testdata/star_expansion_core/sqlite/schema.sql index fb4a21842a..126783157b 100644 --- a/internal/endtoend/testdata/star_expansion_core/sqlite/schema.sql +++ b/internal/endtoend/testdata/star_expansion_core/sqlite/schema.sql @@ -1,2 +1,3 @@ CREATE TABLE foo (a text, b text, "group" text); CREATE TABLE bar (a text, c text); +CREATE TABLE baz (z text);