1use 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
22pub(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
70pub 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(¶meter.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;