Skip to main content

radixdb_executor/binding/
output.rs

1// Copyright 2026 RadixDB Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! SELECT output-shape, source, and expression metadata binding.
16
17use radixdb_core::{DataType, Error, LogicalTypeRef, Result};
18use radixdb_sql::ast::*;
19
20pub use super::output_contract::{
21    BoundOutputColumn, NavigationOutputBinding, OutputBindingHost, QueryOutputColumn,
22};
23use super::types::parse_data_type;
24
25/// Schema-only SELECT output binder owned by the executor crate.
26#[doc(hidden)]
27pub trait OutputBindingExt: OutputBindingHost {
28    /// Bind public result metadata from the same schema/type owner used by
29    /// CTAS. `None` means the SQL is not one SELECT result.
30    fn describe_query_output(&self, sql: &str) -> Result<Option<Vec<QueryOutputColumn>>> {
31        let statements =
32            radixdb_sql::parse_sql(sql).map_err(|error| Error::Parse(error.to_string()))?;
33        let [Statement::Select(select)] = statements.as_slice() else {
34            return Ok(None);
35        };
36        self.bind_select_output(select, &[], 0).map(|columns| {
37            Some(
38                columns
39                    .into_iter()
40                    .map(|column| QueryOutputColumn {
41                        name: column.name,
42                        type_name: column.type_name,
43                        data_type: column.data_type,
44                        logical_type: column.logical_type,
45                        nullable: column.nullable,
46                    })
47                    .collect(),
48            )
49        })
50    }
51
52    /// Bind one table-free scalar expression through the same type owner used
53    /// by SELECT. Stored-routine locals must already be rewritten to typed
54    /// casts over positional parameters before entering this seam.
55    fn bind_scalar_output(&self, expression: &Expression) -> Result<(DataType, bool)> {
56        let (data_type, _, _, nullable) = self.bind_scalar_output_metadata(expression)?;
57        Ok((data_type, nullable))
58    }
59
60    /// Bind the complete logical identity of one table-free scalar expression.
61    /// This is used by PL dependency binding so external-type overloads cannot
62    /// collapse into the closed built-in `DataType` representation.
63    fn bind_scalar_output_metadata(
64        &self,
65        expression: &Expression,
66    ) -> Result<(DataType, LogicalTypeRef, String, bool)> {
67        let (data_type, logical_type, type_name) = self
68            .bind_expression_output_type(expression, &[], &[], &[], 0)?
69            .ok_or_else(|| {
70                Error::NotSupported(
71                    "scalar expression has no bind-time type; add an explicit CAST".to_string(),
72                )
73            })?;
74        Ok((
75            data_type,
76            logical_type,
77            type_name,
78            self.bind_expression_nullable(expression, &[], &[], &[], 0)?,
79        ))
80    }
81
82    /// Bind a DML RETURNING list against the target relation's runtime schema.
83    fn bind_returning_output(
84        &self,
85        table_name: &str,
86        qualifier: &str,
87        returning: &[Expression],
88    ) -> Result<Vec<BoundOutputColumn>> {
89        let schema = self.output_binding_table_schema(table_name)?;
90        let scope = schema
91            .columns
92            .iter()
93            .map(|column| BoundOutputColumn {
94                name: column.name.clone(),
95                qualifier: Some(qualifier.to_lowercase()),
96                data_type: column.data_type,
97                logical_type: column.logical_type(),
98                type_name: column.formatted_data_type(),
99                nullable: column.nullable,
100            })
101            .collect::<Vec<_>>();
102        let mut output = Vec::new();
103        for (ordinal, expression) in returning.iter().enumerate() {
104            match expression {
105                Expression::Star(_) => output.extend(scope.iter().cloned().map(|mut column| {
106                    column.qualifier = None;
107                    column
108                })),
109                Expression::QualifiedStar(star)
110                    if star.qualifier.eq_ignore_ascii_case(qualifier) =>
111                {
112                    output.extend(scope.iter().cloned().map(|mut column| {
113                        column.qualifier = None;
114                        column
115                    }));
116                }
117                Expression::QualifiedStar(star) => {
118                    return Err(Error::InvalidArgument(format!(
119                        "RETURNING cannot bind qualified star '{}.*' for table '{table_name}'",
120                        star.qualifier
121                    )));
122                }
123                expression => {
124                    let (data_type, logical_type, type_name) = self
125                        .bind_expression_output_type(expression, &scope, &[], &[], 0)?
126                        .ok_or_else(|| {
127                            Error::NotSupported(format!(
128                                "RETURNING column {} has no bind-time type; add an explicit CAST",
129                                ordinal + 1
130                            ))
131                        })?;
132                    output.push(BoundOutputColumn {
133                        name: Self::bound_expression_name(expression, ordinal),
134                        qualifier: None,
135                        data_type,
136                        logical_type,
137                        type_name,
138                        nullable: self.bind_expression_nullable(expression, &scope, &[], &[], 0)?,
139                    });
140                }
141            }
142        }
143        Ok(output)
144    }
145
146    fn bind_select_output(
147        &self,
148        select: &SelectStatement,
149        inherited_ctes: &[(String, Vec<BoundOutputColumn>)],
150        depth: usize,
151    ) -> Result<Vec<BoundOutputColumn>> {
152        if depth > 32 {
153            return Err(Error::NotSupported(
154                "CTAS metadata binding exceeded 32 nested query levels".to_string(),
155            ));
156        }
157
158        let mut ctes = inherited_ctes.to_vec();
159        if let Some(with) = &select.with {
160            for cte in &with.ctes {
161                let mut columns = self.bind_select_output(&cte.query, &ctes, depth + 1)?;
162                if !cte.column_names.is_empty() {
163                    if cte.column_names.len() != columns.len() {
164                        return Err(Error::InvalidArgument(format!(
165                            "CTE '{}' declares {} columns but query returns {}",
166                            cte.name.value,
167                            cte.column_names.len(),
168                            columns.len()
169                        )));
170                    }
171                    for (column, alias) in columns.iter_mut().zip(&cte.column_names) {
172                        column.name = alias.value.to_string();
173                    }
174                }
175                ctes.push((cte.name.value_lower.to_string(), columns));
176            }
177        }
178
179        let scope = match select.table_expr.as_deref() {
180            Some(source) => self.bind_table_source(source, &ctes, depth + 1)?,
181            None => Vec::new(),
182        };
183        let navigation = self.output_binding_navigation(select)?;
184        let mut output = Vec::new();
185        for (ordinal, expression) in select.columns.iter().enumerate() {
186            match expression {
187                Expression::Star(_) => output.extend(scope.iter().cloned().map(|mut column| {
188                    column.qualifier = None;
189                    column
190                })),
191                Expression::QualifiedStar(star) => {
192                    let qualifier = star.qualifier.to_lowercase();
193                    let before = output.len();
194                    output.extend(
195                        scope
196                            .iter()
197                            .filter(|column| column.qualifier.as_deref() == Some(&qualifier))
198                            .cloned()
199                            .map(|mut column| {
200                                column.qualifier = None;
201                                column
202                            }),
203                    );
204                    if output.len() == before {
205                        return Err(Error::InvalidArgument(format!(
206                            "CTAS cannot bind qualified star '{}.*'",
207                            star.qualifier
208                        )));
209                    }
210                }
211                _ => {
212                    let (data_type, logical_type, type_name) = self
213                        .bind_expression_output_type(
214                            expression,
215                            &scope,
216                            &ctes,
217                            &navigation,
218                            depth + 1,
219                        )?
220                        .ok_or_else(|| {
221                            Error::NotSupported(format!(
222                                "CTAS output column {} has no bind-time type; add an explicit CAST",
223                                ordinal + 1
224                            ))
225                        })?;
226                    output.push(BoundOutputColumn {
227                        name: Self::bound_expression_name(expression, ordinal),
228                        qualifier: None,
229                        data_type,
230                        logical_type,
231                        type_name,
232                        nullable: self.bind_expression_nullable(
233                            expression,
234                            &scope,
235                            &ctes,
236                            &navigation,
237                            depth + 1,
238                        )?,
239                    });
240                }
241            }
242        }
243
244        for set_operation in &select.set_operations {
245            let right = self.bind_select_output(&set_operation.right, &ctes, depth + 1)?;
246            if right.len() != output.len() {
247                return Err(Error::InvalidArgument(
248                    "set-operation branches have different column counts".to_string(),
249                ));
250            }
251            for (left, right) in output.iter_mut().zip(right) {
252                if left.logical_type != right.logical_type {
253                    let merged = match (left.logical_type, right.logical_type) {
254                        (LogicalTypeRef::Builtin(left), LogicalTypeRef::Builtin(right)) => {
255                            Self::merge_bound_types(left, right)?
256                        }
257                        _ => {
258                            return Err(Error::InvalidArgument(
259                                "set-operation branches have different external types".to_string(),
260                            ))
261                        }
262                    };
263                    left.data_type = merged;
264                    left.logical_type = LogicalTypeRef::Builtin(merged);
265                    left.type_name = merged.to_string();
266                }
267                left.nullable |= right.nullable;
268            }
269        }
270        Ok(output)
271    }
272
273    fn bind_table_source(
274        &self,
275        source: &Expression,
276        ctes: &[(String, Vec<BoundOutputColumn>)],
277        depth: usize,
278    ) -> Result<Vec<BoundOutputColumn>> {
279        match source {
280            Expression::TableSource(table) => {
281                let name = table.name.value_lower.to_string();
282                if let Some((_, columns)) = ctes.iter().rev().find(|(cte, _)| cte == &name) {
283                    return Ok(Self::qualify_bound_columns(
284                        columns.clone(),
285                        table
286                            .alias
287                            .as_ref()
288                            .map_or(&name, |alias| alias.value_lower.as_str()),
289                    ));
290                }
291                if let Ok(schema) = self.output_binding_table_schema(&name) {
292                    let qualifier = table
293                        .alias
294                        .as_ref()
295                        .map_or(name.as_str(), |alias| alias.value_lower.as_str());
296                    return Ok(schema
297                        .columns
298                        .iter()
299                        .map(|column| BoundOutputColumn {
300                            name: column.name.clone(),
301                            qualifier: Some(qualifier.to_string()),
302                            data_type: column.data_type,
303                            logical_type: column.logical_type(),
304                            type_name: column.formatted_data_type(),
305                            nullable: column.nullable,
306                        })
307                        .collect());
308                }
309                if let Some(view) = self.output_binding_view(&name)? {
310                    let statements = radixdb_sql::parse_sql(&view.query).map_err(|error| {
311                        Error::Parse(format!(
312                            "cannot bind view '{}': {}",
313                            view.original_name, error
314                        ))
315                    })?;
316                    let [Statement::Select(query)] = statements.as_slice() else {
317                        return Err(Error::Parse(format!(
318                            "view '{}' does not contain one SELECT",
319                            view.original_name
320                        )));
321                    };
322                    let columns = self.bind_select_output(query, ctes, depth + 1)?;
323                    let qualifier = table
324                        .alias
325                        .as_ref()
326                        .map_or(name.as_str(), |alias| alias.value_lower.as_str());
327                    return Ok(Self::qualify_bound_columns(columns, qualifier));
328                }
329                Err(Error::TableNotFound(name))
330            }
331            Expression::JoinSource(join) => {
332                let mut left = self.bind_table_source(&join.left, ctes, depth + 1)?;
333                let mut right = self.bind_table_source(&join.right, ctes, depth + 1)?;
334
335                // Result nullability belongs to the join result, not to the
336                // underlying table alone. An outer join can synthesize NULLs
337                // for every column on its null-extended side even when those
338                // base columns are declared NOT NULL. Preserve nullability
339                // already introduced by nested joins and widen the complete
340                // affected side here.
341                let join_type = join.join_type.to_ascii_uppercase();
342                if join_type.contains("RIGHT") || join_type.contains("FULL") {
343                    left.iter_mut().for_each(|column| column.nullable = true);
344                }
345                if join_type.contains("LEFT") || join_type.contains("FULL") {
346                    right.iter_mut().for_each(|column| column.nullable = true);
347                }
348
349                left.extend(right);
350                Ok(left)
351            }
352            Expression::SubquerySource(subquery) => {
353                let columns = self.bind_select_output(&subquery.subquery, ctes, depth + 1)?;
354                let qualifier = subquery
355                    .alias
356                    .as_ref()
357                    .map(|alias| alias.value_lower.as_str())
358                    .unwrap_or("subquery");
359                Ok(Self::qualify_bound_columns(columns, qualifier))
360            }
361            Expression::CteReference(reference) => {
362                let name = reference.name.value_lower.as_str();
363                let columns = ctes
364                    .iter()
365                    .rev()
366                    .find(|(cte, _)| cte == name)
367                    .map(|(_, columns)| columns.clone())
368                    .ok_or_else(|| Error::TableNotFound(name.to_string()))?;
369                let qualifier = reference
370                    .alias
371                    .as_ref()
372                    .map_or(name, |alias| alias.value_lower.as_str());
373                Ok(Self::qualify_bound_columns(columns, qualifier))
374            }
375            Expression::ValuesSource(values) => {
376                let width = values.rows.first().map_or(0, Vec::len);
377                if values.rows.iter().any(|row| row.len() != width) {
378                    return Err(Error::InvalidArgument(
379                        "VALUES rows have different column counts".to_string(),
380                    ));
381                }
382                let mut types = vec![None; width];
383                for row in &values.rows {
384                    for (index, expression) in row.iter().enumerate() {
385                        if let Some(data_type) =
386                            self.bind_expression_type(expression, &[], ctes, &[], depth + 1)?
387                        {
388                            types[index] = Some(match types[index] {
389                                Some(current) => Self::merge_bound_types(current, data_type)?,
390                                None => data_type,
391                            });
392                        }
393                    }
394                }
395                let qualifier = values
396                    .alias
397                    .as_ref()
398                    .map(|alias| alias.value_lower.to_string());
399                types
400                    .into_iter()
401                    .enumerate()
402                    .map(|(index, data_type)| {
403                        Ok(BoundOutputColumn {
404                            name: values.column_aliases.get(index).map_or_else(
405                                || format!("column{}", index + 1),
406                                |alias| alias.value.to_string(),
407                            ),
408                            qualifier: qualifier.clone(),
409                            nullable: values.rows.iter().any(|row| {
410                                row.get(index).is_some_and(|value| {
411                                    matches!(value, Expression::NullLiteral(_))
412                                })
413                            }),
414                            data_type: data_type.ok_or_else(|| {
415                                Error::NotSupported(format!(
416                                    "VALUES column {} has no bind-time type",
417                                    index + 1
418                                ))
419                            })?,
420                            logical_type: LogicalTypeRef::Builtin(data_type.ok_or_else(|| {
421                                Error::NotSupported(format!(
422                                    "VALUES column {} has no bind-time type",
423                                    index + 1
424                                ))
425                            })?),
426                            type_name: data_type
427                                .map(|data_type| data_type.to_string())
428                                .unwrap_or_else(|| "NULL".to_string()),
429                        })
430                    })
431                    .collect()
432            }
433            _ => Err(Error::NotSupported(format!(
434                "CTAS metadata binding does not support table source {source}"
435            ))),
436        }
437    }
438
439    fn bind_expression_output_type(
440        &self,
441        expression: &Expression,
442        scope: &[BoundOutputColumn],
443        ctes: &[(String, Vec<BoundOutputColumn>)],
444        navigation: &[NavigationOutputBinding],
445        depth: usize,
446    ) -> Result<Option<(DataType, LogicalTypeRef, String)>> {
447        let direct = match expression {
448            Expression::Identifier(identifier) => {
449                let matches = scope
450                    .iter()
451                    .filter(|column| column.name.eq_ignore_ascii_case(&identifier.value_lower))
452                    .collect::<Vec<_>>();
453                match matches.as_slice() {
454                    [column] => Some((
455                        column.data_type,
456                        column.logical_type,
457                        column.type_name.clone(),
458                    )),
459                    [] => return Err(Error::ColumnNotFound(identifier.value.to_string())),
460                    _ => {
461                        return Err(Error::InvalidArgument(format!(
462                            "ambiguous CTAS column '{}'",
463                            identifier.value
464                        )))
465                    }
466                }
467            }
468            Expression::QualifiedIdentifier(identifier) => scope
469                .iter()
470                .find(|column| {
471                    column.qualifier.as_deref() == Some(identifier.qualifier.value_lower.as_str())
472                        && column
473                            .name
474                            .eq_ignore_ascii_case(&identifier.name.value_lower)
475                })
476                .map(|column| {
477                    (
478                        column.data_type,
479                        column.logical_type,
480                        column.type_name.clone(),
481                    )
482                }),
483            Expression::Aliased(alias) => {
484                return self.bind_expression_output_type(
485                    &alias.expression,
486                    scope,
487                    ctes,
488                    navigation,
489                    depth + 1,
490                )
491            }
492            Expression::Cast(cast) => Some(self.output_binding_type_name(&cast.type_name)?),
493            Expression::FunctionCall(function)
494                if !self.output_binding_functions().exists(&function.function) =>
495            {
496                let argument_types = function
497                    .arguments
498                    .iter()
499                    .map(|argument| {
500                        self.bind_expression_logical_type(
501                            argument,
502                            scope,
503                            ctes,
504                            navigation,
505                            depth + 1,
506                        )
507                    })
508                    .collect::<Result<Vec<_>>>()?;
509                self.output_binding_stored_function(&function.function, &argument_types)?
510                    .map(|(data_type, logical_type, type_name, _)| {
511                        (data_type, logical_type, type_name)
512                    })
513            }
514            _ => None,
515        };
516        if direct.is_some() {
517            return Ok(direct);
518        }
519        Ok(self
520            .bind_expression_type(expression, scope, ctes, navigation, depth)?
521            .map(|data_type| {
522                (
523                    data_type,
524                    LogicalTypeRef::Builtin(data_type),
525                    data_type.to_string(),
526                )
527            }))
528    }
529
530    fn bind_expression_type(
531        &self,
532        expression: &Expression,
533        scope: &[BoundOutputColumn],
534        ctes: &[(String, Vec<BoundOutputColumn>)],
535        navigation: &[NavigationOutputBinding],
536        depth: usize,
537    ) -> Result<Option<DataType>> {
538        use radixdb_sql::ast::{InfixOperator, PrefixOperator};
539        let bound = match expression {
540            Expression::Identifier(identifier) => {
541                let matches: Vec<_> = scope
542                    .iter()
543                    .filter(|column| column.name.eq_ignore_ascii_case(&identifier.value_lower))
544                    .collect();
545                match matches.as_slice() {
546                    [column] => Some(column.data_type),
547                    [] => return Err(Error::ColumnNotFound(identifier.value.to_string())),
548                    _ => {
549                        return Err(Error::InvalidArgument(format!(
550                            "ambiguous CTAS column '{}'",
551                            identifier.value
552                        )))
553                    }
554                }
555            }
556            Expression::QualifiedIdentifier(identifier) => navigation
557                .iter()
558                .find(|path| {
559                    path.display_path
560                        .as_str()
561                        .eq_ignore_ascii_case(&identifier.to_string())
562                })
563                .map(|path| path.terminal_type)
564                .or_else(|| {
565                    scope
566                        .iter()
567                        .find(|column| {
568                            column.qualifier.as_deref()
569                                == Some(identifier.qualifier.value_lower.as_str())
570                                && column
571                                    .name
572                                    .eq_ignore_ascii_case(&identifier.name.value_lower)
573                        })
574                        .map(|column| column.data_type)
575                })
576                .ok_or_else(|| {
577                    Error::ColumnNotFound(format!(
578                        "{}.{}",
579                        identifier.qualifier.value, identifier.name.value
580                    ))
581                })
582                .map(Some)?,
583            Expression::IntegerLiteral(_) => Some(DataType::Integer),
584            Expression::FloatLiteral(_) => Some(DataType::Float),
585            Expression::StringLiteral(literal) => Some(match literal.type_hint.as_deref() {
586                Some(type_hint) => parse_data_type(type_hint)?,
587                None => DataType::Text,
588            }),
589            Expression::BooleanLiteral(_) => Some(DataType::Boolean),
590            Expression::NullLiteral(_) => None,
591            Expression::Aliased(alias) => {
592                return self.bind_expression_type(
593                    &alias.expression,
594                    scope,
595                    ctes,
596                    navigation,
597                    depth + 1,
598                )
599            }
600            Expression::Cast(cast) => Some(parse_data_type(&cast.type_name)?),
601            Expression::Prefix(prefix) => match prefix.op_type {
602                PrefixOperator::Not => Some(DataType::Boolean),
603                _ => {
604                    self.bind_expression_type(&prefix.right, scope, ctes, navigation, depth + 1)?
605                }
606            },
607            Expression::Infix(infix) => match infix.op_type {
608                InfixOperator::Equal
609                | InfixOperator::NotEqual
610                | InfixOperator::LessThan
611                | InfixOperator::LessEqual
612                | InfixOperator::GreaterThan
613                | InfixOperator::GreaterEqual
614                | InfixOperator::And
615                | InfixOperator::Or
616                | InfixOperator::Xor
617                | InfixOperator::Like
618                | InfixOperator::ILike
619                | InfixOperator::NotLike
620                | InfixOperator::NotILike
621                | InfixOperator::Glob
622                | InfixOperator::NotGlob
623                | InfixOperator::Regexp
624                | InfixOperator::NotRegexp
625                | InfixOperator::Is
626                | InfixOperator::IsNot
627                | InfixOperator::IsDistinctFrom
628                | InfixOperator::IsNotDistinctFrom => Some(DataType::Boolean),
629                InfixOperator::Concat | InfixOperator::JsonAccessText => Some(DataType::Text),
630                InfixOperator::JsonAccess => Some(DataType::Json),
631                InfixOperator::VectorDistance => Some(DataType::Float),
632                InfixOperator::BitwiseAnd
633                | InfixOperator::BitwiseOr
634                | InfixOperator::BitwiseXor
635                | InfixOperator::LeftShift
636                | InfixOperator::RightShift => Some(DataType::Integer),
637                InfixOperator::Add
638                | InfixOperator::Subtract
639                | InfixOperator::Multiply
640                | InfixOperator::Divide
641                | InfixOperator::Modulo => {
642                    let left =
643                        self.bind_expression_type(&infix.left, scope, ctes, navigation, depth + 1)?;
644                    let right = self.bind_expression_type(
645                        &infix.right,
646                        scope,
647                        ctes,
648                        navigation,
649                        depth + 1,
650                    )?;
651                    match (left, right) {
652                        (Some(left), Some(right)) => Some(Self::merge_bound_types(left, right)?),
653                        (left, right) => left.or(right),
654                    }
655                }
656                InfixOperator::Index | InfixOperator::Other => None,
657            },
658            Expression::FunctionCall(function) => {
659                self.bind_function_return_type(function, scope, ctes, navigation, depth + 1)?
660            }
661            Expression::Window(window) => self.bind_function_return_type(
662                &window.function,
663                scope,
664                ctes,
665                navigation,
666                depth + 1,
667            )?,
668            Expression::Case(case) => {
669                let mut result_type = case
670                    .when_clauses
671                    .iter()
672                    .map(|clause| {
673                        self.bind_expression_type(
674                            &clause.then_result,
675                            scope,
676                            ctes,
677                            navigation,
678                            depth + 1,
679                        )
680                    })
681                    .collect::<Result<Vec<_>>>()?
682                    .into_iter()
683                    .flatten()
684                    .next();
685                for clause in &case.when_clauses {
686                    if let Some(next) = self.bind_expression_type(
687                        &clause.then_result,
688                        scope,
689                        ctes,
690                        navigation,
691                        depth + 1,
692                    )? {
693                        result_type = Some(match result_type {
694                            Some(current) => Self::merge_bound_types(current, next)?,
695                            None => next,
696                        });
697                    }
698                }
699                if let Some(else_value) = &case.else_value {
700                    if let Some(next) =
701                        self.bind_expression_type(else_value, scope, ctes, navigation, depth + 1)?
702                    {
703                        result_type = Some(match result_type {
704                            Some(current) => Self::merge_bound_types(current, next)?,
705                            None => next,
706                        });
707                    }
708                }
709                result_type
710            }
711            Expression::ScalarSubquery(subquery) => self
712                .bind_select_output(&subquery.subquery, ctes, depth + 1)?
713                .first()
714                .map(|column| column.data_type),
715            Expression::Exists(_)
716            | Expression::AllAny(_)
717            | Expression::In(_)
718            | Expression::InHashSet(_)
719            | Expression::Between(_)
720            | Expression::Like(_) => Some(DataType::Boolean),
721            _ => None,
722        };
723        Ok(bound)
724    }
725
726    fn bind_expression_logical_type(
727        &self,
728        expression: &Expression,
729        scope: &[BoundOutputColumn],
730        ctes: &[(String, Vec<BoundOutputColumn>)],
731        navigation: &[NavigationOutputBinding],
732        depth: usize,
733    ) -> Result<Option<LogicalTypeRef>> {
734        match expression {
735            Expression::NullLiteral(_) => Ok(None),
736            Expression::Identifier(identifier) => {
737                let matches = scope
738                    .iter()
739                    .filter(|column| column.name.eq_ignore_ascii_case(&identifier.value_lower))
740                    .collect::<Vec<_>>();
741                match matches.as_slice() {
742                    [column] => Ok(Some(column.logical_type)),
743                    [] => Err(Error::ColumnNotFound(identifier.value.to_string())),
744                    _ => Err(Error::InvalidArgument(format!(
745                        "ambiguous column '{}'",
746                        identifier.value
747                    ))),
748                }
749            }
750            Expression::QualifiedIdentifier(identifier) => scope
751                .iter()
752                .find(|column| {
753                    column.qualifier.as_deref() == Some(identifier.qualifier.value_lower.as_str())
754                        && column
755                            .name
756                            .eq_ignore_ascii_case(&identifier.name.value_lower)
757                })
758                .map(|column| Some(column.logical_type))
759                .ok_or_else(|| Error::ColumnNotFound(identifier.to_string())),
760            Expression::Aliased(alias) => self.bind_expression_logical_type(
761                &alias.expression,
762                scope,
763                ctes,
764                navigation,
765                depth + 1,
766            ),
767            Expression::Cast(cast) => self
768                .output_binding_type_name(&cast.type_name)
769                .map(|(_, logical_type, _)| Some(logical_type)),
770            Expression::FunctionCall(function)
771                if !self.output_binding_functions().exists(&function.function) =>
772            {
773                let argument_types = function
774                    .arguments
775                    .iter()
776                    .map(|argument| {
777                        self.bind_expression_logical_type(
778                            argument,
779                            scope,
780                            ctes,
781                            navigation,
782                            depth + 1,
783                        )
784                    })
785                    .collect::<Result<Vec<_>>>()?;
786                self.output_binding_stored_function(&function.function, &argument_types)
787                    .and_then(|bound| {
788                        bound
789                            .map(|(_, logical, _, _)| logical)
790                            .map(Some)
791                            .ok_or_else(|| {
792                                Error::InvalidArgument(format!(
793                                    "unknown function {}",
794                                    function.function
795                                ))
796                            })
797                    })
798            }
799            _ => self
800                .bind_expression_type(expression, scope, ctes, navigation, depth)
801                .map(|value| value.map(LogicalTypeRef::Builtin)),
802        }
803    }
804
805    fn bind_expression_nullable(
806        &self,
807        expression: &Expression,
808        scope: &[BoundOutputColumn],
809        ctes: &[(String, Vec<BoundOutputColumn>)],
810        navigation: &[NavigationOutputBinding],
811        depth: usize,
812    ) -> Result<bool> {
813        let nullable = match expression {
814            Expression::Identifier(identifier) => scope
815                .iter()
816                .find(|column| column.name.eq_ignore_ascii_case(&identifier.value_lower))
817                .is_none_or(|column| column.nullable),
818            Expression::QualifiedIdentifier(identifier) => navigation
819                .iter()
820                .find(|path| {
821                    path.display_path
822                        .as_str()
823                        .eq_ignore_ascii_case(&identifier.to_string())
824                })
825                .map(|path| path.nullable)
826                .or_else(|| {
827                    scope
828                        .iter()
829                        .find(|column| {
830                            column.qualifier.as_deref()
831                                == Some(identifier.qualifier.value_lower.as_str())
832                                && column
833                                    .name
834                                    .eq_ignore_ascii_case(&identifier.name.value_lower)
835                        })
836                        .map(|column| column.nullable)
837                })
838                .unwrap_or(true),
839            Expression::Aliased(alias) => {
840                return self.bind_expression_nullable(
841                    &alias.expression,
842                    scope,
843                    ctes,
844                    navigation,
845                    depth,
846                )
847            }
848            Expression::FunctionCall(function)
849                if !self.output_binding_functions().exists(&function.function) =>
850            {
851                let argument_types = function
852                    .arguments
853                    .iter()
854                    .map(|argument| {
855                        self.bind_expression_logical_type(
856                            argument,
857                            scope,
858                            ctes,
859                            navigation,
860                            depth + 1,
861                        )
862                    })
863                    .collect::<Result<Vec<_>>>()?;
864                self.output_binding_stored_function(&function.function, &argument_types)?
865                    .is_none_or(|(_, _, _, nullable)| nullable)
866            }
867            Expression::IntegerLiteral(_)
868            | Expression::FloatLiteral(_)
869            | Expression::StringLiteral(_)
870            | Expression::BooleanLiteral(_) => false,
871            Expression::NullLiteral(_) => true,
872            _ => true,
873        };
874        Ok(nullable)
875    }
876
877    fn bind_function_return_type(
878        &self,
879        function: &FunctionCall,
880        scope: &[BoundOutputColumn],
881        ctes: &[(String, Vec<BoundOutputColumn>)],
882        navigation: &[NavigationOutputBinding],
883        depth: usize,
884    ) -> Result<Option<DataType>> {
885        use radixdb_functions::{FunctionDataType, FunctionReturnRule};
886        let Some(info) = self.output_binding_functions().get_info(&function.function) else {
887            let argument_types = function
888                .arguments
889                .iter()
890                .map(|argument| {
891                    self.bind_expression_logical_type(argument, scope, ctes, navigation, depth + 1)
892                })
893                .collect::<Result<Vec<_>>>()?;
894            return self
895                .output_binding_stored_function(&function.function, &argument_types)
896                .and_then(|bound| {
897                    bound
898                        .map(|(data_type, _, _, _)| data_type)
899                        .map(Some)
900                        .ok_or_else(|| {
901                            Error::InvalidArgument(format!(
902                                "unknown function {}",
903                                function.function
904                            ))
905                        })
906                });
907        };
908        let direct = match info.signature.return_type {
909            FunctionDataType::Integer => Some(DataType::Integer),
910            FunctionDataType::Float => Some(DataType::Float),
911            FunctionDataType::String => Some(DataType::Text),
912            FunctionDataType::Boolean => Some(DataType::Boolean),
913            FunctionDataType::Timestamp | FunctionDataType::Time | FunctionDataType::DateTime => {
914                Some(DataType::Timestamp)
915            }
916            FunctionDataType::Date => Some(DataType::Date),
917            FunctionDataType::Json => Some(DataType::Json),
918            FunctionDataType::Vector => Some(DataType::Vector),
919            FunctionDataType::Any | FunctionDataType::Unknown => None,
920        };
921        if direct.is_some() {
922            return Ok(direct);
923        }
924        let argument_indices: Vec<usize> = match &info.signature.return_rule {
925            FunctionReturnRule::Declared => return Ok(None),
926            FunctionReturnRule::AllArguments => (0..function.arguments.len()).collect(),
927            FunctionReturnRule::Arguments(arguments) => arguments.clone(),
928        };
929        let mut inferred = None;
930        for index in argument_indices {
931            let Some(argument) = function.arguments.get(index) else {
932                continue;
933            };
934            if matches!(argument, Expression::Star(_)) {
935                continue;
936            }
937            if let Some(data_type) =
938                self.bind_expression_type(argument, scope, ctes, navigation, depth + 1)?
939            {
940                inferred = Some(match inferred {
941                    Some(current) => Self::merge_bound_types(current, data_type)?,
942                    None => data_type,
943                });
944            }
945        }
946        Ok(inferred)
947    }
948
949    fn merge_bound_types(left: DataType, right: DataType) -> Result<DataType> {
950        if left == right {
951            return Ok(left);
952        }
953        if matches!(
954            left,
955            DataType::Integer | DataType::Float | DataType::Decimal
956        ) && matches!(
957            right,
958            DataType::Integer | DataType::Float | DataType::Decimal
959        ) {
960            return Ok(if left == DataType::Decimal || right == DataType::Decimal {
961                DataType::Decimal
962            } else if left == DataType::Float || right == DataType::Float {
963                DataType::Float
964            } else {
965                DataType::Integer
966            });
967        }
968        Err(Error::Type(format!(
969            "CTAS expression has incompatible bind-time types {left:?} and {right:?}"
970        )))
971    }
972
973    fn qualify_bound_columns(
974        mut columns: Vec<BoundOutputColumn>,
975        qualifier: &str,
976    ) -> Vec<BoundOutputColumn> {
977        let qualifier = qualifier.to_lowercase();
978        for column in &mut columns {
979            column.qualifier = Some(qualifier.clone());
980        }
981        columns
982    }
983
984    fn bound_expression_name(expression: &Expression, ordinal: usize) -> String {
985        match expression {
986            Expression::Identifier(identifier) => identifier.value.to_string(),
987            Expression::QualifiedIdentifier(identifier) => identifier.name.value.to_string(),
988            Expression::Aliased(alias) => alias.alias.value.to_string(),
989            Expression::FunctionCall(function) => function.function.to_string(),
990            Expression::Cast(cast) => format!("cast_{}", cast.type_name),
991            _ => format!("column{}", ordinal + 1),
992        }
993    }
994}
995
996impl<T: OutputBindingHost + ?Sized> OutputBindingExt for T {}