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 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(¶meter.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;