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