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 .filter(|table| table.catalog_name == "default" && table.namespace_name == "default")
99 .ok_or_else(|| PlannerError::TableNotFound {
100 name: name.to_string(),
101 line: span.start.line,
102 column: span.start.column,
103 })
104 }
105
106 /// Resolve a column reference within a single table context.
107 ///
108 /// Validates that the column exists in the given table and returns its metadata.
109 ///
110 /// # Errors
111 ///
112 /// Returns `PlannerError::ColumnNotFound` if the column doesn't exist in the table.
113 ///
114 /// # Examples
115 ///
116 /// ```ignore
117 /// let table = resolver.resolve_table("users", span)?;
118 /// let column = resolver.resolve_column(table, "id", span)?;
119 /// assert_eq!(column.name, "id");
120 /// ```
121 pub fn resolve_column<'t>(
122 &self,
123 table: &'t TableMetadata,
124 column: &str,
125 span: Span,
126 ) -> Result<&'t crate::catalog::ColumnMetadata, PlannerError> {
127 table
128 .get_column(column)
129 .ok_or_else(|| PlannerError::ColumnNotFound {
130 column: column.to_string(),
131 table: table.name.clone(),
132 line: span.start.line,
133 col: span.start.column,
134 })
135 }
136
137 /// Resolve a column reference with scope for multiple tables.
138 ///
139 /// This method supports resolution in contexts with multiple tables (for future JOIN support).
140 /// In v0.1.1, `tables` should always contain exactly one element.
141 ///
142 /// # Resolution Rules
143 ///
144 /// 1. If `table_qualifier` is specified (e.g., `users.id`), resolve from that table only.
145 /// 2. If `table_qualifier` is `None`:
146 /// - If the column exists in exactly one table, resolve it.
147 /// - If the column exists in multiple tables, return `AmbiguousColumn` error.
148 /// - If the column doesn't exist in any table, return `ColumnNotFound` error.
149 ///
150 /// # Errors
151 ///
152 /// - `PlannerError::TableNotFound` if the specified table qualifier doesn't match any table.
153 /// - `PlannerError::ColumnNotFound` if the column doesn't exist in the resolved table(s).
154 /// - `PlannerError::AmbiguousColumn` if the column exists in multiple tables without qualification.
155 pub fn resolve_column_with_scope(
156 &self,
157 tables: &[&TableMetadata],
158 table_qualifier: Option<&str>,
159 column: &str,
160 span: Span,
161 ) -> Result<ResolvedColumn, PlannerError> {
162 if let Some(qualifier) = table_qualifier {
163 // Explicit table qualifier - find the matching table
164 let table = tables.iter().find(|t| t.name == qualifier).ok_or_else(|| {
165 PlannerError::TableNotFound {
166 name: qualifier.to_string(),
167 line: span.start.line,
168 column: span.start.column,
169 }
170 })?;
171
172 let column_index =
173 table
174 .get_column_index(column)
175 .ok_or_else(|| PlannerError::ColumnNotFound {
176 column: column.to_string(),
177 table: table.name.clone(),
178 line: span.start.line,
179 col: span.start.column,
180 })?;
181
182 let column_meta = &table.columns[column_index];
183 Ok(ResolvedColumn {
184 table_name: table.name.clone(),
185 column_name: column.to_string(),
186 column_index,
187 resolved_type: column_meta.data_type.clone(),
188 })
189 } else {
190 // No qualifier - search all tables
191 let mut matches: Vec<(&TableMetadata, usize)> = Vec::new();
192
193 for table in tables {
194 if let Some(idx) = table.get_column_index(column) {
195 matches.push((table, idx));
196 }
197 }
198
199 match matches.len() {
200 0 => {
201 // Column not found in any table
202 let table_name = tables.first().map(|t| t.name.as_str()).unwrap_or("unknown");
203 Err(PlannerError::ColumnNotFound {
204 column: column.to_string(),
205 table: table_name.to_string(),
206 line: span.start.line,
207 col: span.start.column,
208 })
209 }
210 1 => {
211 // Unique match
212 let (table, column_index) = matches[0];
213 let column_meta = &table.columns[column_index];
214 Ok(ResolvedColumn {
215 table_name: table.name.clone(),
216 column_name: column.to_string(),
217 column_index,
218 resolved_type: column_meta.data_type.clone(),
219 })
220 }
221 _ => {
222 // Ambiguous - column exists in multiple tables
223 let table_names: Vec<String> =
224 matches.iter().map(|(t, _)| t.name.clone()).collect();
225 Err(PlannerError::AmbiguousColumn {
226 column: column.to_string(),
227 tables: table_names,
228 line: span.start.line,
229 col: span.start.column,
230 })
231 }
232 }
233 }
234 }
235
236 /// Expand a wildcard (`*`) to all column names in definition order.
237 ///
238 /// Returns a vector of column names from the table in the order they were defined.
239 ///
240 /// # Examples
241 ///
242 /// ```ignore
243 /// let table = resolver.resolve_table("users", span)?;
244 /// let columns = resolver.expand_wildcard(table);
245 /// // Returns ["id", "name", "email"] in definition order
246 /// ```
247 pub fn expand_wildcard(&self, table: &TableMetadata) -> Vec<String> {
248 table.column_names().into_iter().map(String::from).collect()
249 }
250
251 /// Validate all column references within an expression.
252 ///
253 /// Recursively traverses the expression tree and validates that all column
254 /// references exist in the given table.
255 ///
256 /// # Errors
257 ///
258 /// Returns `PlannerError::ColumnNotFound` if any column reference in the
259 /// expression doesn't exist in the table.
260 pub fn resolve_expr(&self, expr: &Expr, table: &TableMetadata) -> Result<(), PlannerError> {
261 self.resolve_expr_recursive(expr, table)
262 }
263
264 /// Internal recursive implementation for expression resolution.
265 fn resolve_expr_recursive(
266 &self,
267 expr: &Expr,
268 table: &TableMetadata,
269 ) -> Result<(), PlannerError> {
270 match &expr.kind {
271 ExprKind::ColumnRef {
272 table: table_qualifier,
273 column,
274 } => {
275 // If a table qualifier is provided, verify it matches
276 if let Some(qualifier) = table_qualifier
277 && qualifier != &table.name
278 {
279 return Err(PlannerError::TableNotFound {
280 name: qualifier.clone(),
281 line: expr.span.start.line,
282 column: expr.span.start.column,
283 });
284 }
285
286 // Verify the column exists
287 self.resolve_column(table, column, expr.span)?;
288 Ok(())
289 }
290
291 ExprKind::Literal(_) | ExprKind::VectorLiteral(_) => {
292 // Literals don't need resolution
293 Ok(())
294 }
295
296 ExprKind::BinaryOp { left, right, .. } => {
297 self.resolve_expr_recursive(left, table)?;
298 self.resolve_expr_recursive(right, table)?;
299 Ok(())
300 }
301
302 ExprKind::UnaryOp { operand, .. } => self.resolve_expr_recursive(operand, table),
303
304 ExprKind::FunctionCall { args, .. } => {
305 for arg in args {
306 self.resolve_expr_recursive(arg, table)?;
307 }
308 Ok(())
309 }
310
311 ExprKind::Between {
312 expr: e, low, high, ..
313 } => {
314 self.resolve_expr_recursive(e, table)?;
315 self.resolve_expr_recursive(low, table)?;
316 self.resolve_expr_recursive(high, table)?;
317 Ok(())
318 }
319
320 ExprKind::Like {
321 expr: e,
322 pattern,
323 escape,
324 ..
325 } => {
326 self.resolve_expr_recursive(e, table)?;
327 self.resolve_expr_recursive(pattern, table)?;
328 if let Some(esc) = escape {
329 self.resolve_expr_recursive(esc, table)?;
330 }
331 Ok(())
332 }
333
334 ExprKind::InList { expr: e, list, .. } => {
335 self.resolve_expr_recursive(e, table)?;
336 for item in list {
337 self.resolve_expr_recursive(item, table)?;
338 }
339 Ok(())
340 }
341
342 ExprKind::IsNull { expr: e, .. } => self.resolve_expr_recursive(e, table),
343 }
344 }
345}
346
347#[cfg(test)]
348mod tests;