Skip to content

Commit e8ae3ab

Browse files
committed
sql/opt: build CTEs for SQL expressions embedded in PL/pgSQL
This commit fixes an oversight from when we introduced support for CTEs within routies. Namely, SQL expressions can contain subqueries, which in turn can contain CTEs. The logic for building a SQL *statement* within a PL/pgSQL routine correctly handles CTEs by building them into the constructed `RelExpr`. The logic for SQL *expressions* was previously missing the same. To fix this issue, `plpgsqlBuilder.buildSQLExpr` now checks if the built scalar expression contained any CTEs. If it did, the scalar is wrapped in the built CTEs, which are in turn wrapped in a subquery, which is returned as the final scalar result. This prevents issues where the CTEs are built at an outer scope, which can produce invalid plans. Fixes #138273 Release note (bug fix): Fixed a bug existing only in pre-release versions of v25.1. The bug could cause creation of a PL/pgSQL routine with a CTE to fail with an error like the following: `unexpected root expression: with`.
1 parent 5d76239 commit e8ae3ab

File tree

5 files changed

+591
-7
lines changed

5 files changed

+591
-7
lines changed

pkg/sql/logictest/testdata/logic_test/udf_cte

+39
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,42 @@ query T
121121
SELECT f();
122122
----
123123
{10,20,30,40}
124+
125+
subtest regression_138273
126+
127+
statement ok
128+
CREATE SEQUENCE seq_1;
129+
130+
# Verify that the function can be successfully created with a CTE inside a
131+
# scalar expression.
132+
statement ok
133+
CREATE FUNCTION f138273_1() RETURNS TIMESTAMP LANGUAGE PLpgSQL AS $$
134+
DECLARE
135+
decl TIMESTAMP;
136+
BEGIN
137+
WHILE false LOOP
138+
IF true THEN
139+
ELSIF EXISTS (WITH cte(col) AS (SELECT * FROM (VALUES (currval('seq_1'))) AS foo) SELECT 1 FROM cte) THEN
140+
RETURN decl;
141+
END IF;
142+
END LOOP;
143+
END;
144+
$$;
145+
146+
# Test a similar function that actually executes the CTE.
147+
statement ok
148+
CREATE FUNCTION f138273_2() RETURNS INT LANGUAGE PLpgSQL AS $$
149+
BEGIN
150+
SELECT nextval('seq_1');
151+
WHILE EXISTS (WITH cte(col) AS (SELECT * FROM (VALUES (currval('seq_1'))) AS foo) SELECT 1 FROM cte) LOOP
152+
RETURN currval('seq_1');
153+
END LOOP;
154+
END;
155+
$$;
156+
157+
query I
158+
SELECT f138273_2();
159+
----
160+
1
161+
162+
subtest end

pkg/sql/opt/optbuilder/create_trigger.go

