Skip to main content

uqa_sql/semantics/sets/
validation.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Set-returning expression detection and SQL context validation.
8
9use crate::ast::FunctionBinding;
10use crate::plan::{ProjectionPlan, QueryBlockPlan, SourcePlan};
11use crate::{FunctionTypeResolver, RowSchema, ScalarExpr, ScalarFrameBound};
12use crate::{SQLError, SQLParam};
13
14use super::{PhysicalProjection, SetFunctionCatalog};
15use crate::binding::context::BindingContext;
16use crate::semantics::aggregates::is_aggregate;
17
18pub fn builtin_returns_set(name: &str) -> bool {
19    matches!(
20        name,
21        "generate_series"
22            | "pg_listening_channels"
23            | "unnest"
24            | "regexp_split_to_table"
25            | "string_to_table"
26            | "json_array_elements"
27            | "jsonb_array_elements"
28            | "json_array_elements_text"
29            | "jsonb_array_elements_text"
30            | "json_each"
31            | "jsonb_each"
32            | "json_each_text"
33            | "jsonb_each_text"
34            | "json_object_keys"
35            | "jsonb_object_keys"
36            | "aclexplode"
37    )
38}
39
40pub fn function_may_return_set(
41    engine: &dyn crate::routines::RoutineResolution,
42    resolver: &dyn FunctionTypeResolver,
43    name: &str,
44    binding: Option<&FunctionBinding>,
45    args: &[ScalarExpr],
46    schema: &RowSchema,
47    params: &[SQLParam],
48) -> Result<bool, SQLError> {
49    if binding.and_then(|binding| binding.dispatch).is_some() {
50        return Ok(false);
51    }
52    if binding.is_some_and(FunctionBinding::is_polymorphic_builtin_syntax) {
53        return Ok(false);
54    }
55    if binding.is_some_and(|binding| !binding.builtin) {
56        let (argument_names, argument_types, explicit_variadic) =
57            crate::function_call_argument_signature(args, schema, params, Some(resolver))?;
58        let function = engine.resolve_static_sql_function(
59            name,
60            binding,
61            &argument_names,
62            &argument_types,
63            explicit_variadic,
64        )?;
65        return Ok(function.is_some_and(|function| function.def.returns_set()));
66    }
67    let identity = name.to_ascii_lowercase();
68    let builtin = crate::semantics::builtin_function_dispatch_name(&identity);
69    if builtin_returns_set(&builtin) || engine.has_registered_table_function(&identity) {
70        return Ok(true);
71    }
72    let (argument_names, argument_types, explicit_variadic) =
73        crate::function_call_argument_signature(args, schema, params, Some(resolver))?;
74    let Some(overloads) = engine.lookup_visible_sql_functions_for_analysis(name)? else {
75        return Ok(false);
76    };
77    if !crate::is_fixed_builtin(name) {
78        let mut setness = overloads
79            .iter()
80            .filter(|function| !function.def.is_procedure)
81            .map(|function| function.def.returns_set());
82        if let Some(first) = setness.next() {
83            if setness.all(|returns_set| returns_set == first) {
84                return Ok(first);
85            }
86        }
87    }
88    if let Some(resolved) = crate::resolve_fixed_builtin_call(
89        name,
90        binding,
91        &argument_names,
92        &argument_types,
93        explicit_variadic,
94        Some(resolver),
95    )? {
96        if resolved.selected.binding.builtin {
97            return Ok(false);
98        }
99        let function = engine.resolve_static_sql_function(
100            name,
101            Some(&resolved.selected.binding),
102            &argument_names,
103            &argument_types,
104            explicit_variadic,
105        )?;
106        return Ok(function.is_some_and(|function| function.def.returns_set()));
107    }
108    match engine.resolve_static_sql_function(
109        name,
110        binding,
111        &argument_names,
112        &argument_types,
113        explicit_variadic,
114    ) {
115        Ok(function) => Ok(function.is_some_and(|function| function.def.returns_set())),
116        Err(error) if binding.is_none() && error.sqlstate() == Some("42883") => {
117            match crate::type_resolution::builtin_function_type_with_resolver(
118                &builtin,
119                args,
120                &[],
121                schema,
122                params,
123                resolver,
124            ) {
125                Ok(Some(_)) => Ok(false),
126                Ok(None) | Err(_) => Err(error),
127            }
128        }
129        Err(error) => Err(error),
130    }
131}
132
133pub fn resolve_set_function_binding(
134    engine: &dyn SetFunctionCatalog,
135    resolver: &dyn FunctionTypeResolver,
136    name: &str,
137    binding: Option<&FunctionBinding>,
138    args: &[ScalarExpr],
139    schema: &RowSchema,
140    params: &[SQLParam],
141) -> Result<Option<FunctionBinding>, SQLError> {
142    let (argument_names, argument_types, explicit_variadic) =
143        crate::function_call_argument_signature(args, schema, params, Some(resolver))?;
144    if binding.is_none()
145        && engine
146            .lookup_visible_sql_functions_for_analysis(name)?
147            .is_none()
148    {
149        return Ok(binding.cloned());
150    }
151    if let Some(resolved) = crate::resolve_fixed_builtin_call(
152        name,
153        binding,
154        &argument_names,
155        &argument_types,
156        explicit_variadic,
157        Some(resolver),
158    )? {
159        return Ok((!resolved.selected.binding.builtin).then_some(resolved.selected.binding));
160    }
161    let Some(function) = engine.resolve_static_sql_function_match(
162        name,
163        binding,
164        &argument_names,
165        &argument_types,
166        explicit_variadic,
167    )?
168    else {
169        return Ok(binding.cloned());
170    };
171    Ok(Some(function.binding()))
172}
173
174pub fn projections_may_return_set(
175    engine: &dyn SetFunctionCatalog,
176    resolver: &dyn FunctionTypeResolver,
177    projections: &[PhysicalProjection],
178    schema: &RowSchema,
179    params: &[SQLParam],
180) -> Result<bool, SQLError> {
181    for (_, expression) in projections {
182        if expression_may_return_set(engine, resolver, expression, schema, params)? {
183            return Ok(true);
184        }
185    }
186    Ok(false)
187}
188
189#[expect(
190    clippy::too_many_lines,
191    reason = "preserves SELECT schema and row identity"
192)]
193pub fn expression_may_return_set(
194    engine: &dyn crate::routines::RoutineResolution,
195    resolver: &dyn FunctionTypeResolver,
196    expression: &ScalarExpr,
197    schema: &RowSchema,
198    params: &[SQLParam],
199) -> Result<bool, SQLError> {
200    match expression {
201        ScalarExpr::Func {
202            name,
203            binding,
204            args,
205            order_by,
206            filter,
207            ..
208        } => {
209            if function_may_return_set(
210                engine,
211                resolver,
212                name,
213                binding.as_ref(),
214                args,
215                schema,
216                params,
217            )? || expressions_may_return_set(engine, resolver, args, schema, params)?
218                || expressions_may_return_set(
219                    engine,
220                    resolver,
221                    order_by.iter().map(|order| &order.expr),
222                    schema,
223                    params,
224                )?
225            {
226                return Ok(true);
227            }
228            filter.as_deref().map_or(Ok(false), |filter| {
229                expression_may_return_set(engine, resolver, filter, schema, params)
230            })
231        }
232        ScalarExpr::Array(items)
233        | ScalarExpr::Row(items)
234        | ScalarExpr::CompositeRow { items, .. }
235        | ScalarExpr::And(items)
236        | ScalarExpr::Or(items) => {
237            expressions_may_return_set(engine, resolver, items, schema, params)
238        }
239        ScalarExpr::Binary { lhs, rhs, .. } => Ok(expression_may_return_set(
240            engine, resolver, lhs, schema, params,
241        )? || expression_may_return_set(
242            engine, resolver, rhs, schema, params,
243        )?),
244        ScalarExpr::Not(inner)
245        | ScalarExpr::UnaryMinus(inner)
246        | ScalarExpr::IsNull { expr: inner, .. }
247        | ScalarExpr::Cast { expr: inner, .. } => {
248            expression_may_return_set(engine, resolver, inner, schema, params)
249        }
250        ScalarExpr::Between { expr, low, high } => {
251            Ok(
252                expression_may_return_set(engine, resolver, expr, schema, params)?
253                    || expression_may_return_set(engine, resolver, low, schema, params)?
254                    || expression_may_return_set(engine, resolver, high, schema, params)?,
255            )
256        }
257        ScalarExpr::InList { expr, list, .. } => Ok(expression_may_return_set(
258            engine, resolver, expr, schema, params,
259        )? || expressions_may_return_set(
260            engine, resolver, list, schema, params,
261        )?),
262        ScalarExpr::WindowCall {
263            args, spec, filter, ..
264        } => {
265            if expressions_may_return_set(engine, resolver, args, schema, params)?
266                || expressions_may_return_set(
267                    engine,
268                    resolver,
269                    filter.iter().map(AsRef::as_ref),
270                    schema,
271                    params,
272                )?
273                || expressions_may_return_set(engine, resolver, &spec.partition_by, schema, params)?
274                || expressions_may_return_set(
275                    engine,
276                    resolver,
277                    spec.order_by.iter().map(|order| &order.expr),
278                    schema,
279                    params,
280                )?
281            {
282                return Ok(true);
283            }
284            let Some(frame) = spec.frame.as_ref() else {
285                return Ok(false);
286            };
287            Ok(
288                frame_bound_may_return_set(engine, resolver, &frame.start, schema, params)?
289                    || frame_bound_may_return_set(engine, resolver, &frame.end, schema, params)?,
290            )
291        }
292        ScalarExpr::Case {
293            base,
294            when,
295            else_branch,
296        } => {
297            if let Some(base) = base {
298                if expression_may_return_set(engine, resolver, base, schema, params)? {
299                    return Ok(true);
300                }
301            }
302            for (condition, result) in when {
303                if expression_may_return_set(engine, resolver, condition, schema, params)?
304                    || expression_may_return_set(engine, resolver, result, schema, params)?
305                {
306                    return Ok(true);
307                }
308            }
309            if let Some(branch) = else_branch {
310                if expression_may_return_set(engine, resolver, branch, schema, params)? {
311                    return Ok(true);
312                }
313            }
314            Ok(false)
315        }
316        ScalarExpr::InSubquery { expr, .. } => {
317            expression_may_return_set(engine, resolver, expr, schema, params)
318        }
319        ScalarExpr::Default
320        | ScalarExpr::Star
321        | ScalarExpr::QualifiedStar(_)
322        | ScalarExpr::Column(_)
323        | ScalarExpr::Position(_)
324        | ScalarExpr::InternalColumn(_)
325        | ScalarExpr::QualifiedColumn { .. }
326        | ScalarExpr::Literal(_)
327        | ScalarExpr::TypedLiteral { .. }
328        | ScalarExpr::Param(_)
329        | ScalarExpr::ScalarSubquery(_)
330        | ScalarExpr::Exists { .. } => Ok(false),
331    }
332}
333
334fn expressions_may_return_set<'a>(
335    engine: &dyn crate::routines::RoutineResolution,
336    resolver: &dyn FunctionTypeResolver,
337    expressions: impl IntoIterator<Item = &'a ScalarExpr>,
338    schema: &RowSchema,
339    params: &[SQLParam],
340) -> Result<bool, SQLError> {
341    for expression in expressions {
342        if expression_may_return_set(engine, resolver, expression, schema, params)? {
343            return Ok(true);
344        }
345    }
346    Ok(false)
347}
348
349fn frame_bound_may_return_set(
350    engine: &dyn crate::routines::RoutineResolution,
351    resolver: &dyn FunctionTypeResolver,
352    bound: &ScalarFrameBound,
353    schema: &RowSchema,
354    params: &[SQLParam],
355) -> Result<bool, SQLError> {
356    match bound {
357        ScalarFrameBound::Preceding(expression) | ScalarFrameBound::Following(expression) => {
358            expression_may_return_set(engine, resolver, expression, schema, params)
359        }
360        ScalarFrameBound::UnboundedPreceding
361        | ScalarFrameBound::UnboundedFollowing
362        | ScalarFrameBound::CurrentRow => Ok(false),
363    }
364}
365
366fn frame_bound_expression(bound: &ScalarFrameBound) -> Option<&ScalarExpr> {
367    match bound {
368        ScalarFrameBound::Preceding(expression) | ScalarFrameBound::Following(expression) => {
369            Some(expression)
370        }
371        ScalarFrameBound::UnboundedPreceding
372        | ScalarFrameBound::UnboundedFollowing
373        | ScalarFrameBound::CurrentRow => None,
374    }
375}
376
377fn set_context_error(message: impl Into<String>) -> SQLError {
378    SQLError::Routine {
379        sqlstate: "0A000".into(),
380        message: message.into(),
381    }
382}
383
384fn reject_set_descendant<'a>(
385    engine: &dyn SetFunctionCatalog,
386    resolver: &dyn FunctionTypeResolver,
387    expressions: impl IntoIterator<Item = &'a ScalarExpr>,
388    schema: &RowSchema,
389    params: &[SQLParam],
390    message: &str,
391) -> Result<(), SQLError> {
392    if expressions_may_return_set(engine, resolver, expressions, schema, params)? {
393        return Err(set_context_error(message));
394    }
395    Ok(())
396}
397
398#[expect(
399    clippy::too_many_lines,
400    reason = "preserves SELECT schema and row identity"
401)]
402fn validate_set_context(
403    engine: &dyn SetFunctionCatalog,
404    resolver: &dyn FunctionTypeResolver,
405    expression: &ScalarExpr,
406    schema: &RowSchema,
407    params: &[SQLParam],
408) -> Result<(), SQLError> {
409    match expression {
410        ScalarExpr::Func {
411            name,
412            args,
413            order_by,
414            filter,
415            ..
416        } => {
417            let lower = crate::semantics::builtin_function_dispatch_name(name);
418            if is_aggregate(engine, expression) {
419                reject_set_descendant(
420                    engine,
421                    resolver,
422                    args.iter()
423                        .chain(order_by.iter().map(|order| &order.expr))
424                        .chain(filter.iter().map(AsRef::as_ref)),
425                    schema,
426                    params,
427                    "aggregate function calls cannot contain set-returning function calls",
428                )?;
429            } else if lower == "coalesce" {
430                reject_set_descendant(
431                    engine,
432                    resolver,
433                    args,
434                    schema,
435                    params,
436                    "set-returning functions are not allowed in COALESCE",
437                )?;
438            }
439            for argument in args {
440                validate_set_context(engine, resolver, argument, schema, params)?;
441            }
442            for order in order_by {
443                validate_set_context(engine, resolver, &order.expr, schema, params)?;
444            }
445            if let Some(filter) = filter {
446                validate_set_context(engine, resolver, filter, schema, params)?;
447            }
448        }
449        ScalarExpr::WindowCall {
450            args, spec, filter, ..
451        } => {
452            reject_set_descendant(
453                engine,
454                resolver,
455                args.iter()
456                    .chain(filter.iter().map(AsRef::as_ref))
457                    .chain(spec.partition_by.iter())
458                    .chain(spec.order_by.iter().map(|order| &order.expr))
459                    .chain(
460                        spec.frame
461                            .iter()
462                            .flat_map(|frame| [&frame.start, &frame.end])
463                            .filter_map(|bound| frame_bound_expression(bound)),
464                    ),
465                schema,
466                params,
467                "window function calls cannot contain set-returning function calls",
468            )?;
469        }
470        ScalarExpr::Case {
471            base,
472            when,
473            else_branch,
474        } => {
475            let descendants = base
476                .iter()
477                .map(AsRef::as_ref)
478                .chain(
479                    when.iter()
480                        .flat_map(|(condition, result)| [condition, result]),
481                )
482                .chain(else_branch.iter().map(AsRef::as_ref));
483            reject_set_descendant(
484                engine,
485                resolver,
486                descendants,
487                schema,
488                params,
489                "set-returning functions are not allowed in CASE",
490            )?;
491        }
492        ScalarExpr::Not(inner) => {
493            reject_set_descendant(
494                engine,
495                resolver,
496                [inner.as_ref()],
497                schema,
498                params,
499                "argument of NOT must not return a set",
500            )?;
501        }
502        ScalarExpr::UnaryMinus(inner) => {
503            validate_set_context(engine, resolver, inner, schema, params)?;
504        }
505        ScalarExpr::And(items) => {
506            reject_set_descendant(
507                engine,
508                resolver,
509                items,
510                schema,
511                params,
512                "argument of AND must not return a set",
513            )?;
514        }
515        ScalarExpr::Or(items) => {
516            reject_set_descendant(
517                engine,
518                resolver,
519                items,
520                schema,
521                params,
522                "argument of OR must not return a set",
523            )?;
524        }
525        ScalarExpr::InList { expr, list, .. } => {
526            validate_set_context(engine, resolver, expr, schema, params)?;
527            reject_set_descendant(
528                engine,
529                resolver,
530                list,
531                schema,
532                params,
533                "argument of IN must not return a set",
534            )?;
535        }
536        ScalarExpr::Between { expr, low, high } => {
537            reject_set_descendant(
538                engine,
539                resolver,
540                [expr.as_ref(), low.as_ref(), high.as_ref()],
541                schema,
542                params,
543                "argument of AND must not return a set",
544            )?;
545        }
546        ScalarExpr::Array(items)
547        | ScalarExpr::Row(items)
548        | ScalarExpr::CompositeRow { items, .. } => {
549            for item in items {
550                validate_set_context(engine, resolver, item, schema, params)?;
551            }
552        }
553        ScalarExpr::Binary { lhs, rhs, .. } => {
554            validate_set_context(engine, resolver, lhs, schema, params)?;
555            validate_set_context(engine, resolver, rhs, schema, params)?;
556        }
557        ScalarExpr::IsNull { expr, .. } | ScalarExpr::Cast { expr, .. } => {
558            validate_set_context(engine, resolver, expr, schema, params)?;
559        }
560        ScalarExpr::InSubquery { expr, .. } => {
561            reject_set_descendant(
562                engine,
563                resolver,
564                [expr.as_ref()],
565                schema,
566                params,
567                "row comparison operator must not return a set",
568            )?;
569        }
570        ScalarExpr::Default
571        | ScalarExpr::Star
572        | ScalarExpr::QualifiedStar(_)
573        | ScalarExpr::Column(_)
574        | ScalarExpr::Position(_)
575        | ScalarExpr::InternalColumn(_)
576        | ScalarExpr::QualifiedColumn { .. }
577        | ScalarExpr::Literal(_)
578        | ScalarExpr::TypedLiteral { .. }
579        | ScalarExpr::Param(_)
580        | ScalarExpr::ScalarSubquery(_)
581        | ScalarExpr::Exists { .. } => {}
582    }
583    Ok(())
584}
585
586pub fn validate_projection_set_contexts(
587    engine: &dyn SetFunctionCatalog,
588    resolver: &dyn FunctionTypeResolver,
589    projections: &[ProjectionPlan],
590    schema: &RowSchema,
591    params: &[SQLParam],
592) -> Result<(), SQLError> {
593    for projection in projections {
594        validate_set_context(engine, resolver, &projection.expr, schema, params)?;
595    }
596    Ok(())
597}
598
599pub fn validate_query_set_contexts(
600    engine: &dyn SetFunctionCatalog,
601    resolver: &dyn FunctionTypeResolver,
602    statement: &QueryBlockPlan,
603    schema: &RowSchema,
604    params: &[SQLParam],
605) -> Result<(), SQLError> {
606    validate_projection_set_contexts(engine, resolver, &statement.projections, schema, params)?;
607    if let Some(locking) = statement.locking.first() {
608        for projection in &statement.projections {
609            if expression_may_return_set(engine, resolver, &projection.expr, schema, params)? {
610                return Err(SQLError::Unsupported(format!(
611                    "{} is not allowed with set-returning functions in the target list",
612                    locking.strength.sql_name()
613                )));
614            }
615        }
616    }
617    for expression in statement
618        .group_by
619        .iter()
620        .chain(statement.grouping_sets.iter().flatten())
621        .chain(statement.order_by.iter().map(|order| &order.expr))
622        .chain(statement.distinct_on.iter())
623    {
624        validate_set_context(engine, resolver, expression, schema, params)?;
625    }
626    if let Some(predicate) = statement.r#where.as_ref() {
627        reject_set_descendant(
628            engine,
629            resolver,
630            [predicate],
631            schema,
632            params,
633            "set-returning functions are not allowed in WHERE",
634        )?;
635    }
636    if let Some(having) = statement.having.as_ref() {
637        reject_set_descendant(
638            engine,
639            resolver,
640            [having],
641            schema,
642            params,
643            "set-returning functions are not allowed in HAVING",
644        )?;
645    }
646    if let Some(limit) = statement.limit.as_ref() {
647        reject_set_descendant(
648            engine,
649            resolver,
650            [limit],
651            schema,
652            params,
653            "set-returning functions are not allowed in LIMIT",
654        )?;
655    }
656    if let Some(offset) = statement.offset.as_ref() {
657        reject_set_descendant(
658            engine,
659            resolver,
660            [offset],
661            schema,
662            params,
663            "set-returning functions are not allowed in OFFSET",
664        )?;
665    }
666    if let Some(source) = statement.from.as_ref() {
667        validate_source_set_contexts(engine, resolver, source, schema, params)?;
668    }
669    Ok(())
670}
671
672fn validate_source_set_contexts(
673    engine: &dyn SetFunctionCatalog,
674    resolver: &dyn FunctionTypeResolver,
675    source: &SourcePlan,
676    schema: &RowSchema,
677    params: &[SQLParam],
678) -> Result<(), SQLError> {
679    match source {
680        SourcePlan::Join {
681            left, right, on, ..
682        } => {
683            validate_source_set_contexts(engine, resolver, left, schema, params)?;
684            validate_source_set_contexts(engine, resolver, right, schema, params)?;
685            if let Some(condition) = on {
686                reject_set_descendant(
687                    engine,
688                    resolver,
689                    [condition],
690                    schema,
691                    params,
692                    "set-returning functions are not allowed in JOIN conditions",
693                )?;
694            }
695        }
696        SourcePlan::Values { rows, .. } => {
697            validate_values_set_contexts(engine, resolver, rows, schema, params)?;
698        }
699        SourcePlan::Function { args, .. } => {
700            reject_set_descendant(
701                engine,
702                resolver,
703                args,
704                schema,
705                params,
706                "set-returning functions must appear at top level of FROM",
707            )?;
708        }
709        SourcePlan::FunctionGroup { functions, .. } => {
710            for function in functions {
711                reject_set_descendant(
712                    engine,
713                    resolver,
714                    &function.args,
715                    schema,
716                    params,
717                    "set-returning functions must appear at top level of FROM",
718                )?;
719            }
720        }
721        SourcePlan::Subquery { .. } | SourcePlan::Table { .. } => {}
722    }
723    Ok(())
724}
725
726pub fn validate_source_set_contexts_before_build(
727    engine: &dyn SetFunctionCatalog,
728    resolver: &dyn FunctionTypeResolver,
729    source: &SourcePlan,
730    params: &[SQLParam],
731    ctes: &BindingContext<'_>,
732    outer: Option<&RowSchema>,
733) -> Result<(), SQLError> {
734    match source {
735        SourcePlan::Join {
736            left,
737            right,
738            lateral,
739            ..
740        } => {
741            validate_source_set_contexts_before_build(engine, resolver, left, params, ctes, outer)?;
742            let left_schema =
743                crate::binding::bind_source_plan_schema(engine, left, params, ctes, outer)?;
744            let implicit_lateral_function = matches!(
745                right.as_ref(),
746                SourcePlan::Function { .. } | SourcePlan::FunctionGroup { .. }
747            );
748            let right_scope = (*lateral || implicit_lateral_function)
749                .then(|| overlay_set_validation_scope(&left_schema, outer));
750            validate_source_set_contexts_before_build(
751                engine,
752                resolver,
753                right,
754                params,
755                ctes,
756                right_scope.as_ref().or(outer),
757            )
758        }
759        SourcePlan::Values { rows, .. } => {
760            let empty = RowSchema::default();
761            validate_values_set_contexts(engine, resolver, rows, outer.unwrap_or(&empty), params)
762        }
763        SourcePlan::Function { args, .. } => {
764            let empty = RowSchema::default();
765            reject_set_descendant(
766                engine,
767                resolver,
768                args,
769                outer.unwrap_or(&empty),
770                params,
771                "set-returning functions must appear at top level of FROM",
772            )
773        }
774        SourcePlan::FunctionGroup { functions, .. } => {
775            let empty = RowSchema::default();
776            for function in functions {
777                reject_set_descendant(
778                    engine,
779                    resolver,
780                    &function.args,
781                    outer.unwrap_or(&empty),
782                    params,
783                    "set-returning functions must appear at top level of FROM",
784                )?;
785            }
786            Ok(())
787        }
788        SourcePlan::Subquery { .. } | SourcePlan::Table { .. } => Ok(()),
789    }
790}
791
792fn overlay_set_validation_scope(current: &RowSchema, outer: Option<&RowSchema>) -> RowSchema {
793    let Some(outer) = outer else {
794        return current.clone();
795    };
796    let columns = outer
797        .identities()
798        .iter()
799        .enumerate()
800        .map(|(position, identity)| (identity.clone(), outer.column_type(position).cloned()))
801        .collect::<Vec<_>>();
802    RowSchema::with_typed_outer_identities(current, &columns)
803}
804
805pub fn validate_values_set_contexts(
806    engine: &dyn SetFunctionCatalog,
807    resolver: &dyn FunctionTypeResolver,
808    rows: &[Vec<ScalarExpr>],
809    schema: &RowSchema,
810    params: &[SQLParam],
811) -> Result<(), SQLError> {
812    reject_set_descendant(
813        engine,
814        resolver,
815        rows.iter().flatten(),
816        schema,
817        params,
818        "set-returning functions are not allowed in VALUES",
819    )
820}