diff --git a/internal/compiler/output_columns.go b/internal/compiler/output_columns.go index b0a15e6ac4..0122f0d695 100644 --- a/internal/compiler/output_columns.go +++ b/internal/compiler/output_columns.go @@ -592,7 +592,24 @@ func (c *Compiler) sourceTables(qc *QueryCatalog, node ast.Node) ([]*Table, erro tables = append(tables, table) case *ast.RangeSubselect: - cols, err := c.outputColumns(qc, n.Subquery) + lateralQC := qc + if n.Lateral && len(tables) > 0 { + // LATERAL allows the subquery to reference columns from preceding FROM items. + lateralTables := make(map[string]*Table, len(tables)) + for _, table := range tables { + if table.Rel != nil && table.Rel.Name != "" { + lateralTables[table.Rel.Name] = table + } + } + lateralQC = &QueryCatalog{ + catalog: qc.catalog, + ctes: qc.ctes, + embeds: qc.embeds, + lateralTables: lateralTables, + } + } + + cols, err := c.outputColumns(lateralQC, n.Subquery) if err != nil { return nil, err } @@ -637,6 +654,15 @@ func (c *Compiler) sourceTables(qc *QueryCatalog, node ast.Node) ([]*Table, erro return nil, fmt.Errorf("sourceTable: unsupported list item type: %T", n) } } + + // Add LATERAL outer tables to the tables list. + // LATERAL subqueries can reference these outer tables for column resolution. + if qc != nil { + for _, lateralTable := range qc.lateralTables { + tables = append(tables, lateralTable) + } + } + return tables, nil } diff --git a/internal/compiler/query_catalog.go b/internal/compiler/query_catalog.go index 80b59d876c..a65cb53031 100644 --- a/internal/compiler/query_catalog.go +++ b/internal/compiler/query_catalog.go @@ -12,6 +12,8 @@ type QueryCatalog struct { catalog *catalog.Catalog ctes map[string]*Table embeds rewrite.EmbedSet + + lateralTables map[string]*Table } func (comp *Compiler) buildQueryCatalog(c *catalog.Catalog, node ast.Node, embeds rewrite.EmbedSet) (*QueryCatalog, error) { diff --git a/internal/endtoend/testdata/lateral_view_column_ref/issue.md b/internal/endtoend/testdata/lateral_view_column_ref/issue.md new file mode 100644 index 0000000000..7fa8efd9ca --- /dev/null +++ b/internal/endtoend/testdata/lateral_view_column_ref/issue.md @@ -0,0 +1,3 @@ +https://github.com/sqlc-dev/sqlc/issues/4182 + +LATERAL subquery column references from outer query fail with "column does not exist". diff --git a/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/go/db.go b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/go/db.go new file mode 100644 index 0000000000..9d485b5f16 --- /dev/null +++ b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/go/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/go/models.go b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/go/models.go new file mode 100644 index 0000000000..df8c88fcdc --- /dev/null +++ b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/go/models.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +type Foo struct { + ID int32 + Val string +} + +type FooLateral struct { + Val string + Result string +} diff --git a/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/go/query.sql.go b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/go/query.sql.go new file mode 100644 index 0000000000..794aa4e6a3 --- /dev/null +++ b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/go/query.sql.go @@ -0,0 +1,68 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" +) + +const getFooLateral = `-- name: GetFooLateral :many +SELECT val, result FROM foo_lateral +` + +func (q *Queries) GetFooLateral(ctx context.Context) ([]FooLateral, error) { + rows, err := q.db.Query(ctx, getFooLateral) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FooLateral + for rows.Next() { + var i FooLateral + if err := rows.Scan(&i.Val, &i.Result); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getFooLateralDirect = `-- name: GetFooLateralDirect :many +SELECT f.id, f.val, sub.result +FROM foo f +CROSS JOIN LATERAL ( + SELECT (f.val || '-direct')::text AS result +) sub +` + +type GetFooLateralDirectRow struct { + ID int32 + Val string + Result string +} + +func (q *Queries) GetFooLateralDirect(ctx context.Context) ([]GetFooLateralDirectRow, error) { + rows, err := q.db.Query(ctx, getFooLateralDirect) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetFooLateralDirectRow + for rows.Next() { + var i GetFooLateralDirectRow + if err := rows.Scan(&i.ID, &i.Val, &i.Result); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/query.sql b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/query.sql new file mode 100644 index 0000000000..12d747d2a2 --- /dev/null +++ b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/query.sql @@ -0,0 +1,9 @@ +-- name: GetFooLateral :many +SELECT val, result FROM foo_lateral; + +-- name: GetFooLateralDirect :many +SELECT f.id, f.val, sub.result +FROM foo f +CROSS JOIN LATERAL ( + SELECT (f.val || '-direct')::text AS result +) sub; diff --git a/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/schema.sql b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/schema.sql new file mode 100644 index 0000000000..623ec64347 --- /dev/null +++ b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/schema.sql @@ -0,0 +1,13 @@ +CREATE TABLE foo ( + id integer PRIMARY KEY, + val text NOT NULL +); + +-- Reproduces issue #4182: LATERAL subquery referencing outer column +CREATE VIEW foo_lateral AS +SELECT t.val, sub.result +FROM foo t +CROSS JOIN LATERAL ( + SELECT t.val::text AS result + FROM foo LIMIT 1 +) sub; diff --git a/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/sqlc.json b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/sqlc.json new file mode 100644 index 0000000000..9fca5b5230 --- /dev/null +++ b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/pgx/sqlc.json @@ -0,0 +1,17 @@ +{ + "version": "2", + "sql": [ + { + "engine": "postgresql", + "schema": "schema.sql", + "queries": "query.sql", + "gen": { + "go": { + "package": "db", + "out": "go", + "sql_package": "pgx/v5" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/lateral_view_column_ref/postgresql/stdlib/go/db.go b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/stdlib/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/stdlib/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/lateral_view_column_ref/postgresql/stdlib/go/models.go b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/stdlib/go/models.go new file mode 100644 index 0000000000..df8c88fcdc --- /dev/null +++ b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/stdlib/go/models.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +type Foo struct { + ID int32 + Val string +} + +type FooLateral struct { + Val string + Result string +} diff --git a/internal/endtoend/testdata/lateral_view_column_ref/postgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/stdlib/go/query.sql.go new file mode 100644 index 0000000000..6bc53d78a4 --- /dev/null +++ b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/stdlib/go/query.sql.go @@ -0,0 +1,74 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" +) + +const getFooLateral = `-- name: GetFooLateral :many +SELECT val, result FROM foo_lateral +` + +func (q *Queries) GetFooLateral(ctx context.Context) ([]FooLateral, error) { + rows, err := q.db.QueryContext(ctx, getFooLateral) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FooLateral + for rows.Next() { + var i FooLateral + if err := rows.Scan(&i.Val, &i.Result); 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 getFooLateralDirect = `-- name: GetFooLateralDirect :many +SELECT f.id, f.val, sub.result +FROM foo f +CROSS JOIN LATERAL ( + SELECT (f.val || '-direct')::text AS result +) sub +` + +type GetFooLateralDirectRow struct { + ID int32 + Val string + Result string +} + +func (q *Queries) GetFooLateralDirect(ctx context.Context) ([]GetFooLateralDirectRow, error) { + rows, err := q.db.QueryContext(ctx, getFooLateralDirect) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetFooLateralDirectRow + for rows.Next() { + var i GetFooLateralDirectRow + if err := rows.Scan(&i.ID, &i.Val, &i.Result); 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/lateral_view_column_ref/postgresql/stdlib/query.sql b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/stdlib/query.sql new file mode 100644 index 0000000000..12d747d2a2 --- /dev/null +++ b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/stdlib/query.sql @@ -0,0 +1,9 @@ +-- name: GetFooLateral :many +SELECT val, result FROM foo_lateral; + +-- name: GetFooLateralDirect :many +SELECT f.id, f.val, sub.result +FROM foo f +CROSS JOIN LATERAL ( + SELECT (f.val || '-direct')::text AS result +) sub; diff --git a/internal/endtoend/testdata/lateral_view_column_ref/postgresql/stdlib/schema.sql b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/stdlib/schema.sql new file mode 100644 index 0000000000..623ec64347 --- /dev/null +++ b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/stdlib/schema.sql @@ -0,0 +1,13 @@ +CREATE TABLE foo ( + id integer PRIMARY KEY, + val text NOT NULL +); + +-- Reproduces issue #4182: LATERAL subquery referencing outer column +CREATE VIEW foo_lateral AS +SELECT t.val, sub.result +FROM foo t +CROSS JOIN LATERAL ( + SELECT t.val::text AS result + FROM foo LIMIT 1 +) sub; diff --git a/internal/endtoend/testdata/lateral_view_column_ref/postgresql/stdlib/sqlc.json b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/stdlib/sqlc.json new file mode 100644 index 0000000000..480d68e40e --- /dev/null +++ b/internal/endtoend/testdata/lateral_view_column_ref/postgresql/stdlib/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "postgresql", + "schema": "schema.sql", + "queries": "query.sql", + "gen": { + "go": { + "package": "db", + "out": "go" + } + } + } + ] +}