Skip to main content

vibesql_executor/
errors.rs

1#[derive(Debug, Clone, PartialEq)]
2pub enum ExecutorError {
3    TableNotFound(String),
4    TableAlreadyExists(String),
5    /// ALTER TABLE ... RENAME TO target collides with an existing table or
6    /// index name. SQLite reports this with a distinct message that spans both
7    /// the table and index namespaces.
8    RenameTargetExists(String),
9    /// SQLite system table may not be modified (sqlite_master, sqlite_schema)
10    SqliteSystemTableReadOnly {
11        table_name: String,
12        operation: String,
13    },
14    ColumnNotFound {
15        column_name: String,
16        table_name: String,
17        searched_tables: Vec<String>,
18        available_columns: Vec<String>,
19    },
20    InvalidTableQualifier {
21        qualifier: String,
22        column: String,
23        available_tables: Vec<String>,
24    },
25    /// Ambiguous column name in JOIN query (SQLite-compatible error)
26    /// Raised when an unqualified column reference matches columns in multiple tables
27    AmbiguousColumnName {
28        column_name: String,
29    },
30    ColumnAlreadyExists(String),
31    IndexNotFound(String),
32    IndexAlreadyExists(String),
33    InvalidIndexDefinition(String),
34    TriggerNotFound(String),
35    TriggerAlreadyExists(String),
36    SchemaNotFound(String),
37    SchemaAlreadyExists(String),
38    SchemaNotEmpty(String),
39    RoleNotFound(String),
40    TypeNotFound(String),
41    TypeAlreadyExists(String),
42    TypeInUse(String),
43    DependentPrivilegesExist(String),
44    PermissionDenied {
45        role: String,
46        privilege: String,
47        object: String,
48    },
49    ColumnIndexOutOfBounds {
50        index: usize,
51    },
52    TypeMismatch {
53        left: vibesql_types::SqlValue,
54        op: String,
55        right: vibesql_types::SqlValue,
56    },
57    DivisionByZero,
58    /// Integer overflow in arithmetic operation (SQLite-compatible error)
59    /// Format: "integer overflow"
60    IntegerOverflow,
61    InvalidWhereClause(String),
62    /// Wrong number of arguments to a function (SQLite-compatible error)
63    WrongNumberOfArguments {
64        function_name: String,
65    },
66    /// Misuse of aggregate function (SQLite-compatible error)
67    /// e.g., aggregate in WHERE clause or nested aggregates
68    /// Format: "misuse of aggregate function X()"
69    MisuseOfAggregate {
70        function_name: String,
71    },
72    /// Misuse of aggregate in ORDER BY or other execution contexts (SQLite-compatible)
73    /// Format: "misuse of aggregate: X()" (note the colon, not "function")
74    MisuseOfAggregateContext {
75        function_name: String,
76    },
77    /// Misuse of aliased aggregate (SQLite-compatible error)
78    /// e.g., HAVING max(alias) where alias refers to an aggregate like min(x)
79    MisuseOfAliasedAggregate {
80        alias_name: String,
81    },
82    /// Misuse of window function (SQLite-compatible error)
83    /// e.g., window function in WHERE, GROUP BY, or HAVING clause
84    /// Format: "misuse of window function X()"
85    MisuseOfWindowFunction {
86        function_name: String,
87    },
88    /// Misuse of aliased window function (SQLite-compatible error)
89    /// e.g., ORDER BY (SELECT m) where m is an alias for a window function like count() OVER()
90    /// Format: "misuse of aliased window function X"
91    MisuseOfAliasedWindowFunction {
92        alias_name: String,
93    },
94    /// Row value used in scalar context (SQLite-compatible error)
95    /// e.g., HAVING (SELECT (a, b)) — a row value where a scalar is expected
96    /// Format: "row value misused"
97    RowValueMisused,
98    UnsupportedExpression(String),
99    UnsupportedFeature(String),
100    /// SQLite-compatible error message (output as-is without prefix)
101    /// Used when we need to match SQLite's exact error message format
102    SqliteCompatError(String),
103    StorageError(String),
104    SubqueryReturnedMultipleRows {
105        expected: usize,
106        actual: usize,
107    },
108    SubqueryColumnCountMismatch {
109        expected: usize,
110        actual: usize,
111    },
112    /// Set operation (UNION, INTERSECT, EXCEPT) column count mismatch (SQLite-compatible)
113    /// Format: "SELECTs to the left and right of {op} do not have the same number of result columns"
114    SetOperationColumnMismatch {
115        /// The set operator name: "UNION", "UNION ALL", "INTERSECT", "INTERSECT ALL", "EXCEPT", "EXCEPT ALL"
116        operator: String,
117    },
118    ColumnCountMismatch {
119        expected: usize,
120        provided: usize,
121    },
122    InsertColumnCountMismatch {
123        table_name: String,
124        expected: usize,
125        provided: usize,
126        /// Whether explicit column list was provided in INSERT statement
127        has_explicit_columns: bool,
128    },
129    /// VALUES rows have inconsistent number of terms (SQLite-compatible error)
130    /// Format: "all VALUES must have the same number of terms"
131    ValuesRowCountMismatch,
132    /// Column not found in INSERT statement (SQLite-compatible error)
133    /// Format: "table T has no column named C"
134    InsertNoSuchColumn {
135        table_name: String,
136        column_name: String,
137    },
138    /// Cannot INSERT into a generated column (SQLite-compatible error)
139    /// Format: "cannot INSERT into generated column \"C\""
140    CannotInsertIntoGeneratedColumn {
141        column_name: String,
142    },
143    CastError {
144        from_type: String,
145        to_type: String,
146    },
147    TypeConversionError {
148        from: String,
149        to: String,
150    },
151    ConstraintViolation(String),
152    /// A `RAISE(ABORT|FAIL|ROLLBACK, msg)` expression fired inside a trigger
153    /// body (SQLite RAISE). Carries the conflict-resolution action so the
154    /// trigger / DML / transaction layer can apply the correct rollback scope:
155    /// - `Abort` rolls back the current statement (default),
156    /// - `Fail` stops the statement but keeps its already-applied row changes,
157    /// - `Rollback` rolls back the whole enclosing transaction.
158    ///
159    /// `RAISE(IGNORE)` is *not* represented here: it is a control-flow signal
160    /// (abandon the current row, no error) and uses [`ExecutorError::RaiseIgnore`].
161    Raise {
162        action: vibesql_ast::RaiseAction,
163        message: String,
164    },
165    /// A `RAISE(IGNORE)` expression fired inside a trigger body. This is a
166    /// control-flow signal, not a user-visible error: the enclosing trigger /
167    /// row-processing loop catches it, abandons the current row, and continues
168    /// without reporting an error. If it ever escapes to a user (RAISE(IGNORE)
169    /// outside any trigger), it is surfaced as a benign no-op-style message.
170    RaiseIgnore,
171    /// FOREIGN KEY references a parent column set that is not covered by a
172    /// PRIMARY KEY, UNIQUE constraint, or non-partial UNIQUE INDEX in the
173    /// parent table (SQLite-compatible error). Format:
174    /// `foreign key mismatch - "<child>" referencing "<parent>"`
175    ForeignKeyMismatch {
176        child: String,
177        parent: String,
178    },
179    MultiplePrimaryKeys,
180    CannotDropColumn(String),
181    ConstraintNotFound {
182        constraint_name: String,
183        table_name: String,
184    },
185    /// Expression evaluation exceeded maximum recursion depth
186    /// This prevents stack overflow from deeply nested expressions or subqueries
187    ExpressionDepthExceeded {
188        depth: usize,
189        max_depth: usize,
190    },
191    /// Query exceeded maximum execution time
192    QueryTimeoutExceeded {
193        elapsed_seconds: u64,
194        max_seconds: u64,
195    },
196    /// Query exceeded maximum row processing limit
197    RowLimitExceeded {
198        rows_processed: usize,
199        max_rows: usize,
200    },
201    /// Query exceeded maximum memory limit
202    MemoryLimitExceeded {
203        used_bytes: usize,
204        max_bytes: usize,
205    },
206    /// Join exceeded maximum number of tables (SQLite-compatible error)
207    /// Format: "at most 64 tables in a join"
208    JoinTableLimitExceeded {
209        table_count: usize,
210        max_tables: usize,
211    },
212    /// Variable not found in procedural context (with available variables)
213    VariableNotFound {
214        variable_name: String,
215        available_variables: Vec<String>,
216    },
217    /// Label not found in procedural context
218    LabelNotFound(String),
219    /// Procedural SELECT INTO returned wrong number of rows (must be exactly 1)
220    SelectIntoRowCount {
221        expected: usize,
222        actual: usize,
223    },
224    /// Procedural SELECT INTO column count doesn't match variable count
225    SelectIntoColumnCount {
226        expected: usize,
227        actual: usize,
228    },
229    /// Procedure not found (with suggestions)
230    ProcedureNotFound {
231        procedure_name: String,
232        schema_name: String,
233        available_procedures: Vec<String>,
234    },
235    /// Function not found (with suggestions)
236    FunctionNotFound {
237        function_name: String,
238        schema_name: String,
239        available_functions: Vec<String>,
240    },
241    /// Parameter count mismatch with details
242    ParameterCountMismatch {
243        routine_name: String,
244        routine_type: String, // "Procedure" or "Function"
245        expected: usize,
246        actual: usize,
247        parameter_signature: String,
248    },
249    /// Parameter type mismatch with details
250    ParameterTypeMismatch {
251        parameter_name: String,
252        expected_type: String,
253        actual_type: String,
254        actual_value: String,
255    },
256    /// Type error in expression evaluation
257    TypeError(String),
258    /// Function argument count mismatch
259    ArgumentCountMismatch {
260        expected: usize,
261        actual: usize,
262    },
263    /// Recursion limit exceeded in function/procedure calls (with call stack)
264    RecursionLimitExceeded {
265        message: String,
266        call_stack: Vec<String>,
267        max_depth: usize,
268    },
269    /// Function must return a value but did not
270    FunctionMustReturn,
271    /// Invalid control flow (e.g., LEAVE/ITERATE outside of loop)
272    InvalidControlFlow(String),
273    /// Invalid function body syntax
274    InvalidFunctionBody(String),
275    /// Function attempted to modify data (read-only violation)
276    FunctionReadOnlyViolation(String),
277    /// Parse error
278    ParseError(String),
279    /// Invalid EXTRACT field for the given value type
280    InvalidExtractField {
281        field: String,
282        value_type: String,
283    },
284    /// Arrow array downcast failed (columnar execution)
285    ArrowDowncastError {
286        expected_type: String,
287        context: String,
288    },
289    /// Type mismatch in columnar operations
290    ColumnarTypeMismatch {
291        operation: String,
292        left_type: String,
293        right_type: Option<String>,
294    },
295    /// SIMD operation failed (returned None or error)
296    SimdOperationFailed {
297        operation: String,
298        reason: String,
299    },
300    /// Column not found in batch by index
301    ColumnarColumnNotFound {
302        column_index: usize,
303        batch_columns: usize,
304    },
305    /// Column not found in batch by name
306    ColumnarColumnNotFoundByName {
307        column_name: String,
308    },
309    /// Column length mismatch in batch operations
310    ColumnarLengthMismatch {
311        context: String,
312        expected: usize,
313        actual: usize,
314    },
315    /// Unsupported array type for columnar operation
316    UnsupportedArrayType {
317        operation: String,
318        array_type: String,
319    },
320    /// Invalid or incompatible geometry type in spatial function
321    SpatialGeometryError {
322        function_name: String,
323        message: String,
324    },
325    /// Spatial operation failed (distance, intersection, etc.)
326    SpatialOperationFailed {
327        function_name: String,
328        message: String,
329    },
330    /// Spatial function argument count or type mismatch
331    SpatialArgumentError {
332        function_name: String,
333        expected: String,
334        actual: String,
335    },
336    /// Cursor already exists with this name
337    CursorAlreadyExists(String),
338    /// Cursor not found
339    CursorNotFound(String),
340    /// Cursor is already open
341    CursorAlreadyOpen(String),
342    /// Cursor is not open (must OPEN before FETCH)
343    CursorNotOpen(String),
344    /// Cursor does not support backward movement (not declared with SCROLL)
345    CursorNotScrollable(String),
346    /// Assertion constraint violation (SQL:1999 Feature F671/F672)
347    AssertionViolation {
348        assertion_name: String,
349    },
350    /// ORDER BY column number out of range (SQLite-compatible error)
351    OrderByOutOfRange {
352        term_position: usize, // 1-indexed position of the ORDER BY term
353        column_number: i64,   // The column number that was specified
354        select_list_len: usize,
355    },
356    /// GROUP BY column number out of range (SQLite-compatible error)
357    GroupByOutOfRange {
358        term_position: usize, // 1-indexed position of the GROUP BY term
359        column_number: i64,   // The column number that was specified
360        select_list_len: usize,
361    },
362    /// ORDER BY term doesn't match any column in the result set
363    /// Used for compound queries (UNION, INTERSECT, EXCEPT) where ORDER BY
364    /// must reference result columns by position or alias
365    OrderByTermNotInResultSet {
366        term_position: usize, // 1-indexed position of the ORDER BY term
367    },
368    /// Invalid LIMIT or OFFSET value (SQLite-compatible error)
369    InvalidLimitOffset {
370        clause: String, // "LIMIT" or "OFFSET"
371        value: String,  // The value that was provided
372        reason: String, // Why it's invalid
373    },
374    /// JOIN USING column not present in both tables (SQLite-compatible error)
375    /// Format: "cannot join using column X - column not present in both tables"
376    JoinUsingColumnNotPresent {
377        column_name: String,
378    },
379    /// ON clause references a table that appears to the right (SQLite-compatible error)
380    /// Format: "ON clause references tables to its right"
381    /// This error occurs when a JOIN ON clause references a table that hasn't been
382    /// introduced yet (appears later in the FROM clause).
383    OnClauseReferencesRightTable,
384    /// No such column (SQLite-compatible error)
385    /// Format: "no such column: X" or "no such column: table.column"
386    NoSuchColumn {
387        column_ref: String,
388    },
389    /// No such function (SQLite-compatible error)
390    /// Format: "no such function: X"
391    NoSuchFunction {
392        function_name: String,
393    },
394    /// No such index (SQLite-compatible error, e.g. INDEXED BY with an
395    /// unknown index or an index on a different table)
396    /// Format: "no such index: X"
397    NoSuchIndex {
398        index_name: String,
399    },
400    Other(String),
401}
402
403/// Find the closest matching string using simple Levenshtein distance
404fn find_closest_match<'a>(target: &str, candidates: &'a [String]) -> Option<&'a String> {
405    if candidates.is_empty() {
406        return None;
407    }
408
409    let target_lower = target.to_lowercase();
410
411    // First check for exact case-insensitive match
412    if let Some(exact) = candidates.iter().find(|c| c.to_lowercase() == target_lower) {
413        return Some(exact);
414    }
415
416    // Calculate Levenshtein distance for each candidate
417    let mut best_match: Option<(&String, usize)> = None;
418
419    for candidate in candidates {
420        let distance = levenshtein_distance(&target_lower, &candidate.to_lowercase());
421
422        // Only suggest if distance is small relative to the target length
423        // (e.g., within 2 edits or 30% of the length)
424        let max_distance = (target.len() / 3).max(2);
425
426        if distance <= max_distance {
427            if let Some((_, best_distance)) = best_match {
428                if distance < best_distance {
429                    best_match = Some((candidate, distance));
430                }
431            } else {
432                best_match = Some((candidate, distance));
433            }
434        }
435    }
436
437    best_match.map(|(s, _)| s)
438}
439
440/// Calculate Levenshtein distance between two strings
441fn levenshtein_distance(s1: &str, s2: &str) -> usize {
442    let len1 = s1.len();
443    let len2 = s2.len();
444
445    if len1 == 0 {
446        return len2;
447    }
448    if len2 == 0 {
449        return len1;
450    }
451
452    let mut prev_row: Vec<usize> = (0..=len2).collect();
453    let mut curr_row = vec![0; len2 + 1];
454
455    for (i, c1) in s1.chars().enumerate() {
456        curr_row[0] = i + 1;
457
458        for (j, c2) in s2.chars().enumerate() {
459            let cost = if c1 == c2 { 0 } else { 1 };
460            curr_row[j + 1] = (curr_row[j] + 1).min(prev_row[j + 1] + 1).min(prev_row[j] + cost);
461        }
462
463        std::mem::swap(&mut prev_row, &mut curr_row);
464    }
465
466    prev_row[len2]
467}
468
469impl std::fmt::Display for ExecutorError {
470    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471        use vibesql_l10n::vibe_msg;
472        match self {
473            ExecutorError::TableNotFound(name) => {
474                write!(f, "{}", vibe_msg!("executor-table-not-found", name = name.as_str()))
475            }
476            ExecutorError::TableAlreadyExists(name) => {
477                write!(f, "{}", vibe_msg!("executor-table-already-exists", name = name.as_str()))
478            }
479            ExecutorError::RenameTargetExists(name) => {
480                // SQLite-compatible error message format. SQLite 3.51.0 emits
481                // this exact string for `ALTER TABLE ... RENAME TO <name>` when
482                // <name> already names a table or an index.
483                write!(f, "there is already another table or index with this name: {}", name)
484            }
485            ExecutorError::SqliteSystemTableReadOnly { table_name, operation } => {
486                // SQLite-compatible error message format
487                write!(f, "table {} may not be {}", table_name, operation)
488            }
489            ExecutorError::ColumnNotFound {
490                column_name,
491                table_name,
492                searched_tables,
493                available_columns,
494            } => {
495                if searched_tables.is_empty() {
496                    write!(
497                        f,
498                        "{}",
499                        vibe_msg!(
500                            "executor-column-not-found-simple",
501                            column_name = column_name.as_str(),
502                            table_name = table_name.as_str()
503                        )
504                    )
505                } else if available_columns.is_empty() {
506                    let searched = searched_tables.join(", ");
507                    write!(
508                        f,
509                        "{}",
510                        vibe_msg!(
511                            "executor-column-not-found-searched",
512                            column_name = column_name.as_str(),
513                            searched_tables = searched.as_str()
514                        )
515                    )
516                } else {
517                    let searched = searched_tables.join(", ");
518                    let available = available_columns.join(", ");
519                    write!(
520                        f,
521                        "{}",
522                        vibe_msg!(
523                            "executor-column-not-found-with-available",
524                            column_name = column_name.as_str(),
525                            searched_tables = searched.as_str(),
526                            available_columns = available.as_str()
527                        )
528                    )
529                }
530            }
531            ExecutorError::InvalidTableQualifier { qualifier, column, available_tables } => {
532                let available = available_tables.join(", ");
533                write!(
534                    f,
535                    "{}",
536                    vibe_msg!(
537                        "executor-invalid-table-qualifier",
538                        qualifier = qualifier.as_str(),
539                        column = column.as_str(),
540                        available_tables = available.as_str()
541                    )
542                )
543            }
544            ExecutorError::AmbiguousColumnName { column_name } => {
545                // SQLite-compatible error message format
546                write!(f, "ambiguous column name: {}", column_name)
547            }
548            ExecutorError::ColumnAlreadyExists(name) => {
549                write!(f, "{}", vibe_msg!("executor-column-already-exists", name = name.as_str()))
550            }
551            ExecutorError::IndexNotFound(name) => {
552                write!(f, "{}", vibe_msg!("executor-index-not-found", name = name.as_str()))
553            }
554            ExecutorError::IndexAlreadyExists(name) => {
555                write!(f, "{}", vibe_msg!("executor-index-already-exists", name = name.as_str()))
556            }
557            ExecutorError::InvalidIndexDefinition(msg) => {
558                write!(
559                    f,
560                    "{}",
561                    vibe_msg!("executor-invalid-index-definition", message = msg.as_str())
562                )
563            }
564            ExecutorError::TriggerNotFound(name) => {
565                write!(f, "{}", vibe_msg!("executor-trigger-not-found", name = name.as_str()))
566            }
567            ExecutorError::TriggerAlreadyExists(name) => {
568                write!(f, "{}", vibe_msg!("executor-trigger-already-exists", name = name.as_str()))
569            }
570            ExecutorError::SchemaNotFound(name) => {
571                write!(f, "{}", vibe_msg!("executor-schema-not-found", name = name.as_str()))
572            }
573            ExecutorError::SchemaAlreadyExists(name) => {
574                write!(f, "{}", vibe_msg!("executor-schema-already-exists", name = name.as_str()))
575            }
576            ExecutorError::SchemaNotEmpty(name) => {
577                write!(f, "{}", vibe_msg!("executor-schema-not-empty", name = name.as_str()))
578            }
579            ExecutorError::RoleNotFound(name) => {
580                write!(f, "{}", vibe_msg!("executor-role-not-found", name = name.as_str()))
581            }
582            ExecutorError::TypeNotFound(name) => {
583                write!(f, "{}", vibe_msg!("executor-type-not-found", name = name.as_str()))
584            }
585            ExecutorError::TypeAlreadyExists(name) => {
586                write!(f, "{}", vibe_msg!("executor-type-already-exists", name = name.as_str()))
587            }
588            ExecutorError::TypeInUse(name) => {
589                write!(f, "{}", vibe_msg!("executor-type-in-use", name = name.as_str()))
590            }
591            ExecutorError::DependentPrivilegesExist(msg) => {
592                write!(
593                    f,
594                    "{}",
595                    vibe_msg!("executor-dependent-privileges-exist", message = msg.as_str())
596                )
597            }
598            ExecutorError::PermissionDenied { role, privilege, object } => {
599                write!(
600                    f,
601                    "{}",
602                    vibe_msg!(
603                        "executor-permission-denied",
604                        role = role.as_str(),
605                        privilege = privilege.as_str(),
606                        object = object.as_str()
607                    )
608                )
609            }
610            ExecutorError::ColumnIndexOutOfBounds { index } => {
611                write!(
612                    f,
613                    "{}",
614                    vibe_msg!("executor-column-index-out-of-bounds", index = *index as i64)
615                )
616            }
617            ExecutorError::TypeMismatch { left, op, right } => {
618                let left_str = format!("{:?}", left);
619                let right_str = format!("{:?}", right);
620                write!(
621                    f,
622                    "{}",
623                    vibe_msg!(
624                        "executor-type-mismatch",
625                        left = left_str.as_str(),
626                        op = op.as_str(),
627                        right = right_str.as_str()
628                    )
629                )
630            }
631            ExecutorError::DivisionByZero => {
632                write!(f, "{}", vibe_msg!("executor-division-by-zero"))
633            }
634            ExecutorError::IntegerOverflow => {
635                // SQLite-compatible error message format
636                write!(f, "integer overflow")
637            }
638            ExecutorError::InvalidWhereClause(msg) => {
639                write!(f, "{}", vibe_msg!("executor-invalid-where-clause", message = msg.as_str()))
640            }
641            ExecutorError::WrongNumberOfArguments { function_name } => {
642                // SQLite-compatible error message format
643                write!(f, "wrong number of arguments to function {}()", function_name)
644            }
645            ExecutorError::MisuseOfAggregate { function_name } => {
646                // SQLite-compatible error message format
647                write!(f, "misuse of aggregate function {}()", function_name)
648            }
649            ExecutorError::MisuseOfAggregateContext { function_name } => {
650                // SQLite-compatible error message format for ORDER BY and execution contexts
651                // Note: uses colon instead of "function" to match SQLite's expr.c format
652                write!(f, "misuse of aggregate: {}()", function_name)
653            }
654            ExecutorError::MisuseOfAliasedAggregate { alias_name } => {
655                // SQLite-compatible error message format
656                write!(f, "misuse of aliased aggregate {}", alias_name)
657            }
658            ExecutorError::MisuseOfWindowFunction { function_name } => {
659                // SQLite-compatible error message format
660                write!(f, "misuse of window function {}()", function_name)
661            }
662            ExecutorError::MisuseOfAliasedWindowFunction { alias_name } => {
663                // SQLite-compatible error message format
664                write!(f, "misuse of aliased window function {}", alias_name)
665            }
666            ExecutorError::RowValueMisused => {
667                // SQLite-compatible error message format
668                write!(f, "row value misused")
669            }
670            ExecutorError::UnsupportedExpression(msg) => {
671                write!(
672                    f,
673                    "{}",
674                    vibe_msg!("executor-unsupported-expression", message = msg.as_str())
675                )
676            }
677            ExecutorError::UnsupportedFeature(msg) => {
678                write!(f, "{}", vibe_msg!("executor-unsupported-feature", message = msg.as_str()))
679            }
680            ExecutorError::SqliteCompatError(msg) => {
681                // Output the message as-is without any prefix (for SQLite compatibility)
682                write!(f, "{}", msg)
683            }
684            ExecutorError::StorageError(msg) => {
685                write!(f, "{}", vibe_msg!("executor-storage-error", message = msg.as_str()))
686            }
687            ExecutorError::SubqueryReturnedMultipleRows { expected, actual } => {
688                write!(
689                    f,
690                    "{}",
691                    vibe_msg!(
692                        "executor-subquery-returned-multiple-rows",
693                        expected = *expected as i64,
694                        actual = *actual as i64
695                    )
696                )
697            }
698            ExecutorError::SubqueryColumnCountMismatch { expected, actual } => {
699                write!(
700                    f,
701                    "{}",
702                    vibe_msg!(
703                        "executor-subquery-column-count-mismatch",
704                        expected = *expected as i64,
705                        actual = *actual as i64
706                    )
707                )
708            }
709            ExecutorError::SetOperationColumnMismatch { operator } => {
710                write!(
711                    f,
712                    "SELECTs to the left and right of {} do not have the same number of result columns",
713                    operator
714                )
715            }
716            ExecutorError::ColumnCountMismatch { expected, provided } => {
717                write!(
718                    f,
719                    "{}",
720                    vibe_msg!(
721                        "executor-column-count-mismatch",
722                        expected = *expected as i64,
723                        provided = *provided as i64
724                    )
725                )
726            }
727            ExecutorError::InsertColumnCountMismatch {
728                table_name,
729                expected,
730                provided,
731                has_explicit_columns,
732            } => {
733                if *has_explicit_columns {
734                    // SQLite format when explicit column list: "M values for N columns"
735                    write!(f, "{} values for {} columns", provided, expected)
736                } else {
737                    // SQLite format when no column list: "table T has N columns but M values were supplied"
738                    write!(
739                        f,
740                        "table {} has {} columns but {} values were supplied",
741                        table_name, expected, provided
742                    )
743                }
744            }
745            ExecutorError::ValuesRowCountMismatch => {
746                // SQLite format for inconsistent VALUES row lengths
747                write!(f, "all VALUES must have the same number of terms")
748            }
749            ExecutorError::InsertNoSuchColumn { table_name, column_name } => {
750                // SQLite format: "table T has no column named C"
751                write!(f, "table {} has no column named {}", table_name, column_name)
752            }
753            ExecutorError::CannotInsertIntoGeneratedColumn { column_name } => {
754                // SQLite format: cannot INSERT into generated column "C"
755                write!(f, "cannot INSERT into generated column \"{}\"", column_name)
756            }
757            ExecutorError::CastError { from_type, to_type } => {
758                write!(
759                    f,
760                    "{}",
761                    vibe_msg!(
762                        "executor-cast-error",
763                        from_type = from_type.as_str(),
764                        to_type = to_type.as_str()
765                    )
766                )
767            }
768            ExecutorError::TypeConversionError { from, to } => {
769                write!(
770                    f,
771                    "{}",
772                    vibe_msg!(
773                        "executor-type-conversion-error",
774                        from = from.as_str(),
775                        to = to.as_str()
776                    )
777                )
778            }
779            ExecutorError::ConstraintViolation(msg) => {
780                write!(f, "{}", vibe_msg!("executor-constraint-violation", message = msg.as_str()))
781            }
782            ExecutorError::Raise { message, .. } => {
783                // SQLite reports the RAISE() message verbatim (error code 19,
784                // SQLITE_CONSTRAINT). e.g. `Runtime error: value too big`.
785                write!(f, "{}", message)
786            }
787            ExecutorError::RaiseIgnore => {
788                // Should normally be intercepted before reaching a user. If it
789                // escapes (RAISE(IGNORE) outside a trigger), describe it plainly.
790                write!(f, "RAISE(IGNORE) used outside of a trigger body")
791            }
792            ExecutorError::ForeignKeyMismatch { child, parent } => {
793                // SQLite-compatible error format from fkey.c::sqlite3FkLocateIndex.
794                write!(f, "foreign key mismatch - \"{}\" referencing \"{}\"", child, parent)
795            }
796            ExecutorError::MultiplePrimaryKeys => {
797                write!(f, "{}", vibe_msg!("executor-multiple-primary-keys"))
798            }
799            ExecutorError::CannotDropColumn(msg) => {
800                write!(f, "{}", vibe_msg!("executor-cannot-drop-column", message = msg.as_str()))
801            }
802            ExecutorError::ConstraintNotFound { constraint_name, table_name } => {
803                write!(
804                    f,
805                    "{}",
806                    vibe_msg!(
807                        "executor-constraint-not-found",
808                        constraint_name = constraint_name.as_str(),
809                        table_name = table_name.as_str()
810                    )
811                )
812            }
813            ExecutorError::ExpressionDepthExceeded { depth, max_depth } => {
814                write!(
815                    f,
816                    "{}",
817                    vibe_msg!(
818                        "executor-expression-depth-exceeded",
819                        depth = *depth as i64,
820                        max_depth = *max_depth as i64
821                    )
822                )
823            }
824            ExecutorError::QueryTimeoutExceeded { elapsed_seconds, max_seconds } => {
825                write!(
826                    f,
827                    "{}",
828                    vibe_msg!(
829                        "executor-query-timeout-exceeded",
830                        elapsed_seconds = *elapsed_seconds as i64,
831                        max_seconds = *max_seconds as i64
832                    )
833                )
834            }
835            ExecutorError::RowLimitExceeded { rows_processed, max_rows } => {
836                write!(
837                    f,
838                    "{}",
839                    vibe_msg!(
840                        "executor-row-limit-exceeded",
841                        rows_processed = *rows_processed as i64,
842                        max_rows = *max_rows as i64
843                    )
844                )
845            }
846            ExecutorError::MemoryLimitExceeded { used_bytes, max_bytes } => {
847                let used_gb = format!("{:.2}", *used_bytes as f64 / 1024.0 / 1024.0 / 1024.0);
848                let max_gb = format!("{:.2}", *max_bytes as f64 / 1024.0 / 1024.0 / 1024.0);
849                write!(
850                    f,
851                    "{}",
852                    vibe_msg!(
853                        "executor-memory-limit-exceeded",
854                        used_gb = used_gb.as_str(),
855                        max_gb = max_gb.as_str()
856                    )
857                )
858            }
859            ExecutorError::JoinTableLimitExceeded { max_tables, .. } => {
860                // SQLite-compatible error message: "at most 64 tables in a join"
861                write!(f, "at most {} tables in a join", max_tables)
862            }
863            ExecutorError::VariableNotFound { variable_name, available_variables } => {
864                if available_variables.is_empty() {
865                    write!(
866                        f,
867                        "{}",
868                        vibe_msg!(
869                            "executor-variable-not-found-simple",
870                            variable_name = variable_name.as_str()
871                        )
872                    )
873                } else {
874                    let available = available_variables.join(", ");
875                    write!(
876                        f,
877                        "{}",
878                        vibe_msg!(
879                            "executor-variable-not-found-with-available",
880                            variable_name = variable_name.as_str(),
881                            available_variables = available.as_str()
882                        )
883                    )
884                }
885            }
886            ExecutorError::LabelNotFound(name) => {
887                write!(f, "{}", vibe_msg!("executor-label-not-found", name = name.as_str()))
888            }
889            ExecutorError::SelectIntoRowCount { expected, actual } => {
890                let plural = if *actual == 1 { "" } else { "s" };
891                write!(
892                    f,
893                    "{}",
894                    vibe_msg!(
895                        "executor-select-into-row-count",
896                        expected = *expected as i64,
897                        actual = *actual as i64,
898                        plural = plural
899                    )
900                )
901            }
902            ExecutorError::SelectIntoColumnCount { expected, actual } => {
903                let expected_plural = if *expected == 1 { "" } else { "s" };
904                let actual_plural = if *actual == 1 { "" } else { "s" };
905                write!(
906                    f,
907                    "{}",
908                    vibe_msg!(
909                        "executor-select-into-column-count",
910                        expected = *expected as i64,
911                        expected_plural = expected_plural,
912                        actual = *actual as i64,
913                        actual_plural = actual_plural
914                    )
915                )
916            }
917            ExecutorError::ProcedureNotFound {
918                procedure_name,
919                schema_name,
920                available_procedures,
921            } => {
922                if available_procedures.is_empty() {
923                    write!(
924                        f,
925                        "{}",
926                        vibe_msg!(
927                            "executor-procedure-not-found-simple",
928                            procedure_name = procedure_name.as_str(),
929                            schema_name = schema_name.as_str()
930                        )
931                    )
932                } else {
933                    let suggestion = find_closest_match(procedure_name, available_procedures);
934                    if let Some(similar) = suggestion {
935                        // Use multi-line format with suggestion
936                        write!(
937                            f,
938                            "{}\nAvailable procedures: {}\nDid you mean '{}'?",
939                            vibe_msg!(
940                                "executor-procedure-not-found-simple",
941                                procedure_name = procedure_name.as_str(),
942                                schema_name = schema_name.as_str()
943                            ),
944                            available_procedures.join(", "),
945                            similar
946                        )
947                    } else {
948                        write!(
949                            f,
950                            "{}\nAvailable procedures: {}",
951                            vibe_msg!(
952                                "executor-procedure-not-found-simple",
953                                procedure_name = procedure_name.as_str(),
954                                schema_name = schema_name.as_str()
955                            ),
956                            available_procedures.join(", ")
957                        )
958                    }
959                }
960            }
961            ExecutorError::FunctionNotFound { function_name, schema_name, available_functions } => {
962                if available_functions.is_empty() {
963                    write!(
964                        f,
965                        "{}",
966                        vibe_msg!(
967                            "executor-function-not-found-simple",
968                            function_name = function_name.as_str(),
969                            schema_name = schema_name.as_str()
970                        )
971                    )
972                } else {
973                    let suggestion = find_closest_match(function_name, available_functions);
974                    if let Some(similar) = suggestion {
975                        write!(
976                            f,
977                            "{}\nAvailable functions: {}\nDid you mean '{}'?",
978                            vibe_msg!(
979                                "executor-function-not-found-simple",
980                                function_name = function_name.as_str(),
981                                schema_name = schema_name.as_str()
982                            ),
983                            available_functions.join(", "),
984                            similar
985                        )
986                    } else {
987                        write!(
988                            f,
989                            "{}\nAvailable functions: {}",
990                            vibe_msg!(
991                                "executor-function-not-found-simple",
992                                function_name = function_name.as_str(),
993                                schema_name = schema_name.as_str()
994                            ),
995                            available_functions.join(", ")
996                        )
997                    }
998                }
999            }
1000            ExecutorError::ParameterCountMismatch {
1001                routine_name,
1002                routine_type,
1003                expected,
1004                actual,
1005                parameter_signature,
1006            } => {
1007                let expected_plural = if *expected == 1 { "" } else { "s" };
1008                let actual_plural = if *actual == 1 { "" } else { "s" };
1009                write!(
1010                    f,
1011                    "{}",
1012                    vibe_msg!(
1013                        "executor-parameter-count-mismatch",
1014                        routine_type = routine_type.as_str(),
1015                        routine_name = routine_name.as_str(),
1016                        expected = *expected as i64,
1017                        expected_plural = expected_plural,
1018                        parameter_signature = parameter_signature.as_str(),
1019                        actual = *actual as i64,
1020                        actual_plural = actual_plural
1021                    )
1022                )
1023            }
1024            ExecutorError::ParameterTypeMismatch {
1025                parameter_name,
1026                expected_type,
1027                actual_type,
1028                actual_value,
1029            } => {
1030                write!(
1031                    f,
1032                    "{}",
1033                    vibe_msg!(
1034                        "executor-parameter-type-mismatch",
1035                        parameter_name = parameter_name.as_str(),
1036                        expected_type = expected_type.as_str(),
1037                        actual_type = actual_type.as_str(),
1038                        actual_value = actual_value.as_str()
1039                    )
1040                )
1041            }
1042            ExecutorError::TypeError(msg) => {
1043                write!(f, "{}", vibe_msg!("executor-type-error", message = msg.as_str()))
1044            }
1045            ExecutorError::ArgumentCountMismatch { expected, actual } => {
1046                write!(
1047                    f,
1048                    "{}",
1049                    vibe_msg!(
1050                        "executor-argument-count-mismatch",
1051                        expected = *expected as i64,
1052                        actual = *actual as i64
1053                    )
1054                )
1055            }
1056            ExecutorError::RecursionLimitExceeded { message, call_stack, max_depth } => {
1057                write!(
1058                    f,
1059                    "{}",
1060                    vibe_msg!(
1061                        "executor-recursion-limit-exceeded",
1062                        max_depth = *max_depth as i64,
1063                        message = message.as_str()
1064                    )
1065                )?;
1066                if !call_stack.is_empty() {
1067                    write!(f, "\n{}", vibe_msg!("executor-recursion-call-stack"))?;
1068                    for call in call_stack {
1069                        write!(f, "\n  {}", call)?;
1070                    }
1071                }
1072                Ok(())
1073            }
1074            ExecutorError::FunctionMustReturn => {
1075                write!(f, "{}", vibe_msg!("executor-function-must-return"))
1076            }
1077            ExecutorError::InvalidControlFlow(msg) => {
1078                write!(f, "{}", vibe_msg!("executor-invalid-control-flow", message = msg.as_str()))
1079            }
1080            ExecutorError::InvalidFunctionBody(msg) => {
1081                write!(f, "{}", vibe_msg!("executor-invalid-function-body", message = msg.as_str()))
1082            }
1083            ExecutorError::FunctionReadOnlyViolation(msg) => {
1084                write!(
1085                    f,
1086                    "{}",
1087                    vibe_msg!("executor-function-read-only-violation", message = msg.as_str())
1088                )
1089            }
1090            ExecutorError::ParseError(msg) => {
1091                write!(f, "{}", vibe_msg!("executor-parse-error", message = msg.as_str()))
1092            }
1093            ExecutorError::InvalidExtractField { field, value_type } => {
1094                write!(
1095                    f,
1096                    "{}",
1097                    vibe_msg!(
1098                        "executor-invalid-extract-field",
1099                        field = field.as_str(),
1100                        value_type = value_type.as_str()
1101                    )
1102                )
1103            }
1104            ExecutorError::ArrowDowncastError { expected_type, context } => {
1105                write!(
1106                    f,
1107                    "{}",
1108                    vibe_msg!(
1109                        "executor-arrow-downcast-error",
1110                        expected_type = expected_type.as_str(),
1111                        context = context.as_str()
1112                    )
1113                )
1114            }
1115            ExecutorError::ColumnarTypeMismatch { operation, left_type, right_type } => {
1116                if let Some(right) = right_type {
1117                    write!(
1118                        f,
1119                        "{}",
1120                        vibe_msg!(
1121                            "executor-columnar-type-mismatch-binary",
1122                            operation = operation.as_str(),
1123                            left_type = left_type.as_str(),
1124                            right_type = right.as_str()
1125                        )
1126                    )
1127                } else {
1128                    write!(
1129                        f,
1130                        "{}",
1131                        vibe_msg!(
1132                            "executor-columnar-type-mismatch-unary",
1133                            operation = operation.as_str(),
1134                            left_type = left_type.as_str()
1135                        )
1136                    )
1137                }
1138            }
1139            ExecutorError::SimdOperationFailed { operation, reason } => {
1140                write!(
1141                    f,
1142                    "{}",
1143                    vibe_msg!(
1144                        "executor-simd-operation-failed",
1145                        operation = operation.as_str(),
1146                        reason = reason.as_str()
1147                    )
1148                )
1149            }
1150            ExecutorError::ColumnarColumnNotFound { column_index, batch_columns } => {
1151                write!(
1152                    f,
1153                    "{}",
1154                    vibe_msg!(
1155                        "executor-columnar-column-not-found",
1156                        column_index = *column_index as i64,
1157                        batch_columns = *batch_columns as i64
1158                    )
1159                )
1160            }
1161            ExecutorError::ColumnarColumnNotFoundByName { column_name } => {
1162                write!(
1163                    f,
1164                    "{}",
1165                    vibe_msg!(
1166                        "executor-columnar-column-not-found-by-name",
1167                        column_name = column_name.as_str()
1168                    )
1169                )
1170            }
1171            ExecutorError::ColumnarLengthMismatch { context, expected, actual } => {
1172                write!(
1173                    f,
1174                    "{}",
1175                    vibe_msg!(
1176                        "executor-columnar-length-mismatch",
1177                        context = context.as_str(),
1178                        expected = *expected as i64,
1179                        actual = *actual as i64
1180                    )
1181                )
1182            }
1183            ExecutorError::UnsupportedArrayType { operation, array_type } => {
1184                write!(
1185                    f,
1186                    "{}",
1187                    vibe_msg!(
1188                        "executor-unsupported-array-type",
1189                        operation = operation.as_str(),
1190                        array_type = array_type.as_str()
1191                    )
1192                )
1193            }
1194            ExecutorError::SpatialGeometryError { function_name, message } => {
1195                write!(
1196                    f,
1197                    "{}",
1198                    vibe_msg!(
1199                        "executor-spatial-geometry-error",
1200                        function_name = function_name.as_str(),
1201                        message = message.as_str()
1202                    )
1203                )
1204            }
1205            ExecutorError::SpatialOperationFailed { function_name, message } => {
1206                write!(
1207                    f,
1208                    "{}",
1209                    vibe_msg!(
1210                        "executor-spatial-operation-failed",
1211                        function_name = function_name.as_str(),
1212                        message = message.as_str()
1213                    )
1214                )
1215            }
1216            ExecutorError::SpatialArgumentError { function_name, expected, actual } => {
1217                write!(
1218                    f,
1219                    "{}",
1220                    vibe_msg!(
1221                        "executor-spatial-argument-error",
1222                        function_name = function_name.as_str(),
1223                        expected = expected.as_str(),
1224                        actual = actual.as_str()
1225                    )
1226                )
1227            }
1228            ExecutorError::CursorAlreadyExists(name) => {
1229                write!(f, "{}", vibe_msg!("executor-cursor-already-exists", name = name.as_str()))
1230            }
1231            ExecutorError::CursorNotFound(name) => {
1232                write!(f, "{}", vibe_msg!("executor-cursor-not-found", name = name.as_str()))
1233            }
1234            ExecutorError::CursorAlreadyOpen(name) => {
1235                write!(f, "{}", vibe_msg!("executor-cursor-already-open", name = name.as_str()))
1236            }
1237            ExecutorError::CursorNotOpen(name) => {
1238                write!(f, "{}", vibe_msg!("executor-cursor-not-open", name = name.as_str()))
1239            }
1240            ExecutorError::CursorNotScrollable(name) => {
1241                write!(f, "{}", vibe_msg!("executor-cursor-not-scrollable", name = name.as_str()))
1242            }
1243            ExecutorError::AssertionViolation { assertion_name } => {
1244                write!(f, "Assertion '{}' violated", assertion_name)
1245            }
1246            ExecutorError::OrderByOutOfRange {
1247                term_position,
1248                column_number: _,
1249                select_list_len,
1250            } => {
1251                // SQLite-compatible error format: "1st ORDER BY term out of range - should be between 1 and N"
1252                let ordinal = match term_position {
1253                    1 => "1st".to_string(),
1254                    2 => "2nd".to_string(),
1255                    3 => "3rd".to_string(),
1256                    n => format!("{}th", n),
1257                };
1258                if *select_list_len == 0 {
1259                    write!(f, "{} ORDER BY term out of range - should be between 1 and 1", ordinal)
1260                } else {
1261                    write!(
1262                        f,
1263                        "{} ORDER BY term out of range - should be between 1 and {}",
1264                        ordinal, select_list_len
1265                    )
1266                }
1267            }
1268            ExecutorError::GroupByOutOfRange {
1269                term_position,
1270                column_number: _,
1271                select_list_len,
1272            } => {
1273                // SQLite-compatible error format: "1st GROUP BY term out of range - should be between 1 and N"
1274                let ordinal = match term_position {
1275                    1 => "1st".to_string(),
1276                    2 => "2nd".to_string(),
1277                    3 => "3rd".to_string(),
1278                    n => format!("{}th", n),
1279                };
1280                if *select_list_len == 0 {
1281                    write!(f, "{} GROUP BY term out of range - should be between 1 and 1", ordinal)
1282                } else {
1283                    write!(
1284                        f,
1285                        "{} GROUP BY term out of range - should be between 1 and {}",
1286                        ordinal, select_list_len
1287                    )
1288                }
1289            }
1290            ExecutorError::OrderByTermNotInResultSet { term_position } => {
1291                // SQLite-compatible error format: "1st ORDER BY term does not match any column in the result set"
1292                let ordinal = match term_position {
1293                    1 => "1st".to_string(),
1294                    2 => "2nd".to_string(),
1295                    3 => "3rd".to_string(),
1296                    n => format!("{}th", n),
1297                };
1298                write!(f, "{} ORDER BY term does not match any column in the result set", ordinal)
1299            }
1300            ExecutorError::InvalidLimitOffset { clause, value, reason } => {
1301                write!(f, "{} value {} {}", clause, value, reason)
1302            }
1303            ExecutorError::JoinUsingColumnNotPresent { column_name } => {
1304                write!(
1305                    f,
1306                    "{}",
1307                    vibe_msg!(
1308                        "executor-join-using-column-not-present",
1309                        column_name = column_name.as_str()
1310                    )
1311                )
1312            }
1313            ExecutorError::OnClauseReferencesRightTable => {
1314                // SQLite-compatible error message format
1315                write!(f, "ON clause references tables to its right")
1316            }
1317            ExecutorError::NoSuchColumn { column_ref } => {
1318                write!(
1319                    f,
1320                    "{}",
1321                    vibe_msg!("executor-no-such-column", column_ref = column_ref.as_str())
1322                )
1323            }
1324            ExecutorError::NoSuchFunction { function_name } => {
1325                write!(
1326                    f,
1327                    "{}",
1328                    vibe_msg!("executor-no-such-function", function_name = function_name.as_str())
1329                )
1330            }
1331            ExecutorError::NoSuchIndex { index_name } => {
1332                // SQLite-compatible error message format
1333                write!(f, "no such index: {}", index_name)
1334            }
1335            ExecutorError::Other(msg) => {
1336                write!(f, "{}", vibe_msg!("executor-other", message = msg.as_str()))
1337            }
1338        }
1339    }
1340}
1341
1342impl ExecutorError {
1343    /// True for the SQLite-compatible "non-deterministic use of <fn>() in
1344    /// <context>" error raised when a date/time function resolves the current
1345    /// time (or applies 'localtime'/'utc') inside a CHECK constraint,
1346    /// generated column, or index expression.
1347    ///
1348    /// Lenient evaluation sites (e.g. index-maintenance paths that map other
1349    /// evaluation errors to NULL/false) use this to ensure the rejection is
1350    /// PROPAGATED instead of swallowed — the enclosing statement must abort.
1351    pub fn is_non_deterministic_use(&self) -> bool {
1352        matches!(
1353            self,
1354            ExecutorError::SqliteCompatError(msg) if msg.starts_with("non-deterministic use of ")
1355        )
1356    }
1357
1358    /// Qualify a `TableNotFound` for an unqualified table name with the implicit
1359    /// `main.` schema prefix, matching sqlite3's reporting when a table is
1360    /// resolved against the database schema (e.g. a table referenced in a view
1361    /// definition body that has since been dropped: `no such table: main.test2`,
1362    /// trigger4-3.1/3.2/3.3).
1363    ///
1364    /// Only bare names are rewritten — names that already carry a schema
1365    /// qualifier (contain a `.`) and every other error variant are returned
1366    /// unchanged.
1367    pub fn with_main_schema_qualifier(self) -> ExecutorError {
1368        match self {
1369            ExecutorError::TableNotFound(name) if !name.contains('.') => {
1370                ExecutorError::TableNotFound(format!("main.{}", name))
1371            }
1372            other => other,
1373        }
1374    }
1375}
1376
1377impl std::error::Error for ExecutorError {}
1378
1379impl From<vibesql_storage::StorageError> for ExecutorError {
1380    fn from(err: vibesql_storage::StorageError) -> Self {
1381        match err {
1382            vibesql_storage::StorageError::TableNotFound(name) => {
1383                ExecutorError::TableNotFound(name)
1384            }
1385            vibesql_storage::StorageError::IndexAlreadyExists(name) => {
1386                ExecutorError::IndexAlreadyExists(name)
1387            }
1388            vibesql_storage::StorageError::IndexNotFound(name) => {
1389                ExecutorError::IndexNotFound(name)
1390            }
1391            vibesql_storage::StorageError::ColumnCountMismatch { expected, actual } => {
1392                ExecutorError::ColumnCountMismatch { expected, provided: actual }
1393            }
1394            vibesql_storage::StorageError::ColumnIndexOutOfBounds { index } => {
1395                ExecutorError::ColumnIndexOutOfBounds { index }
1396            }
1397            vibesql_storage::StorageError::CatalogError(msg) => ExecutorError::StorageError(msg),
1398            vibesql_storage::StorageError::TransactionError(msg) => {
1399                ExecutorError::StorageError(msg)
1400            }
1401            vibesql_storage::StorageError::RowNotFound => {
1402                ExecutorError::StorageError("Row not found".to_string())
1403            }
1404            vibesql_storage::StorageError::NullConstraintViolation { column } => {
1405                ExecutorError::ConstraintViolation(format!(
1406                    "NOT NULL constraint violation: column '{}' cannot be NULL",
1407                    column
1408                ))
1409            }
1410            vibesql_storage::StorageError::TypeMismatch { column, expected, actual } => {
1411                ExecutorError::StorageError(format!(
1412                    "Type mismatch in column '{}': expected {}, got {}",
1413                    column, expected, actual
1414                ))
1415            }
1416            vibesql_storage::StorageError::ColumnNotFound { column_name, table_name } => {
1417                ExecutorError::StorageError(format!(
1418                    "Column '{}' not found in table '{}'",
1419                    column_name, table_name
1420                ))
1421            }
1422            vibesql_storage::StorageError::UniqueConstraintViolation(msg) => {
1423                // SQLite-compatible: output the message as-is without prefix
1424                // Format: "UNIQUE constraint failed: table.column"
1425                ExecutorError::SqliteCompatError(msg)
1426            }
1427            vibesql_storage::StorageError::InvalidIndexColumn(msg) => {
1428                ExecutorError::StorageError(msg)
1429            }
1430            vibesql_storage::StorageError::NotImplemented(msg) => {
1431                ExecutorError::StorageError(format!("Not implemented: {}", msg))
1432            }
1433            vibesql_storage::StorageError::IoError(msg) => {
1434                ExecutorError::StorageError(format!("I/O error: {}", msg))
1435            }
1436            vibesql_storage::StorageError::InvalidPageSize { expected, actual } => {
1437                ExecutorError::StorageError(format!(
1438                    "Invalid page size: expected {}, got {}",
1439                    expected, actual
1440                ))
1441            }
1442            vibesql_storage::StorageError::InvalidPageId(page_id) => {
1443                ExecutorError::StorageError(format!("Invalid page ID: {}", page_id))
1444            }
1445            vibesql_storage::StorageError::LockError(msg) => {
1446                ExecutorError::StorageError(format!("Lock error: {}", msg))
1447            }
1448            vibesql_storage::StorageError::MemoryBudgetExceeded { used, budget } => {
1449                ExecutorError::StorageError(format!(
1450                    "Memory budget exceeded: using {} bytes, budget is {} bytes",
1451                    used, budget
1452                ))
1453            }
1454            vibesql_storage::StorageError::NoIndexToEvict => ExecutorError::StorageError(
1455                "No index available to evict (all indexes are already disk-backed)".to_string(),
1456            ),
1457            vibesql_storage::StorageError::Other(msg) => ExecutorError::StorageError(msg),
1458        }
1459    }
1460}
1461
1462impl From<vibesql_catalog::CatalogError> for ExecutorError {
1463    fn from(err: vibesql_catalog::CatalogError) -> Self {
1464        match err {
1465            vibesql_catalog::CatalogError::TableAlreadyExists(name) => {
1466                ExecutorError::TableAlreadyExists(name)
1467            }
1468            vibesql_catalog::CatalogError::TableNotFound { table_name } => {
1469                ExecutorError::TableNotFound(table_name)
1470            }
1471            vibesql_catalog::CatalogError::ColumnAlreadyExists(name) => {
1472                ExecutorError::ColumnAlreadyExists(name)
1473            }
1474            vibesql_catalog::CatalogError::ColumnNotFound { column_name, table_name } => {
1475                ExecutorError::ColumnNotFound {
1476                    column_name,
1477                    table_name,
1478                    searched_tables: vec![],
1479                    available_columns: vec![],
1480                }
1481            }
1482            vibesql_catalog::CatalogError::SchemaNotFound(name) => {
1483                ExecutorError::SchemaNotFound(name)
1484            }
1485            vibesql_catalog::CatalogError::SchemaAlreadyExists(name) => {
1486                ExecutorError::SchemaAlreadyExists(name)
1487            }
1488            vibesql_catalog::CatalogError::SchemaNotEmpty(name) => {
1489                ExecutorError::SchemaNotEmpty(name)
1490            }
1491            vibesql_catalog::CatalogError::RoleAlreadyExists(name) => {
1492                ExecutorError::StorageError(format!("Role '{}' already exists", name))
1493            }
1494            vibesql_catalog::CatalogError::RoleNotFound(name) => ExecutorError::RoleNotFound(name),
1495            // Advanced SQL:1999 objects
1496            vibesql_catalog::CatalogError::DomainAlreadyExists(name) => {
1497                ExecutorError::Other(format!("Domain '{}' already exists", name))
1498            }
1499            vibesql_catalog::CatalogError::DomainNotFound(name) => {
1500                ExecutorError::Other(format!("Domain '{}' not found", name))
1501            }
1502            vibesql_catalog::CatalogError::DomainInUse { domain_name, dependent_columns } => {
1503                ExecutorError::Other(format!(
1504                    "Domain '{}' is still in use by {} column(s): {}",
1505                    domain_name,
1506                    dependent_columns.len(),
1507                    dependent_columns
1508                        .iter()
1509                        .map(|(t, c)| format!("{}.{}", t, c))
1510                        .collect::<Vec<_>>()
1511                        .join(", ")
1512                ))
1513            }
1514            vibesql_catalog::CatalogError::SequenceAlreadyExists(name) => {
1515                ExecutorError::Other(format!("Sequence '{}' already exists", name))
1516            }
1517            vibesql_catalog::CatalogError::SequenceNotFound(name) => {
1518                ExecutorError::Other(format!("Sequence '{}' not found", name))
1519            }
1520            vibesql_catalog::CatalogError::SequenceInUse { sequence_name, dependent_columns } => {
1521                ExecutorError::Other(format!(
1522                    "Sequence '{}' is still in use by {} column(s): {}",
1523                    sequence_name,
1524                    dependent_columns.len(),
1525                    dependent_columns
1526                        .iter()
1527                        .map(|(t, c)| format!("{}.{}", t, c))
1528                        .collect::<Vec<_>>()
1529                        .join(", ")
1530                ))
1531            }
1532            vibesql_catalog::CatalogError::TypeAlreadyExists(name) => {
1533                ExecutorError::TypeAlreadyExists(name)
1534            }
1535            vibesql_catalog::CatalogError::TypeNotFound(name) => ExecutorError::TypeNotFound(name),
1536            vibesql_catalog::CatalogError::TypeInUse(name) => ExecutorError::TypeInUse(name),
1537            vibesql_catalog::CatalogError::CollationAlreadyExists(name) => {
1538                ExecutorError::Other(format!("Collation '{}' already exists", name))
1539            }
1540            vibesql_catalog::CatalogError::CollationNotFound(name) => {
1541                ExecutorError::Other(format!("Collation '{}' not found", name))
1542            }
1543            vibesql_catalog::CatalogError::CharacterSetAlreadyExists(name) => {
1544                ExecutorError::Other(format!("Character set '{}' already exists", name))
1545            }
1546            vibesql_catalog::CatalogError::CharacterSetNotFound(name) => {
1547                ExecutorError::Other(format!("Character set '{}' not found", name))
1548            }
1549            vibesql_catalog::CatalogError::TranslationAlreadyExists(name) => {
1550                ExecutorError::Other(format!("Translation '{}' already exists", name))
1551            }
1552            vibesql_catalog::CatalogError::TranslationNotFound(name) => {
1553                ExecutorError::Other(format!("Translation '{}' not found", name))
1554            }
1555            vibesql_catalog::CatalogError::ViewAlreadyExists(name) => {
1556                ExecutorError::Other(format!("View '{}' already exists", name))
1557            }
1558            vibesql_catalog::CatalogError::ViewNotFound(name) => {
1559                ExecutorError::Other(format!("View '{}' not found", name))
1560            }
1561            vibesql_catalog::CatalogError::ViewInUse { view_name, dependent_views } => {
1562                ExecutorError::Other(format!(
1563                    "View or table '{}' is still in use by {} view(s): {}",
1564                    view_name,
1565                    dependent_views.len(),
1566                    dependent_views.join(", ")
1567                ))
1568            }
1569            vibesql_catalog::CatalogError::TriggerAlreadyExists(name) => {
1570                ExecutorError::TriggerAlreadyExists(name)
1571            }
1572            vibesql_catalog::CatalogError::TriggerNotFound(name) => {
1573                ExecutorError::TriggerNotFound(name)
1574            }
1575            vibesql_catalog::CatalogError::AssertionAlreadyExists(name) => {
1576                ExecutorError::Other(format!("Assertion '{}' already exists", name))
1577            }
1578            vibesql_catalog::CatalogError::AssertionNotFound(name) => {
1579                ExecutorError::Other(format!("Assertion '{}' not found", name))
1580            }
1581            vibesql_catalog::CatalogError::FunctionAlreadyExists(name) => {
1582                ExecutorError::Other(format!("Function '{}' already exists", name))
1583            }
1584            vibesql_catalog::CatalogError::FunctionNotFound(name) => {
1585                ExecutorError::Other(format!("Function '{}' not found", name))
1586            }
1587            vibesql_catalog::CatalogError::ProcedureAlreadyExists(name) => {
1588                ExecutorError::Other(format!("Procedure '{}' already exists", name))
1589            }
1590            vibesql_catalog::CatalogError::ProcedureNotFound(name) => {
1591                ExecutorError::Other(format!("Procedure '{}' not found", name))
1592            }
1593            vibesql_catalog::CatalogError::ConstraintAlreadyExists(name) => {
1594                ExecutorError::ConstraintViolation(format!("Constraint '{}' already exists", name))
1595            }
1596            vibesql_catalog::CatalogError::ConstraintNotFound(name) => {
1597                ExecutorError::ConstraintNotFound {
1598                    constraint_name: name,
1599                    table_name: "unknown".to_string(),
1600                }
1601            }
1602            vibesql_catalog::CatalogError::IndexAlreadyExists { index_name, table_name } => {
1603                ExecutorError::IndexAlreadyExists(format!("{} on table {}", index_name, table_name))
1604            }
1605            vibesql_catalog::CatalogError::IndexNotFound { index_name, table_name } => {
1606                ExecutorError::IndexNotFound(format!("{} on table {}", index_name, table_name))
1607            }
1608            vibesql_catalog::CatalogError::CircularForeignKey { table_name, message } => {
1609                ExecutorError::ConstraintViolation(format!(
1610                    "Circular foreign key dependency on table '{}': {}",
1611                    table_name, message
1612                ))
1613            }
1614        }
1615    }
1616}
1617
1618#[cfg(test)]
1619mod tests {
1620    use super::ExecutorError;
1621
1622    #[test]
1623    fn with_main_schema_qualifier_prefixes_bare_table_name() {
1624        let err = ExecutorError::TableNotFound("test2".to_string());
1625        assert_eq!(
1626            err.with_main_schema_qualifier(),
1627            ExecutorError::TableNotFound("main.test2".to_string())
1628        );
1629    }
1630
1631    #[test]
1632    fn with_main_schema_qualifier_leaves_already_qualified_name() {
1633        let err = ExecutorError::TableNotFound("other.test2".to_string());
1634        assert_eq!(
1635            err.with_main_schema_qualifier(),
1636            ExecutorError::TableNotFound("other.test2".to_string())
1637        );
1638    }
1639
1640    #[test]
1641    fn with_main_schema_qualifier_ignores_other_variants() {
1642        let err = ExecutorError::ColumnNotFound {
1643            column_name: "x".to_string(),
1644            table_name: "t".to_string(),
1645            searched_tables: vec!["t".to_string()],
1646            available_columns: vec![],
1647        };
1648        assert_eq!(err.clone().with_main_schema_qualifier(), err);
1649    }
1650}