alopex_sql/planner/
name_resolver.rs

1//! Name resolution for SQL queries.
2//!
3//! This module provides functionality for resolving table and column references
4//! in SQL statements, validating their existence against the catalog, and
5//! expanding wildcards in SELECT statements.
6
7use crate::ast::Span;
8use crate::ast::expr::{Expr, ExprKind};
9use crate::catalog::{Catalog, TableMetadata};
10use crate::planner::error::PlannerError;
11use crate::planner::types::ResolvedType;
12
13/// A resolved column reference with full metadata.
14///
15/// Contains all information needed to access a column after name resolution,
16/// including the table name, column name, column index (for efficient access),
17/// and the resolved type.
18///
19/// # Examples
20///
21/// ```
22/// use alopex_sql::planner::name_resolver::ResolvedColumn;
23/// use alopex_sql::planner::types::ResolvedType;
24///
25/// let resolved = ResolvedColumn {
26///     table_name: "users".to_string(),
27///     column_name: "id".to_string(),
28///     column_index: 0,
29///     resolved_type: ResolvedType::Integer,
30/// };
31///
32/// assert_eq!(resolved.table_name, "users");
33/// assert_eq!(resolved.column_index, 0);
34/// ```
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct ResolvedColumn {
37    /// The table name containing this column.
38    pub table_name: String,
39    /// The column name.
40    pub column_name: String,
41    /// The index of the column in the table's column list (0-based).
42    pub column_index: usize,
43    /// The resolved type of the column.
44    pub resolved_type: ResolvedType,
45}
46
47/// Name resolver for validating table and column references.
48///
49/// The `NameResolver` validates SQL identifiers against the catalog,
50/// ensuring that referenced tables and columns exist. It also handles
51/// wildcard expansion for `SELECT *` queries.
52///
53/// # Design Notes
54///
55/// - In v0.1.1, only single-table operations are supported (no JOINs).
56/// - The `resolve_column_with_scope` method is prepared for future JOIN support (v0.3.0+).
57/// - Wildcard expansion returns columns in table definition order.
58///
59/// # Examples
60///
61/// ```ignore
62/// use alopex_sql::catalog::MemoryCatalog;
63/// use alopex_sql::planner::name_resolver::NameResolver;
64///
65/// let catalog = MemoryCatalog::new();
66/// let resolver = NameResolver::new(&catalog);
67///
68/// // Resolve a table reference
69/// let table = resolver.resolve_table("users", span)?;
70/// ```
71pub struct NameResolver<'a, C: Catalog> {
72    catalog: &'a C,
73}
74
75impl<'a, C: Catalog> NameResolver<'a, C> {
76    /// Create a new name resolver with the given catalog.
77    pub fn new(catalog: &'a C) -> Self {
78        Self { catalog }
79    }
80
81    /// Resolve a table reference.
82    ///
83    /// Validates that the table exists in the catalog and returns its metadata.
84    ///
85    /// # Errors
86    ///
87    /// Returns `PlannerError::TableNotFound` if the table doesn't exist.
88    ///
89    /// # Examples
90    ///
91    /// ```ignore
92    /// let table = resolver.resolve_table("users", span)?;
93    /// assert_eq!(table.name, "users");
94    /// ```
95    pub fn resolve_table(&self, name: &str, span: Span) -> Result<&TableMetadata, PlannerError> {
96        self.catalog
97            .get_table(name)
98            .ok_or_else(|| PlannerError::TableNotFound {
99                name: name.to_string(),
100                line: span.start.line,
101                column: span.start.column,
102            })
103    }
104
105    /// Resolve a column reference within a single table context.
106    ///
107    /// Validates that the column exists in the given table and returns its metadata.
108    ///
109    /// # Errors
110    ///
111    /// Returns `PlannerError::ColumnNotFound` if the column doesn't exist in the table.
112    ///
113    /// # Examples
114    ///
115    /// ```ignore
116    /// let table = resolver.resolve_table("users", span)?;
117    /// let column = resolver.resolve_column(table, "id", span)?;
118    /// assert_eq!(column.name, "id");
119    /// ```
120    pub fn resolve_column<'t>(
121        &self,
122        table: &'t TableMetadata,
123        column: &str,
124        span: Span,
125    ) -> Result<&'t crate::catalog::ColumnMetadata, PlannerError> {
126        table
127            .get_column(column)
128            .ok_or_else(|| PlannerError::ColumnNotFound {
129                column: column.to_string(),
130                table: table.name.clone(),
131                line: span.start.line,
132                col: span.start.column,
133            })
134    }
135
136    /// Resolve a column reference with scope for multiple tables.
137    ///
138    /// This method supports resolution in contexts with multiple tables (for future JOIN support).
139    /// In v0.1.1, `tables` should always contain exactly one element.
140    ///
141    /// # Resolution Rules
142    ///
143    /// 1. If `table_qualifier` is specified (e.g., `users.id`), resolve from that table only.
144    /// 2. If `table_qualifier` is `None`:
145    ///    - If the column exists in exactly one table, resolve it.
146    ///    - If the column exists in multiple tables, return `AmbiguousColumn` error.
147    ///    - If the column doesn't exist in any table, return `ColumnNotFound` error.
148    ///
149    /// # Errors
150    ///
151    /// - `PlannerError::TableNotFound` if the specified table qualifier doesn't match any table.
152    /// - `PlannerError::ColumnNotFound` if the column doesn't exist in the resolved table(s).
153    /// - `PlannerError::AmbiguousColumn` if the column exists in multiple tables without qualification.
154    pub fn resolve_column_with_scope(
155        &self,
156        tables: &[&TableMetadata],
157        table_qualifier: Option<&str>,
158        column: &str,
159        span: Span,
160    ) -> Result<ResolvedColumn, PlannerError> {
161        if let Some(qualifier) = table_qualifier {
162            // Explicit table qualifier - find the matching table
163            let table = tables.iter().find(|t| t.name == qualifier).ok_or_else(|| {
164                PlannerError::TableNotFound {
165                    name: qualifier.to_string(),
166                    line: span.start.line,
167                    column: span.start.column,
168                }
169            })?;
170
171            let column_index =
172                table
173                    .get_column_index(column)
174                    .ok_or_else(|| PlannerError::ColumnNotFound {
175                        column: column.to_string(),
176                        table: table.name.clone(),
177                        line: span.start.line,
178                        col: span.start.column,
179                    })?;
180
181            let column_meta = &table.columns[column_index];
182            Ok(ResolvedColumn {
183                table_name: table.name.clone(),
184                column_name: column.to_string(),
185                column_index,
186                resolved_type: column_meta.data_type.clone(),
187            })
188        } else {
189            // No qualifier - search all tables
190            let mut matches: Vec<(&TableMetadata, usize)> = Vec::new();
191
192            for table in tables {
193                if let Some(idx) = table.get_column_index(column) {
194                    matches.push((table, idx));
195                }
196            }
197
198            match matches.len() {
199                0 => {
200                    // Column not found in any table
201                    let table_name = tables.first().map(|t| t.name.as_str()).unwrap_or("unknown");
202                    Err(PlannerError::ColumnNotFound {
203                        column: column.to_string(),
204                        table: table_name.to_string(),
205                        line: span.start.line,
206                        col: span.start.column,
207                    })
208                }
209                1 => {
210                    // Unique match
211                    let (table, column_index) = matches[0];
212                    let column_meta = &table.columns[column_index];
213                    Ok(ResolvedColumn {
214                        table_name: table.name.clone(),
215                        column_name: column.to_string(),
216                        column_index,
217                        resolved_type: column_meta.data_type.clone(),
218                    })
219                }
220                _ => {
221                    // Ambiguous - column exists in multiple tables
222                    let table_names: Vec<String> =
223                        matches.iter().map(|(t, _)| t.name.clone()).collect();
224                    Err(PlannerError::AmbiguousColumn {
225                        column: column.to_string(),
226                        tables: table_names,
227                        line: span.start.line,
228                        col: span.start.column,
229                    })
230                }
231            }
232        }
233    }
234
235    /// Expand a wildcard (`*`) to all column names in definition order.
236    ///
237    /// Returns a vector of column names from the table in the order they were defined.
238    ///
239    /// # Examples
240    ///
241    /// ```ignore
242    /// let table = resolver.resolve_table("users", span)?;
243    /// let columns = resolver.expand_wildcard(table);
244    /// // Returns ["id", "name", "email"] in definition order
245    /// ```
246    pub fn expand_wildcard(&self, table: &TableMetadata) -> Vec<String> {
247        table.column_names().into_iter().map(String::from).collect()
248    }
249
250    /// Validate all column references within an expression.
251    ///
252    /// Recursively traverses the expression tree and validates that all column
253    /// references exist in the given table.
254    ///
255    /// # Errors
256    ///
257    /// Returns `PlannerError::ColumnNotFound` if any column reference in the
258    /// expression doesn't exist in the table.
259    pub fn resolve_expr(&self, expr: &Expr, table: &TableMetadata) -> Result<(), PlannerError> {
260        self.resolve_expr_recursive(expr, table)
261    }
262
263    /// Internal recursive implementation for expression resolution.
264    fn resolve_expr_recursive(
265        &self,
266        expr: &Expr,
267        table: &TableMetadata,
268    ) -> Result<(), PlannerError> {
269        match &expr.kind {
270            ExprKind::ColumnRef {
271                table: table_qualifier,
272                column,
273            } => {
274                // If a table qualifier is provided, verify it matches
275                if let Some(qualifier) = table_qualifier
276                    && qualifier != &table.name
277                {
278                    return Err(PlannerError::TableNotFound {
279                        name: qualifier.clone(),
280                        line: expr.span.start.line,
281                        column: expr.span.start.column,
282                    });
283                }
284
285                // Verify the column exists
286                self.resolve_column(table, column, expr.span)?;
287                Ok(())
288            }
289
290            ExprKind::Literal(_) | ExprKind::VectorLiteral(_) => {
291                // Literals don't need resolution
292                Ok(())
293            }
294
295            ExprKind::BinaryOp { left, right, .. } => {
296                self.resolve_expr_recursive(left, table)?;
297                self.resolve_expr_recursive(right, table)?;
298                Ok(())
299            }
300
301            ExprKind::UnaryOp { operand, .. } => self.resolve_expr_recursive(operand, table),
302
303            ExprKind::FunctionCall { args, .. } => {
304                for arg in args {
305                    self.resolve_expr_recursive(arg, table)?;
306                }
307                Ok(())
308            }
309
310            ExprKind::Between {
311                expr: e, low, high, ..
312            } => {
313                self.resolve_expr_recursive(e, table)?;
314                self.resolve_expr_recursive(low, table)?;
315                self.resolve_expr_recursive(high, table)?;
316                Ok(())
317            }
318
319            ExprKind::Like {
320                expr: e,
321                pattern,
322                escape,
323                ..
324            } => {
325                self.resolve_expr_recursive(e, table)?;
326                self.resolve_expr_recursive(pattern, table)?;
327                if let Some(esc) = escape {
328                    self.resolve_expr_recursive(esc, table)?;
329                }
330                Ok(())
331            }
332
333            ExprKind::InList { expr: e, list, .. } => {
334                self.resolve_expr_recursive(e, table)?;
335                for item in list {
336                    self.resolve_expr_recursive(item, table)?;
337                }
338                Ok(())
339            }
340
341            ExprKind::IsNull { expr: e, .. } => self.resolve_expr_recursive(e, table),
342        }
343    }
344}
345
346#[cfg(test)]
347mod tests;