From 7b7558572b5567ce5c6d6acfcf3125c6598b1737 Mon Sep 17 00:00:00 2001 From: shafi-VM Date: Mon, 29 Jun 2026 12:59:09 +0530 Subject: [PATCH] opt: cast inlined constant to the variable's type in InlineConstVar InlineConstVar replaces a variable that a filter restricts to a constant (e.g. x in `x = 4 AND x IN (1, 2, 3, 4)`) with that constant elsewhere in the filters. The constant is matched against the variable using Equivalent rather than Identical, so a constant whose type is equivalent but not identical to the variable's type could be substituted directly -- for example a STRING constant for a NAME column. That changes the result of type-sensitive expressions: pg_typeof(n) became pg_typeof('hello'), returning 'text' instead of 'name', so a filter like pg_typeof(n)::TEXT = 'name' folded to false and the row was incorrectly dropped. Cast the inlined constant to the variable's type when the two are not identical, so type-sensitive expressions still observe the original type. The common case where the types are identical is unaffected -- no cast is added. Fixes #170544 Epic: none Release note (bug fix): Fixed a bug that could cause a query to return incorrect results when a column was compared to a constant of an equivalent but different type (for example, a NAME column compared to a string literal) and a type-sensitive expression such as pg_typeof was applied to the same column. The optimizer could substitute the constant using the wrong type, changing the result of the type-sensitive expression. --- pkg/sql/logictest/testdata/logic_test/select | 33 +++++++++++ pkg/sql/opt/norm/inline_funcs.go | 10 ++++ pkg/sql/opt/norm/testdata/rules/inline | 60 ++++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/pkg/sql/logictest/testdata/logic_test/select b/pkg/sql/logictest/testdata/logic_test/select index 8c803fee57e8..ffe8bd220019 100644 --- a/pkg/sql/logictest/testdata/logic_test/select +++ b/pkg/sql/logictest/testdata/logic_test/select @@ -1013,3 +1013,36 @@ query ITT SELECT * FROM t146637 WHERE (a = 0 OR a = 100) AND b = 'foo' ORDER BY a DESC LIMIT 1; ---- 100 foo bar + +# Regression test for #170544. InlineConstVar must cast an inlined constant to +# the variable's type when the two are equivalent but not identical, so that +# type-sensitive expressions such as pg_typeof are not changed. +statement ok +CREATE TABLE t170544_name (n NAME); +INSERT INTO t170544_name VALUES ('hello') + +# pg_typeof(n) must still observe NAME (not STRING), so the row is returned. +query T +SELECT n FROM t170544_name WHERE n = 'hello' AND pg_typeof(n)::TEXT = 'name' +---- +hello + +# The cast added when inlining must not introduce an evaluation error for +# out-of-range or lossy casts, because the constant only reaches dead rows. +# INT8 -> INT2 overflow: the query returns zero rows rather than erroring. +statement ok +CREATE TABLE t170544_int2 (i2 INT2); +INSERT INTO t170544_int2 VALUES (5) + +query I +SELECT i2 FROM t170544_int2 WHERE i2 = 40000 AND i2 + 0 = 5 +---- + +# VARCHAR(n) truncation: the query returns zero rows rather than incorrect rows. +statement ok +CREATE TABLE t170544_varchar (s VARCHAR(3)); +INSERT INTO t170544_varchar VALUES ('abc') + +query T +SELECT s FROM t170544_varchar WHERE s = 'abcdef' AND lower(s) = 'abc' +---- diff --git a/pkg/sql/opt/norm/inline_funcs.go b/pkg/sql/opt/norm/inline_funcs.go index c60a69da3e5a..b1b1a697c689 100644 --- a/pkg/sql/opt/norm/inline_funcs.go +++ b/pkg/sql/opt/norm/inline_funcs.go @@ -388,6 +388,16 @@ func (c *CustomFuncs) InlineConstVar(f memo.FiltersExpr) memo.FiltersExpr { replace = func(nd opt.Expr) opt.Expr { if t, ok := nd.(*memo.VariableExpr); ok { if e, ok := vals[t.Col]; ok { + // The constant was matched against the variable using Equivalent + // (not Identical) above, so its type may differ from the + // variable's (e.g. a STRING constant for a NAME column). + // Substituting the constant directly would change the result of + // type-sensitive expressions such as pg_typeof, so cast the + // constant to the variable's type when the two are not identical. + colType := c.mem.Metadata().ColumnMeta(t.Col).Type + if !e.DataType().Identical(colType) { + return c.f.ConstructCast(e, colType) + } return e } } diff --git a/pkg/sql/opt/norm/testdata/rules/inline b/pkg/sql/opt/norm/testdata/rules/inline index be0f9a2f920c..9b52bee650c2 100644 --- a/pkg/sql/opt/norm/testdata/rules/inline +++ b/pkg/sql/opt/norm/testdata/rules/inline @@ -226,6 +226,66 @@ project │ └── true [as=column14:14] └── false +# Regression test for #170544. InlineConstVar must not replace a variable with a +# constant whose type is equivalent but not identical to the variable's type +# without a cast, since that changes the result of type-sensitive expressions +# such as pg_typeof. Here n is NAME and the constant 'hello' is STRING; the +# inlined constant is cast back to NAME so pg_typeof(n) still reports 'name' and +# the row is not incorrectly filtered out. +exec-ddl +CREATE TABLE t_name (n NAME) +---- + +norm expect=InlineConstVar +SELECT * FROM t_name WHERE n = 'hello' AND pg_typeof(n)::TEXT = 'name' +---- +select + ├── columns: n:1!null + ├── fd: ()-->(1) + ├── scan t_name + │ └── columns: n:1 + └── filters + └── n:1 = 'hello' [outer=(1), constraints=(/1: [/'hello' - /'hello']; tight), fd=()-->(1)] + +# Out-of-range constant (#170544). i2 = 40000 is a contradiction for an INT2 +# column, so no row is ever live. The cast that InlineConstVar adds (40000::INT2, +# which would overflow) must not be folded eagerly, or the query would error +# instead of returning zero rows. +exec-ddl +CREATE TABLE t_int2 (i2 INT2) +---- + +norm +SELECT * FROM t_int2 WHERE i2 = 40000 AND i2 + 0 = 5 +---- +select + ├── columns: i2:1!null + ├── immutable + ├── fd: ()-->(1) + ├── scan t_int2 + │ └── columns: i2:1 + └── filters + ├── i2:1 = 40000 [outer=(1), constraints=(/1: [/40000 - /40000]; tight), fd=()-->(1)] + └── 40000::INT2::INT8 = 5 [immutable] + +# Length-limited type (#170544). 'abcdef' truncates to 'abc' when cast to +# VARCHAR(3), but s = 'abcdef' is a contradiction, so the truncated constant is +# only ever inlined into dead rows. +exec-ddl +CREATE TABLE t_varchar (s VARCHAR(3)) +---- + +norm +SELECT * FROM t_varchar WHERE s = 'abcdef' AND lower(s) = 'abc' +---- +select + ├── columns: s:1!null + ├── fd: ()-->(1) + ├── scan t_varchar + │ └── columns: s:1 + └── filters + └── s:1 = 'abcdef' [outer=(1), constraints=(/1: [/'abcdef' - /'abcdef']; tight), fd=()-->(1)] + # -------------------------------------------------- # InlineProjectConstants # --------------------------------------------------