From c1f5c616a4429443928054c58b7a9f43e69fbc5f Mon Sep 17 00:00:00 2001 From: Alignyx <317810310+Alignyx@users.noreply.github.com> Date: Sun, 6 Sep 2026 17:28:31 +0800 Subject: [PATCH] sql: reject qualified column names that match whole-row references When column lookup failed, optbuilder tried to resolve the final name as a whole-row reference even if the original name had a table prefix. Consequently, SELECT t1.t1 FROM t1 returned the row of t1 when t1 only contained f1, and predicates using the same typo silently used that row. Restrict the whole-row fallback to unqualified references. Qualified missing columns retain the resolver's undefined-column error, while bare whole-row references and existing columns retain their current behavior. Add optbuilder regression cases for qualified references, aliases, predicates, multiple sources, CTEs, derived tables, and correlation. Fixes #120625 See also: #173881 Epic: none Release note (bug fix): Fixed a bug where a qualified reference to a nonexistent column, such as SELECT t1.t1 FROM t1, could silently return an entire table row. Such references now report an undefined-column error (SQLSTATE 42703). Unqualified whole-row references such as SELECT t1 FROM t1 and references to existing columns remain valid. --- pkg/sql/opt/optbuilder/scope.go | 12 ++++-- .../optbuilder/testdata/qualified-whole-row | 42 +++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 pkg/sql/opt/optbuilder/testdata/qualified-whole-row diff --git a/pkg/sql/opt/optbuilder/scope.go b/pkg/sql/opt/optbuilder/scope.go index 2904a8a97875..fb35c29ba52d 100644 --- a/pkg/sql/opt/optbuilder/scope.go +++ b/pkg/sql/opt/optbuilder/scope.go @@ -1079,10 +1079,14 @@ func (s *scope) VisitPre(expr tree.Expr) (recurse bool, newExpr tree.Expr) { } // Attempt to resolve as columnname.*, which allows items // such as SELECT row_to_json(tbl_name) FROM tbl_name to work. - return func() (bool, tree.Expr) { - defer wrapColTupleStarPanic(resolveErr) - return s.VisitPre(columnNameAsTupleStar(string(t.ColumnName))) - }() + // Qualified column references such as t1.t1 must not fall back to + // whole-row tuple expansion; retain the resolver error instead. + if t.TableName == nil || t.TableName.Object() == "" { + return func() (bool, tree.Expr) { + defer wrapColTupleStarPanic(resolveErr) + return s.VisitPre(columnNameAsTupleStar(string(t.ColumnName))) + }() + } } if sqlerrors.IsUndefinedRelationError(resolveErr) && t.TableName.Object() != "" { // If we are inside a PL/pgSQL routine and the prefix names a diff --git a/pkg/sql/opt/optbuilder/testdata/qualified-whole-row b/pkg/sql/opt/optbuilder/testdata/qualified-whole-row new file mode 100644 index 000000000000..c95d90594fe1 --- /dev/null +++ b/pkg/sql/opt/optbuilder/testdata/qualified-whole-row @@ -0,0 +1,42 @@ +exec-ddl +CREATE TABLE t1(f1 INT) +---- + +exec-ddl +CREATE TABLE t2(f1 INT) +---- + +build +SELECT t1.t1 FROM t1 +---- +error (42703): column "t1.t1" does not exist + +build +SELECT a.a FROM t1 AS a +---- +error (42703): column "a.a" does not exist + +build +SELECT * FROM t1 WHERE t1.t2 = 1 +---- +error (42703): column "t1.t2" does not exist + +build +SELECT t1.t2 FROM t1, t2 +---- +error (42703): column "t1.t2" does not exist + +build +WITH c AS (SELECT 1 AS x) SELECT c.y FROM c +---- +error (42703): column "c.y" does not exist + +build +SELECT d.y FROM (SELECT 1 AS x) AS d +---- +error (42703): column "d.y" does not exist + +build +SELECT (SELECT o.t2 FROM t1) FROM t1 AS o +---- +error (42703): column "o.t2" does not exist