1use std::{
2 borrow::Borrow,
3 collections::{HashMap, HashSet},
4 fmt,
5 hash::{Hash, Hasher},
6 ops::Deref,
7};
8
9use vibesql_catalog::TableIdentifier;
10
11#[derive(Debug, Clone, Eq)]
14pub struct TableKey(String);
15
16impl TableKey {
17 #[inline]
19 pub fn new(name: impl AsRef<str>) -> Self {
20 TableKey(name.as_ref().to_lowercase())
21 }
22
23 #[inline]
25 pub fn as_str(&self) -> &str {
26 &self.0
27 }
28
29 #[inline]
31 pub fn into_inner(self) -> String {
32 self.0
33 }
34}
35
36impl PartialEq for TableKey {
37 fn eq(&self, other: &Self) -> bool {
38 self.0 == other.0
39 }
40}
41
42impl Hash for TableKey {
43 fn hash<H: Hasher>(&self, state: &mut H) {
44 self.0.hash(state);
45 }
46}
47
48impl Deref for TableKey {
49 type Target = str;
50
51 fn deref(&self) -> &Self::Target {
52 &self.0
53 }
54}
55
56impl AsRef<str> for TableKey {
57 fn as_ref(&self) -> &str {
58 &self.0
59 }
60}
61
62impl Borrow<str> for TableKey {
63 fn borrow(&self) -> &str {
64 &self.0
65 }
66}
67
68impl fmt::Display for TableKey {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 write!(f, "{}", self.0)
71 }
72}
73
74impl From<String> for TableKey {
75 fn from(s: String) -> Self {
76 TableKey::new(s)
77 }
78}
79
80impl From<&str> for TableKey {
81 fn from(s: &str) -> Self {
82 TableKey::new(s)
83 }
84}
85
86impl From<TableKey> for String {
87 fn from(key: TableKey) -> Self {
88 key.0
89 }
90}
91
92impl From<&TableKey> for TableKey {
93 fn from(key: &TableKey) -> Self {
94 key.clone()
95 }
96}
97
98impl From<&String> for TableKey {
99 fn from(s: &String) -> Self {
100 TableKey::new(s)
101 }
102}
103
104#[derive(Debug, Clone)]
106pub struct CombinedSchema {
107 pub table_schemas: HashMap<TableIdentifier, (usize, vibesql_catalog::TableSchema)>,
111 pub total_columns: usize,
113 pub hidden_columns: HashSet<usize>,
118 pub outer_schema: Option<Box<CombinedSchema>>,
122 pub duplicate_aliases: HashSet<TableIdentifier>,
126 pub joined_columns: HashSet<String>,
131 pub using_coalesce_indices: HashMap<String, Vec<usize>>,
139 pub column_replacement_map: HashMap<usize, usize>,
147 pub alias_tables: HashSet<TableIdentifier>,
152 pub shadowed_tables: HashMap<TableIdentifier, HashSet<TableIdentifier>>,
158}
159
160impl CombinedSchema {
161 pub fn empty() -> Self {
166 CombinedSchema {
167 table_schemas: HashMap::new(),
168 total_columns: 0,
169 hidden_columns: HashSet::new(),
170 outer_schema: None,
171 duplicate_aliases: HashSet::new(),
172 joined_columns: HashSet::new(),
173 using_coalesce_indices: HashMap::new(),
174 column_replacement_map: HashMap::new(),
175 alias_tables: HashSet::new(),
176 shadowed_tables: HashMap::new(),
177 }
178 }
179
180 pub fn from_table(table_name: String, schema: vibesql_catalog::TableSchema) -> Self {
185 let total_columns = schema.columns.len();
186 let mut table_schemas = HashMap::new();
187 let table_id = TableIdentifier::unquoted(&table_name);
188 table_schemas.insert(table_id, (0, schema));
189 CombinedSchema {
190 table_schemas,
191 total_columns,
192 hidden_columns: HashSet::new(),
193 outer_schema: None,
194 duplicate_aliases: HashSet::new(),
195 joined_columns: HashSet::new(),
196 using_coalesce_indices: HashMap::new(),
197 column_replacement_map: HashMap::new(),
198 alias_tables: HashSet::new(),
199 shadowed_tables: HashMap::new(),
200 }
201 }
202
203 pub fn from_derived_table(
207 alias: String,
208 column_names: Vec<String>,
209 column_types: Vec<vibesql_types::DataType>,
210 ) -> Self {
211 let total_columns = column_names.len();
212
213 let columns: Vec<vibesql_catalog::ColumnSchema> = column_names
215 .into_iter()
216 .zip(column_types)
217 .map(|(name, data_type)| vibesql_catalog::ColumnSchema {
218 name,
219 data_type,
220 nullable: true, default_value: None, generated_expr: None, collation: None, is_exact_integer_type: false, })
226 .collect();
227
228 let schema = vibesql_catalog::TableSchema::new(alias.clone(), columns);
229 let mut table_schemas = HashMap::new();
230 let table_id = TableIdentifier::unquoted(&alias);
231 table_schemas.insert(table_id, (0, schema));
232 CombinedSchema {
233 table_schemas,
234 total_columns,
235 hidden_columns: HashSet::new(),
236 outer_schema: None,
237 duplicate_aliases: HashSet::new(),
238 joined_columns: HashSet::new(),
239 using_coalesce_indices: HashMap::new(),
240 column_replacement_map: HashMap::new(),
241 alias_tables: HashSet::new(),
242 shadowed_tables: HashMap::new(),
243 }
244 }
245
246 pub fn add_join_alias(mut self, alias: &str) -> Self {
258 let mut joined_col_entries: Vec<(usize, vibesql_catalog::ColumnSchema)> = Vec::new();
267 for joined_col in &self.joined_columns {
268 if let Some(indices) = self.using_coalesce_indices.get(joined_col) {
270 if let Some(&first_idx) = indices.first() {
271 for (table_id, (start_idx, table_schema)) in &self.table_schemas {
273 if self.alias_tables.contains(table_id) {
274 continue;
275 }
276 for (col_idx, col) in table_schema.columns.iter().enumerate() {
277 let absolute_idx = *start_idx + col_idx;
278 if absolute_idx == first_idx {
279 joined_col_entries.push((first_idx, col.clone()));
282 break;
283 }
284 }
285 }
286 }
287 }
288 }
289
290 let mut other_columns: Vec<(usize, vibesql_catalog::ColumnSchema)> = Vec::new();
292 for (table_id, (start_idx, table_schema)) in &self.table_schemas {
293 if self.alias_tables.contains(table_id) {
294 continue;
295 }
296 for (col_idx, col) in table_schema.columns.iter().enumerate() {
297 let absolute_idx = *start_idx + col_idx;
298 if self.hidden_columns.contains(&absolute_idx) {
300 continue;
301 }
302 let is_joined = self.joined_columns.contains(&col.name.to_lowercase());
304 if is_joined {
305 continue;
306 }
307 other_columns.push((absolute_idx, col.clone()));
308 }
309 }
310
311 other_columns.sort_by_key(|(idx, _)| *idx);
313
314 joined_col_entries.sort_by_key(|(idx, _)| *idx);
316 let mut all_columns = joined_col_entries;
317 all_columns.extend(other_columns);
318
319 let columns: Vec<vibesql_catalog::ColumnSchema> =
320 all_columns.iter().map(|(_, col)| col.clone()).collect();
321
322 let schema = vibesql_catalog::TableSchema::new(alias.to_string(), columns);
323 let table_id = TableIdentifier::unquoted(alias);
324 self.table_schemas.insert(table_id.clone(), (0, schema));
327 self.alias_tables.insert(table_id.clone());
328
329 let shadowed: HashSet<TableIdentifier> = self
334 .table_schemas
335 .keys()
336 .filter(|t| !self.alias_tables.contains(*t) && *t != &table_id)
337 .cloned()
338 .collect();
339 self.shadowed_tables.insert(table_id, shadowed);
340
341 self
342 }
343
344 pub fn combine(
349 left: CombinedSchema,
350 right_table_name: String,
351 right_schema: vibesql_catalog::TableSchema,
352 ) -> Self {
353 let mut table_schemas = left.table_schemas;
354 let mut duplicate_aliases = left.duplicate_aliases;
355 let left_total = left.total_columns;
356 let right_columns = right_schema.columns.len();
357 let right_id = TableIdentifier::unquoted(&right_table_name);
358
359 if let Some((_, existing_schema)) = table_schemas.get(&right_id) {
365 if existing_schema != &right_schema {
368 duplicate_aliases.insert(right_id.clone());
370 }
371 }
373
374 table_schemas.insert(right_id, (left_total, right_schema));
376 CombinedSchema {
377 table_schemas,
378 total_columns: left_total + right_columns,
379 hidden_columns: left.hidden_columns,
380 outer_schema: left.outer_schema,
381 duplicate_aliases,
382 joined_columns: left.joined_columns,
383 using_coalesce_indices: left.using_coalesce_indices,
384 column_replacement_map: left.column_replacement_map,
385 alias_tables: left.alias_tables,
386 shadowed_tables: left.shadowed_tables,
387 }
388 }
389
390 pub fn merge(left: CombinedSchema, right: CombinedSchema) -> Self {
400 let mut table_schemas = left.table_schemas;
401 let mut duplicate_aliases = left.duplicate_aliases;
402 let left_total = left.total_columns;
403
404 for (table_id, (start_index, schema)) in right.table_schemas {
406 let adjusted_start = left_total + start_index;
407
408 if let Some((_, existing_schema)) = table_schemas.get(&table_id) {
410 if existing_schema != &schema {
412 duplicate_aliases.insert(table_id.clone());
413 }
414
415 let synthetic_key = TableIdentifier::unquoted(&format!(
422 "__selfjoin_right_{}_{}",
423 table_id.canonical(),
424 adjusted_start
425 ));
426 table_schemas.insert(synthetic_key, (adjusted_start, schema));
427 } else {
428 table_schemas.insert(table_id, (adjusted_start, schema));
430 }
431 }
432
433 let mut hidden_columns = left.hidden_columns;
435 for idx in right.hidden_columns {
436 hidden_columns.insert(left_total + idx);
437 }
438
439 duplicate_aliases.extend(right.duplicate_aliases);
441
442 let mut joined_columns = left.joined_columns;
444 joined_columns.extend(right.joined_columns);
445
446 let mut using_coalesce_indices = left.using_coalesce_indices;
449 for (col_name, indices) in right.using_coalesce_indices {
450 let adjusted_indices: Vec<usize> = indices.iter().map(|idx| left_total + idx).collect();
451 using_coalesce_indices
452 .entry(col_name)
453 .or_insert_with(Vec::new)
454 .extend(adjusted_indices);
455 }
456
457 let mut column_replacement_map = left.column_replacement_map;
459 for (hidden_idx, replacement_idx) in right.column_replacement_map {
460 column_replacement_map.insert(left_total + hidden_idx, left_total + replacement_idx);
461 }
462
463 let mut alias_tables = left.alias_tables;
465 alias_tables.extend(right.alias_tables);
466
467 let mut shadowed_tables = left.shadowed_tables;
469 shadowed_tables.extend(right.shadowed_tables);
470
471 CombinedSchema {
472 table_schemas,
473 total_columns: left_total + right.total_columns,
474 hidden_columns,
475 outer_schema: left.outer_schema,
476 duplicate_aliases,
477 joined_columns,
478 using_coalesce_indices,
479 column_replacement_map,
480 alias_tables,
481 shadowed_tables,
482 }
483 }
484
485 pub fn get_column_index(&self, table: Option<&str>, column: &str) -> Option<usize> {
492 let current_result = if let Some(table_name) = table {
494 let table_id = TableIdentifier::unquoted(table_name);
497 if let Some((start_index, schema)) = self.table_schemas.get(&table_id) {
498 if self.alias_tables.contains(&table_id) {
503 let col_lower = column.to_lowercase();
505 if let Some(indices) = self.using_coalesce_indices.get(&col_lower) {
506 return indices.first().copied();
507 }
508 return self.get_column_index(None, column);
510 }
511 schema.get_column_index(column).map(|idx| start_index + idx)
512 } else {
513 None
514 }
515 } else {
516 let column_lower = column.to_lowercase();
526 let is_joined_column = self.joined_columns.contains(&column_lower);
527
528 let mut best_match: Option<usize> = None;
529 let mut best_match_is_hidden = false;
530
531 for (table_id, (start_index, schema)) in &self.table_schemas {
532 if self.alias_tables.contains(table_id) {
536 continue;
537 }
538 if let Some(idx) = schema.get_column_index(column) {
539 let absolute_idx = start_index + idx;
540 let is_hidden = self.hidden_columns.contains(&absolute_idx);
541
542 let should_update = match (best_match, is_joined_column) {
545 (None, _) => true,
546 (Some(_), true) if best_match_is_hidden && !is_hidden => true,
548 (Some(current_best), _)
550 if absolute_idx < current_best
551 && (!is_joined_column || is_hidden == best_match_is_hidden) =>
552 {
553 true
554 }
555 _ => false,
556 };
557
558 if should_update {
559 best_match = Some(absolute_idx);
560 best_match_is_hidden = is_hidden;
561 }
562 }
563 }
564 best_match
565 };
566
567 if current_result.is_some() {
569 return current_result;
570 }
571
572 if let Some(outer) = &self.outer_schema {
576 return outer.get_column_index(table, column);
577 }
578
579 None
581 }
582
583 pub fn get_column_affinity(
588 &self,
589 table: Option<&str>,
590 column: &str,
591 ) -> Option<vibesql_types::TypeAffinity> {
592 if let Some(table_name) = table {
593 let table_id = TableIdentifier::unquoted(table_name);
595 if let Some((_start_index, schema)) = self.table_schemas.get(&table_id) {
596 if let Some(col_idx) = schema.get_column_index(column) {
597 return Some(schema.columns[col_idx].data_type.sqlite_affinity());
598 }
599 }
600 } else {
601 for (table_id, (_start_index, schema)) in &self.table_schemas {
603 if self.alias_tables.contains(table_id) {
605 continue;
606 }
607 if let Some(col_idx) = schema.get_column_index(column) {
608 return Some(schema.columns[col_idx].data_type.sqlite_affinity());
609 }
610 }
611 }
612 None
613 }
614
615 pub fn is_column_ambiguous(&self, column: &str) -> bool {
625 let column_lower = column.to_lowercase();
628 if self.joined_columns.contains(&column_lower) {
629 return false;
630 }
631
632 let mut match_count = 0;
633 for (table_id, (_start_index, schema)) in &self.table_schemas {
634 if self.alias_tables.contains(table_id) {
637 continue;
638 }
639 if schema.get_column_index(column).is_some() {
640 match_count += 1;
641 if match_count > 1 {
642 return true;
643 }
644 }
645 }
646 false
647 }
648
649 #[inline]
661 pub fn has_column(&self, column: &str) -> bool {
662 for (_start_index, schema) in self.table_schemas.values() {
664 if schema.get_column_index(column).is_some() {
665 return true;
666 }
667 }
668 false
669 }
670
671 pub fn validate_qualified_reference(
690 &self,
691 table: &str,
692 column: &str,
693 ) -> Result<(), crate::errors::ExecutorError> {
694 let table_id = TableIdentifier::unquoted(table);
695 if self.duplicate_aliases.contains(&table_id) {
696 return Err(crate::errors::ExecutorError::AmbiguousColumnName {
697 column_name: format!("{}.{}", table, column),
698 });
699 }
700 Ok(())
701 }
702
703 pub fn get_table(&self, table_name: &str) -> Option<&(usize, vibesql_catalog::TableSchema)> {
705 self.table_schemas.get(&TableIdentifier::unquoted(table_name))
706 }
707
708 pub fn contains_table(&self, table_name: &str) -> bool {
710 self.table_schemas.contains_key(&TableIdentifier::unquoted(table_name))
711 }
712
713 pub fn table_names(&self) -> Vec<String> {
715 self.table_schemas.keys().map(|table_id| table_id.display().to_string()).collect()
716 }
717
718 pub fn insert_table(
720 &mut self,
721 name: String,
722 start_index: usize,
723 schema: vibesql_catalog::TableSchema,
724 ) {
725 let table_id = TableIdentifier::unquoted(&name);
726 self.table_schemas.insert(table_id, (start_index, schema));
727 }
728
729 pub fn get_original_column_name(&self, table: Option<&str>, column: &str) -> String {
742 if let Some(table_name) = table {
743 let table_id = TableIdentifier::unquoted(table_name);
745 if let Some((_start_index, schema)) = self.table_schemas.get(&table_id) {
746 if let Some(idx) = schema.get_column_index(column) {
747 return schema.columns[idx].name.clone();
748 }
749 }
750 } else {
751 let mut best_match: Option<(usize, String)> = None;
754 for (start_index, schema) in self.table_schemas.values() {
755 if let Some(idx) = schema.get_column_index(column) {
756 let name = schema.columns[idx].name.clone();
757 match &best_match {
758 None => best_match = Some((*start_index, name)),
759 Some((current_start, _)) if *start_index < *current_start => {
760 best_match = Some((*start_index, name));
761 }
762 _ => {}
763 }
764 }
765 }
766 if let Some((_, name)) = best_match {
767 return name;
768 }
769 }
770 column.to_string()
772 }
773
774 pub fn get_full_column_name(&self, table: Option<&str>, column: &str) -> String {
791 if let Some(table_name) = table {
792 let table_id = TableIdentifier::unquoted(table_name);
794 if let Some((_start_index, schema)) = self.table_schemas.get(&table_id) {
795 if let Some(idx) = schema.get_column_index(column) {
796 return format!("{}.{}", schema.name, schema.columns[idx].name);
798 }
799 }
800 } else {
801 let mut best_match: Option<(usize, String, String)> = None;
804 for (start_index, schema) in self.table_schemas.values() {
805 if let Some(idx) = schema.get_column_index(column) {
806 let table_name = schema.name.clone();
807 let col_name = schema.columns[idx].name.clone();
808 match &best_match {
809 None => best_match = Some((*start_index, table_name, col_name)),
810 Some((current_start, _, _)) if *start_index < *current_start => {
811 best_match = Some((*start_index, table_name, col_name));
812 }
813 _ => {}
814 }
815 }
816 }
817 if let Some((_, table_name, col_name)) = best_match {
818 return format!("{}.{}", table_name, col_name);
819 }
820 }
821 column.to_string()
823 }
824
825 #[inline]
835 pub fn is_column_hidden(&self, idx: usize) -> bool {
836 self.hidden_columns.contains(&idx)
837 }
838
839 pub fn hide_column(&mut self, idx: usize) {
843 self.hidden_columns.insert(idx);
844 }
845
846 pub fn add_joined_column(&mut self, column: &str) {
855 self.joined_columns.insert(column.to_lowercase());
856 }
857
858 pub fn add_using_coalesce_pair(&mut self, column: &str, left_idx: usize, right_idx: usize) {
871 let indices = self
872 .using_coalesce_indices
873 .entry(column.to_lowercase())
874 .or_insert_with(Vec::new);
875
876 if !indices.contains(&left_idx) {
881 indices.insert(0, left_idx);
882 }
883 if !indices.contains(&right_idx) {
885 indices.push(right_idx);
886 }
887 }
888
889 pub fn get_using_coalesce_pair(&self, column: &str) -> Option<(usize, usize)> {
898 self.using_coalesce_indices
899 .get(&column.to_lowercase())
900 .filter(|indices| indices.len() >= 2)
901 .map(|indices| (indices[0], indices[1]))
902 }
903
904 pub fn get_using_coalesce_indices(&self, column: &str) -> Option<&Vec<usize>> {
909 self.using_coalesce_indices.get(&column.to_lowercase())
910 }
911
912 pub fn add_column_replacement(&mut self, hidden_idx: usize, replacement_idx: usize) {
917 self.column_replacement_map.insert(hidden_idx, replacement_idx);
918 }
919
920 pub fn get_column_replacement(&self, hidden_idx: usize) -> Option<usize> {
922 self.column_replacement_map.get(&hidden_idx).copied()
923 }
924
925 pub fn get_using_coalesce_rest_for_left(&self, left_idx: usize) -> Option<&[usize]> {
933 for indices in self.using_coalesce_indices.values() {
934 if !indices.is_empty() && indices[0] == left_idx && indices.len() > 1 {
935 return Some(&indices[1..]);
936 }
937 }
938 None
939 }
940
941 pub fn get_using_coalesce_right_for_left(&self, left_idx: usize) -> Option<usize> {
946 for indices in self.using_coalesce_indices.values() {
947 if !indices.is_empty() && indices[0] == left_idx && indices.len() > 1 {
948 return Some(indices[1]);
949 }
950 }
951 None
952 }
953
954 pub fn is_using_coalesce_right_side(&self, idx: usize) -> bool {
959 for indices in self.using_coalesce_indices.values() {
961 if indices.len() > 1 && indices[1..].contains(&idx) {
962 return true;
963 }
964 }
965 false
966 }
967
968 pub fn get_all_coalesce_indices_for_column(&self, idx: usize) -> Option<&Vec<usize>> {
977 for indices in self.using_coalesce_indices.values() {
978 if indices.contains(&idx) && indices.len() > 1 {
979 return Some(indices);
980 }
981 }
982 None
983 }
984
985 pub fn build_column_name_map(&self) -> std::collections::HashMap<String, usize> {
994 let mut map = std::collections::HashMap::new();
995
996 let mut entries: Vec<_> = self.table_schemas.iter()
998 .filter(|(table_id, _)| !self.alias_tables.contains(*table_id))
999 .map(|(_, (start_index, schema))| (*start_index, schema))
1000 .collect();
1001 entries.sort_by_key(|(start_index, _)| *start_index);
1002
1003 for (start_index, schema) in entries {
1004 for (idx, col) in schema.columns.iter().enumerate() {
1005 let absolute_idx = start_index + idx;
1006 let name = &col.name;
1007
1008 if !map.contains_key(name) {
1010 map.insert(name.clone(), absolute_idx);
1011 }
1012
1013 let lower = name.to_lowercase();
1015 if !map.contains_key(&lower) {
1016 map.insert(lower, absolute_idx);
1017 }
1018 }
1019 }
1020
1021 map
1022 }
1023}
1024
1025#[derive(Debug)]
1030pub struct SchemaBuilder {
1031 table_schemas: HashMap<TableIdentifier, (usize, vibesql_catalog::TableSchema)>,
1032 column_offset: usize,
1033 hidden_columns: HashSet<usize>,
1034 duplicate_aliases: HashSet<TableIdentifier>,
1035 joined_columns: HashSet<String>,
1036 using_coalesce_indices: HashMap<String, Vec<usize>>,
1037 column_replacement_map: HashMap<usize, usize>,
1038 alias_tables: HashSet<TableIdentifier>,
1039 shadowed_tables: HashMap<TableIdentifier, HashSet<TableIdentifier>>,
1040}
1041
1042impl SchemaBuilder {
1043 pub fn new() -> Self {
1045 SchemaBuilder {
1046 table_schemas: HashMap::new(),
1047 column_offset: 0,
1048 hidden_columns: HashSet::new(),
1049 duplicate_aliases: HashSet::new(),
1050 joined_columns: HashSet::new(),
1051 using_coalesce_indices: HashMap::new(),
1052 column_replacement_map: HashMap::new(),
1053 alias_tables: HashSet::new(),
1054 shadowed_tables: HashMap::new(),
1055 }
1056 }
1057
1058 pub fn from_schema(schema: CombinedSchema) -> Self {
1062 let column_offset = schema.total_columns;
1063 SchemaBuilder {
1064 table_schemas: schema.table_schemas,
1065 column_offset,
1066 hidden_columns: schema.hidden_columns,
1067 duplicate_aliases: schema.duplicate_aliases,
1068 joined_columns: schema.joined_columns,
1069 using_coalesce_indices: schema.using_coalesce_indices,
1070 column_replacement_map: schema.column_replacement_map,
1071 alias_tables: schema.alias_tables,
1072 shadowed_tables: schema.shadowed_tables,
1073 }
1074 }
1075
1076 pub fn add_table(&mut self, name: String, schema: vibesql_catalog::TableSchema) -> &mut Self {
1082 let num_columns = schema.columns.len();
1083 let table_id = TableIdentifier::unquoted(&name);
1084
1085 if let Some((_, existing_schema)) = self.table_schemas.get(&table_id) {
1088 if existing_schema != &schema {
1090 self.duplicate_aliases.insert(table_id.clone());
1091 }
1092 }
1093
1094 self.table_schemas.insert(table_id, (self.column_offset, schema));
1095 self.column_offset += num_columns;
1096 self
1097 }
1098
1099 pub fn build(self) -> CombinedSchema {
1103 CombinedSchema {
1104 table_schemas: self.table_schemas,
1105 total_columns: self.column_offset,
1106 hidden_columns: self.hidden_columns,
1107 outer_schema: None,
1108 duplicate_aliases: self.duplicate_aliases,
1109 joined_columns: self.joined_columns,
1110 using_coalesce_indices: self.using_coalesce_indices,
1111 column_replacement_map: self.column_replacement_map,
1112 alias_tables: self.alias_tables,
1113 shadowed_tables: self.shadowed_tables,
1114 }
1115 }
1116
1117 pub fn add_column_replacement(&mut self, hidden_idx: usize, replacement_idx: usize) {
1119 self.column_replacement_map.insert(hidden_idx, replacement_idx);
1120 }
1121}
1122
1123impl Default for SchemaBuilder {
1124 fn default() -> Self {
1125 Self::new()
1126 }
1127}
1128
1129#[cfg(test)]
1130mod tests {
1131 use vibesql_catalog::ColumnSchema;
1132 use vibesql_types::DataType;
1133
1134 use super::*;
1135
1136 fn table_schema_with_columns(
1138 table_name: &str,
1139 columns: Vec<(&str, DataType)>,
1140 ) -> vibesql_catalog::TableSchema {
1141 let cols: Vec<ColumnSchema> = columns
1142 .into_iter()
1143 .map(|(name, data_type)| ColumnSchema::new(name.to_string(), data_type, true))
1144 .collect();
1145 vibesql_catalog::TableSchema::new(table_name.to_string(), cols)
1146 }
1147
1148 fn table_schema_with_column(
1150 table_name: &str,
1151 column_name: &str,
1152 ) -> vibesql_catalog::TableSchema {
1153 table_schema_with_columns(table_name, vec![(column_name, DataType::Integer)])
1154 }
1155
1156 #[test]
1161 fn test_from_table_uppercase_insertion_case_insensitive_lookup() {
1162 let schema = CombinedSchema::from_table(
1164 "ITEM".to_string(),
1165 table_schema_with_column("ITEM", "price"),
1166 );
1167
1168 assert!(schema.get_column_index(Some("ITEM"), "price").is_some(), "ITEM should find price");
1170 assert!(schema.get_column_index(Some("item"), "price").is_some(), "item should find price");
1171 assert!(schema.get_column_index(Some("Item"), "price").is_some(), "Item should find price");
1172 assert!(schema.get_column_index(Some("iTEM"), "price").is_some(), "iTEM should find price");
1173 }
1174
1175 #[test]
1176 fn test_from_table_lowercase_insertion_case_insensitive_lookup() {
1177 let schema = CombinedSchema::from_table(
1179 "item".to_string(),
1180 table_schema_with_column("item", "price"),
1181 );
1182
1183 assert!(schema.get_column_index(Some("ITEM"), "price").is_some());
1185 assert!(schema.get_column_index(Some("item"), "price").is_some());
1186 assert!(schema.get_column_index(Some("Item"), "price").is_some());
1187 }
1188
1189 #[test]
1190 fn test_from_table_mixedcase_insertion_case_insensitive_lookup() {
1191 let schema = CombinedSchema::from_table(
1193 "MyTable".to_string(),
1194 table_schema_with_column("MyTable", "id"),
1195 );
1196
1197 assert!(schema.get_column_index(Some("MYTABLE"), "id").is_some());
1199 assert!(schema.get_column_index(Some("mytable"), "id").is_some());
1200 assert!(schema.get_column_index(Some("MyTable"), "id").is_some());
1201 assert!(schema.get_column_index(Some("myTable"), "id").is_some());
1202 }
1203
1204 #[test]
1209 fn test_from_derived_table_case_insensitive_alias() {
1210 let schema = CombinedSchema::from_derived_table(
1212 "SUBQ".to_string(),
1213 vec!["col1".to_string(), "col2".to_string()],
1214 vec![DataType::Integer, DataType::Varchar { max_length: None }],
1215 );
1216
1217 assert!(schema.get_column_index(Some("SUBQ"), "col1").is_some());
1219 assert!(schema.get_column_index(Some("subq"), "col1").is_some());
1220 assert!(schema.get_column_index(Some("Subq"), "col1").is_some());
1221 }
1222
1223 #[test]
1228 fn test_combine_case_insensitive_both_tables() {
1229 let left = CombinedSchema::from_table(
1231 "ORDERS".to_string(),
1232 table_schema_with_columns(
1233 "ORDERS",
1234 vec![("order_id", DataType::Integer), ("customer_id", DataType::Integer)],
1235 ),
1236 );
1237
1238 let combined = CombinedSchema::combine(
1240 left,
1241 "Items".to_string(),
1242 table_schema_with_columns(
1243 "Items",
1244 vec![("item_id", DataType::Integer), ("price", DataType::DoublePrecision)],
1245 ),
1246 );
1247
1248 assert!(combined.get_column_index(Some("orders"), "order_id").is_some());
1250 assert!(combined.get_column_index(Some("ORDERS"), "order_id").is_some());
1251 assert!(combined.get_column_index(Some("Orders"), "customer_id").is_some());
1252
1253 assert!(combined.get_column_index(Some("items"), "item_id").is_some());
1255 assert!(combined.get_column_index(Some("ITEMS"), "item_id").is_some());
1256 assert!(combined.get_column_index(Some("Items"), "price").is_some());
1257
1258 assert_eq!(combined.get_column_index(Some("orders"), "order_id"), Some(0));
1260 assert_eq!(combined.get_column_index(Some("orders"), "customer_id"), Some(1));
1261 assert_eq!(combined.get_column_index(Some("items"), "item_id"), Some(2));
1262 assert_eq!(combined.get_column_index(Some("items"), "price"), Some(3));
1263 }
1264
1265 #[test]
1266 fn test_combine_multiple_joins_case_insensitive() {
1267 let orders = CombinedSchema::from_table(
1269 "O".to_string(), table_schema_with_column("O", "order_id"),
1271 );
1272
1273 let with_customers = CombinedSchema::combine(
1274 orders,
1275 "C".to_string(),
1276 table_schema_with_column("C", "customer_id"),
1277 );
1278
1279 let with_items = CombinedSchema::combine(
1280 with_customers,
1281 "I".to_string(),
1282 table_schema_with_column("I", "item_id"),
1283 );
1284
1285 assert!(with_items.get_column_index(Some("o"), "order_id").is_some());
1287 assert!(with_items.get_column_index(Some("O"), "order_id").is_some());
1288 assert!(with_items.get_column_index(Some("c"), "customer_id").is_some());
1289 assert!(with_items.get_column_index(Some("C"), "customer_id").is_some());
1290 assert!(with_items.get_column_index(Some("i"), "item_id").is_some());
1291 assert!(with_items.get_column_index(Some("I"), "item_id").is_some());
1292 }
1293
1294 #[test]
1299 fn test_unqualified_column_lookup_no_ambiguity() {
1300 let schema = CombinedSchema::from_table(
1301 "USERS".to_string(),
1302 table_schema_with_columns(
1303 "USERS",
1304 vec![("id", DataType::Integer), ("name", DataType::Varchar { max_length: None })],
1305 ),
1306 );
1307
1308 assert!(schema.get_column_index(None, "id").is_some());
1310 assert!(schema.get_column_index(None, "name").is_some());
1311 assert!(schema.get_column_index(None, "missing").is_none());
1312 }
1313
1314 #[test]
1315 fn test_column_case_sensitive_with_fallback() {
1316 let schema = CombinedSchema::from_table(
1318 "users".to_string(),
1319 table_schema_with_column("users", "UserName"),
1320 );
1321
1322 assert!(schema.get_column_index(Some("users"), "UserName").is_some());
1324 assert!(schema.get_column_index(Some("users"), "username").is_some());
1326 assert!(schema.get_column_index(Some("users"), "USERNAME").is_some());
1327 }
1328
1329 #[test]
1333 fn test_tpcds_q6_case_insensitive_column_lookup_issue_4111() {
1334 let schema = CombinedSchema::from_table(
1336 "J".to_string(), table_schema_with_columns(
1338 "item",
1339 vec![
1340 ("i_item_sk", DataType::Integer),
1341 ("i_current_price", DataType::DoublePrecision), ("i_category", DataType::Varchar { max_length: None }),
1343 ],
1344 ),
1345 );
1346
1347 assert!(
1351 schema.get_column_index(Some("J"), "I_CURRENT_PRICE").is_some(),
1352 "J.I_CURRENT_PRICE should find i_current_price via case-insensitive lookup"
1353 );
1354 assert!(
1355 schema.get_column_index(Some("J"), "I_CATEGORY").is_some(),
1356 "J.I_CATEGORY should find i_category via case-insensitive lookup"
1357 );
1358 assert!(
1359 schema.get_column_index(Some("j"), "I_CURRENT_PRICE").is_some(),
1360 "j.I_CURRENT_PRICE should find i_current_price"
1361 );
1362 assert!(
1363 schema.get_column_index(Some("J"), "i_current_price").is_some(),
1364 "J.i_current_price should find via exact match"
1365 );
1366 }
1367
1368 #[test]
1369 fn test_column_distinct_cases_exact_match() {
1370 let cols: Vec<vibesql_catalog::ColumnSchema> = vec![
1373 vibesql_catalog::ColumnSchema::new("value".to_string(), DataType::Integer, true),
1374 vibesql_catalog::ColumnSchema::new("VALUE".to_string(), DataType::Integer, true),
1375 vibesql_catalog::ColumnSchema::new("Value".to_string(), DataType::Integer, true),
1376 ];
1377 let table_schema = vibesql_catalog::TableSchema::new("data".to_string(), cols);
1378 let schema = CombinedSchema::from_table("data".to_string(), table_schema);
1379
1380 assert_eq!(schema.get_column_index(Some("data"), "value"), Some(0));
1382 assert_eq!(schema.get_column_index(Some("data"), "VALUE"), Some(1));
1383 assert_eq!(schema.get_column_index(Some("data"), "Value"), Some(2));
1384 }
1385
1386 #[test]
1391 fn test_schema_builder_add_table_case_insensitive() {
1392 let mut builder = SchemaBuilder::new();
1393
1394 builder.add_table("ORDERS".to_string(), table_schema_with_column("ORDERS", "order_id"));
1396 builder.add_table("Items".to_string(), table_schema_with_column("Items", "item_id"));
1397
1398 let schema = builder.build();
1399
1400 assert!(schema.get_column_index(Some("orders"), "order_id").is_some());
1402 assert!(schema.get_column_index(Some("ORDERS"), "order_id").is_some());
1403 assert!(schema.get_column_index(Some("items"), "item_id").is_some());
1404 assert!(schema.get_column_index(Some("ITEMS"), "item_id").is_some());
1405 }
1406
1407 #[test]
1408 fn test_schema_builder_from_schema_preserves_case_insensitivity() {
1409 let initial = CombinedSchema::from_table(
1411 "PRODUCTS".to_string(),
1412 table_schema_with_columns(
1413 "PRODUCTS",
1414 vec![("id", DataType::Integer), ("name", DataType::Varchar { max_length: None })],
1415 ),
1416 );
1417
1418 assert!(initial.get_column_index(Some("products"), "id").is_some());
1420
1421 let mut builder = SchemaBuilder::from_schema(initial);
1423 builder
1424 .add_table("Categories".to_string(), table_schema_with_column("Categories", "cat_id"));
1425
1426 let final_schema = builder.build();
1427
1428 assert!(final_schema.get_column_index(Some("products"), "id").is_some());
1430 assert!(final_schema.get_column_index(Some("PRODUCTS"), "id").is_some());
1431 assert!(final_schema.get_column_index(Some("Products"), "name").is_some());
1432
1433 assert!(final_schema.get_column_index(Some("categories"), "cat_id").is_some());
1435 assert!(final_schema.get_column_index(Some("CATEGORIES"), "cat_id").is_some());
1436 }
1437
1438 #[test]
1439 fn test_schema_builder_from_schema_multiple_tables() {
1440 let orders = CombinedSchema::from_table(
1442 "Orders".to_string(),
1443 table_schema_with_column("Orders", "order_id"),
1444 );
1445 let combined = CombinedSchema::combine(
1446 orders,
1447 "Items".to_string(),
1448 table_schema_with_column("Items", "item_id"),
1449 );
1450
1451 let mut builder = SchemaBuilder::from_schema(combined);
1453 builder
1454 .add_table("CUSTOMERS".to_string(), table_schema_with_column("CUSTOMERS", "cust_id"));
1455
1456 let final_schema = builder.build();
1457
1458 assert!(final_schema.get_column_index(Some("orders"), "order_id").is_some());
1460 assert!(final_schema.get_column_index(Some("ORDERS"), "order_id").is_some());
1461 assert!(final_schema.get_column_index(Some("items"), "item_id").is_some());
1462 assert!(final_schema.get_column_index(Some("ITEMS"), "item_id").is_some());
1463 assert!(final_schema.get_column_index(Some("customers"), "cust_id").is_some());
1464 assert!(final_schema.get_column_index(Some("CUSTOMERS"), "cust_id").is_some());
1465
1466 assert_eq!(final_schema.get_column_index(Some("orders"), "order_id"), Some(0));
1468 assert_eq!(final_schema.get_column_index(Some("items"), "item_id"), Some(1));
1469 assert_eq!(final_schema.get_column_index(Some("customers"), "cust_id"), Some(2));
1470 }
1471
1472 #[test]
1477 fn test_issue_3633_correlated_subquery_alias_case() {
1478 let schema = CombinedSchema::from_table(
1484 "J".to_string(), table_schema_with_columns(
1486 "items",
1487 vec![("price", DataType::DoublePrecision), ("quantity", DataType::Integer)],
1488 ),
1489 );
1490
1491 assert!(
1494 schema.get_column_index(Some("J"), "price").is_some(),
1495 "Uppercase J should find price (parser case)"
1496 );
1497 assert!(
1498 schema.get_column_index(Some("j"), "price").is_some(),
1499 "Lowercase j should find price (normalized case)"
1500 );
1501 }
1502
1503 #[test]
1504 fn test_issue_3633_multi_table_join_with_aliases() {
1505 let orders = CombinedSchema::from_table(
1507 "O".to_string(),
1508 table_schema_with_columns(
1509 "orders",
1510 vec![("id", DataType::Integer), ("date", DataType::Date)],
1511 ),
1512 );
1513
1514 let combined = CombinedSchema::combine(
1515 orders,
1516 "I".to_string(),
1517 table_schema_with_columns(
1518 "items",
1519 vec![("order_id", DataType::Integer), ("amount", DataType::DoublePrecision)],
1520 ),
1521 );
1522
1523 assert_eq!(combined.get_column_index(Some("O"), "id"), Some(0));
1526 assert_eq!(combined.get_column_index(Some("o"), "id"), Some(0));
1527 assert_eq!(combined.get_column_index(Some("O"), "date"), Some(1));
1528 assert_eq!(combined.get_column_index(Some("I"), "order_id"), Some(2));
1529 assert_eq!(combined.get_column_index(Some("i"), "order_id"), Some(2));
1530 assert_eq!(combined.get_column_index(Some("I"), "amount"), Some(3));
1531 }
1532
1533 #[test]
1538 fn test_nonexistent_table_returns_none() {
1539 let schema = CombinedSchema::from_table(
1540 "users".to_string(),
1541 table_schema_with_column("users", "id"),
1542 );
1543
1544 assert!(schema.get_column_index(Some("nonexistent"), "id").is_none());
1545 assert!(schema.get_column_index(Some("NONEXISTENT"), "id").is_none());
1546 }
1547
1548 #[test]
1549 fn test_nonexistent_column_returns_none() {
1550 let schema = CombinedSchema::from_table(
1551 "users".to_string(),
1552 table_schema_with_column("users", "id"),
1553 );
1554
1555 assert!(schema.get_column_index(Some("users"), "nonexistent").is_none());
1556 assert!(schema.get_column_index(Some("USERS"), "nonexistent").is_none());
1557 }
1558
1559 #[test]
1560 fn test_empty_table_name() {
1561 let schema = CombinedSchema::from_table("".to_string(), table_schema_with_column("", "id"));
1562
1563 assert!(schema.get_column_index(Some(""), "id").is_some());
1565 }
1566
1567 #[test]
1568 fn test_total_columns_tracking() {
1569 let mut builder = SchemaBuilder::new();
1570 builder.add_table(
1571 "t1".to_string(),
1572 table_schema_with_columns(
1573 "t1",
1574 vec![("a", DataType::Integer), ("b", DataType::Integer)],
1575 ),
1576 );
1577 builder.add_table(
1578 "t2".to_string(),
1579 table_schema_with_columns("t2", vec![("c", DataType::Integer)]),
1580 );
1581
1582 let schema = builder.build();
1583 assert_eq!(schema.total_columns, 3);
1584 }
1585
1586 #[test]
1591 fn test_is_column_ambiguous_single_table() {
1592 let schema = CombinedSchema::from_table(
1594 "test1".to_string(),
1595 table_schema_with_columns(
1596 "test1",
1597 vec![("f1", DataType::Integer), ("f2", DataType::Integer)],
1598 ),
1599 );
1600
1601 assert!(!schema.is_column_ambiguous("f1"));
1602 assert!(!schema.is_column_ambiguous("f2"));
1603 assert!(!schema.is_column_ambiguous("nonexistent"));
1604 }
1605
1606 #[test]
1607 fn test_is_column_ambiguous_two_tables_no_overlap() {
1608 let test1 = CombinedSchema::from_table(
1610 "test1".to_string(),
1611 table_schema_with_columns(
1612 "test1",
1613 vec![("f1", DataType::Integer), ("f2", DataType::Integer)],
1614 ),
1615 );
1616 let schema = CombinedSchema::combine(
1617 test1,
1618 "test2".to_string(),
1619 table_schema_with_columns(
1620 "test2",
1621 vec![("f3", DataType::Integer), ("f4", DataType::Integer)],
1622 ),
1623 );
1624
1625 assert!(!schema.is_column_ambiguous("f1"));
1626 assert!(!schema.is_column_ambiguous("f2"));
1627 assert!(!schema.is_column_ambiguous("f3"));
1628 assert!(!schema.is_column_ambiguous("f4"));
1629 }
1630
1631 #[test]
1632 fn test_is_column_ambiguous_two_tables_with_overlap() {
1633 let test1 = CombinedSchema::from_table(
1639 "test1".to_string(),
1640 table_schema_with_columns(
1641 "test1",
1642 vec![("f1", DataType::Integer), ("f2", DataType::Integer)],
1643 ),
1644 );
1645 let schema = CombinedSchema::combine(
1646 test1,
1647 "test2".to_string(),
1648 table_schema_with_columns(
1649 "test2",
1650 vec![("f1", DataType::Integer), ("f2", DataType::Integer)],
1651 ),
1652 );
1653
1654 assert!(schema.is_column_ambiguous("f1"), "f1 should be ambiguous");
1656 assert!(schema.is_column_ambiguous("f2"), "f2 should be ambiguous");
1657
1658 assert!(!schema.is_column_ambiguous("f3"));
1660 }
1661
1662 #[test]
1663 fn test_is_column_ambiguous_case_insensitive() {
1664 let test1 = CombinedSchema::from_table(
1666 "test1".to_string(),
1667 table_schema_with_columns("test1", vec![("F1", DataType::Integer)]),
1668 );
1669 let schema = CombinedSchema::combine(
1670 test1,
1671 "test2".to_string(),
1672 table_schema_with_columns("test2", vec![("f1", DataType::Integer)]),
1673 );
1674
1675 assert!(schema.is_column_ambiguous("f1"));
1677 assert!(schema.is_column_ambiguous("F1"));
1678 assert!(schema.is_column_ambiguous("F1")); }
1680
1681 #[test]
1682 fn test_is_column_ambiguous_partial_overlap() {
1683 let test1 = CombinedSchema::from_table(
1685 "test1".to_string(),
1686 table_schema_with_columns(
1687 "test1",
1688 vec![("id", DataType::Integer), ("name", DataType::Varchar { max_length: None })],
1689 ),
1690 );
1691 let schema = CombinedSchema::combine(
1692 test1,
1693 "test2".to_string(),
1694 table_schema_with_columns(
1695 "test2",
1696 vec![
1697 ("id", DataType::Integer), ("value", DataType::Integer), ],
1700 ),
1701 );
1702
1703 assert!(schema.is_column_ambiguous("id"));
1705
1706 assert!(!schema.is_column_ambiguous("name"));
1708 assert!(!schema.is_column_ambiguous("value"));
1709 }
1710
1711 #[test]
1712 fn test_is_column_ambiguous_three_tables() {
1713 let t1 = CombinedSchema::from_table(
1715 "t1".to_string(),
1716 table_schema_with_columns(
1717 "t1",
1718 vec![("a", DataType::Integer), ("b", DataType::Integer)],
1719 ),
1720 );
1721 let t1_t2 = CombinedSchema::combine(
1722 t1,
1723 "t2".to_string(),
1724 table_schema_with_columns(
1725 "t2",
1726 vec![("b", DataType::Integer), ("c", DataType::Integer)],
1727 ),
1728 );
1729 let schema = CombinedSchema::combine(
1730 t1_t2,
1731 "t3".to_string(),
1732 table_schema_with_columns(
1733 "t3",
1734 vec![("c", DataType::Integer), ("d", DataType::Integer)],
1735 ),
1736 );
1737
1738 assert!(!schema.is_column_ambiguous("a"));
1740
1741 assert!(schema.is_column_ambiguous("b"));
1743
1744 assert!(schema.is_column_ambiguous("c"));
1746
1747 assert!(!schema.is_column_ambiguous("d"));
1749 }
1750
1751 #[test]
1756 fn test_joined_column_not_ambiguous_natural_join() {
1757 let t1 = CombinedSchema::from_table(
1760 "t1".to_string(),
1761 table_schema_with_columns(
1762 "t1",
1763 vec![("a", DataType::Integer), ("b", DataType::Integer), ("c", DataType::Integer)],
1764 ),
1765 );
1766 let mut schema = CombinedSchema::combine(
1767 t1,
1768 "t2".to_string(),
1769 table_schema_with_columns(
1770 "t2",
1771 vec![("b", DataType::Integer), ("c", DataType::Integer), ("d", DataType::Integer)],
1772 ),
1773 );
1774
1775 assert!(
1777 schema.is_column_ambiguous("b"),
1778 "b should be ambiguous before NATURAL JOIN processing"
1779 );
1780 assert!(
1781 schema.is_column_ambiguous("c"),
1782 "c should be ambiguous before NATURAL JOIN processing"
1783 );
1784
1785 schema.add_joined_column("b");
1787 schema.add_joined_column("c");
1788
1789 assert!(!schema.is_column_ambiguous("b"), "b should NOT be ambiguous after NATURAL JOIN");
1791 assert!(!schema.is_column_ambiguous("c"), "c should NOT be ambiguous after NATURAL JOIN");
1792
1793 assert!(!schema.is_column_ambiguous("a"));
1795 assert!(!schema.is_column_ambiguous("d"));
1796 }
1797
1798 #[test]
1799 fn test_joined_column_case_insensitive() {
1800 let t1 = CombinedSchema::from_table(
1802 "t1".to_string(),
1803 table_schema_with_columns("t1", vec![("Name", DataType::Integer)]),
1804 );
1805 let mut schema = CombinedSchema::combine(
1806 t1,
1807 "t2".to_string(),
1808 table_schema_with_columns("t2", vec![("NAME", DataType::Integer)]),
1809 );
1810
1811 assert!(schema.is_column_ambiguous("name"));
1813 assert!(schema.is_column_ambiguous("NAME"));
1814 assert!(schema.is_column_ambiguous("Name"));
1815
1816 schema.add_joined_column("name");
1818
1819 assert!(!schema.is_column_ambiguous("name"));
1821 assert!(!schema.is_column_ambiguous("NAME"));
1822 assert!(!schema.is_column_ambiguous("Name"));
1823 }
1824
1825 #[test]
1826 fn test_joined_column_with_using_clause() {
1827 let t1 = CombinedSchema::from_table(
1830 "t1".to_string(),
1831 table_schema_with_columns(
1832 "t1",
1833 vec![("id", DataType::Integer), ("value1", DataType::Integer)],
1834 ),
1835 );
1836 let mut schema = CombinedSchema::combine(
1837 t1,
1838 "t2".to_string(),
1839 table_schema_with_columns(
1840 "t2",
1841 vec![("id", DataType::Integer), ("value2", DataType::Integer)],
1842 ),
1843 );
1844
1845 schema.add_joined_column("id");
1847
1848 assert!(!schema.is_column_ambiguous("id"));
1850
1851 assert!(!schema.is_column_ambiguous("value1"));
1853 assert!(!schema.is_column_ambiguous("value2"));
1854 }
1855}