1use std::collections::HashMap;
6
7use nodedb_types::DatabaseId;
8
9use crate::error::{Result, SqlError};
10use crate::functions::registry::FunctionRegistry;
11use crate::parser::normalize::table_name_from_factor;
12use crate::temporal::TemporalScope;
13use crate::types::{CollectionInfo, ColumnInfo, EngineType, SqlCatalog};
14
15pub use super::column_naming::{BareColumn, ColumnNaming};
16
17const BITEMPORAL_AUDIT_COLUMNS: [&str; 3] = ["_ts_system", "_ts_valid_from", "_ts_valid_until"];
21
22#[derive(Debug, Clone)]
24pub struct ResolvedTable {
25 pub name: String,
26 pub alias: Option<String>,
27 pub info: CollectionInfo,
28}
29
30impl ResolvedTable {
31 pub fn ref_name(&self) -> &str {
33 self.alias.as_deref().unwrap_or(&self.name)
34 }
35
36 pub fn row_qualifier(&self) -> Option<&str> {
42 match self.info.engine {
43 EngineType::Array => None,
44 EngineType::DocumentSchemaless
45 | EngineType::DocumentStrict
46 | EngineType::KeyValue
47 | EngineType::Columnar
48 | EngineType::Timeseries
49 | EngineType::Spatial
50 | EngineType::ContinuousAggregate => Some(self.ref_name()),
51 }
52 }
53}
54
55#[derive(Debug, Default, Clone)]
57pub struct TableScope {
58 pub tables: HashMap<String, ResolvedTable>,
60 order: Vec<String>,
62 output_names: Vec<String>,
66 outer: Option<Box<TableScope>>,
71 qualified_only: Vec<String>,
76 statement_output: bool,
83 cp_functions: bool,
89 column_naming: ColumnNaming,
92}
93
94impl TableScope {
95 pub fn new() -> Self {
96 Self::default()
97 }
98
99 pub fn add(&mut self, table: ResolvedTable) -> Result<()> {
101 let key = table.ref_name().to_string();
102 if self.tables.contains_key(&key) {
103 return Err(SqlError::Parse {
104 detail: format!("duplicate table reference: {key}"),
105 });
106 }
107 self.order.push(key.clone());
108 self.tables.insert(key, table);
109 Ok(())
110 }
111
112 pub fn add_qualified_only(&mut self, table: ResolvedTable) -> Result<()> {
117 let key = table.ref_name().to_string();
118 self.add(table)?;
119 self.qualified_only.push(key);
120 Ok(())
121 }
122
123 fn column_exists(&self, table: &ResolvedTable, column: &str) -> bool {
124 if table.info.open_schema {
125 return true;
126 }
127 if table.info.bitemporal && BITEMPORAL_AUDIT_COLUMNS.contains(&column) {
128 return true;
129 }
130 let rules = crate::engine_rules::resolve_engine_rules(table.info.engine);
131 if rules.implicit_columns().contains(&column) {
132 return true;
133 }
134 if rules.value_column_name_is_free()
137 && !table.info.columns.iter().any(|c| !c.is_primary_key)
138 {
139 return true;
140 }
141 table.info.columns.iter().any(|c| c.name == column)
142 }
143
144 fn validate_column(&self, table: &ResolvedTable, column: &str) -> Result<()> {
145 if self.column_exists(table, column) {
146 Ok(())
147 } else {
148 Err(SqlError::UnknownColumn {
149 table: table.name.clone(),
150 column: column.into(),
151 })
152 }
153 }
154
155 pub fn single_table(&self) -> Option<&ResolvedTable> {
157 if self.tables.len() == 1 {
158 self.tables.values().next()
159 } else {
160 Option::None
161 }
162 }
163
164 pub fn is_row_scope(&self) -> bool {
170 !self.tables.is_empty() || self.outer.is_some()
171 }
172
173 pub fn nested_in(mut self, outer: TableScope) -> Self {
177 self.outer = Some(Box::new(outer));
178 self.statement_output = false;
179 self.cp_functions = false;
180 self.column_naming = ColumnNaming::AsWritten;
181 self
182 }
183
184 pub fn as_statement_output(mut self) -> Self {
187 self.statement_output = true;
188 self
189 }
190
191 pub fn is_statement_output(&self) -> bool {
194 self.statement_output
195 }
196
197 pub fn allowing_cp_functions(mut self) -> Self {
200 self.cp_functions = true;
201 self
202 }
203
204 pub fn allows_cp_functions(&self) -> bool {
206 self.cp_functions
207 }
208
209 pub fn with_output_names(&self, names: impl IntoIterator<Item = String>) -> Self {
215 let mut out = self.clone();
216 out.output_names.extend(names);
217 out
218 }
219
220 pub fn single(table: ResolvedTable) -> Result<Self> {
223 let mut scope = Self::new();
224 scope.add(table)?;
225 Ok(scope)
226 }
227
228 pub fn check_name(&self, table_ref: Option<&str>, column: &str) -> Result<()> {
230 let col = column.to_lowercase();
231
232 if let Some(tref) = table_ref {
233 let tref_lower = tref.to_lowercase();
234 return match self.tables.get(&tref_lower) {
235 Some(table) => self.validate_column(table, &col),
236 None => match &self.outer {
240 Some(outer) => outer.check_name(Some(&tref_lower), &col),
241 None => Err(SqlError::UnknownTable { name: tref_lower }),
242 },
243 };
244 }
245
246 self.resolve_bare(&col).map(|_| ())
247 }
248
249 pub fn resolve_bare(&self, column: &str) -> Result<BareColumn<'_>> {
263 let col = column.to_lowercase();
264 if self.output_names.iter().any(|n| n == &col) {
265 return Ok(BareColumn::Output);
266 }
267 let bare: Vec<&ResolvedTable> = self
268 .order
269 .iter()
270 .filter(|key| !self.qualified_only.contains(*key))
271 .map(|key| &self.tables[key])
272 .collect();
273 let declared: Vec<&ResolvedTable> = bare
274 .iter()
275 .copied()
276 .filter(|table| table.info.columns.iter().any(|c| c.name == col))
277 .collect();
278 match declared.as_slice() {
279 &[table] => return Ok(BareColumn::Relation(table)),
280 [] => {}
281 _ => return Err(SqlError::AmbiguousColumn { column: col }),
282 }
283 let accepting: Vec<&ResolvedTable> = bare
284 .iter()
285 .copied()
286 .filter(|table| self.column_exists(table, &col))
287 .collect();
288 match accepting.as_slice() {
289 &[table] => Ok(BareColumn::Relation(table)),
290 [] => match &self.outer {
291 Some(outer) => outer.resolve_bare(&col).map(|_| BareColumn::Outer),
292 None => Err(SqlError::UnknownColumn {
293 table: self
294 .order
295 .first()
296 .cloned()
297 .unwrap_or_else(|| "<unknown>".into()),
298 column: col,
299 }),
300 },
301 _ => Err(SqlError::AmbiguousColumn { column: col }),
302 }
303 }
304
305 pub fn with_column_naming(mut self, naming: ColumnNaming) -> Self {
307 self.column_naming = naming;
308 self
309 }
310
311 pub fn column_naming(&self) -> ColumnNaming {
313 self.column_naming
314 }
315
316 pub fn check_using_column(&self, right_ref: Option<&str>, column: &str) -> Result<()> {
319 let col = column.to_lowercase();
320 let right_key = right_ref.map(str::to_lowercase);
321 let right = right_key.as_deref().and_then(|key| self.tables.get(key));
322 let carriers = self
323 .order
324 .iter()
325 .filter(|key| !self.qualified_only.contains(*key))
326 .filter(|key| self.column_exists(&self.tables[*key], &col));
327 let has_both = match (right, right_key.as_deref()) {
328 (Some(right), Some(key)) => {
329 self.column_exists(right, &col) && carriers.into_iter().any(|k| k != key)
330 }
331 _ => carriers.count() >= 2,
332 };
333 if has_both {
334 Ok(())
335 } else {
336 Err(SqlError::UnknownColumn {
337 table: right_key.unwrap_or_else(|| "<unknown>".into()),
338 column: col,
339 })
340 }
341 }
342
343 pub fn tables_in_order(&self) -> impl Iterator<Item = &ResolvedTable> {
345 self.order.iter().map(|key| &self.tables[key])
346 }
347
348 pub fn table_by_ref(&self, ref_name: &str) -> Option<&ResolvedTable> {
350 self.tables.get(&ref_name.to_lowercase())
351 }
352
353 pub fn declared_column(&self, table_ref: Option<&str>, column: &str) -> Option<&ColumnInfo> {
355 let col = column.to_lowercase();
356 match table_ref {
357 Some(tref) => self
358 .table_by_ref(tref)?
359 .info
360 .columns
361 .iter()
362 .find(|c| c.name == col),
363 None => self
364 .tables_in_order()
365 .find_map(|t| t.info.columns.iter().find(|c| c.name == col)),
366 }
367 }
368
369 pub fn resolve_from(
375 catalog: &dyn SqlCatalog,
376 functions: &FunctionRegistry,
377 temporal: TemporalScope,
378 from: &[sqlparser::ast::TableWithJoins],
379 ) -> Result<Self> {
380 let mut scope = Self::new();
381 let from_names = crate::resolver::derived::from_relation_names(from);
382 let factors = from.iter().flat_map(|twj| {
383 std::iter::once(&twj.relation).chain(twj.joins.iter().map(|j| &j.relation))
384 });
385 for factor in factors {
386 scope
387 .resolve_table_factor(catalog, functions, temporal, factor)
388 .map_err(|e| {
389 crate::resolver::derived::refuse_correlated_derived(e, factor, &from_names)
390 })?;
391 }
392 Ok(scope)
393 }
394
395 fn resolve_table_factor(
396 &mut self,
397 catalog: &dyn SqlCatalog,
398 functions: &FunctionRegistry,
399 temporal: TemporalScope,
400 factor: &sqlparser::ast::TableFactor,
401 ) -> Result<()> {
402 if let Some(resolved) = crate::resolver::array_tvf::resolve_array_tvf(catalog, factor)? {
406 self.add(resolved)?;
407 return Ok(());
408 }
409 if let sqlparser::ast::TableFactor::Derived {
416 lateral,
417 subquery,
418 alias: Some(alias),
419 ..
420 } = factor
421 {
422 let alias_str = crate::reserved::check_ast_identifier(&alias.name)?;
423 let declared: Vec<String> = alias
424 .columns
425 .iter()
426 .map(|column| crate::reserved::check_ast_identifier(&column.name))
427 .collect::<Result<_>>()?;
428 let plan = if *lateral {
429 None
430 } else {
431 Some(crate::planner::select::plan_query(
432 subquery, catalog, functions, temporal,
433 )?)
434 };
435 let info = crate::resolver::derived::infer_subquery_relation(
436 catalog,
437 &alias_str,
438 subquery,
439 plan.as_ref(),
440 functions,
441 temporal,
442 )?;
443 self.add(ResolvedTable {
444 name: alias_str.clone(),
445 alias: Some(alias_str),
446 info: crate::resolver::derived::rename_output_columns(info, &declared),
447 })?;
448 return Ok(());
449 }
450 if let Some((name, alias)) = table_name_from_factor(factor)? {
451 let info = catalog
452 .resolve_relation(DatabaseId::DEFAULT, &name)?
453 .ok_or_else(|| SqlError::UnknownTable { name: name.clone() })?;
454 self.add(ResolvedTable { name, alias, info })?;
455 }
456 Ok(())
457 }
458}
459
460#[cfg(test)]
462pub(crate) mod test_support {
463 use super::{ResolvedTable, TableScope};
464 use crate::types::{CollectionInfo, EngineType};
465
466 pub(crate) fn open_scope(collection: &str) -> TableScope {
468 let info = CollectionInfo {
469 name: collection.into(),
470 engine: EngineType::DocumentSchemaless,
471 columns: Vec::new(),
472 primary_key: None,
473 has_auto_tier: false,
474 indexes: Vec::new(),
475 bitemporal: false,
476 primary: nodedb_types::PrimaryEngine::Document,
477 vector_primary: None,
478 partition_strategy: nodedb_types::PartitionStrategy::CollectionHomed,
479 open_schema: CollectionInfo::open_schema_for(EngineType::DocumentSchemaless),
480 };
481 TableScope::single(ResolvedTable {
482 name: info.name.clone(),
483 alias: None,
484 info,
485 })
486 .expect("single-relation scope")
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::test_support::open_scope;
493 use super::*;
494 use crate::types::{CollectionInfo, ColumnInfo, EngineType, SqlDataType};
495 use nodedb_types::PrimaryEngine;
496
497 fn strict_collection(name: &str, columns: Vec<&str>) -> CollectionInfo {
498 CollectionInfo {
499 name: name.into(),
500 engine: EngineType::DocumentStrict,
501 columns: columns
502 .into_iter()
503 .map(|c| ColumnInfo {
504 name: c.into(),
505 data_type: SqlDataType::String,
506 nullable: true,
507 is_primary_key: false,
508 default: None,
509 raw_type: None,
510 int_width: None,
511 float_width: None,
512 })
513 .collect(),
514 primary_key: None,
515 has_auto_tier: false,
516 indexes: Vec::new(),
517 bitemporal: false,
518 primary: PrimaryEngine::Document,
519 vector_primary: None,
520 partition_strategy: nodedb_types::PartitionStrategy::CollectionHomed,
521 open_schema: CollectionInfo::open_schema_for(EngineType::DocumentStrict),
522 }
523 }
524
525 fn scope_with(info: CollectionInfo) -> TableScope {
526 let mut scope = TableScope::new();
527 scope
528 .add(ResolvedTable {
529 name: info.name.clone(),
530 alias: None,
531 info,
532 })
533 .expect("add failed");
534 scope
535 }
536
537 fn open_collection(name: &str) -> CollectionInfo {
538 CollectionInfo {
539 engine: EngineType::DocumentSchemaless,
540 open_schema: CollectionInfo::open_schema_for(EngineType::DocumentSchemaless),
541 ..strict_collection(name, Vec::new())
542 }
543 }
544
545 fn array_relation(name: &str, columns: Vec<&str>) -> CollectionInfo {
546 CollectionInfo {
547 engine: EngineType::Array,
548 open_schema: false,
549 ..strict_collection(name, columns)
550 }
551 }
552
553 fn join_scope(relations: Vec<(&str, CollectionInfo)>) -> TableScope {
555 let mut scope = TableScope::new();
556 for (alias, info) in relations {
557 scope
558 .add(ResolvedTable {
559 name: info.name.clone(),
560 alias: Some(alias.into()),
561 info,
562 })
563 .expect("add failed");
564 }
565 scope.with_column_naming(ColumnNaming::JoinRow)
566 }
567
568 fn owner<'a>(scope: &'a TableScope, column: &str) -> Option<&'a str> {
569 match scope.resolve_bare(column).expect("resolves") {
570 BareColumn::Relation(table) => Some(table.ref_name()),
571 BareColumn::Output | BareColumn::Outer => None,
572 }
573 }
574
575 #[test]
576 fn column_declared_by_one_relation_resolves_there() {
577 let scope = join_scope(vec![
578 ("a", strict_collection("articles", vec!["id", "title"])),
579 ("o", strict_collection("orders", vec!["id", "amount"])),
580 ]);
581 assert_eq!(owner(&scope, "title"), Some("a"));
582 assert_eq!(owner(&scope, "amount"), Some("o"));
583 }
584
585 #[test]
586 fn declared_relation_wins_over_a_schemaless_one() {
587 let scope = join_scope(vec![
588 ("e", open_collection("events")),
589 ("a", strict_collection("articles", vec!["id", "title"])),
590 ]);
591 assert_eq!(owner(&scope, "title"), Some("a"));
592 }
593
594 #[test]
595 fn column_declared_by_two_relations_is_ambiguous() {
596 let scope = join_scope(vec![
597 ("a", strict_collection("articles", vec!["id", "title"])),
598 ("b", strict_collection("blogs", vec!["id", "title"])),
599 ]);
600 let err = scope.resolve_bare("title").expect_err("ambiguous");
601 assert!(
602 matches!(err, SqlError::AmbiguousColumn { ref column } if column == "title"),
603 "{err:?}"
604 );
605 }
606
607 #[test]
608 fn undeclared_column_resolves_to_the_only_schemaless_relation() {
609 let scope = join_scope(vec![
610 ("a", strict_collection("articles", vec!["id", "title"])),
611 ("e", open_collection("events")),
612 ]);
613 assert_eq!(owner(&scope, "payload"), Some("e"));
614 }
615
616 #[test]
617 fn undeclared_column_over_two_schemaless_relations_is_ambiguous() {
618 let scope = join_scope(vec![
619 ("e", open_collection("events")),
620 ("d", open_collection("devices")),
621 ]);
622 let err = scope.resolve_bare("region").expect_err("ambiguous");
623 assert!(
624 matches!(err, SqlError::AmbiguousColumn { ref column } if column == "region"),
625 "{err:?}"
626 );
627 }
628
629 #[test]
630 fn column_no_relation_accepts_is_unknown() {
631 let scope = join_scope(vec![
632 ("a", strict_collection("articles", vec!["id", "title"])),
633 ("o", strict_collection("orders", vec!["id", "amount"])),
634 ]);
635 let err = scope.resolve_bare("ghost").expect_err("unknown");
636 assert!(matches!(err, SqlError::UnknownColumn { .. }), "{err:?}");
637 }
638
639 #[test]
640 fn output_name_resolves_before_relations() {
641 let scope = join_scope(vec![
642 ("e", open_collection("events")),
643 ("d", open_collection("devices")),
644 ])
645 .with_output_names(["total".to_string()]);
646 assert!(matches!(
647 scope.resolve_bare("total").expect("resolves"),
648 BareColumn::Output
649 ));
650 assert_eq!(
651 scope.bare_column_qualifier("total").expect("resolves"),
652 None
653 );
654 }
655
656 #[test]
657 fn join_row_naming_qualifies_a_bare_column_with_its_owner() {
658 let scope = join_scope(vec![
659 ("a", strict_collection("articles", vec!["id", "title"])),
660 ("e", open_collection("events")),
661 ]);
662 assert_eq!(
663 scope.bare_column_qualifier("title").expect("resolves"),
664 Some("a".to_string())
665 );
666 assert_eq!(
667 scope.bare_column_qualifier("payload").expect("resolves"),
668 Some("e".to_string())
669 );
670 }
671
672 #[test]
673 fn as_written_naming_checks_but_keeps_a_bare_column_bare() {
674 let scope = join_scope(vec![
675 ("a", strict_collection("articles", vec!["id", "title"])),
676 ("o", strict_collection("orders", vec!["id", "amount"])),
677 ])
678 .with_column_naming(ColumnNaming::AsWritten);
679 assert_eq!(
680 scope.bare_column_qualifier("title").expect("resolves"),
681 None
682 );
683 assert!(
684 scope.bare_column_qualifier("id").is_err(),
685 "still ambiguous"
686 );
687 }
688
689 #[test]
690 fn array_relation_columns_carry_no_qualifier() {
691 let scope = join_scope(vec![
692 ("a", strict_collection("articles", vec!["id", "title"])),
693 ("g", array_relation("genome", vec!["chrom", "qual"])),
694 ]);
695 assert_eq!(scope.bare_column_qualifier("qual").expect("resolves"), None);
696 assert_eq!(scope.qualified_column_qualifier("g"), None);
697 assert_eq!(scope.qualified_column_qualifier("a"), Some("a".to_string()));
698 }
699
700 #[test]
701 fn output_row_naming_drops_relation_qualifiers() {
702 let scope = join_scope(vec![
703 ("a", strict_collection("articles", vec!["id", "title"])),
704 ("o", strict_collection("orders", vec!["id", "amount"])),
705 ])
706 .with_column_naming(ColumnNaming::OutputRow);
707 assert_eq!(scope.qualified_column_qualifier("a"), None);
708 assert_eq!(
709 scope.bare_column_qualifier("title").expect("resolves"),
710 None
711 );
712 assert_eq!(
713 scope.qualified_column_qualifier("outer_ref"),
714 Some("outer_ref".to_string()),
715 "a qualifier naming no relation here is kept"
716 );
717 }
718
719 #[test]
720 fn nested_scope_does_not_inherit_join_row_naming() {
721 let outer = join_scope(vec![
722 ("a", strict_collection("articles", vec!["id", "title"])),
723 ("o", strict_collection("orders", vec!["id", "amount"])),
724 ]);
725 let inner = scope_with(strict_collection("tags", vec!["tag"])).nested_in(outer);
726 assert_eq!(inner.column_naming(), ColumnNaming::AsWritten);
727 assert!(matches!(
728 inner.resolve_bare("amount").expect("resolves"),
729 BareColumn::Outer
730 ));
731 }
732
733 #[test]
734 fn using_column_must_exist_on_both_sides() {
735 let scope = join_scope(vec![
736 ("a", strict_collection("articles", vec!["id", "title"])),
737 ("o", strict_collection("orders", vec!["id", "amount"])),
738 ]);
739 scope
740 .check_using_column(Some("o"), "id")
741 .expect("both sides carry id");
742 assert!(scope.check_using_column(Some("o"), "title").is_err());
743 assert!(scope.check_using_column(Some("o"), "amount").is_err());
744 }
745
746 #[test]
747 fn using_column_over_two_schemaless_relations_is_not_ambiguous() {
748 let scope = join_scope(vec![
749 ("e", open_collection("events")),
750 ("d", open_collection("devices")),
751 ]);
752 scope
753 .check_using_column(Some("d"), "device_id")
754 .expect("USING names the column on both sides");
755 }
756
757 #[test]
765 fn quoted_identifier_resolves_as_column() {
766 let scope = scope_with(strict_collection("users", vec!["userid", "email"]));
767 scope.check_name(None, "userid").expect("must resolve");
768 }
769
770 #[test]
777 fn unknown_column_in_strict_collection_yields_unknown_column_error() {
778 let scope = scope_with(strict_collection("users", vec!["id", "email"]));
779 let err = scope
780 .check_name(None, "ghost_col")
781 .expect_err("must reject an unknown column");
782 assert!(
783 matches!(err, SqlError::UnknownColumn { ref column, .. } if column == "ghost_col"),
784 "expected UnknownColumn(ghost_col), got {err:?}"
785 );
786 assert!(
788 !matches!(err, SqlError::Unsupported { .. }),
789 "must not surface Unsupported for a missing column"
790 );
791 }
792
793 #[test]
796 fn any_column_accepted_in_schemaless_collection() {
797 let scope = open_scope("events");
798 scope
799 .check_name(None, "ghost_col")
800 .expect("a schemaless relation must accept any column");
801 }
802
803 #[test]
805 fn qualified_column_resolves_correctly() {
806 let scope = scope_with(strict_collection("t", vec!["col", "other"]));
807 scope
808 .check_name(Some("t"), "col")
809 .expect("a qualified column must resolve");
810 }
811
812 #[test]
815 fn qualified_unknown_column_in_strict_collection() {
816 let scope = scope_with(strict_collection("t", vec!["id"]));
817 let err = scope
818 .check_name(Some("t"), "missing")
819 .expect_err("must reject an unknown column");
820 assert!(
821 matches!(err, SqlError::UnknownColumn { .. }),
822 "expected UnknownColumn, got {err:?}"
823 );
824 }
825}