+9-6
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,15 @@ func (b *Builder) buildFunctionForTrigger(
179179

180180
// The trigger function can reference the NEW and OLD transition relations,
181181
// aliased in the trigger definition.
182+
if len(ct.Transitions) > 0 {
183+
// Save the existing CTEs in the builder and restore them after the function
184+
// body is built.
185+
prevCTEs := b.ctes
186+
b.ctes = nil
187+
defer func() {
188+
b.ctes = prevCTEs
189+
}()
190+
}
182191
for _, transition := range ct.Transitions {
183192
// Build a fake relational expression with a column corresponding to each
184193
// column from the table.
@@ -203,12 +212,6 @@ func (b *Builder) buildFunctionForTrigger(
203212
funcScope.ctes[string(transition.Name)] = cte
204213
b.addCTE(cte)
205214
}
206-
if len(ct.Transitions) > 0 {
207-
defer func() {
208-
// Reset the CTEs in the builder after the function body is built.
209-
b.ctes = nil
210-
}()
211-
}
212215

213216
// The trigger function takes a set of implicitly-defined parameters, two of
214217
// which are determined by the table's record type. Add them to the trigger

pkg/sql/opt/optbuilder/plpgsql.go

+22-1
Original file line numberDiff line numberDiff line change
@@ -2201,13 +2201,34 @@ func (b *plpgsqlBuilder) buildSQLExpr(expr ast.Expr, typ *types.T, s *scope) opt
22012201
// For lazy SQL evaluation, replace all expressions with NULL.
22022202
return memo.NullSingleton
22032203
}
2204+
// Save any outer CTEs before building the expression, which may have
2205+
// subqueries with inner CTEs.
2206+
prevCTEs := b.ob.ctes
2207+
b.ob.ctes = nil
2208+
defer func() {
2209+
b.ob.ctes = prevCTEs
2210+
}()
22042211
expr, _ = tree.WalkExpr(s, expr)
22052212
typedExpr, err := expr.TypeCheck(b.ob.ctx, b.ob.semaCtx, typ)
22062213
if err != nil {
22072214
panic(err)
22082215
}
22092216
scalar := b.ob.buildScalar(typedExpr, s, nil, nil, b.colRefs)
2210-
return b.coerceType(scalar, typ)
2217+
scalar = b.coerceType(scalar, typ)
2218+
if len(b.ob.ctes) == 0 {
2219+
return scalar
2220+
}
2221+
// There was at least one CTE within the scalar expression. It is possible to
2222+
// "hoist" them above this point, but building them eagerly here means that
2223+
// callers don't have to worry about CTE handling.
2224+
f := b.ob.factory
2225+
valuesCol := f.Metadata().AddColumn("", scalar.DataType())
2226+
valuesExpr := f.ConstructValues(
2227+
memo.ScalarListExpr{f.ConstructTuple(memo.ScalarListExpr{scalar}, scalar.DataType())},
2228+
&memo.ValuesPrivate{Cols: opt.ColList{valuesCol}, ID: f.Metadata().NextUniqueID()},
2229+
)
2230+
withExpr := b.ob.buildWiths(valuesExpr, b.ob.ctes)
2231+
return f.ConstructSubquery(withExpr, &memo.SubqueryPrivate{})
22112232
}
22122233

22132234
// buildSQLStatement type-checks and builds the given SQL statement into a

pkg/sql/opt/optbuilder/testdata/create_function

+33
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,36 @@ build
138138
CREATE FUNCTION f() RETURNS UNKNOWN LANGUAGE SQL AS $$ SELECT NULL; $$;
139139
----
140140
error (42P13): SQL functions cannot return type unknown
141+
142+
# Regression test for #138273 - correctly build CTEs in SQL expressions
143+
# embedded in a PL/pgSQL routine.
144+
build
145+
CREATE FUNCTION f138273_1() RETURNS TIMESTAMP LANGUAGE PLpgSQL AS $$
146+
DECLARE
147+
decl TIMESTAMP;
148+
BEGIN
149+
WHILE false LOOP
150+
IF true THEN
151+
ELSIF EXISTS (WITH cte(col) AS (SELECT * FROM (VALUES (random())) AS foo) SELECT 1 FROM cte) THEN
152+
RETURN decl;
153+
END IF;
154+
END LOOP;
155+
END;
156+
$$;
157+
----
158+
create-function
159+
├── CREATE FUNCTION f138273_1()
160+
│ RETURNS TIMESTAMP
161+
│ LANGUAGE plpgsql
162+
│ AS $$DECLARE
163+
│ decl TIMESTAMP;
164+
│ BEGIN
165+
│ WHILE false LOOP
166+
│ IF true THEN
167+
│ ELSIF EXISTS (WITH cte (col) AS (SELECT foo.column1 FROM (VALUES (random())) AS foo) SELECT 1 FROM cte) THEN
168+
│ RETURN decl;
169+
│ END IF;
170+
│ END LOOP;
171+
│ END;
172+
│ $$
173+
└── no dependencies

0 commit comments

Comments
 (0)