Skip to main content

alopex_sql/planner/
error.rs

1//! Planner error types for the Alopex SQL dialect.
2//!
3//! This module defines error types for the planning phase, including:
4//! - Catalog errors (ALOPEX-C*): Table/column/index lookup failures
5//! - Type errors (ALOPEX-T*): Type mismatches, constraint violations
6//! - Feature errors (ALOPEX-F*): Unsupported features
7
8use crate::ast::Span;
9use thiserror::Error;
10
11/// Planner errors for the Alopex SQL dialect.
12#[derive(Debug, Clone, PartialEq, Eq, Error)]
13pub enum PlannerError {
14    /// Invalid PRAGMA name or value.
15    #[error("invalid PRAGMA '{name}': {reason}")]
16    InvalidPragma { name: String, reason: String },
17    // === Catalog Errors (ALOPEX-C*) ===
18    /// ALOPEX-C001: Table not found.
19    #[error("error[ALOPEX-C001]: table '{name}' not found at line {line}, column {column}")]
20    TableNotFound {
21        name: String,
22        line: u64,
23        column: u64,
24    },
25
26    /// ALOPEX-C002: Table already exists.
27    #[error("error[ALOPEX-C002]: table '{name}' already exists")]
28    TableAlreadyExists { name: String },
29
30    /// ALOPEX-C003: Column not found.
31    #[error(
32        "error[ALOPEX-C003]: column '{column}' not found in table '{table}' at line {line}, column {col}"
33    )]
34    ColumnNotFound {
35        column: String,
36        table: String,
37        line: u64,
38        col: u64,
39    },
40
41    /// ALOPEX-C004: Ambiguous column reference.
42    #[error(
43        "error[ALOPEX-C004]: ambiguous column '{column}' found in tables: {tables:?} at line {line}, column {col}"
44    )]
45    AmbiguousColumn {
46        column: String,
47        tables: Vec<String>,
48        line: u64,
49        col: u64,
50    },
51
52    /// ALOPEX-C005: Index already exists.
53    #[error("error[ALOPEX-C005]: index '{name}' already exists")]
54    IndexAlreadyExists { name: String },
55
56    /// ALOPEX-C006: Index not found.
57    #[error("error[ALOPEX-C006]: index '{name}' not found")]
58    IndexNotFound { name: String },
59
60    /// ALOPEX-C007: FROM-clause table function not in the closed registry.
61    #[error(
62        "error[ALOPEX-C007]: table function '{name}' does not exist at line {line}, column {column}"
63    )]
64    UnknownTableFunction {
65        name: String,
66        line: u64,
67        column: u64,
68    },
69
70    // === Type Errors (ALOPEX-T*) ===
71    /// ALOPEX-T001: Type mismatch.
72    #[error(
73        "error[ALOPEX-T001]: type mismatch at line {line}, column {column}: expected {expected}, found {found}"
74    )]
75    TypeMismatch {
76        expected: String,
77        found: String,
78        line: u64,
79        column: u64,
80    },
81
82    /// ALOPEX-T002: Invalid operator for type.
83    #[error(
84        "error[ALOPEX-T002]: invalid operator '{op}' for type '{type_name}' at line {line}, column {column}"
85    )]
86    InvalidOperator {
87        op: String,
88        type_name: String,
89        line: u64,
90        column: u64,
91    },
92
93    /// ALOPEX-T003: NULL constraint violation.
94    #[error(
95        "error[ALOPEX-T003]: null constraint violation for column '{column}' at line {line}, column {col}"
96    )]
97    NullConstraintViolation { column: String, line: u64, col: u64 },
98
99    /// ALOPEX-T004: Vector dimension mismatch.
100    #[error(
101        "error[ALOPEX-T004]: vector dimension mismatch at line {line}, column {column}: expected {expected}, found {found}"
102    )]
103    VectorDimensionMismatch {
104        expected: u32,
105        found: u32,
106        line: u64,
107        column: u64,
108    },
109
110    /// ALOPEX-T005: Invalid metric.
111    #[error(
112        "error[ALOPEX-T005]: invalid metric '{value}' at line {line}, column {column}. Valid options: cosine, l2, inner"
113    )]
114    InvalidMetric {
115        value: String,
116        line: u64,
117        column: u64,
118    },
119
120    /// ALOPEX-T006: Column count does not match value count.
121    #[error(
122        "error[ALOPEX-T006]: column count ({columns}) does not match value count ({values}) at line {line}, column {column}"
123    )]
124    ColumnValueCountMismatch {
125        columns: usize,
126        values: usize,
127        line: u64,
128        column: u64,
129    },
130
131    /// ALOPEX-T007: Invalid expression for the current context.
132    #[error("error[ALOPEX-T007]: invalid expression: {message}")]
133    InvalidExpression { message: String },
134
135    /// ALOPEX-T008: Set-operation inputs expose different numbers of columns.
136    #[error(
137        "error[ALOPEX-T008]: set operation column count mismatch: left {left}, right {right} at line {line}, column {column}"
138    )]
139    SetOperationColumnCountMismatch {
140        left: usize,
141        right: usize,
142        line: u64,
143        column: u64,
144    },
145
146    /// ALOPEX-T009: A CTE column-name list does not match its query width.
147    #[error(
148        "error[ALOPEX-T009]: common table expression '{cte}' declares {declared} column names but its query returns {actual} columns at line {line}, column {column}"
149    )]
150    CteColumnCountMismatch {
151        cte: String,
152        declared: usize,
153        actual: usize,
154        line: u64,
155        column: u64,
156    },
157
158    /// ALOPEX-T010: A CTE column-name list contains the same name twice.
159    #[error(
160        "error[ALOPEX-T010]: common table expression '{cte}' declares column '{name}' more than once at line {line}, column {column}"
161    )]
162    DuplicateCteColumn {
163        cte: String,
164        name: String,
165        line: u64,
166        column: u64,
167    },
168
169    /// ALOPEX-T011: VALUES rows expose different numbers of columns.
170    #[error(
171        "error[ALOPEX-T011]: VALUES row {row} has {actual} columns but row 1 has {expected} at line {line}, column {column}"
172    )]
173    ValuesColumnCountMismatch {
174        row: usize,
175        expected: usize,
176        actual: usize,
177        line: u64,
178        column: u64,
179    },
180
181    /// ALOPEX-T012: A relation alias column list does not match the relation
182    /// width. Covers derived tables, base tables, and table functions
183    /// (issue #151).
184    #[error(
185        "error[ALOPEX-T012]: relation alias '{alias}' declares {declared} column names but the relation has {actual} columns at line {line}, column {column}"
186    )]
187    TableAliasColumnCountMismatch {
188        alias: String,
189        declared: usize,
190        actual: usize,
191        line: u64,
192        column: u64,
193    },
194
195    /// ALOPEX-T013: Row operands expose different numbers of fields.
196    #[error(
197        "error[ALOPEX-T013]: row value has {actual} fields but its comparison operand has {expected} at line {line}, column {column}"
198    )]
199    RowArityMismatch {
200        expected: usize,
201        actual: usize,
202        line: u64,
203        column: u64,
204    },
205
206    /// ALOPEX-T014: DISTINCT ON keys do not form the initial ORDER BY prefix.
207    /// Message and semantics follow PostgreSQL error 42P10 (D2 in
208    /// docs/sql-distinct-on.md).
209    #[error(
210        "error[ALOPEX-T014]: SELECT DISTINCT ON expressions must match initial ORDER BY expressions at line {line}, column {column}"
211    )]
212    DistinctOnOrderByMismatch { line: u64, column: u64 },
213
214    /// ALOPEX-T015: RIGHT or FULL JOIN with a LATERAL right side (issue #151).
215    /// PostgreSQL rejects the same shape: the correlated side cannot be the
216    /// null-supplying side of the join.
217    #[error(
218        "error[ALOPEX-T015]: {join_type} JOIN cannot have a LATERAL right side at line {line}, column {column}"
219    )]
220    LateralJoinTypeUnsupported {
221        join_type: String,
222        line: u64,
223        column: u64,
224    },
225
226    // === Feature Errors (ALOPEX-F*) ===
227    /// ALOPEX-F001: Unsupported feature.
228    #[error(
229        "error[ALOPEX-F001]: feature '{feature}' is not supported in this version. Expected in {version}"
230    )]
231    UnsupportedFeature {
232        feature: String,
233        version: String,
234        line: u64,
235        column: u64,
236    },
237}
238
239impl PlannerError {
240    /// Create a TableNotFound error from a span.
241    pub fn table_not_found(name: impl Into<String>, span: Span) -> Self {
242        Self::TableNotFound {
243            name: name.into(),
244            line: span.start.line,
245            column: span.start.column,
246        }
247    }
248
249    /// Create a TableAlreadyExists error.
250    pub fn table_already_exists(name: impl Into<String>) -> Self {
251        Self::TableAlreadyExists { name: name.into() }
252    }
253
254    /// Create an UnknownTableFunction error from a span.
255    pub fn unknown_table_function(name: impl Into<String>, span: Span) -> Self {
256        Self::UnknownTableFunction {
257            name: name.into(),
258            line: span.start.line,
259            column: span.start.column,
260        }
261    }
262
263    /// Create a LateralJoinTypeUnsupported error from a span.
264    pub fn lateral_join_type_unsupported(join_type: impl Into<String>, span: Span) -> Self {
265        Self::LateralJoinTypeUnsupported {
266            join_type: join_type.into(),
267            line: span.start.line,
268            column: span.start.column,
269        }
270    }
271
272    /// Create a ColumnNotFound error from a span.
273    pub fn column_not_found(
274        column: impl Into<String>,
275        table: impl Into<String>,
276        span: Span,
277    ) -> Self {
278        Self::ColumnNotFound {
279            column: column.into(),
280            table: table.into(),
281            line: span.start.line,
282            col: span.start.column,
283        }
284    }
285
286    /// Create an AmbiguousColumn error from a span.
287    pub fn ambiguous_column(column: impl Into<String>, tables: Vec<String>, span: Span) -> Self {
288        Self::AmbiguousColumn {
289            column: column.into(),
290            tables,
291            line: span.start.line,
292            col: span.start.column,
293        }
294    }
295
296    /// Create an IndexAlreadyExists error.
297    pub fn index_already_exists(name: impl Into<String>) -> Self {
298        Self::IndexAlreadyExists { name: name.into() }
299    }
300
301    /// Create an IndexNotFound error.
302    pub fn index_not_found(name: impl Into<String>) -> Self {
303        Self::IndexNotFound { name: name.into() }
304    }
305
306    /// Create a TypeMismatch error from a span.
307    pub fn type_mismatch(
308        expected: impl Into<String>,
309        found: impl Into<String>,
310        span: Span,
311    ) -> Self {
312        Self::TypeMismatch {
313            expected: expected.into(),
314            found: found.into(),
315            line: span.start.line,
316            column: span.start.column,
317        }
318    }
319
320    /// Create an InvalidExpression error.
321    pub fn invalid_expression(message: impl Into<String>) -> Self {
322        Self::InvalidExpression {
323            message: message.into(),
324        }
325    }
326
327    /// Create a set-operation column-count error from a span.
328    pub fn set_operation_column_count_mismatch(left: usize, right: usize, span: Span) -> Self {
329        Self::SetOperationColumnCountMismatch {
330            left,
331            right,
332            line: span.start.line,
333            column: span.start.column,
334        }
335    }
336
337    /// Create a CTE column-count error from a span.
338    pub fn cte_column_count_mismatch(
339        cte: impl Into<String>,
340        declared: usize,
341        actual: usize,
342        span: Span,
343    ) -> Self {
344        Self::CteColumnCountMismatch {
345            cte: cte.into(),
346            declared,
347            actual,
348            line: span.start.line,
349            column: span.start.column,
350        }
351    }
352
353    /// Create a duplicate CTE column-name error from a span.
354    pub fn duplicate_cte_column(
355        cte: impl Into<String>,
356        column_name: impl Into<String>,
357        span: Span,
358    ) -> Self {
359        Self::DuplicateCteColumn {
360            cte: cte.into(),
361            name: column_name.into(),
362            line: span.start.line,
363            column: span.start.column,
364        }
365    }
366
367    /// Create a VALUES row-width error from a span.
368    pub fn values_column_count_mismatch(
369        row: usize,
370        expected: usize,
371        actual: usize,
372        span: Span,
373    ) -> Self {
374        Self::ValuesColumnCountMismatch {
375            row,
376            expected,
377            actual,
378            line: span.start.line,
379            column: span.start.column,
380        }
381    }
382
383    /// Create a derived-table alias-width error from a span.
384    pub fn table_alias_column_count_mismatch(
385        alias: impl Into<String>,
386        declared: usize,
387        actual: usize,
388        span: Span,
389    ) -> Self {
390        Self::TableAliasColumnCountMismatch {
391            alias: alias.into(),
392            declared,
393            actual,
394            line: span.start.line,
395            column: span.start.column,
396        }
397    }
398
399    /// Create an InvalidOperator error from a span.
400    pub fn invalid_operator(
401        op: impl Into<String>,
402        type_name: impl Into<String>,
403        span: Span,
404    ) -> Self {
405        Self::InvalidOperator {
406            op: op.into(),
407            type_name: type_name.into(),
408            line: span.start.line,
409            column: span.start.column,
410        }
411    }
412
413    /// Create a NullConstraintViolation error from a span.
414    pub fn null_constraint_violation(column: impl Into<String>, span: Span) -> Self {
415        Self::NullConstraintViolation {
416            column: column.into(),
417            line: span.start.line,
418            col: span.start.column,
419        }
420    }
421
422    /// Create a VectorDimensionMismatch error from a span.
423    pub fn vector_dimension_mismatch(expected: u32, found: u32, span: Span) -> Self {
424        Self::VectorDimensionMismatch {
425            expected,
426            found,
427            line: span.start.line,
428            column: span.start.column,
429        }
430    }
431
432    /// Create an InvalidMetric error from a span.
433    pub fn invalid_metric(value: impl Into<String>, span: Span) -> Self {
434        Self::InvalidMetric {
435            value: value.into(),
436            line: span.start.line,
437            column: span.start.column,
438        }
439    }
440
441    /// Create a ColumnValueCountMismatch error from a span.
442    pub fn column_value_count_mismatch(columns: usize, values: usize, span: Span) -> Self {
443        Self::ColumnValueCountMismatch {
444            columns,
445            values,
446            line: span.start.line,
447            column: span.start.column,
448        }
449    }
450
451    /// Create a DistinctOnOrderByMismatch error from a span.
452    pub fn distinct_on_order_by_mismatch(span: Span) -> Self {
453        Self::DistinctOnOrderByMismatch {
454            line: span.start.line,
455            column: span.start.column,
456        }
457    }
458
459    /// Create an UnsupportedFeature error from a span.
460    pub fn unsupported_feature(
461        feature: impl Into<String>,
462        version: impl Into<String>,
463        span: Span,
464    ) -> Self {
465        Self::UnsupportedFeature {
466            feature: feature.into(),
467            version: version.into(),
468            line: span.start.line,
469            column: span.start.column,
470        }
471    }
472}