From 2143d55cc17bf7beba4cd5c168772606dced7e5d Mon Sep 17 00:00:00 2001 From: lawrence3699 Date: Wed, 22 Apr 2026 13:15:24 +1000 Subject: [PATCH] Validate unknown columns in SELECT WHERE clauses --- internal/compiler/output_columns.go | 106 ++++++++++++++++++ .../invalid_where_unknown_column/issue.md | 1 + .../postgresql/pgx/query.sql | 2 + .../postgresql/pgx/schema.sql | 1 + .../postgresql/pgx/sqlc.yaml | 10 ++ .../postgresql/pgx/stderr/base.txt | 2 + .../postgresql/pgx/stderr/managed-db.txt | 2 + .../issue.md | 1 + .../postgresql/pgx/query.sql | 4 + .../postgresql/pgx/schema.sql | 1 + .../postgresql/pgx/sqlc.yaml | 10 ++ .../postgresql/pgx/stderr/base.txt | 2 + .../postgresql/pgx/stderr/managed-db.txt | 2 + 13 files changed, 144 insertions(+) create mode 100644 internal/endtoend/testdata/invalid_where_unknown_column/issue.md create mode 100644 internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/query.sql create mode 100644 internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/schema.sql create mode 100644 internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/sqlc.yaml create mode 100644 internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/stderr/base.txt create mode 100644 internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/stderr/managed-db.txt create mode 100644 internal/endtoend/testdata/invalid_where_unknown_column_subquery/issue.md create mode 100644 internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/query.sql create mode 100644 internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/schema.sql create mode 100644 internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/sqlc.yaml create mode 100644 internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/stderr/base.txt create mode 100644 internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/stderr/managed-db.txt diff --git a/internal/compiler/output_columns.go b/internal/compiler/output_columns.go index dbd486359a..28003dd71e 100644 --- a/internal/compiler/output_columns.go +++ b/internal/compiler/output_columns.go @@ -67,6 +67,10 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er targets = n.TargetList isUnion := len(targets.Items) == 0 && n.Larg != nil + if err := c.findColumnsInClause(qc, n.WhereClause, [][]*Table{tables}); err != nil { + return nil, err + } + if n.GroupClause != nil { for _, item := range n.GroupClause.Items { if err := findColumnForNode(item, tables, targets); err != nil { @@ -722,6 +726,108 @@ func findColumnForNode(item ast.Node, tables []*Table, targetList *ast.List) err return findColumnForRef(ref, tables, targetList) } +func (c *Compiler) findColumnsInClause(qc *QueryCatalog, node ast.Node, scopes [][]*Table) error { + if node == nil { + return nil + } + + validator := &columnRefClauseValidator{ + compiler: c, + qc: qc, + scopes: scopes, + } + astutils.Walk(validator, node) + return validator.err +} + +type columnRefClauseValidator struct { + compiler *Compiler + qc *QueryCatalog + scopes [][]*Table + err error +} + +func (v *columnRefClauseValidator) Visit(node ast.Node) astutils.Visitor { + if node == nil || v.err != nil { + return nil + } + + if selectStmt, ok := node.(*ast.SelectStmt); ok { + tables, err := v.compiler.sourceTables(v.qc, selectStmt) + if err != nil { + v.err = err + return nil + } + scopes := append([][]*Table{tables}, v.scopes...) + if err := v.compiler.findColumnsInClause(v.qc, selectStmt.WhereClause, scopes); err != nil { + v.err = err + } + return nil + } + + if ref, ok := node.(*ast.ColumnRef); ok { + if err := findColumnForRefInScopes(ref, v.scopes); err != nil { + v.err = err + return nil + } + } + + return v +} + +func findColumnForRefInScopes(ref *ast.ColumnRef, scopes [][]*Table) error { + parts := stringSlice(ref.Fields) + var schema, alias, name string + switch len(parts) { + case 1: + name = parts[0] + case 2: + alias = parts[0] + name = parts[1] + case 3: + schema = parts[0] + alias = parts[1] + name = parts[2] + default: + return fmt.Errorf("unknown number of fields: %d", len(parts)) + } + + for _, tables := range scopes { + var found int + for _, t := range tables { + if schema != "" && t.Rel.Schema != schema { + continue + } + if alias != "" && t.Rel.Name != alias { + continue + } + for _, c := range t.Columns { + if c.Name == name { + found++ + break + } + } + } + if found == 0 { + continue + } + if found > 1 { + return &sqlerr.Error{ + Code: "42703", + Message: fmt.Sprintf("column reference %q is ambiguous", name), + Location: ref.Location, + } + } + return nil + } + + return &sqlerr.Error{ + Code: "42703", + Message: fmt.Sprintf("column reference %q not found", name), + Location: ref.Location, + } +} + func findColumnForRef(ref *ast.ColumnRef, tables []*Table, targetList *ast.List) error { parts := stringSlice(ref.Fields) var alias, name string diff --git a/internal/endtoend/testdata/invalid_where_unknown_column/issue.md b/internal/endtoend/testdata/invalid_where_unknown_column/issue.md new file mode 100644 index 0000000000..3fb89ab546 --- /dev/null +++ b/internal/endtoend/testdata/invalid_where_unknown_column/issue.md @@ -0,0 +1 @@ +#4264 diff --git a/internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/query.sql b/internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/query.sql new file mode 100644 index 0000000000..0f3397cced --- /dev/null +++ b/internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/query.sql @@ -0,0 +1,2 @@ +-- name: GetUsers :many +select * from "user" where is_deleted = false; diff --git a/internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/schema.sql b/internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/schema.sql new file mode 100644 index 0000000000..a9975a719d --- /dev/null +++ b/internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/schema.sql @@ -0,0 +1 @@ +create table "user" ("name" text not null, deleted bool not null); diff --git a/internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/sqlc.yaml b/internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/sqlc.yaml new file mode 100644 index 0000000000..5dc63e3f91 --- /dev/null +++ b/internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: "postgresql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" + sql_package: "pgx/v5" diff --git a/internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/stderr/base.txt b/internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/stderr/base.txt new file mode 100644 index 0000000000..f01a73e71f --- /dev/null +++ b/internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/stderr/base.txt @@ -0,0 +1,2 @@ +# package querytest +query.sql:2:28: column reference "is_deleted" not found diff --git a/internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/stderr/managed-db.txt b/internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/stderr/managed-db.txt new file mode 100644 index 0000000000..1c79713c33 --- /dev/null +++ b/internal/endtoend/testdata/invalid_where_unknown_column/postgresql/pgx/stderr/managed-db.txt @@ -0,0 +1,2 @@ +# package querytest +query.sql:2:41: column "is_deleted" does not exist diff --git a/internal/endtoend/testdata/invalid_where_unknown_column_subquery/issue.md b/internal/endtoend/testdata/invalid_where_unknown_column_subquery/issue.md new file mode 100644 index 0000000000..3fb89ab546 --- /dev/null +++ b/internal/endtoend/testdata/invalid_where_unknown_column_subquery/issue.md @@ -0,0 +1 @@ +#4264 diff --git a/internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/query.sql b/internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/query.sql new file mode 100644 index 0000000000..572c77b9f9 --- /dev/null +++ b/internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/query.sql @@ -0,0 +1,4 @@ +-- name: GetUsers :many +select * from "user" where exists ( + select 1 from "user" as u2 where is_deleted = false +); diff --git a/internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/schema.sql b/internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/schema.sql new file mode 100644 index 0000000000..a9975a719d --- /dev/null +++ b/internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/schema.sql @@ -0,0 +1 @@ +create table "user" ("name" text not null, deleted bool not null); diff --git a/internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/sqlc.yaml b/internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/sqlc.yaml new file mode 100644 index 0000000000..5dc63e3f91 --- /dev/null +++ b/internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: "postgresql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" + sql_package: "pgx/v5" diff --git a/internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/stderr/base.txt b/internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/stderr/base.txt new file mode 100644 index 0000000000..5693b1c72a --- /dev/null +++ b/internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/stderr/base.txt @@ -0,0 +1,2 @@ +# package querytest +query.sql:3:36: column reference "is_deleted" not found diff --git a/internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/stderr/managed-db.txt b/internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/stderr/managed-db.txt new file mode 100644 index 0000000000..edb6c29703 --- /dev/null +++ b/internal/endtoend/testdata/invalid_where_unknown_column_subquery/postgresql/pgx/stderr/managed-db.txt @@ -0,0 +1,2 @@ +# package querytest +query.sql:3:49: column "is_deleted" does not exist