Skip to main content

radixdb_executor/navigation/
binding.rs

1use super::*;
2
3impl NavigationExpr {
4    pub fn identity(&self) -> &NavigationPathIdentity {
5        &self.identity
6    }
7
8    pub fn root_relation(&self) -> &RootRelationInstance {
9        &self.identity.root
10    }
11
12    pub fn steps(&self) -> &[ReferenceStep] {
13        &self.steps
14    }
15
16    pub fn terminal_column(&self) -> &SchemaColumnId {
17        &self.identity.terminal_column
18    }
19
20    pub fn terminal_type(&self) -> DataType {
21        self.terminal_type
22    }
23
24    pub fn nullable(&self) -> bool {
25        self.nullable
26    }
27
28    pub fn display_path(&self) -> &str {
29        &self.display_path
30    }
31
32    pub fn validate(&self, engine: &dyn Engine) -> Result<()> {
33        engine.validate_schema_table_id(self.identity.root.table())?;
34        for step in &self.steps {
35            engine.validate_schema_table_id(step.source_column().table())?;
36            engine.validate_schema_table_id(step.target_table())?;
37        }
38        engine.validate_schema_table_id(self.terminal_column().table())
39    }
40}
41
42#[derive(Debug, Clone)]
43struct ScopeRoot {
44    ordinal: u32,
45    visible_name_lower: String,
46    table: Option<SchemaTableId>,
47}
48
49fn collect_source_visible_names(source: &Expression, output: &mut FxHashSet<String>) {
50    match source {
51        Expression::TableSource(table) => {
52            output.insert(
53                table
54                    .alias
55                    .as_ref()
56                    .unwrap_or(&table.name)
57                    .value_lower()
58                    .to_string(),
59            );
60        }
61        Expression::JoinSource(join) => {
62            collect_source_visible_names(&join.left, output);
63            collect_source_visible_names(&join.right, output);
64        }
65        Expression::SubquerySource(subquery) => {
66            if let Some(alias) = &subquery.alias {
67                output.insert(alias.value_lower().to_string());
68            }
69        }
70        Expression::CteReference(cte) => {
71            output.insert(
72                cte.alias
73                    .as_ref()
74                    .unwrap_or(&cte.name)
75                    .value_lower()
76                    .to_string(),
77            );
78        }
79        Expression::ValuesSource(values) => {
80            if let Some(alias) = &values.alias {
81                output.insert(alias.value_lower().to_string());
82            }
83        }
84        Expression::FunctionTableSource(function) => {
85            if let Some(alias) = &function.alias {
86                output.insert(alias.value_lower().to_string());
87            }
88        }
89        _ => {}
90    }
91}
92
93struct NavigationBinder<'a> {
94    engine: &'a dyn Engine,
95    ctx: Option<&'a ExecutionContext>,
96    next_relation_ordinal: u32,
97}
98
99impl<'a> NavigationBinder<'a> {
100    fn new(engine: &'a dyn Engine) -> Self {
101        Self {
102            engine,
103            ctx: None,
104            next_relation_ordinal: 0,
105        }
106    }
107
108    fn with_context(engine: &'a dyn Engine, ctx: &'a ExecutionContext) -> Self {
109        Self {
110            engine,
111            ctx: Some(ctx),
112            next_relation_ordinal: 0,
113        }
114    }
115
116    fn bind_select(
117        &mut self,
118        select: &SelectStatement,
119        output: &mut Vec<NavigationExpr>,
120    ) -> Result<()> {
121        let visible_ctes: FxHashSet<&str> = select
122            .with
123            .as_ref()
124            .into_iter()
125            .flat_map(|with| with.ctes.iter())
126            .map(|cte| cte.name.value_lower())
127            .collect();
128
129        let mut roots = Vec::new();
130        if let Some(source) = select.table_expr.as_deref() {
131            self.collect_roots(source, &visible_ctes, &mut roots)?;
132        }
133
134        self.visit_select_expressions(select, &roots, output)
135    }
136
137    fn visit_select_expressions(
138        &mut self,
139        select: &SelectStatement,
140        roots: &[ScopeRoot],
141        output: &mut Vec<NavigationExpr>,
142    ) -> Result<()> {
143        if let Some(source) = select.table_expr.as_deref() {
144            self.visit_source_expressions(source, roots, output)?;
145        }
146        for expression in select.distinct_on.iter().chain(&select.columns) {
147            self.visit_expression(expression, roots, output)?;
148        }
149        if let Some(expression) = select.where_clause.as_deref() {
150            self.visit_expression(expression, roots, output)?;
151        }
152        for expression in &select.group_by.columns {
153            self.visit_expression(expression, roots, output)?;
154        }
155        if let GroupByModifier::GroupingSets(sets) = &select.group_by.modifier {
156            for set in sets {
157                for expression in set {
158                    self.visit_expression(expression, roots, output)?;
159                }
160            }
161        }
162        if let Some(expression) = select.having.as_deref() {
163            self.visit_expression(expression, roots, output)?;
164        }
165        for window in &select.window_defs {
166            for expression in &window.partition_by {
167                self.visit_expression(expression, roots, output)?;
168            }
169            for order in &window.order_by {
170                self.visit_expression(&order.expression, roots, output)?;
171            }
172            if let Some(frame) = &window.frame {
173                self.visit_frame(frame, roots, output)?;
174            }
175        }
176        for order in &select.order_by {
177            self.visit_expression(&order.expression, roots, output)?;
178        }
179        if let Some(expression) = select.limit.as_deref() {
180            self.visit_expression(expression, roots, output)?;
181        }
182        if let Some(expression) = select.offset.as_deref() {
183            self.visit_expression(expression, roots, output)?;
184        }
185        Ok(())
186    }
187
188    fn collect_roots(
189        &mut self,
190        source: &Expression,
191        visible_ctes: &FxHashSet<&str>,
192        roots: &mut Vec<ScopeRoot>,
193    ) -> Result<()> {
194        match source {
195            Expression::TableSource(table) => {
196                let visible_name = table.alias.as_ref().unwrap_or(&table.name);
197                let table_id = if visible_ctes.contains(table.name.value_lower())
198                    || self
199                        .ctx
200                        .is_some_and(|ctx| ctx.get_cte_by_lower(table.name.value_lower()).is_some())
201                    || !self.engine.table_exists(table.name.value())?
202                {
203                    None
204                } else {
205                    Some(self.engine.bind_schema_table_id(table.name.value())?)
206                };
207                self.push_root(visible_name.value_lower(), table_id, roots)?;
208            }
209            Expression::JoinSource(join) => {
210                self.collect_roots(&join.left, visible_ctes, roots)?;
211                self.collect_roots(&join.right, visible_ctes, roots)?;
212            }
213            Expression::SubquerySource(subquery) => {
214                if let Some(alias) = &subquery.alias {
215                    self.push_root(alias.value_lower(), None, roots)?;
216                }
217            }
218            Expression::CteReference(cte) => {
219                let visible_name = cte.alias.as_ref().unwrap_or(&cte.name);
220                self.push_root(visible_name.value_lower(), None, roots)?;
221            }
222            Expression::ValuesSource(values) => {
223                if let Some(alias) = &values.alias {
224                    self.push_root(alias.value_lower(), None, roots)?;
225                }
226            }
227            Expression::FunctionTableSource(function) => {
228                if let Some(alias) = &function.alias {
229                    self.push_root(alias.value_lower(), None, roots)?;
230                }
231            }
232            _ => {}
233        }
234        Ok(())
235    }
236
237    fn push_root(
238        &mut self,
239        visible_name_lower: &str,
240        table: Option<SchemaTableId>,
241        roots: &mut Vec<ScopeRoot>,
242    ) -> Result<()> {
243        let ordinal = self.next_relation_ordinal;
244        self.next_relation_ordinal =
245            self.next_relation_ordinal.checked_add(1).ok_or_else(|| {
246                Error::navigation(
247                    NavigationErrorCode::UnsupportedReferenceShape,
248                    "query contains too many relation instances",
249                )
250            })?;
251        roots.push(ScopeRoot {
252            ordinal,
253            visible_name_lower: visible_name_lower.to_string(),
254            table,
255        });
256        Ok(())
257    }
258
259    fn visit_source_expressions(
260        &mut self,
261        source: &Expression,
262        roots: &[ScopeRoot],
263        output: &mut Vec<NavigationExpr>,
264    ) -> Result<()> {
265        match source {
266            Expression::TableSource(table) => {
267                if let Some(as_of) = &table.as_of {
268                    self.visit_expression(&as_of.value, roots, output)?;
269                }
270            }
271            Expression::JoinSource(join) => {
272                self.visit_source_expressions(&join.left, roots, output)?;
273                self.visit_source_expressions(&join.right, roots, output)?;
274                if let Some(condition) = &join.condition {
275                    self.visit_expression(condition, roots, output)?;
276                }
277            }
278            Expression::ValuesSource(values) => {
279                for row in &values.rows {
280                    for expression in row {
281                        self.visit_expression(expression, roots, output)?;
282                    }
283                }
284            }
285            Expression::FunctionTableSource(function) => {
286                for expression in &function.arguments {
287                    self.visit_expression(expression, roots, output)?;
288                }
289            }
290            Expression::SubquerySource(_) | Expression::CteReference(_) => {}
291            _ => {}
292        }
293        Ok(())
294    }
295
296    fn visit_expression(
297        &mut self,
298        expression: &Expression,
299        roots: &[ScopeRoot],
300        output: &mut Vec<NavigationExpr>,
301    ) -> Result<()> {
302        match expression {
303            Expression::QualifiedIdentifier(path) => {
304                if let Some(bound) = self.bind_path(path, roots)? {
305                    output.push(bound);
306                }
307            }
308            Expression::QualifiedStar(star) => {
309                self.bind_navigation_wildcard(&star.qualifier, roots)?
310            }
311            Expression::Prefix(value) => self.visit_expression(&value.right, roots, output)?,
312            Expression::Infix(value) => {
313                self.visit_expression(&value.left, roots, output)?;
314                self.visit_expression(&value.right, roots, output)?;
315            }
316            Expression::List(value) => {
317                for expression in &value.elements {
318                    self.visit_expression(expression, roots, output)?;
319                }
320            }
321            Expression::Distinct(value) => self.visit_expression(&value.expr, roots, output)?,
322            Expression::Exists(value) => {
323                self.visit_correlated_navigation(&value.subquery, roots, output)?
324            }
325            Expression::AllAny(value) => {
326                self.visit_expression(&value.left, roots, output)?;
327                self.visit_correlated_navigation(&value.subquery, roots, output)?;
328            }
329            Expression::In(value) => {
330                self.visit_expression(&value.left, roots, output)?;
331                self.visit_expression(&value.right, roots, output)?;
332            }
333            Expression::InHashSet(value) => self.visit_expression(&value.column, roots, output)?,
334            Expression::Between(value) => {
335                self.visit_expression(&value.expr, roots, output)?;
336                self.visit_expression(&value.lower, roots, output)?;
337                self.visit_expression(&value.upper, roots, output)?;
338            }
339            Expression::Like(value) => {
340                self.visit_expression(&value.left, roots, output)?;
341                self.visit_expression(&value.pattern, roots, output)?;
342                if let Some(escape) = &value.escape {
343                    self.visit_expression(escape, roots, output)?;
344                }
345            }
346            Expression::ScalarSubquery(value) => {
347                self.visit_correlated_navigation(&value.subquery, roots, output)?
348            }
349            Expression::ExpressionList(value) => {
350                for expression in &value.expressions {
351                    self.visit_expression(expression, roots, output)?;
352                }
353            }
354            Expression::Case(value) => {
355                if let Some(expression) = &value.value {
356                    self.visit_expression(expression, roots, output)?;
357                }
358                for clause in &value.when_clauses {
359                    self.visit_expression(&clause.condition, roots, output)?;
360                    self.visit_expression(&clause.then_result, roots, output)?;
361                }
362                if let Some(expression) = &value.else_value {
363                    self.visit_expression(expression, roots, output)?;
364                }
365            }
366            Expression::Cast(value) => self.visit_expression(&value.expr, roots, output)?,
367            Expression::FunctionCall(value) => {
368                for expression in &value.arguments {
369                    self.visit_expression(expression, roots, output)?;
370                }
371                for order in &value.order_by {
372                    self.visit_expression(&order.expression, roots, output)?;
373                }
374                if let Some(filter) = &value.filter {
375                    self.visit_expression(filter, roots, output)?;
376                }
377            }
378            Expression::Aliased(value) => {
379                self.visit_expression(&value.expression, roots, output)?;
380            }
381            Expression::Window(value) => {
382                for expression in &value.function.arguments {
383                    self.visit_expression(expression, roots, output)?;
384                }
385                for order in &value.function.order_by {
386                    self.visit_expression(&order.expression, roots, output)?;
387                }
388                if let Some(filter) = &value.function.filter {
389                    self.visit_expression(filter, roots, output)?;
390                }
391                for expression in &value.partition_by {
392                    self.visit_expression(expression, roots, output)?;
393                }
394                for order in &value.order_by {
395                    self.visit_expression(&order.expression, roots, output)?;
396                }
397                if let Some(frame) = &value.frame {
398                    self.visit_frame(frame, roots, output)?;
399                }
400            }
401            Expression::SubquerySource(_) => {}
402            Expression::TableSource(_)
403            | Expression::JoinSource(_)
404            | Expression::ValuesSource(_)
405            | Expression::FunctionTableSource(_)
406            | Expression::Identifier(_)
407            | Expression::IntegerLiteral(_)
408            | Expression::FloatLiteral(_)
409            | Expression::StringLiteral(_)
410            | Expression::BooleanLiteral(_)
411            | Expression::NullLiteral(_)
412            | Expression::IntervalLiteral(_)
413            | Expression::BoundValue(_)
414            | Expression::Parameter(_)
415            | Expression::CteReference(_)
416            | Expression::Star(_)
417            | Expression::Default(_) => {}
418        }
419        Ok(())
420    }
421
422    fn visit_correlated_navigation(
423        &mut self,
424        select: &SelectStatement,
425        outer_roots: &[ScopeRoot],
426        output: &mut Vec<NavigationExpr>,
427    ) -> Result<()> {
428        let mut shadowed = FxHashSet::default();
429        if let Some(source) = select.table_expr.as_deref() {
430            collect_source_visible_names(source, &mut shadowed);
431        }
432        if let Some(with) = &select.with {
433            shadowed.extend(
434                with.ctes
435                    .iter()
436                    .map(|cte| cte.name.value_lower().to_string()),
437            );
438        }
439        let visible_outer = outer_roots
440            .iter()
441            .filter(|root| !shadowed.contains(&root.visible_name_lower))
442            .cloned()
443            .collect::<Vec<_>>();
444        if visible_outer.is_empty() {
445            return Ok(());
446        }
447        if select.table_expr.is_none() {
448            return self.visit_select_expressions(select, &visible_outer, output);
449        }
450
451        let names = visible_outer
452            .iter()
453            .map(|root| root.visible_name_lower.as_str())
454            .collect::<FxHashSet<_>>();
455        let mut candidates = Vec::new();
456        radixdb_sql::ast::walk_select_tree(select, &mut |expression| {
457            let Expression::QualifiedIdentifier(path) = expression else {
458                return;
459            };
460            if path.component_count() > 2 && names.contains(path.qualifier.value_lower()) {
461                candidates.push(path.clone());
462            }
463        });
464        for path in candidates {
465            if let Some(bound) = self.bind_path(&path, &visible_outer)? {
466                output.push(bound);
467            }
468        }
469        Ok(())
470    }
471
472    fn visit_frame(
473        &mut self,
474        frame: &WindowFrame,
475        roots: &[ScopeRoot],
476        output: &mut Vec<NavigationExpr>,
477    ) -> Result<()> {
478        self.visit_frame_bound(&frame.start, roots, output)?;
479        if let Some(end) = &frame.end {
480            self.visit_frame_bound(end, roots, output)?;
481        }
482        Ok(())
483    }
484
485    fn visit_frame_bound(
486        &mut self,
487        bound: &WindowFrameBound,
488        roots: &[ScopeRoot],
489        output: &mut Vec<NavigationExpr>,
490    ) -> Result<()> {
491        if let WindowFrameBound::Preceding(expression) | WindowFrameBound::Following(expression) =
492            bound
493        {
494            self.visit_expression(expression, roots, output)?;
495        }
496        Ok(())
497    }
498
499    fn bind_path(
500        &self,
501        path: &QualifiedIdentifier,
502        roots: &[ScopeRoot],
503    ) -> Result<Option<NavigationExpr>> {
504        let components: Vec<&str> = path.components().map(|item| item.value()).collect();
505        debug_assert!(components.len() >= 2);
506        let root_name_lower = components[0].to_lowercase();
507        let explicit_roots: Vec<&ScopeRoot> = roots
508            .iter()
509            .filter(|root| root.visible_name_lower == root_name_lower)
510            .collect();
511
512        if !explicit_roots.is_empty() {
513            // Alias-first resolution leaves every ordinary two-component name
514            // to the existing column binder, including duplicate-alias errors.
515            if components.len() == 2 {
516                return Ok(None);
517            }
518            if explicit_roots.len() != 1 {
519                return Err(Error::navigation(
520                    NavigationErrorCode::AmbiguousRoot,
521                    format!("relation root '{}' is ambiguous", components[0]),
522                ));
523            }
524            let root = explicit_roots[0];
525            if root.table.is_none() {
526                return Err(Error::navigation(
527                    NavigationErrorCode::UnsupportedReferenceShape,
528                    format!(
529                        "navigation root '{}' is not a physical catalog table",
530                        components[0]
531                    ),
532                ));
533            }
534            return self
535                .bind_from_root(
536                    root,
537                    &components[1..components.len() - 1],
538                    components.last().unwrap(),
539                    path,
540                )
541                .map(Some);
542        }
543
544        let source_component = components[0];
545        let mut candidates = Vec::new();
546        let mut non_reference_roots = 0usize;
547        for root in roots {
548            let Some(table) = &root.table else {
549                continue;
550            };
551            let schema = self.engine.get_table_schema(table.table_name())?;
552            if !schema.has_column(source_component) {
553                continue;
554            }
555            let source = self.engine.bind_schema_column_id(table, source_component)?;
556            if self.engine.get_reference_descriptor(&source)?.is_some() {
557                candidates.push(root);
558            } else {
559                non_reference_roots += 1;
560            }
561        }
562
563        if candidates.len() > 1 {
564            return Err(Error::navigation(
565                NavigationErrorCode::AmbiguousRoot,
566                format!(
567                    "reference shorthand '{}' matches {} relation instances; qualify the root",
568                    source_component,
569                    candidates.len()
570                ),
571            ));
572        }
573        if let Some(root) = candidates.first() {
574            return self
575                .bind_from_root(
576                    root,
577                    &components[..components.len() - 1],
578                    components.last().unwrap(),
579                    path,
580                )
581                .map(Some);
582        }
583        if non_reference_roots > 0 {
584            return Err(Error::navigation(
585                NavigationErrorCode::NotAReference,
586                format!("column '{}' is not a navigable reference", source_component),
587            ));
588        }
589        // A two-part name with no alias/FK candidate may be an ordinary
590        // correlated qualifier resolved by a legacy outer scope. Only a
591        // longer path is unambiguously navigation syntax at this stage.
592        if components.len() == 2 {
593            return Ok(None);
594        }
595        Err(Error::navigation(
596            NavigationErrorCode::UnknownRoot,
597            format!(
598                "'{}' is neither a visible relation root nor an unambiguous reference column",
599                source_component
600            ),
601        ))
602    }
603
604    fn bind_from_root(
605        &self,
606        root: &ScopeRoot,
607        step_names: &[&str],
608        terminal_name: &str,
609        path: &QualifiedIdentifier,
610    ) -> Result<NavigationExpr> {
611        if step_names.len() > MAX_NAVIGATION_STEPS {
612            return Err(Error::navigation(
613                NavigationErrorCode::UnsupportedReferenceShape,
614                format!(
615                    "navigation path '{}' has {} steps; maximum is {MAX_NAVIGATION_STEPS}",
616                    path,
617                    step_names.len()
618                ),
619            ));
620        }
621        let root_table = root.table.as_ref().expect("physical root checked");
622        let mut current_table = root_table.clone();
623        let mut steps = Vec::with_capacity(step_names.len());
624        let mut nullable = false;
625
626        for step_name in step_names {
627            let source = self
628                .engine
629                .bind_schema_column_id(&current_table, step_name)
630                .map_err(|error| match error {
631                    Error::ColumnNotFound(_) => Error::navigation(
632                        NavigationErrorCode::NotAReference,
633                        format!(
634                            "'{}.{}' is not a reference column",
635                            current_table.table_name(),
636                            step_name
637                        ),
638                    ),
639                    other => other,
640                })?;
641            let descriptor = self
642                .engine
643                .get_reference_descriptor(&source)?
644                .ok_or_else(|| {
645                    Error::navigation(
646                        NavigationErrorCode::NotAReference,
647                        format!(
648                            "'{}.{}' is not a reference column",
649                            current_table.table_name(),
650                            step_name
651                        ),
652                    )
653                })?;
654            nullable |= descriptor.source_nullable();
655            let step = ReferenceStep {
656                identity: ReferenceStepIdentity {
657                    source_column: descriptor.source().clone(),
658                    target_key_column: descriptor.target().clone(),
659                },
660                target_key: descriptor.target_key(),
661                source_nullable: descriptor.source_nullable(),
662            };
663            current_table = descriptor.target().table().clone();
664            steps.push(step);
665        }
666
667        let terminal_column = self
668            .engine
669            .bind_schema_column_id(&current_table, terminal_name)
670            .map_err(|error| match error {
671                Error::ColumnNotFound(_) => Error::navigation(
672                    NavigationErrorCode::TargetColumnNotFound,
673                    format!(
674                        "target column '{}.{}' does not exist",
675                        current_table.table_name(),
676                        terminal_name
677                    ),
678                ),
679                other => other,
680            })?;
681        let terminal_schema = self.engine.get_table_schema(current_table.table_name())?;
682        let terminal = terminal_schema
683            .get_column(terminal_column.ordinal())
684            .ok_or_else(|| {
685                Error::navigation(
686                    NavigationErrorCode::SchemaChanged,
687                    format!(
688                        "terminal column ordinal {} no longer exists in '{}'",
689                        terminal_column.ordinal(),
690                        current_table.table_name()
691                    ),
692                )
693            })?;
694        nullable |= terminal.nullable;
695
696        if self.engine.schema_epoch() != root_table.schema_generation() {
697            return Err(Error::navigation(
698                NavigationErrorCode::SchemaChanged,
699                format!("schema generation changed while binding path '{}'", path),
700            ));
701        }
702
703        let root_relation = RootRelationInstance {
704            ordinal: root.ordinal,
705            table: root_table.clone(),
706        };
707        let identity = NavigationPathIdentity {
708            root: root_relation,
709            steps: steps.iter().map(|step| step.identity.clone()).collect(),
710            terminal_column,
711        };
712        Ok(NavigationExpr {
713            identity,
714            steps,
715            terminal_type: terminal.data_type,
716            nullable,
717            display_path: path.to_string(),
718        })
719    }
720
721    fn bind_navigation_wildcard(&self, qualifier: &str, roots: &[ScopeRoot]) -> Result<()> {
722        let qualifier_lower = qualifier.to_lowercase();
723        if roots
724            .iter()
725            .any(|root| root.visible_name_lower == qualifier_lower)
726        {
727            return Ok(());
728        }
729
730        let mut matches = 0usize;
731        for root in roots {
732            let Some(table) = &root.table else {
733                continue;
734            };
735            let schema = self.engine.get_table_schema(table.table_name())?;
736            if !schema.has_column(qualifier) {
737                continue;
738            }
739            let source = self.engine.bind_schema_column_id(table, qualifier)?;
740            if self.engine.get_reference_descriptor(&source)?.is_some() {
741                matches += 1;
742            }
743        }
744        match matches {
745            0 => Ok(()),
746            1 => Err(Error::navigation(
747                NavigationErrorCode::UnsupportedReferenceShape,
748                format!("navigation wildcard '{}.*' is not allowed", qualifier),
749            )),
750            _ => Err(Error::navigation(
751                NavigationErrorCode::AmbiguousRoot,
752                format!("navigation wildcard root '{}' is ambiguous", qualifier),
753            )),
754        }
755    }
756}
757
758/// Bind every navigation path in a SELECT without reading rows or choosing a
759/// physical execution strategy.
760pub fn bind_navigation_paths(
761    engine: &dyn Engine,
762    select: &SelectStatement,
763) -> Result<Vec<NavigationExpr>> {
764    let generation = engine.schema_epoch();
765    let mut output = Vec::new();
766    NavigationBinder::new(engine).bind_select(select, &mut output)?;
767    if engine.schema_epoch() != generation {
768        return Err(Error::navigation(
769            NavigationErrorCode::SchemaChanged,
770            "schema generation changed while binding statement",
771        ));
772    }
773    Ok(output)
774}
775
776pub fn bind_reference_expand_plan(
777    engine: &dyn Engine,
778    select: &SelectStatement,
779) -> Result<Option<ReferenceExpandPlan>> {
780    let paths = bind_navigation_paths(engine, select)?;
781    if paths.is_empty() {
782        Ok(None)
783    } else {
784        ReferenceExpandPlan::build(paths).map(Some)
785    }
786}
787
788pub fn bind_reference_expand_plan_for_execution(
789    engine: &dyn Engine,
790    select: &SelectStatement,
791    ctx: &ExecutionContext,
792) -> Result<Option<ReferenceExpandPlan>> {
793    let generation = engine.schema_epoch();
794    let mut paths = Vec::new();
795    NavigationBinder::with_context(engine, ctx).bind_select(select, &mut paths)?;
796    if engine.schema_epoch() != generation {
797        return Err(Error::navigation(
798            NavigationErrorCode::SchemaChanged,
799            "schema generation changed while binding statement",
800        ));
801    }
802    if paths.is_empty() {
803        Ok(None)
804    } else {
805        ReferenceExpandPlan::build(paths).map(Some)
806    }
807}
808
809/// Enforce the permanent read-only boundary before a write statement opens a
810/// transaction, creates a statement savepoint, or reads source rows.
811pub fn reject_navigation_in_write_statement(
812    engine: &dyn Engine,
813    statement: &Statement,
814) -> Result<()> {
815    let mut paths = Vec::new();
816    match statement {
817        Statement::Insert(insert) => {
818            let roots = dml_target_roots(engine, &insert.table_name, None)?;
819            let mut binder = NavigationBinder::new(engine);
820            for row in &insert.values {
821                for expression in row {
822                    bind_dml_expression(engine, &mut binder, &roots, expression, &mut paths)?;
823                }
824            }
825            if let Some(select) = insert.select.as_deref() {
826                bind_dml_select_tree(engine, select, &mut paths)?;
827            }
828            for expression in insert.update_expressions.iter().chain(&insert.returning) {
829                bind_dml_expression(engine, &mut binder, &roots, expression, &mut paths)?;
830            }
831        }
832        Statement::Update(update) => {
833            let roots = dml_target_roots(engine, &update.table_name, None)?;
834            let mut binder = NavigationBinder::new(engine);
835            for expression in update.updates.values() {
836                bind_dml_expression(engine, &mut binder, &roots, expression, &mut paths)?;
837            }
838            if let Some(expression) = update.where_clause.as_deref() {
839                bind_dml_expression(engine, &mut binder, &roots, expression, &mut paths)?;
840            }
841            for expression in &update.returning {
842                bind_dml_expression(engine, &mut binder, &roots, expression, &mut paths)?;
843            }
844        }
845        Statement::Delete(delete) => {
846            let roots = dml_target_roots(engine, &delete.table_name, delete.alias.as_ref())?;
847            let mut binder = NavigationBinder::new(engine);
848            if let Some(expression) = delete.where_clause.as_deref() {
849                bind_dml_expression(engine, &mut binder, &roots, expression, &mut paths)?;
850            }
851            for expression in &delete.returning {
852                bind_dml_expression(engine, &mut binder, &roots, expression, &mut paths)?;
853            }
854        }
855        Statement::CreateTable(create) => {
856            if let Some(select) = create.as_select.as_deref() {
857                bind_dml_select_tree(engine, select, &mut paths)?;
858            }
859        }
860        Statement::CreateView(create) => {
861            bind_dml_select_tree(engine, &create.query, &mut paths)?;
862        }
863        Statement::Explain(explain) => {
864            return reject_navigation_in_write_statement(engine, &explain.statement)
865        }
866        _ => return Ok(()),
867    }
868
869    if let Some(path) = paths.first() {
870        if matches!(statement, Statement::CreateView(_)) {
871            return Err(Error::navigation(
872                NavigationErrorCode::UnsupportedReferenceShape,
873                format!(
874                    "navigation path '{}' cannot be persisted in a VIEW definition in the first version",
875                    path.display_path()
876                ),
877            ));
878        }
879        return Err(Error::navigation(
880            NavigationErrorCode::ReadOnly,
881            format!(
882                "navigation path '{}' cannot appear in a write statement; name the target table and relationship explicitly",
883                path.display_path()
884            ),
885        ));
886    }
887    Ok(())
888}
889
890fn dml_target_roots(
891    engine: &dyn Engine,
892    table_name: &Identifier,
893    alias: Option<&Identifier>,
894) -> Result<Vec<ScopeRoot>> {
895    let table = if engine.table_exists(table_name.value())? {
896        Some(engine.bind_schema_table_id(table_name.value())?)
897    } else {
898        None
899    };
900    Ok(vec![ScopeRoot {
901        ordinal: 0,
902        visible_name_lower: alias.unwrap_or(table_name).value_lower().to_string(),
903        table,
904    }])
905}
906
907fn bind_dml_expression(
908    engine: &dyn Engine,
909    binder: &mut NavigationBinder<'_>,
910    roots: &[ScopeRoot],
911    expression: &Expression,
912    output: &mut Vec<NavigationExpr>,
913) -> Result<()> {
914    binder.visit_expression(expression, roots, output)?;
915    if output.is_empty() {
916        bind_nested_dml_selects(engine, expression, output)?;
917    }
918    Ok(())
919}
920
921fn bind_nested_dml_selects(
922    engine: &dyn Engine,
923    expression: &Expression,
924    output: &mut Vec<NavigationExpr>,
925) -> Result<()> {
926    let mut error = None;
927    radixdb_sql::ast::walk_expression_tree(expression, &mut |node| {
928        if error.is_some() || !output.is_empty() {
929            return;
930        }
931        let nested = match node {
932            Expression::Exists(value) => Some(value.subquery.as_ref()),
933            Expression::AllAny(value) => Some(value.subquery.as_ref()),
934            Expression::ScalarSubquery(value) => Some(value.subquery.as_ref()),
935            Expression::SubquerySource(value) => Some(value.subquery.as_ref()),
936            _ => None,
937        };
938        if let Some(select) = nested {
939            if let Err(current) = bind_dml_select_tree(engine, select, output) {
940                error = Some(current);
941            }
942        }
943    });
944    match error {
945        Some(error) => Err(error),
946        None => Ok(()),
947    }
948}
949
950fn bind_dml_select_tree(
951    engine: &dyn Engine,
952    select: &SelectStatement,
953    output: &mut Vec<NavigationExpr>,
954) -> Result<()> {
955    NavigationBinder::new(engine).bind_select(select, output)?;
956    if !output.is_empty() {
957        return Ok(());
958    }
959    if let Some(with) = &select.with {
960        for cte in &with.ctes {
961            bind_dml_select_tree(engine, &cte.query, output)?;
962            if !output.is_empty() {
963                return Ok(());
964            }
965        }
966    }
967    for operation in &select.set_operations {
968        bind_dml_select_tree(engine, &operation.right, output)?;
969        if !output.is_empty() {
970            return Ok(());
971        }
972    }
973
974    let mut error = None;
975    radixdb_sql::ast::walk_select_tree(select, &mut |node| {
976        if error.is_some() || !output.is_empty() {
977            return;
978        }
979        let nested = match node {
980            Expression::Exists(value) => Some(value.subquery.as_ref()),
981            Expression::AllAny(value) => Some(value.subquery.as_ref()),
982            Expression::ScalarSubquery(value) => Some(value.subquery.as_ref()),
983            Expression::SubquerySource(value) => Some(value.subquery.as_ref()),
984            _ => None,
985        };
986        if let Some(nested) = nested {
987            if let Err(current) = bind_dml_select_tree(engine, nested, output) {
988                error = Some(current);
989            }
990        }
991    });
992    match error {
993        Some(error) => Err(error),
994        None => Ok(()),
995    }
996}