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        | ScalarExpr::Collated { expr: inner, .. } => {
249            expression_may_return_set(engine, resolver, inner, schema, params)
250        }
251        ScalarExpr::Between { expr, low, high } => {
252            Ok(
253                expression_may_return_set(engine, resolver, expr, schema, params)?
254                    || expression_may_return_set(engine, resolver, low, schema, params)?
255                    || expression_may_return_set(engine, resolver, high, schema, params)?,
256            )
257        }
258        ScalarExpr::InList { expr, list, .. } => Ok(expression_may_return_set(
259            engine, resolver, expr, schema, params,
260        )? || expressions_may_return_set(
261            engine, resolver, list, schema, params,
262        )?),
263        ScalarExpr::WindowCall {
264            args, spec, filter, ..
265        } => {
266            if expressions_may_return_set(engine, resolver, args, schema, params)?
267                || expressions_may_return_set(
268                    engine,
269                    resolver,
270                    filter.iter().map(AsRef::as_ref),
271                    schema,
272                    params,
273                )?
274                || expressions_may_return_set(engine, resolver, &spec.partition_by, schema, params)?
275                || expressions_may_return_set(
276                    engine,
277                    resolver,
278                    spec.order_by.iter().map(|order| &order.expr),
279                    schema,
280                    params,
281                )?
282            {
283                return Ok(true);
284            }
285            let Some(frame) = spec.frame.as_ref() else {
286                return Ok(false);
287            };
288            Ok(
289                frame_bound_may_return_set(engine, resolver, &frame.start, schema, params)?
290                    || frame_bound_may_return_set(engine, resolver, &frame.end, schema, params)?,
291            )
292        }
293        ScalarExpr::Case {
294            base,
295            when,
296            else_branch,
297        } => {
298            if let Some(base) = base {
299                if expression_may_return_set(engine, resolver, base, schema, params)? {
300                    return Ok(true);
301                }
302            }
303            for (condition, result) in when {
304                if expression_may_return_set(engine, resolver, condition, schema, params)?
305                    || expression_may_return_set(engine, resolver, result, schema, params)?
306                {
307                    return Ok(true);
308                }
309            }
310            if let Some(branch) = else_branch {
311                if expression_may_return_set(engine, resolver, branch, schema, params)? {
312                    return Ok(true);
313                }
314            }
315            Ok(false)
316        }
317        ScalarExpr::InSubquery { expr, .. } => {
318            expression_may_return_set(engine, resolver, expr, schema, params)
319        }
320        ScalarExpr::Default
321        | ScalarExpr::Star
322        | ScalarExpr::QualifiedStar(_)
323        | ScalarExpr::Column(_)
324        | ScalarExpr::Position(_)
325        | ScalarExpr::InternalColumn(_)
326        | ScalarExpr::QualifiedColumn { .. }
327        | ScalarExpr::Literal(_)
328        | ScalarExpr::TypedLiteral { .. }
329        | ScalarExpr::Param(_)
330        | ScalarExpr::ScalarSubquery(_)
331        | ScalarExpr::Exists { .. } => Ok(false),
332    }
333}
334
335fn expressions_may_return_set<'a>(
336    engine: &dyn crate::routines::RoutineResolution,
337    resolver: &dyn FunctionTypeResolver,
338    expressions: impl IntoIterator<Item = &'a ScalarExpr>,
339    schema: &RowSchema,
340    params: &[SQLParam],
341) -> Result<bool, SQLError> {
342    for expression in expressions {
343        if expression_may_return_set(engine, resolver, expression, schema, params)? {
344            return Ok(true);
345        }
346    }
347    Ok(false)
348}
349
350fn frame_bound_may_return_set(
351    engine: &dyn crate::routines::RoutineResolution,
352    resolver: &dyn FunctionTypeResolver,
353    bound: &ScalarFrameBound,
354    schema: &RowSchema,
355    params: &[SQLParam],
356) -> Result<bool, SQLError> {
357    match bound {
358        ScalarFrameBound::Preceding(expression) | ScalarFrameBound::Following(expression) => {
359            expression_may_return_set(engine, resolver, expression, schema, params)
360        }
361        ScalarFrameBound::UnboundedPreceding
362        | ScalarFrameBound::UnboundedFollowing
363        | ScalarFrameBound::CurrentRow => Ok(false),
364    }
365}
366
367fn frame_bound_expression(bound: &ScalarFrameBound) -> Option<&ScalarExpr> {
368    match bound {
369        ScalarFrameBound::Preceding(expression) | ScalarFrameBound::Following(expression) => {
370            Some(expression)
371        }
372        ScalarFrameBound::UnboundedPreceding
373        | ScalarFrameBound::UnboundedFollowing
374        | ScalarFrameBound::CurrentRow => None,
375    }
376}
377
378fn set_context_error(message: impl Into<String>) -> SQLError {
379    SQLError::Routine {
380        sqlstate: "0A000".into(),
381        message: message.into(),
382    }
383}
384
385fn reject_set_descendant<'a>(
386    engine: &dyn SetFunctionCatalog,
387    resolver: &dyn FunctionTypeResolver,
388    expressions: impl IntoIterator<Item = &'a ScalarExpr>,
389    schema: &RowSchema,
390    params: &[SQLParam],
391    message: &str,
392) -> Result<(), SQLError> {
393    if expressions_may_return_set(engine, resolver, expressions, schema, params)? {
394        return Err(set_context_error(message));
395    }
396    Ok(())
397}
398
399#[expect(
400    clippy::too_many_lines,
401    reason = "preserves SELECT schema and row identity"
402)]
403fn validate_set_context(
404    engine: &dyn SetFunctionCatalog,
405    resolver: &dyn FunctionTypeResolver,
406    expression: &ScalarExpr,
407    schema: &RowSchema,
408    params: &[SQLParam],
409) -> Result<(), SQLError> {
410    match expression {
411        ScalarExpr::Func {
412            name,
413            args,
414            order_by,
415            filter,
416            ..
417        } => {
418            let lower = crate::semantics::builtin_function_dispatch_name(name);
419            if is_aggregate(engine, expression) {
420                reject_set_descendant(
421                    engine,
422                    resolver,
423                    args.iter()
424                        .chain(order_by.iter().map(|order| &order.expr))
425                        .chain(filter.iter().map(AsRef::as_ref)),
426                    schema,
427                    params,
428                    "aggregate function calls cannot contain set-returning function calls",
429                )?;
430            } else if lower == "coalesce" {
431                reject_set_descendant(
432                    engine,
433                    resolver,
434                    args,
435                    schema,
436                    params,
437                    "set-returning functions are not allowed in COALESCE",
438                )?;
439            }
440            for argument in args {
441                validate_set_context(engine, resolver, argument, schema, params)?;
442            }
443            for order in order_by {
444                validate_set_context(engine, resolver, &order.expr, schema, params)?;
445            }
446            if let Some(filter) = filter {
447                validate_set_context(engine, resolver, filter, schema, params)?;
448            }
449        }
450        ScalarExpr::WindowCall {
451            args, spec, filter, ..
452        } => {
453            reject_set_descendant(
454                engine,
455                resolver,
456                args.iter()
457                    .chain(filter.iter().map(AsRef::as_ref))
458                    .chain(spec.partition_by.iter())
459                    .chain(spec.order_by.iter().map(|order| &order.expr))
460                    .chain(
461                        spec.frame
462                            .iter()
463                            .flat_map(|frame| [&frame.start, &frame.end])
464                            .filter_map(|bound| frame_bound_expression(bound)),
465                    ),
466                schema,
467                params,
468                "window function calls cannot contain set-returning function calls",
469            )?;
470        }
471        ScalarExpr::Case {
472            base,
473            when,
474            else_branch,
475        } => {
476            let descendants = base
477                .iter()
478                .map(AsRef::as_ref)
479                .chain(
480                    when.iter()
481                        .flat_map(|(condition, result)| [condition, result]),
482                )
483                .chain(else_branch.iter().map(AsRef::as_ref));
484            reject_set_descendant(
485                engine,
486                resolver,
487                descendants,
488                schema,
489                params,
490                "set-returning functions are not allowed in CASE",
491            )?;
492        }
493        ScalarExpr::Not(inner) => {
494            reject_set_descendant(
495                engine,
496                resolver,
497                [inner.as_ref()],
498                schema,
499                params,
500                "argument of NOT must not return a set",
501            )?;
502        }
503        ScalarExpr::UnaryMinus(inner) => {
504            validate_set_context(engine, resolver, inner, schema, params)?;
505        }
506        ScalarExpr::And(items) => {
507            reject_set_descendant(
508                engine,
509                resolver,
510                items,
511                schema,
512                params,
513                "argument of AND must not return a set",
514            )?;
515        }
516        ScalarExpr::Or(items) => {
517            reject_set_descendant(
518                engine,
519                resolver,
520                items,
521                schema,
522                params,
523                "argument of OR must not return a set",
524            )?;
525        }
526        ScalarExpr::InList { expr, list, .. } => {
527            validate_set_context(engine, resolver, expr, schema, params)?;
528            reject_set_descendant(
529                engine,
530                resolver,
531                list,
532                schema,
533                params,
534                "argument of IN must not return a set",
535            )?;
536        }
537        ScalarExpr::Between { expr, low, high } => {
538            reject_set_descendant(
539                engine,
540                resolver,
541                [expr.as_ref(), low.as_ref(), high.as_ref()],
542                schema,
543                params,
544                "argument of AND must not return a set",
545            )?;
546        }
547        ScalarExpr::Array(items)
548        | ScalarExpr::Row(items)
549        | ScalarExpr::CompositeRow { items, .. } => {
550            for item in items {
551                validate_set_context(engine, resolver, item, schema, params)?;
552            }
553        }
554        ScalarExpr::Binary { lhs, rhs, .. } => {
555            validate_set_context(engine, resolver, lhs, schema, params)?;
556            validate_set_context(engine, resolver, rhs, schema, params)?;
557        }
558        ScalarExpr::IsNull { expr, .. }
559        | ScalarExpr::Cast { expr, .. }
560        | ScalarExpr::Collated { expr, .. } => {
561            validate_set_context(engine, resolver, expr, schema, params)?;
562        }
563        ScalarExpr::InSubquery { expr, .. } => {
564            reject_set_descendant(
565                engine,
566                resolver,
567                [expr.as_ref()],
568                schema,
569                params,
570                "row comparison operator must not return a set",
571            )?;
572        }
573        ScalarExpr::Default
574        | ScalarExpr::Star
575        | ScalarExpr::QualifiedStar(_)
576        | ScalarExpr::Column(_)
577        | ScalarExpr::Position(_)
578        | ScalarExpr::InternalColumn(_)
579        | ScalarExpr::QualifiedColumn { .. }
580        | ScalarExpr::Literal(_)
581        | ScalarExpr::TypedLiteral { .. }
582        | ScalarExpr::Param(_)
583        | ScalarExpr::ScalarSubquery(_)
584        | ScalarExpr::Exists { .. } => {}
585    }
586    Ok(())
587}
588
589pub fn validate_projection_set_contexts(
590    engine: &dyn SetFunctionCatalog,
591    resolver: &dyn FunctionTypeResolver,
592    projections: &[ProjectionPlan],
593    schema: &RowSchema,
594    params: &[SQLParam],
595) -> Result<(), SQLError> {
596    for projection in projections {
597        validate_set_context(engine, resolver, &projection.expr, schema, params)?;
598    }
599    Ok(())
600}
601
602pub fn validate_query_set_contexts(
603    engine: &dyn SetFunctionCatalog,
604    resolver: &dyn FunctionTypeResolver,
605    statement: &QueryBlockPlan,
606    schema: &RowSchema,
607    params: &[SQLParam],
608) -> Result<(), SQLError> {
609    validate_projection_set_contexts(engine, resolver, &statement.projections, schema, params)?;
610    if let Some(locking) = statement.locking.first() {
611        for projection in &statement.projections {
612            if expression_may_return_set(engine, resolver, &projection.expr, schema, params)? {
613                return Err(SQLError::Unsupported(format!(
614                    "{} is not allowed with set-returning functions in the target list",
615                    locking.strength.sql_name()
616                )));
617            }
618        }
619    }
620    for expression in statement
621        .group_by
622        .iter()
623        .chain(statement.grouping_sets.iter().flatten())
624        .chain(statement.order_by.iter().map(|order| &order.expr))
625        .chain(statement.distinct_on.iter())
626    {
627        validate_set_context(engine, resolver, expression, schema, params)?;
628    }
629    if let Some(predicate) = statement.r#where.as_ref() {
630        reject_set_descendant(
631            engine,
632            resolver,
633            [predicate],
634            schema,
635            params,
636            "set-returning functions are not allowed in WHERE",
637        )?;
638    }
639    if let Some(having) = statement.having.as_ref() {
640        reject_set_descendant(
641            engine,
642            resolver,
643            [having],
644            schema,
645            params,
646            "set-returning functions are not allowed in HAVING",
647        )?;
648    }
649    if let Some(limit) = statement.limit.as_ref() {
650        reject_set_descendant(
651            engine,
652            resolver,
653            [limit],
654            schema,
655            params,
656            "set-returning functions are not allowed in LIMIT",
657        )?;
658    }
659    if let Some(offset) = statement.offset.as_ref() {
660        reject_set_descendant(
661            engine,
662            resolver,
663            [offset],
664            schema,
665            params,
666            "set-returning functions are not allowed in OFFSET",
667        )?;
668    }
669    if let Some(source) = statement.from.as_ref() {
670        validate_source_set_contexts(engine, resolver, source, schema, params)?;
671    }
672    Ok(())
673}
674
675fn validate_source_set_contexts(
676    engine: &dyn SetFunctionCatalog,
677    resolver: &dyn FunctionTypeResolver,
678    source: &SourcePlan,
679    schema: &RowSchema,
680    params: &[SQLParam],
681) -> Result<(), SQLError> {
682    match source {
683        SourcePlan::Join {
684            left, right, on, ..
685        } => {
686            validate_source_set_contexts(engine, resolver, left, schema, params)?;
687            validate_source_set_contexts(engine, resolver, right, schema, params)?;
688            if let Some(condition) = on {
689                reject_set_descendant(
690                    engine,
691                    resolver,
692                    [condition],
693                    schema,
694                    params,
695                    "set-returning functions are not allowed in JOIN conditions",
696                )?;
697            }
698        }
699        SourcePlan::Values { rows, .. } => {
700            validate_values_set_contexts(engine, resolver, rows, schema, params)?;
701        }
702        SourcePlan::Function { args, .. } => {
703            reject_set_descendant(
704                engine,
705                resolver,
706                args,
707                schema,
708                params,
709                "set-returning functions must appear at top level of FROM",
710            )?;
711        }
712        SourcePlan::FunctionGroup { functions, .. } => {
713            for function in functions {
714                reject_set_descendant(
715                    engine,
716                    resolver,
717                    &function.args,
718                    schema,
719                    params,
720                    "set-returning functions must appear at top level of FROM",
721                )?;
722            }
723        }
724        SourcePlan::Subquery { .. } | SourcePlan::Table { .. } => {}
725    }
726    Ok(())
727}
728
729pub fn validate_source_set_contexts_before_build(
730    engine: &dyn SetFunctionCatalog,
731    resolver: &dyn FunctionTypeResolver,
732    source: &SourcePlan,
733    params: &[SQLParam],
734    ctes: &BindingContext<'_>,
735    outer: Option<&RowSchema>,
736) -> Result<(), SQLError> {
737    match source {
738        SourcePlan::Join {
739            left,
740            right,
741            lateral,
742            ..
743        } => {
744            validate_source_set_contexts_before_build(engine, resolver, left, params, ctes, outer)?;
745            let left_schema =
746                crate::binding::bind_source_plan_schema(engine, left, params, ctes, outer)?;
747            let implicit_lateral_function = matches!(
748                right.as_ref(),
749                SourcePlan::Function { .. } | SourcePlan::FunctionGroup { .. }
750            );
751            let right_scope = (*lateral || implicit_lateral_function)
752                .then(|| overlay_set_validation_scope(&left_schema, outer));
753            validate_source_set_contexts_before_build(
754                engine,
755                resolver,
756                right,
757                params,
758                ctes,
759                right_scope.as_ref().or(outer),
760            )
761        }
762        SourcePlan::Values { rows, .. } => {
763            let empty = RowSchema::default();
764            validate_values_set_contexts(engine, resolver, rows, outer.unwrap_or(&empty), params)
765        }
766        SourcePlan::Function { args, .. } => {
767            let empty = RowSchema::default();
768            reject_set_descendant(
769                engine,
770                resolver,
771                args,
772                outer.unwrap_or(&empty),
773                params,
774                "set-returning functions must appear at top level of FROM",
775            )
776        }
777        SourcePlan::FunctionGroup { functions, .. } => {
778            let empty = RowSchema::default();
779            for function in functions {
780                reject_set_descendant(
781                    engine,
782                    resolver,
783                    &function.args,
784                    outer.unwrap_or(&empty),
785                    params,
786                    "set-returning functions must appear at top level of FROM",
787                )?;
788            }
789            Ok(())
790        }
791        SourcePlan::Subquery { .. } | SourcePlan::Table { .. } => Ok(()),
792    }
793}
794
795fn overlay_set_validation_scope(current: &RowSchema, outer: Option<&RowSchema>) -> RowSchema {
796    let Some(outer) = outer else {
797        return current.clone();
798    };
799    let columns = outer
800        .identities()
801        .iter()
802        .enumerate()
803        .map(|(position, identity)| (identity.clone(), outer.column_type(position).cloned()))
804        .collect::<Vec<_>>();
805    RowSchema::with_typed_outer_identities(current, &columns)
806}
807
808pub fn validate_values_set_contexts(
809    engine: &dyn SetFunctionCatalog,
810    resolver: &dyn FunctionTypeResolver,
811    rows: &[Vec<ScalarExpr>],
812    schema: &RowSchema,
813    params: &[SQLParam],
814) -> Result<(), SQLError> {
815    reject_set_descendant(
816        engine,
817        resolver,
818        rows.iter().flatten(),
819        schema,
820        params,
821        "set-returning functions are not allowed in VALUES",
822    )
823}