Skip to main content

uqa_sql/semantics/
table_functions.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Table-function binding, declared result types, and source aliases.
8
9use crate::ast::{
10    ColumnType, FunctionBinding, FunctionParamMode, FunctionReturns, RoutineInvocationBinding,
11};
12use crate::routines::{routine_returns_anonymous_record, RoutineResolution, SQLUserFunction};
13use crate::type_resolution::{BuiltinFunctionOverload, FunctionTypeResolver};
14use crate::{RowSchema, SQLError, SQLParam, ScalarExpr};
15use std::sync::Arc;
16
17pub struct ResolvedUserTableFunction {
18    pub function: Arc<SQLUserFunction>,
19    pub binding: FunctionBinding,
20}
21
22/// Correct the old integer series declaration, which recorded a nonexistent default on the three-argument overload. This only accepts the exact builtin catalog shape; it never resolves a persisted call by its display name.
23pub(crate) fn upgrade_legacy_table_function_binding(
24    binding: &mut Option<FunctionBinding>,
25    argument_count: usize,
26) -> bool {
27    let Some(binding) = binding else {
28        return false;
29    };
30    if argument_count != 2
31        || !binding.builtin
32        || binding.object_id.is_some()
33        || binding.name != "pg_catalog.generate_series"
34        || binding.dispatch.is_some()
35        || binding.invocation.is_some()
36        || binding.resolution_error.is_some()
37    {
38        return false;
39    }
40    let [first, second, third] = binding.argument_types.as_slice() else {
41        return false;
42    };
43    if !matches!(first.as_str(), "integer" | "bigint") || first != second || first != third {
44        return false;
45    }
46    binding.argument_types.truncate(2);
47    true
48}
49
50pub fn user_function_output_columns_for(function: &SQLUserFunction) -> Option<Vec<String>> {
51    let outputs = function.def.output_params();
52    if outputs.is_empty() {
53        return None;
54    }
55    Some(
56        outputs
57            .iter()
58            .enumerate()
59            .map(|(position, parameter)| {
60                if parameter.name.is_empty() {
61                    format!("column{}", position + 1)
62                } else {
63                    parameter.name.clone()
64                }
65            })
66            .collect(),
67    )
68}
69
70/// A named composite returned by a function expands to its live attributes in FROM, including a single composite OUT parameter.
71pub fn user_function_composite_result(
72    routines: &dyn RoutineResolution,
73    function: &SQLUserFunction,
74    invocation: Option<&RoutineInvocationBinding>,
75) -> Result<Option<Arc<crate::expr::composites::CompositeTypeDescriptor>>, SQLError> {
76    let outputs = function.def.output_params();
77    if outputs.len() > 1 {
78        return Ok(None);
79    }
80    let declared = match (&function.def.returns, outputs.first()) {
81        (_, Some(output)) => Some(output.type_name.as_str()),
82        (FunctionReturns::Scalar { type_name } | FunctionReturns::SetOf { type_name }, None) => {
83            Some(type_name.as_str())
84        }
85        _ => None,
86    };
87    let Some(name) = invocation
88        .and_then(|value| value.return_type.as_deref())
89        .or(declared)
90    else {
91        return Ok(None);
92    };
93    let Some(ColumnType::Composite(reference)) = routines.resolve_type_name(name)? else {
94        return Ok(None);
95    };
96    crate::expr::composites::descriptor(routines.composite_types(), reference.oid).map(Some)
97}
98
99fn validate_user_table_function_column_definition(
100    function: &SQLUserFunction,
101    declared_types: &[String],
102) -> Result<(), SQLError> {
103    let returns_anonymous_record = routine_returns_anonymous_record(&function.def);
104    if declared_types.is_empty() {
105        if returns_anonymous_record {
106            return Err(SQLError::Routine {
107                sqlstate: "42601".into(),
108                message: "a column definition list is required for functions returning \"record\""
109                    .into(),
110            });
111        }
112        return Ok(());
113    }
114    if returns_anonymous_record {
115        return Ok(());
116    }
117    if function.def.output_params().len() > 1 {
118        return Err(redundant_out_column_definition_error());
119    }
120    Err(SQLError::Routine {
121        sqlstate: "42601".into(),
122        message: "a column definition list is only allowed for functions returning \"record\""
123            .into(),
124    })
125}
126
127pub fn validate_table_function_column_definition(
128    name: &str,
129    binding: Option<&FunctionBinding>,
130    user_function: Option<&SQLUserFunction>,
131    declared_types: &[String],
132) -> Result<(), SQLError> {
133    if let Some(function) = user_function {
134        return validate_user_table_function_column_definition(function, declared_types);
135    }
136    if declared_types.is_empty() || binding.is_some_and(|binding| !binding.builtin) {
137        return Ok(());
138    }
139    let builtin = crate::semantics::builtin_function_dispatch_name(&name.to_ascii_lowercase());
140    if matches!(
141        builtin.as_str(),
142        "json_each"
143            | "jsonb_each"
144            | "json_each_text"
145            | "jsonb_each_text"
146            | "pg_get_sequence_data"
147            | "pg_sequence_parameters"
148            | "aclexplode"
149    ) {
150        return Err(redundant_out_column_definition_error());
151    }
152    Ok(())
153}
154
155fn redundant_out_column_definition_error() -> SQLError {
156    SQLError::Routine {
157        sqlstate: "42601".into(),
158        message: "a column definition list is redundant for a function with OUT parameters".into(),
159    }
160}
161
162pub fn resolve_user_table_function(
163    routines: &dyn RoutineResolution,
164    name: &str,
165    binding: Option<&FunctionBinding>,
166    args: &[ScalarExpr],
167    input_schema: &RowSchema,
168    params: &[SQLParam],
169    resolver: &dyn FunctionTypeResolver,
170) -> Result<Option<ResolvedUserTableFunction>, SQLError> {
171    let Some(binding) = resolve_table_function_binding(
172        routines,
173        name,
174        binding,
175        args,
176        input_schema,
177        params,
178        resolver,
179    )?
180    else {
181        return Ok(None);
182    };
183    if binding.builtin {
184        return Ok(None);
185    }
186    let (argument_names, argument_types, explicit_variadic) =
187        crate::function_call_argument_signature(args, input_schema, params, Some(resolver))?;
188    let Some(matched) = routines.resolve_static_sql_function_match(
189        name,
190        Some(&binding),
191        &argument_names,
192        &argument_types,
193        explicit_variadic,
194    )?
195    else {
196        return Ok(None);
197    };
198    Ok(Some(ResolvedUserTableFunction {
199        binding: matched.binding(),
200        function: matched.function,
201    }))
202}
203
204pub fn resolve_table_function_binding(
205    routines: &dyn RoutineResolution,
206    name: &str,
207    binding: Option<&FunctionBinding>,
208    args: &[ScalarExpr],
209    input_schema: &RowSchema,
210    params: &[SQLParam],
211    resolver: &dyn FunctionTypeResolver,
212) -> Result<Option<FunctionBinding>, SQLError> {
213    if let Some(binding) = binding {
214        return Ok(Some(binding.clone()));
215    }
216    let identity = name.to_ascii_lowercase();
217    let builtin = crate::semantics::builtin_function_dispatch_name(&identity);
218    let (argument_names, argument_types, explicit_variadic) =
219        crate::function_call_argument_signature(args, input_schema, params, Some(resolver))?;
220    let builtins = builtin_table_function_overloads(&builtin, &argument_types);
221    if !builtins.is_empty() || has_builtin_table_function_overloads(&builtin) {
222        return routines
223            .resolve_table_function_overload_with_builtins(
224                name,
225                None,
226                &argument_names,
227                &argument_types,
228                explicit_variadic,
229                &builtins,
230            )
231            .map(|resolved| resolved.map(|resolved| resolved.binding));
232    }
233    let builtin_surface = is_builtin_table_function(&builtin)
234        || crate::registry::is_operator_join_table_function(&builtin)
235        || routines.has_registered_table_function(&identity);
236    if routines.lookup_visible_sql_functions(name)?.is_none() {
237        return Ok(None);
238    }
239    match routines.resolve_static_sql_function_match(
240        name,
241        None,
242        &argument_names,
243        &argument_types,
244        explicit_variadic,
245    ) {
246        Ok(Some(function)) => Ok(Some(function.binding())),
247        Ok(None) => Ok(None),
248        Err(error) if builtin_surface && error.sqlstate() == Some("42883") => Ok(None),
249        Err(error) => Err(error),
250    }
251}
252
253fn has_builtin_table_function_overloads(name: &str) -> bool {
254    matches!(
255        name,
256        "generate_series"
257            | "pg_listening_channels"
258            | "unnest"
259            | "regexp_split_to_table"
260            | "string_to_table"
261            | "json_array_elements"
262            | "jsonb_array_elements"
263            | "json_array_elements_text"
264            | "jsonb_array_elements_text"
265            | "json_object_keys"
266            | "jsonb_object_keys"
267            | "json_each"
268            | "jsonb_each"
269            | "json_each_text"
270            | "jsonb_each_text"
271            | "pg_get_sequence_data"
272            | "pg_sequence_parameters"
273            | "aclexplode"
274    )
275}
276
277fn builtin_table_function_overloads(
278    name: &str,
279    argument_types: &[Option<ColumnType>],
280) -> Vec<BuiltinFunctionOverload> {
281    let canonical_name = format!("pg_catalog.{name}");
282    let overload = |argument_types: Vec<ColumnType>,
283                    default_arguments: usize,
284                    return_type: ColumnType| BuiltinFunctionOverload {
285        variadic: false,
286        name: canonical_name.clone(),
287        argument_names: vec![None; argument_types.len()],
288        argument_types,
289        default_arguments,
290        return_type,
291    };
292    match name {
293        "pg_listening_channels" => vec![overload(Vec::new(), 0, ColumnType::Text)],
294        "generate_series" => crate::type_resolution::fixed_builtin_overloads(name)
295            .expect("generate_series has fixed catalog signatures")
296            .into_iter()
297            .filter(|overload| {
298                matches!(
299                    overload.return_type,
300                    ColumnType::Integer | ColumnType::BigInteger
301                )
302            })
303            .collect(),
304        "unnest" => {
305            let [Some(argument)] = argument_types else {
306                return Vec::new();
307            };
308            let Some(element) = crate::type_resolution::array_element_type(argument) else {
309                return Vec::new();
310            };
311            vec![overload(vec![ColumnType::AnyArray], 0, element.clone())]
312        }
313        "regexp_split_to_table" | "string_to_table" => vec![overload(
314            vec![ColumnType::Text, ColumnType::Text],
315            0,
316            ColumnType::Text,
317        )],
318        "json_array_elements" => vec![overload(vec![ColumnType::Json], 0, ColumnType::Json)],
319        "jsonb_array_elements" => vec![overload(vec![ColumnType::JsonB], 0, ColumnType::JsonB)],
320        "json_array_elements_text" | "json_object_keys" => {
321            vec![overload(vec![ColumnType::Json], 0, ColumnType::Text)]
322        }
323        "jsonb_array_elements_text" | "jsonb_object_keys" => {
324            vec![overload(vec![ColumnType::JsonB], 0, ColumnType::Text)]
325        }
326        "json_each" | "json_each_text" => {
327            vec![overload(vec![ColumnType::Json], 0, ColumnType::Record)]
328        }
329        "jsonb_each" | "jsonb_each_text" => {
330            vec![overload(vec![ColumnType::JsonB], 0, ColumnType::Record)]
331        }
332        "pg_get_sequence_data" => vec![overload(vec![ColumnType::Regclass], 0, ColumnType::Record)],
333        "pg_sequence_parameters" => {
334            vec![overload(vec![ColumnType::Oid], 0, ColumnType::Record)]
335        }
336        "aclexplode" => vec![overload(
337            vec![ColumnType::Array(Box::new(ColumnType::AclItem))],
338            0,
339            ColumnType::Record,
340        )],
341        _ => Vec::new(),
342    }
343}
344
345pub fn is_builtin_table_function(name: &str) -> bool {
346    matches!(
347        name,
348        "generate_series"
349            | "pg_listening_channels"
350            | "unnest"
351            | "regexp_split_to_table"
352            | "string_to_table"
353            | "json_array_elements"
354            | "jsonb_array_elements"
355            | "json_array_elements_text"
356            | "jsonb_array_elements_text"
357            | "json_object_keys"
358            | "jsonb_object_keys"
359            | "json_each"
360            | "jsonb_each"
361            | "json_each_text"
362            | "jsonb_each_text"
363            | "pg_get_sequence_data"
364            | "pg_sequence_parameters"
365            | "aclexplode"
366            | "create_analyzer"
367            | "drop_analyzer"
368            | "list_analyzers"
369            | "analyze_text"
370            | "fts_index_stats"
371            | "set_table_analyzer"
372            | "pagerank"
373            | "graph_pagerank"
374            | "hits"
375            | "graph_hits"
376            | "betweenness"
377            | "graph_betweenness"
378            | "graph_edges"
379            | "rpq"
380            | "cypher"
381    )
382}
383
384pub fn table_function_empty_schema(
385    name: &str,
386    output_name: &str,
387    alias: Option<&str>,
388    column_aliases: &[String],
389    output_width: usize,
390    ordinality: bool,
391) -> Vec<String> {
392    let lower = crate::semantics::builtin_function_dispatch_name(&name.to_ascii_lowercase());
393    let columns = if lower == "unnest" {
394        let width = output_width.max(1);
395        let default_column = if width == 1 {
396            alias.unwrap_or(output_name)
397        } else {
398            output_name
399        };
400        vec![default_column.to_string(); width]
401    } else {
402        match lower.as_str() {
403            "json_each" | "jsonb_each" | "json_each_text" | "jsonb_each_text" => {
404                vec!["key".into(), "value".into()]
405            }
406            "pg_get_sequence_data" => vec!["last_value".into(), "is_called".into()],
407            "aclexplode" => vec![
408                "grantor".into(),
409                "grantee".into(),
410                "privilege_type".into(),
411                "is_grantable".into(),
412            ],
413            "pg_sequence_parameters" => vec![
414                "start_value".into(),
415                "minimum_value".into(),
416                "maximum_value".into(),
417                "increment".into(),
418                "cycle_option".into(),
419                "cache_size".into(),
420                "data_type".into(),
421            ],
422            "pagerank" | "graph_pagerank" | "hits" | "graph_hits" | "betweenness"
423            | "graph_betweenness" => vec!["_doc_id".into(), "_score".into()],
424            "rpq" => vec!["vertex_id".into()],
425            "list_analyzers" => vec!["analyzer_name".into()],
426            "analyze_text" => vec!["analysis".into()],
427            "fts_index_stats" => vec![
428                "table_name".into(),
429                "field".into(),
430                "analyzer".into(),
431                "posting_count".into(),
432                "doc_length_count".into(),
433                "indexed_doc_count".into(),
434                "term_count".into(),
435                "total_field_length".into(),
436            ],
437            "text_similarity_join"
438            | "vector_similarity_join"
439            | "graph_join"
440            | "hybrid_join"
441            | "cross_paradigm_join" => {
442                vec!["left_doc_id".into(), "right_doc_id".into(), "_score".into()]
443            }
444            "generate_series"
445            | "pg_listening_channels"
446            | "regexp_split_to_table"
447            | "string_to_table"
448            | "json_array_elements"
449            | "jsonb_array_elements"
450            | "json_array_elements_text"
451            | "jsonb_array_elements_text"
452            | "json_object_keys"
453            | "jsonb_object_keys" => vec![scalar_table_function_default_column(
454                &lower,
455                output_name,
456                alias,
457                &[],
458            )],
459            _ => {
460                let minimum_width = 1;
461                let aliased_value_width = if ordinality && column_aliases.len() > minimum_width {
462                    column_aliases.len() - 1
463                } else {
464                    column_aliases.len()
465                };
466                vec![
467                    scalar_table_function_default_column(&lower, output_name, alias, &[]);
468                    minimum_width.max(aliased_value_width)
469                ]
470            }
471        }
472    };
473    apply_table_function_aliases(columns, column_aliases, ordinality)
474}
475
476pub fn apply_table_function_aliases(
477    mut columns: Vec<String>,
478    column_aliases: &[String],
479    ordinality: bool,
480) -> Vec<String> {
481    if ordinality {
482        columns.push("ordinality".into());
483    }
484    for (column, alias) in columns.iter_mut().zip(column_aliases) {
485        column.clone_from(alias);
486    }
487    columns
488}
489
490pub fn validate_table_function_alias_count(
491    table_alias: &str,
492    available: usize,
493    specified: usize,
494) -> Result<(), SQLError> {
495    if specified <= available {
496        return Ok(());
497    }
498    Err(SQLError::Routine {
499        sqlstate: "42P10".into(),
500        message: format!(
501            "table \"{table_alias}\" has {available} columns available but {specified} columns specified"
502        ),
503    })
504}
505
506pub struct TableFunctionTypeRequest<'a> {
507    pub name: &'a str,
508    pub binding: Option<&'a FunctionBinding>,
509    pub args: &'a [ScalarExpr],
510    pub user_function: Option<&'a SQLUserFunction>,
511    pub user_invocation: Option<&'a RoutineInvocationBinding>,
512    pub declared_types: &'a [String],
513    pub columns: &'a [String],
514    pub ordinality: bool,
515}
516
517#[expect(
518    clippy::too_many_lines,
519    reason = "preserves source schema and row identity"
520)]
521pub fn table_function_column_types(
522    routines: &dyn RoutineResolution,
523    request: TableFunctionTypeRequest<'_>,
524    input_schema: &crate::RowSchema,
525    params: &[SQLParam],
526    resolver: &dyn FunctionTypeResolver,
527) -> Vec<Option<ColumnType>> {
528    let TableFunctionTypeRequest {
529        name,
530        binding,
531        args,
532        user_function,
533        user_invocation,
534        declared_types,
535        columns,
536        ordinality,
537    } = request;
538    let value_columns = if ordinality {
539        columns
540            .get(..columns.len().saturating_sub(1))
541            .unwrap_or(&[])
542    } else {
543        columns
544    };
545    let align = |types: Vec<Option<ColumnType>>| {
546        if types.len() == value_columns.len() {
547            types
548        } else if let [ty] = types.as_slice() {
549            vec![ty.clone(); value_columns.len()]
550        } else {
551            vec![None; value_columns.len()]
552        }
553    };
554    let mut types = if !declared_types.is_empty() {
555        align(
556            declared_types
557                .iter()
558                .map(|ty| ColumnType::from_sql_name(ty).ok())
559                .collect(),
560        )
561    } else if let Some(function) = user_function {
562        align(user_function_column_types(
563            routines,
564            function,
565            user_invocation,
566        ))
567    } else {
568        let normalized =
569            crate::semantics::builtin_function_dispatch_name(&name.to_ascii_lowercase());
570        align(match normalized.as_str() {
571            "pg_listening_channels" => vec![Some(ColumnType::Text)],
572            "generate_series" => vec![resolve_table_function_binding(
573                routines,
574                name,
575                binding,
576                args,
577                input_schema,
578                params,
579                resolver,
580            )
581            .ok()
582            .flatten()
583            .as_ref()
584            .and_then(crate::type_resolution::fixed_builtin_return_type)],
585            "analyze_text" => vec![Some(ColumnType::JsonB)],
586            "unnest" => args
587                .iter()
588                .map(|argument| {
589                    crate::scalar_type_with_resolver(argument, input_schema, params, resolver)
590                        .ok()
591                        .flatten()
592                        .and_then(|ty| crate::type_resolution::array_element_type(&ty).cloned())
593                })
594                .collect(),
595            "regexp_split_to_table"
596            | "string_to_table"
597            | "json_object_keys"
598            | "jsonb_object_keys" => vec![Some(ColumnType::Text)],
599            "json_array_elements" => vec![Some(ColumnType::Json)],
600            "jsonb_array_elements" => vec![Some(ColumnType::JsonB)],
601            "json_array_elements_text" | "jsonb_array_elements_text" => {
602                vec![Some(ColumnType::Text)]
603            }
604            "json_each" => vec![Some(ColumnType::Text), Some(ColumnType::Json)],
605            "jsonb_each" => vec![Some(ColumnType::Text), Some(ColumnType::JsonB)],
606            "json_each_text" | "jsonb_each_text" => {
607                vec![Some(ColumnType::Text), Some(ColumnType::Text)]
608            }
609            "pg_get_sequence_data" => {
610                vec![Some(ColumnType::BigInteger), Some(ColumnType::Boolean)]
611            }
612            "pg_sequence_parameters" => vec![
613                Some(ColumnType::BigInteger),
614                Some(ColumnType::BigInteger),
615                Some(ColumnType::BigInteger),
616                Some(ColumnType::BigInteger),
617                Some(ColumnType::Boolean),
618                Some(ColumnType::BigInteger),
619                Some(ColumnType::Oid),
620            ],
621            "aclexplode" => vec![
622                Some(ColumnType::Oid),
623                Some(ColumnType::Oid),
624                Some(ColumnType::Text),
625                Some(ColumnType::Boolean),
626            ],
627            "pagerank" | "graph_pagerank" | "hits" | "graph_hits" | "betweenness"
628            | "graph_betweenness" => vec![
629                Some(ColumnType::BigInteger),
630                Some(ColumnType::DoublePrecision),
631            ],
632            "rpq" => vec![Some(ColumnType::BigInteger)],
633            "text_similarity_join"
634            | "vector_similarity_join"
635            | "graph_join"
636            | "hybrid_join"
637            | "cross_paradigm_join" => vec![
638                Some(ColumnType::BigInteger),
639                Some(ColumnType::BigInteger),
640                Some(ColumnType::DoublePrecision),
641            ],
642            _ => user_table_function_column_types(
643                routines,
644                name,
645                args,
646                input_schema,
647                params,
648                resolver,
649            ),
650        })
651    };
652    if ordinality {
653        types.push(Some(ColumnType::BigInteger));
654    }
655    types
656}
657
658fn user_table_function_column_types(
659    routines: &dyn RoutineResolution,
660    name: &str,
661    args: &[ScalarExpr],
662    input_schema: &crate::RowSchema,
663    params: &[SQLParam],
664    resolver: &dyn FunctionTypeResolver,
665) -> Vec<Option<ColumnType>> {
666    let Ok((argument_names, argument_types, explicit_variadic)) =
667        crate::function_call_argument_signature(args, input_schema, params, Some(resolver))
668    else {
669        return Vec::new();
670    };
671    let Ok(Some(matched)) = routines.resolve_static_sql_function_match(
672        name,
673        None,
674        &argument_names,
675        &argument_types,
676        explicit_variadic,
677    ) else {
678        return Vec::new();
679    };
680    user_function_column_types(routines, &matched.function, Some(&matched.invocation))
681}
682
683fn user_function_column_types(
684    routines: &dyn RoutineResolution,
685    function: &SQLUserFunction,
686    invocation: Option<&RoutineInvocationBinding>,
687) -> Vec<Option<ColumnType>> {
688    if let Ok(Some(descriptor)) = user_function_composite_result(routines, function, invocation) {
689        return descriptor
690            .attributes
691            .iter()
692            .map(|attribute| Some(attribute.ty.clone()))
693            .collect();
694    }
695    let outputs = function
696        .def
697        .params
698        .iter()
699        .enumerate()
700        .filter(|(_, parameter)| {
701            matches!(
702                parameter.mode,
703                FunctionParamMode::Out | FunctionParamMode::InOut | FunctionParamMode::Table
704            )
705        })
706        .collect::<Vec<_>>();
707    if !outputs.is_empty() {
708        return outputs
709            .into_iter()
710            .map(|(index, parameter)| {
711                let type_name = invocation
712                    .and_then(|binding| binding.parameter_types.get(index))
713                    .unwrap_or(&parameter.type_name);
714                resolve_table_function_column_type(routines, type_name)
715            })
716            .collect();
717    }
718    match &function.def.returns {
719        FunctionReturns::Scalar { type_name } | FunctionReturns::SetOf { type_name } => {
720            let type_name = invocation
721                .and_then(|binding| binding.return_type.as_ref())
722                .unwrap_or(type_name);
723            vec![resolve_table_function_column_type(routines, type_name)]
724        }
725        FunctionReturns::None | FunctionReturns::Table => Vec::new(),
726    }
727}
728
729fn resolve_table_function_column_type(
730    routines: &dyn RoutineResolution,
731    type_name: &str,
732) -> Option<ColumnType> {
733    routines
734        .resolve_type_name(type_name)
735        .ok()
736        .flatten()
737        .or_else(|| ColumnType::from_sql_name(type_name).ok())
738}
739
740pub fn is_json_array_table_function(name: &str) -> bool {
741    matches!(
742        name,
743        "json_array_elements"
744            | "jsonb_array_elements"
745            | "json_array_elements_text"
746            | "jsonb_array_elements_text"
747    )
748}
749
750pub fn scalar_table_function_default_column(
751    normalized_name: &str,
752    output_name: &str,
753    alias: Option<&str>,
754    column_aliases: &[String],
755) -> String {
756    column_aliases.first().cloned().unwrap_or_else(|| {
757        if is_json_array_table_function(normalized_name) {
758            "value".into()
759        } else {
760            alias.unwrap_or(output_name).to_string()
761        }
762    })
763}
764pub fn join_alias_columns(
765    schema: &crate::RowSchema,
766    alias: &str,
767    column_aliases: &[String],
768) -> Result<Vec<String>, SQLError> {
769    let available = schema.len();
770    let specified = column_aliases.len();
771    if specified > available {
772        return Err(SQLError::Routine {
773            sqlstate: "42P10".into(),
774            message: format!(
775                "join expression \"{alias}\" has {available} columns available but {specified} columns specified"
776            ),
777        });
778    }
779    Ok(schema
780        .columns()
781        .iter()
782        .enumerate()
783        .map(|(position, column)| {
784            column_aliases
785                .get(position)
786                .cloned()
787                .unwrap_or_else(|| schema.public_name(position).unwrap_or(column).to_string())
788        })
789        .collect())
790}
791
792pub fn alias_join_schema(
793    schema: &crate::RowSchema,
794    alias: Option<&str>,
795    column_aliases: &[String],
796) -> Result<crate::RowSchema, SQLError> {
797    let Some(alias) = alias else {
798        if column_aliases.is_empty() {
799            return Ok(schema.clone());
800        }
801        return Err(SQLError::Internal(
802            "JOIN column aliases exist without a relation alias".into(),
803        ));
804    };
805    let columns = join_alias_columns(schema, alias, column_aliases)?;
806    Ok(crate::RowSchema::with_qualified_types(
807        alias,
808        columns,
809        schema.column_types().to_vec(),
810    ))
811}
812
813#[cfg(test)]
814mod tests;