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        name: canonical_name.clone(),
286        argument_names: vec![None; argument_types.len()],
287        argument_types,
288        default_arguments,
289        return_type,
290    };
291    match name {
292        "pg_listening_channels" => vec![overload(Vec::new(), 0, ColumnType::Text)],
293        "generate_series" => crate::type_resolution::fixed_builtin_overloads(name)
294            .expect("generate_series has fixed catalog signatures")
295            .into_iter()
296            .filter(|overload| {
297                matches!(
298                    overload.return_type,
299                    ColumnType::Integer | ColumnType::BigInteger
300                )
301            })
302            .collect(),
303        "unnest" => {
304            let [Some(argument)] = argument_types else {
305                return Vec::new();
306            };
307            let Some(element) = crate::type_resolution::array_element_type(argument) else {
308                return Vec::new();
309            };
310            vec![overload(vec![ColumnType::AnyArray], 0, element.clone())]
311        }
312        "regexp_split_to_table" | "string_to_table" => vec![overload(
313            vec![ColumnType::Text, ColumnType::Text],
314            0,
315            ColumnType::Text,
316        )],
317        "json_array_elements" => vec![overload(vec![ColumnType::Json], 0, ColumnType::Json)],
318        "jsonb_array_elements" => vec![overload(vec![ColumnType::JsonB], 0, ColumnType::JsonB)],
319        "json_array_elements_text" | "json_object_keys" => {
320            vec![overload(vec![ColumnType::Json], 0, ColumnType::Text)]
321        }
322        "jsonb_array_elements_text" | "jsonb_object_keys" => {
323            vec![overload(vec![ColumnType::JsonB], 0, ColumnType::Text)]
324        }
325        "json_each" | "json_each_text" => {
326            vec![overload(vec![ColumnType::Json], 0, ColumnType::Record)]
327        }
328        "jsonb_each" | "jsonb_each_text" => {
329            vec![overload(vec![ColumnType::JsonB], 0, ColumnType::Record)]
330        }
331        "pg_get_sequence_data" => vec![overload(vec![ColumnType::Regclass], 0, ColumnType::Record)],
332        "pg_sequence_parameters" => {
333            vec![overload(vec![ColumnType::Oid], 0, ColumnType::Record)]
334        }
335        "aclexplode" => vec![overload(
336            vec![ColumnType::Array(Box::new(ColumnType::AclItem))],
337            0,
338            ColumnType::Record,
339        )],
340        _ => Vec::new(),
341    }
342}
343
344pub fn is_builtin_table_function(name: &str) -> bool {
345    matches!(
346        name,
347        "generate_series"
348            | "pg_listening_channels"
349            | "unnest"
350            | "regexp_split_to_table"
351            | "string_to_table"
352            | "json_array_elements"
353            | "jsonb_array_elements"
354            | "json_array_elements_text"
355            | "jsonb_array_elements_text"
356            | "json_object_keys"
357            | "jsonb_object_keys"
358            | "json_each"
359            | "jsonb_each"
360            | "json_each_text"
361            | "jsonb_each_text"
362            | "pg_get_sequence_data"
363            | "pg_sequence_parameters"
364            | "aclexplode"
365            | "create_analyzer"
366            | "drop_analyzer"
367            | "list_analyzers"
368            | "analyze_text"
369            | "fts_index_stats"
370            | "set_table_analyzer"
371            | "pagerank"
372            | "graph_pagerank"
373            | "hits"
374            | "graph_hits"
375            | "betweenness"
376            | "graph_betweenness"
377            | "graph_edges"
378            | "rpq"
379            | "cypher"
380    )
381}
382
383pub fn table_function_empty_schema(
384    name: &str,
385    output_name: &str,
386    alias: Option<&str>,
387    column_aliases: &[String],
388    output_width: usize,
389    ordinality: bool,
390) -> Vec<String> {
391    let lower = crate::semantics::builtin_function_dispatch_name(&name.to_ascii_lowercase());
392    let columns = if lower == "unnest" {
393        let width = output_width.max(1);
394        let default_column = if width == 1 {
395            alias.unwrap_or(output_name)
396        } else {
397            output_name
398        };
399        vec![default_column.to_string(); width]
400    } else {
401        match lower.as_str() {
402            "json_each" | "jsonb_each" | "json_each_text" | "jsonb_each_text" => {
403                vec!["key".into(), "value".into()]
404            }
405            "pg_get_sequence_data" => vec!["last_value".into(), "is_called".into()],
406            "aclexplode" => vec![
407                "grantor".into(),
408                "grantee".into(),
409                "privilege_type".into(),
410                "is_grantable".into(),
411            ],
412            "pg_sequence_parameters" => vec![
413                "start_value".into(),
414                "minimum_value".into(),
415                "maximum_value".into(),
416                "increment".into(),
417                "cycle_option".into(),
418                "cache_size".into(),
419                "data_type".into(),
420            ],
421            "pagerank" | "graph_pagerank" | "hits" | "graph_hits" | "betweenness"
422            | "graph_betweenness" => vec!["_doc_id".into(), "_score".into()],
423            "rpq" => vec!["vertex_id".into()],
424            "list_analyzers" => vec!["analyzer_name".into()],
425            "analyze_text" => vec!["analysis".into()],
426            "fts_index_stats" => vec![
427                "table_name".into(),
428                "field".into(),
429                "analyzer".into(),
430                "posting_count".into(),
431                "doc_length_count".into(),
432                "indexed_doc_count".into(),
433                "term_count".into(),
434                "total_field_length".into(),
435            ],
436            "text_similarity_join"
437            | "vector_similarity_join"
438            | "graph_join"
439            | "hybrid_join"
440            | "cross_paradigm_join" => {
441                vec!["left_doc_id".into(), "right_doc_id".into(), "_score".into()]
442            }
443            "generate_series"
444            | "pg_listening_channels"
445            | "regexp_split_to_table"
446            | "string_to_table"
447            | "json_array_elements"
448            | "jsonb_array_elements"
449            | "json_array_elements_text"
450            | "jsonb_array_elements_text"
451            | "json_object_keys"
452            | "jsonb_object_keys" => vec![scalar_table_function_default_column(
453                &lower,
454                output_name,
455                alias,
456                &[],
457            )],
458            _ => {
459                let minimum_width = 1;
460                let aliased_value_width = if ordinality && column_aliases.len() > minimum_width {
461                    column_aliases.len() - 1
462                } else {
463                    column_aliases.len()
464                };
465                vec![
466                    scalar_table_function_default_column(&lower, output_name, alias, &[]);
467                    minimum_width.max(aliased_value_width)
468                ]
469            }
470        }
471    };
472    apply_table_function_aliases(columns, column_aliases, ordinality)
473}
474
475pub fn apply_table_function_aliases(
476    mut columns: Vec<String>,
477    column_aliases: &[String],
478    ordinality: bool,
479) -> Vec<String> {
480    if ordinality {
481        columns.push("ordinality".into());
482    }
483    for (column, alias) in columns.iter_mut().zip(column_aliases) {
484        column.clone_from(alias);
485    }
486    columns
487}
488
489pub fn validate_table_function_alias_count(
490    table_alias: &str,
491    available: usize,
492    specified: usize,
493) -> Result<(), SQLError> {
494    if specified <= available {
495        return Ok(());
496    }
497    Err(SQLError::Routine {
498        sqlstate: "42P10".into(),
499        message: format!(
500            "table \"{table_alias}\" has {available} columns available but {specified} columns specified"
501        ),
502    })
503}
504
505pub struct TableFunctionTypeRequest<'a> {
506    pub name: &'a str,
507    pub binding: Option<&'a FunctionBinding>,
508    pub args: &'a [ScalarExpr],
509    pub user_function: Option<&'a SQLUserFunction>,
510    pub user_invocation: Option<&'a RoutineInvocationBinding>,
511    pub declared_types: &'a [String],
512    pub columns: &'a [String],
513    pub ordinality: bool,
514}
515
516#[expect(
517    clippy::too_many_lines,
518    reason = "preserves source schema and row identity"
519)]
520pub fn table_function_column_types(
521    routines: &dyn RoutineResolution,
522    request: TableFunctionTypeRequest<'_>,
523    input_schema: &crate::RowSchema,
524    params: &[SQLParam],
525    resolver: &dyn FunctionTypeResolver,
526) -> Vec<Option<ColumnType>> {
527    let TableFunctionTypeRequest {
528        name,
529        binding,
530        args,
531        user_function,
532        user_invocation,
533        declared_types,
534        columns,
535        ordinality,
536    } = request;
537    let value_columns = if ordinality {
538        columns
539            .get(..columns.len().saturating_sub(1))
540            .unwrap_or(&[])
541    } else {
542        columns
543    };
544    let align = |types: Vec<Option<ColumnType>>| {
545        if types.len() == value_columns.len() {
546            types
547        } else if let [ty] = types.as_slice() {
548            vec![ty.clone(); value_columns.len()]
549        } else {
550            vec![None; value_columns.len()]
551        }
552    };
553    let mut types = if !declared_types.is_empty() {
554        align(
555            declared_types
556                .iter()
557                .map(|ty| ColumnType::from_sql_name(ty).ok())
558                .collect(),
559        )
560    } else if let Some(function) = user_function {
561        align(user_function_column_types(
562            routines,
563            function,
564            user_invocation,
565        ))
566    } else {
567        let normalized =
568            crate::semantics::builtin_function_dispatch_name(&name.to_ascii_lowercase());
569        align(match normalized.as_str() {
570            "pg_listening_channels" => vec![Some(ColumnType::Text)],
571            "generate_series" => vec![resolve_table_function_binding(
572                routines,
573                name,
574                binding,
575                args,
576                input_schema,
577                params,
578                resolver,
579            )
580            .ok()
581            .flatten()
582            .as_ref()
583            .and_then(crate::type_resolution::fixed_builtin_return_type)],
584            "analyze_text" => vec![Some(ColumnType::JsonB)],
585            "unnest" => args
586                .iter()
587                .map(|argument| {
588                    crate::scalar_type_with_resolver(argument, input_schema, params, resolver)
589                        .ok()
590                        .flatten()
591                        .and_then(|ty| crate::type_resolution::array_element_type(&ty).cloned())
592                })
593                .collect(),
594            "regexp_split_to_table"
595            | "string_to_table"
596            | "json_object_keys"
597            | "jsonb_object_keys" => vec![Some(ColumnType::Text)],
598            "json_array_elements" => vec![Some(ColumnType::Json)],
599            "jsonb_array_elements" => vec![Some(ColumnType::JsonB)],
600            "json_array_elements_text" | "jsonb_array_elements_text" => {
601                vec![Some(ColumnType::Text)]
602            }
603            "json_each" => vec![Some(ColumnType::Text), Some(ColumnType::Json)],
604            "jsonb_each" => vec![Some(ColumnType::Text), Some(ColumnType::JsonB)],
605            "json_each_text" | "jsonb_each_text" => {
606                vec![Some(ColumnType::Text), Some(ColumnType::Text)]
607            }
608            "pg_get_sequence_data" => {
609                vec![Some(ColumnType::BigInteger), Some(ColumnType::Boolean)]
610            }
611            "pg_sequence_parameters" => vec![
612                Some(ColumnType::BigInteger),
613                Some(ColumnType::BigInteger),
614                Some(ColumnType::BigInteger),
615                Some(ColumnType::BigInteger),
616                Some(ColumnType::Boolean),
617                Some(ColumnType::BigInteger),
618                Some(ColumnType::Oid),
619            ],
620            "aclexplode" => vec![
621                Some(ColumnType::Oid),
622                Some(ColumnType::Oid),
623                Some(ColumnType::Text),
624                Some(ColumnType::Boolean),
625            ],
626            "pagerank" | "graph_pagerank" | "hits" | "graph_hits" | "betweenness"
627            | "graph_betweenness" => vec![
628                Some(ColumnType::BigInteger),
629                Some(ColumnType::DoublePrecision),
630            ],
631            "rpq" => vec![Some(ColumnType::BigInteger)],
632            "text_similarity_join"
633            | "vector_similarity_join"
634            | "graph_join"
635            | "hybrid_join"
636            | "cross_paradigm_join" => vec![
637                Some(ColumnType::BigInteger),
638                Some(ColumnType::BigInteger),
639                Some(ColumnType::DoublePrecision),
640            ],
641            _ => user_table_function_column_types(
642                routines,
643                name,
644                args,
645                input_schema,
646                params,
647                resolver,
648            ),
649        })
650    };
651    if ordinality {
652        types.push(Some(ColumnType::BigInteger));
653    }
654    types
655}
656
657fn user_table_function_column_types(
658    routines: &dyn RoutineResolution,
659    name: &str,
660    args: &[ScalarExpr],
661    input_schema: &crate::RowSchema,
662    params: &[SQLParam],
663    resolver: &dyn FunctionTypeResolver,
664) -> Vec<Option<ColumnType>> {
665    let Ok((argument_names, argument_types, explicit_variadic)) =
666        crate::function_call_argument_signature(args, input_schema, params, Some(resolver))
667    else {
668        return Vec::new();
669    };
670    let Ok(Some(matched)) = routines.resolve_static_sql_function_match(
671        name,
672        None,
673        &argument_names,
674        &argument_types,
675        explicit_variadic,
676    ) else {
677        return Vec::new();
678    };
679    user_function_column_types(routines, &matched.function, Some(&matched.invocation))
680}
681
682fn user_function_column_types(
683    routines: &dyn RoutineResolution,
684    function: &SQLUserFunction,
685    invocation: Option<&RoutineInvocationBinding>,
686) -> Vec<Option<ColumnType>> {
687    if let Ok(Some(descriptor)) = user_function_composite_result(routines, function, invocation) {
688        return descriptor
689            .attributes
690            .iter()
691            .map(|attribute| Some(attribute.ty.clone()))
692            .collect();
693    }
694    let outputs = function
695        .def
696        .params
697        .iter()
698        .enumerate()
699        .filter(|(_, parameter)| {
700            matches!(
701                parameter.mode,
702                FunctionParamMode::Out | FunctionParamMode::InOut | FunctionParamMode::Table
703            )
704        })
705        .collect::<Vec<_>>();
706    if !outputs.is_empty() {
707        return outputs
708            .into_iter()
709            .map(|(index, parameter)| {
710                let type_name = invocation
711                    .and_then(|binding| binding.parameter_types.get(index))
712                    .unwrap_or(&parameter.type_name);
713                resolve_table_function_column_type(routines, type_name)
714            })
715            .collect();
716    }
717    match &function.def.returns {
718        FunctionReturns::Scalar { type_name } | FunctionReturns::SetOf { type_name } => {
719            let type_name = invocation
720                .and_then(|binding| binding.return_type.as_ref())
721                .unwrap_or(type_name);
722            vec![resolve_table_function_column_type(routines, type_name)]
723        }
724        FunctionReturns::None | FunctionReturns::Table => Vec::new(),
725    }
726}
727
728fn resolve_table_function_column_type(
729    routines: &dyn RoutineResolution,
730    type_name: &str,
731) -> Option<ColumnType> {
732    routines
733        .resolve_type_name(type_name)
734        .ok()
735        .flatten()
736        .or_else(|| ColumnType::from_sql_name(type_name).ok())
737}
738
739pub fn is_json_array_table_function(name: &str) -> bool {
740    matches!(
741        name,
742        "json_array_elements"
743            | "jsonb_array_elements"
744            | "json_array_elements_text"
745            | "jsonb_array_elements_text"
746    )
747}
748
749pub fn scalar_table_function_default_column(
750    normalized_name: &str,
751    output_name: &str,
752    alias: Option<&str>,
753    column_aliases: &[String],
754) -> String {
755    column_aliases.first().cloned().unwrap_or_else(|| {
756        if is_json_array_table_function(normalized_name) {
757            "value".into()
758        } else {
759            alias.unwrap_or(output_name).to_string()
760        }
761    })
762}
763pub fn join_alias_columns(
764    schema: &crate::RowSchema,
765    alias: &str,
766    column_aliases: &[String],
767) -> Result<Vec<String>, SQLError> {
768    let available = schema.len();
769    let specified = column_aliases.len();
770    if specified > available {
771        return Err(SQLError::Routine {
772            sqlstate: "42P10".into(),
773            message: format!(
774                "join expression \"{alias}\" has {available} columns available but {specified} columns specified"
775            ),
776        });
777    }
778    Ok(schema
779        .columns()
780        .iter()
781        .enumerate()
782        .map(|(position, column)| {
783            column_aliases
784                .get(position)
785                .cloned()
786                .unwrap_or_else(|| schema.public_name(position).unwrap_or(column).to_string())
787        })
788        .collect())
789}
790
791pub fn alias_join_schema(
792    schema: &crate::RowSchema,
793    alias: Option<&str>,
794    column_aliases: &[String],
795) -> Result<crate::RowSchema, SQLError> {
796    let Some(alias) = alias else {
797        if column_aliases.is_empty() {
798            return Ok(schema.clone());
799        }
800        return Err(SQLError::Internal(
801            "JOIN column aliases exist without a relation alias".into(),
802        ));
803    };
804    let columns = join_alias_columns(schema, alias, column_aliases)?;
805    Ok(crate::RowSchema::with_qualified_types(
806        alias,
807        columns,
808        schema.column_types().to_vec(),
809    ))
810}
811
812#[cfg(test)]
813mod tests;