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;