vibesql_executor/
schema.rs

1use std::collections::HashMap;
2
3/// Represents the combined schema from multiple tables (for JOINs)
4#[derive(Debug, Clone)]
5pub struct CombinedSchema {
6    /// Map from table name to (start_index, TableSchema)
7    /// start_index is where this table's columns begin in the combined row
8    pub table_schemas: HashMap<String, (usize, vibesql_catalog::TableSchema)>,
9    /// Total number of columns across all tables
10    pub total_columns: usize,
11}
12
13impl CombinedSchema {
14    /// Create a new combined schema from a single table
15    pub fn from_table(table_name: String, schema: vibesql_catalog::TableSchema) -> Self {
16        let total_columns = schema.columns.len();
17        let mut table_schemas = HashMap::new();
18        // Use lowercase table name for consistent lookups in predicate decomposition
19        table_schemas.insert(table_name.to_lowercase(), (0, schema));
20        CombinedSchema { table_schemas, total_columns }
21    }
22
23    /// Create a new combined schema from a derived table (subquery result)
24    pub fn from_derived_table(
25        alias: String,
26        column_names: Vec<String>,
27        column_types: Vec<vibesql_types::DataType>,
28    ) -> Self {
29        let total_columns = column_names.len();
30
31        // Build column definitions
32        let columns: Vec<vibesql_catalog::ColumnSchema> = column_names
33            .into_iter()
34            .zip(column_types)
35            .map(|(name, data_type)| vibesql_catalog::ColumnSchema {
36                name,
37                data_type,
38                nullable: true,      // Derived table columns are always nullable
39                default_value: None, // Derived table columns have no defaults
40            })
41            .collect();
42
43        let schema = vibesql_catalog::TableSchema::new(alias.clone(), columns);
44        let mut table_schemas = HashMap::new();
45        // Use lowercase alias for consistent lookups
46        table_schemas.insert(alias.to_lowercase(), (0, schema));
47        CombinedSchema { table_schemas, total_columns }
48    }
49
50    /// Combine two schemas (for JOIN operations)
51    pub fn combine(
52        left: CombinedSchema,
53        right_table: String,
54        right_schema: vibesql_catalog::TableSchema,
55    ) -> Self {
56        let mut table_schemas = left.table_schemas;
57        let left_total = left.total_columns;
58        let right_columns = right_schema.columns.len();
59        // Use lowercase table name for consistent lookups
60        table_schemas.insert(right_table.to_lowercase(), (left_total, right_schema));
61        CombinedSchema { table_schemas, total_columns: left_total + right_columns }
62    }
63
64    /// Look up a column by name (optionally qualified with table name)
65    /// Uses case-insensitive matching for table/alias and column names
66    pub fn get_column_index(&self, table: Option<&str>, column: &str) -> Option<usize> {
67        if let Some(table_name) = table {
68            // Qualified column reference (table.column)
69            // Try exact match first for performance
70            if let Some((start_index, schema)) = self.table_schemas.get(table_name) {
71                return schema.get_column_index(column).map(|idx| start_index + idx);
72            }
73
74            // Fall back to case-insensitive table/alias name lookup
75            let table_name_lower = table_name.to_lowercase();
76            for (key, (start_index, schema)) in self.table_schemas.iter() {
77                if key.to_lowercase() == table_name_lower {
78                    return schema.get_column_index(column).map(|idx| start_index + idx);
79                }
80            }
81            None
82        } else {
83            // Unqualified column reference - search all tables
84            for (start_index, schema) in self.table_schemas.values() {
85                if let Some(idx) = schema.get_column_index(column) {
86                    return Some(start_index + idx);
87                }
88            }
89            None
90        }
91    }
92}
93
94/// Builder for incrementally constructing a CombinedSchema
95///
96/// Builds schemas in O(n) time instead of O(n²) by tracking
97/// the column offset as tables are added.
98#[derive(Debug)]
99pub struct SchemaBuilder {
100    table_schemas: HashMap<String, (usize, vibesql_catalog::TableSchema)>,
101    column_offset: usize,
102}
103
104impl SchemaBuilder {
105    /// Create a new empty schema builder
106    pub fn new() -> Self {
107        SchemaBuilder {
108            table_schemas: HashMap::new(),
109            column_offset: 0,
110        }
111    }
112
113    /// Create a schema builder initialized with an existing CombinedSchema
114    pub fn from_schema(schema: CombinedSchema) -> Self {
115        let column_offset = schema.total_columns;
116        SchemaBuilder {
117            table_schemas: schema.table_schemas,
118            column_offset,
119        }
120    }
121
122    /// Add a table to the schema
123    ///
124    /// This is an O(1) operation - columns are not copied, just indexed
125    pub fn add_table(&mut self, name: String, schema: vibesql_catalog::TableSchema) -> &mut Self {
126        let num_columns = schema.columns.len();
127        self.table_schemas.insert(name, (self.column_offset, schema));
128        self.column_offset += num_columns;
129        self
130    }
131
132    /// Build the final CombinedSchema
133    ///
134    /// This consumes the builder and produces the schema in O(1) time
135    pub fn build(self) -> CombinedSchema {
136        CombinedSchema {
137            table_schemas: self.table_schemas,
138            total_columns: self.column_offset,
139        }
140    }
141}
142
143impl Default for SchemaBuilder {
144    fn default() -> Self {
145        Self::new()
146    }
147}