Skip to main content

featherdb_core/
error.rs

1//! Error types for FeatherDB
2
3use crate::PageId;
4use std::fmt;
5use thiserror::Error;
6
7/// Result type alias using FeatherDB's Error
8pub type Result<T> = std::result::Result<T, Error>;
9
10/// Query context for tracking position in SQL
11#[derive(Debug, Clone, Default)]
12pub struct QueryContext {
13    /// The original SQL query
14    pub sql: String,
15    /// Current line number (1-indexed)
16    pub line: usize,
17    /// Current column number (1-indexed)
18    pub column: usize,
19}
20
21impl QueryContext {
22    /// Create a new query context
23    pub fn new(sql: impl Into<String>) -> Self {
24        QueryContext {
25            sql: sql.into(),
26            line: 1,
27            column: 1,
28        }
29    }
30
31    /// Create a context with position
32    pub fn with_position(sql: impl Into<String>, line: usize, column: usize) -> Self {
33        QueryContext {
34            sql: sql.into(),
35            line,
36            column,
37        }
38    }
39
40    /// Get position from byte offset in the SQL string
41    pub fn position_from_offset(&self, offset: usize) -> (usize, usize) {
42        let mut line = 1;
43        let mut column = 1;
44
45        for (i, ch) in self.sql.char_indices() {
46            if i >= offset {
47                break;
48            }
49            if ch == '\n' {
50                line += 1;
51                column = 1;
52            } else {
53                column += 1;
54            }
55        }
56
57        (line, column)
58    }
59}
60
61/// Rich query error with context and suggestions
62#[derive(Debug, Clone)]
63pub struct QueryError {
64    /// Error message
65    pub message: String,
66    /// The SQL query that caused the error
67    pub sql: String,
68    /// Line number where error occurred (1-indexed)
69    pub line: usize,
70    /// Column number where error occurred (1-indexed)
71    pub column: usize,
72    /// Suggested fix for the error
73    pub suggestion: Option<String>,
74    /// Additional help text
75    pub help: Option<String>,
76    /// Length of the error span (for underlining)
77    pub span_length: usize,
78}
79
80impl QueryError {
81    /// Create a new query error
82    pub fn new(
83        message: impl Into<String>,
84        sql: impl Into<String>,
85        line: usize,
86        column: usize,
87    ) -> Self {
88        QueryError {
89            message: message.into(),
90            sql: sql.into(),
91            line,
92            column,
93            suggestion: None,
94            help: None,
95            span_length: 1,
96        }
97    }
98
99    /// Add a suggestion
100    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
101        self.suggestion = Some(suggestion.into());
102        self
103    }
104
105    /// Add help text
106    pub fn with_help(mut self, help: impl Into<String>) -> Self {
107        self.help = Some(help.into());
108        self
109    }
110
111    /// Set the span length for underlining
112    pub fn with_span(mut self, length: usize) -> Self {
113        self.span_length = length;
114        self
115    }
116
117    /// Format the error with source context
118    pub fn format_with_context(&self) -> String {
119        let mut output = String::new();
120
121        // Error header
122        output.push_str(&format!("Error: {}\n", self.message));
123        output.push_str(&format!("  --> query:{}:{}\n", self.line, self.column));
124        output.push_str("  |\n");
125
126        // Show the relevant line(s)
127        let lines: Vec<&str> = self.sql.lines().collect();
128        if self.line > 0 && self.line <= lines.len() {
129            let line_content = lines[self.line - 1];
130            output.push_str(&format!("{:3} | {}\n", self.line, line_content));
131
132            // Add the error indicator
133            let padding = " ".repeat(self.column.saturating_sub(1));
134            let underline = "^".repeat(self.span_length.max(1));
135            output.push_str(&format!("  | {}{}\n", padding, underline));
136        }
137
138        output.push_str("  |\n");
139
140        // Add suggestion if present
141        if let Some(ref suggestion) = self.suggestion {
142            output.push_str(&format!("  = suggestion: {}\n", suggestion));
143        }
144
145        // Add help if present
146        if let Some(ref help) = self.help {
147            output.push_str(&format!("Help: {}\n", help));
148        }
149
150        output
151    }
152}
153
154impl fmt::Display for QueryError {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        write!(f, "{}", self.format_with_context())
157    }
158}
159
160impl std::error::Error for QueryError {}
161
162/// Calculate Levenshtein distance between two strings
163pub fn levenshtein_distance(a: &str, b: &str) -> usize {
164    let a_len = a.chars().count();
165    let b_len = b.chars().count();
166
167    if a_len == 0 {
168        return b_len;
169    }
170    if b_len == 0 {
171        return a_len;
172    }
173
174    let a_chars: Vec<char> = a.chars().collect();
175    let b_chars: Vec<char> = b.chars().collect();
176
177    let mut matrix = vec![vec![0usize; b_len + 1]; a_len + 1];
178
179    for (i, row) in matrix.iter_mut().enumerate().take(a_len + 1) {
180        row[0] = i;
181    }
182    for (j, val) in matrix[0].iter_mut().enumerate().take(b_len + 1) {
183        *val = j;
184    }
185
186    for i in 1..=a_len {
187        for j in 1..=b_len {
188            let cost =
189                if a_chars[i - 1].to_lowercase().next() == b_chars[j - 1].to_lowercase().next() {
190                    0
191                } else {
192                    1
193                };
194
195            matrix[i][j] = (matrix[i - 1][j] + 1)
196                .min(matrix[i][j - 1] + 1)
197                .min(matrix[i - 1][j - 1] + cost);
198        }
199    }
200
201    matrix[a_len][b_len]
202}
203
204/// Find the best match from a list of candidates
205pub fn find_best_match<'a>(
206    input: &str,
207    candidates: &[&'a str],
208    max_distance: usize,
209) -> Option<&'a str> {
210    let input_lower = input.to_lowercase();
211
212    // First, check for case-insensitive exact match
213    for &candidate in candidates {
214        if candidate.to_lowercase() == input_lower {
215            return Some(candidate);
216        }
217    }
218
219    // Find the best fuzzy match
220    let mut best_match = None;
221    let mut best_distance = max_distance + 1;
222
223    for &candidate in candidates {
224        let distance = levenshtein_distance(input, candidate);
225        if distance < best_distance {
226            best_distance = distance;
227            best_match = Some(candidate);
228        }
229    }
230
231    if best_distance <= max_distance {
232        best_match
233    } else {
234        None
235    }
236}
237
238/// Common SQL keyword typos and their corrections
239pub fn suggest_keyword(typo: &str) -> Option<&'static str> {
240    let typo_upper = typo.to_uppercase();
241    match typo_upper.as_str() {
242        // SELECT variants
243        "SELCT" | "SELEC" | "SLECT" | "SELET" | "SEELCT" | "SELEKT" => Some("SELECT"),
244        // FROM variants
245        "FORM" | "FOMR" | "FRM" | "FRMO" | "FRON" => Some("FROM"),
246        // WHERE variants
247        "WHRE" | "WEHRE" | "WHER" | "WHEER" | "WAERE" => Some("WHERE"),
248        // INSERT variants
249        "INSRT" | "INSER" | "INSET" | "INSRET" | "ISERT" => Some("INSERT"),
250        // INTO variants
251        "ITNO" | "INOT" | "INT" | "INTO" => Some("INTO"),
252        // UPDATE variants
253        "UDPATE" | "UPDAE" | "UPDAT" | "UPDTE" | "UPADTE" => Some("UPDATE"),
254        // DELETE variants
255        "DELTE" | "DELEET" | "DELET" | "DELETEE" | "DELATE" => Some("DELETE"),
256        // CREATE variants
257        "CRAETE" | "CREAT" | "CRATE" | "CREAE" | "CRETAE" => Some("CREATE"),
258        // TABLE variants
259        "TABEL" | "TABL" | "TABLEE" | "TALBE" => Some("TABLE"),
260        // VALUES variants
261        "VALEUS" | "VLAUES" | "VALES" | "VALUESS" => Some("VALUES"),
262        // ORDER variants
263        "OREDR" | "ORDR" | "ODER" | "ORDEER" => Some("ORDER"),
264        // GROUP variants
265        "GRUOP" | "GOUP" | "GROPU" | "GROP" => Some("GROUP"),
266        // JOIN variants
267        "JION" | "JOUN" | "JON" | "JOINN" => Some("JOIN"),
268        // LEFT variants
269        "LFET" | "LETF" | "LEF" | "LEFTT" => Some("LEFT"),
270        // RIGHT variants
271        "RIGTH" | "RIGT" | "RIHGT" | "RIGHTT" => Some("RIGHT"),
272        // INNER variants
273        "INER" | "INNR" | "INEER" | "INNAR" => Some("INNER"),
274        // OUTER variants
275        "OTER" | "OUTR" | "OUUTER" | "OUTEER" => Some("OUTER"),
276        // LIMIT variants
277        "LIMT" | "LIMI" | "LIMTI" | "LMIT" => Some("LIMIT"),
278        // OFFSET variants
279        "OFFEST" | "OFFSE" | "OFSET" | "OFFET" => Some("OFFSET"),
280        // HAVING variants
281        "HAVNG" | "HAVIG" | "AHVING" | "HVAIN" => Some("HAVING"),
282        // DISTINCT variants
283        "DISTINT" | "DISTICT" | "DISTNCT" | "DISITINCT" => Some("DISTINCT"),
284        // AND variants
285        "AN" | "AD" | "ANN" | "ANDD" => Some("AND"),
286        // OR variants (be careful, "OR" is very short)
287        "OOR" | "ORR" => Some("OR"),
288        // NOT variants
289        "NOTT" | "NTO" | "NOO" => Some("NOT"),
290        // NULL variants
291        "NOLL" | "NULLL" | "NUL" | "NUUL" => Some("NULL"),
292        // INDEX variants
293        "INDX" | "IDNEX" | "INEDX" | "INDXE" => Some("INDEX"),
294        // DROP variants
295        "DRPO" | "DOP" | "DORP" | "DROOP" => Some("DROP"),
296        // ALTER variants
297        "ALTR" | "ALTRE" | "ATLER" | "ALETR" => Some("ALTER"),
298        // SET variants
299        "SE" | "SETT" | "STE" => Some("SET"),
300        // AS variants
301        "ASS" => Some("AS"),
302        // BY variants
303        "BYY" => Some("BY"),
304        // PRIMARY variants
305        "PRIMAY" | "PRIAMRY" | "PIRMARY" | "PRIMRY" => Some("PRIMARY"),
306        // KEY variants
307        "KYE" | "KEE" | "KEYY" => Some("KEY"),
308        // UNIQUE variants
309        "UNIQE" | "UNQUE" | "UINQUE" | "UNIQUEE" => Some("UNIQUE"),
310        // DEFAULT variants
311        "DEFUALT" | "DFAULT" | "DEAFULT" | "DEFALT" => Some("DEFAULT"),
312        // CONSTRAINT variants
313        "CONSTRAIN" | "CONSTRAING" | "CONSTRIANT" => Some("CONSTRAINT"),
314        // FOREIGN variants
315        "FORIEGN" | "FOREGIN" | "FORIGN" | "FOERIGN" => Some("FOREIGN"),
316        // REFERENCES variants
317        "REFERNCES" | "REFERECES" | "REFERNECE" | "REFRENCES" => Some("REFERENCES"),
318        // CASCADE variants
319        "CASCAD" | "CASCASE" | "CASCADEE" => Some("CASCADE"),
320        // TRANSACTION variants
321        "TRANACTION" | "TRASACTION" | "TRANSCATION" => Some("TRANSACTION"),
322        // BEGIN variants
323        "BEGINN" | "BGIN" | "BIGIN" | "BEGN" => Some("BEGIN"),
324        // COMMIT variants
325        "COMIT" | "COMMITT" | "COMMMIT" | "COMMTI" => Some("COMMIT"),
326        // ROLLBACK variants
327        "ROLBACK" | "ROLLBCK" | "ROOLBACK" | "ROLLBAK" => Some("ROLLBACK"),
328        _ => None,
329    }
330}
331
332/// Configuration error type
333#[derive(Debug, Clone, Error)]
334pub enum ConfigError {
335    /// Value is too low for a configuration field
336    #[error("Configuration error: {field} value {actual} is too low (minimum: {min})")]
337    ValueTooLow {
338        field: String,
339        min: u64,
340        actual: u64,
341    },
342
343    /// Value is too high for a configuration field
344    #[error("Configuration error: {field} value {actual} is too high (maximum: {max})")]
345    ValueTooHigh {
346        field: String,
347        max: u64,
348        actual: u64,
349    },
350
351    /// Invalid value for a configuration field
352    #[error("Configuration error: {field} has invalid value: {reason}")]
353    InvalidValue { field: String, reason: String },
354}
355
356/// Main error type for FeatherDB operations
357#[derive(Debug, Error)]
358pub enum Error {
359    // ============ User-facing errors (actionable) ============
360    /// Table not found
361    #[error("Table '{table}' not found{}", suggestion.as_ref().map(|s| format!(". Did you mean '{}'?", s)).unwrap_or_default())]
362    TableNotFound {
363        table: String,
364        suggestion: Option<String>,
365    },
366
367    /// Column not found in table
368    #[error("Column '{column}' not found in table '{table}'{}", suggestion.as_ref().map(|s| format!(". Did you mean '{}'?", s)).unwrap_or_default())]
369    ColumnNotFound {
370        column: String,
371        table: String,
372        suggestion: Option<String>,
373    },
374
375    /// Ambiguous column reference
376    #[error(
377        "Ambiguous column '{column}'. Could be from: {tables}. Please qualify with table name."
378    )]
379    AmbiguousColumn { column: String, tables: String },
380
381    /// Index not found
382    #[error("Index '{index}' not found on table '{table}'")]
383    IndexNotFound { index: String, table: String },
384
385    /// SQL syntax error
386    #[error("Syntax error at line {line}, column {column}: {message}")]
387    SyntaxError {
388        message: String,
389        line: usize,
390        column: usize,
391    },
392
393    /// Type mismatch error
394    #[error("Type mismatch: expected {expected}, got {actual}")]
395    TypeError { expected: String, actual: String },
396
397    /// Unique constraint violation
398    #[error("unique constraint violated: duplicate value in column '{column}' of table '{table}'")]
399    UniqueViolation { table: String, column: String },
400
401    /// Primary key violation
402    #[error("Primary key violation: duplicate key in table '{table}'")]
403    PrimaryKeyViolation { table: String },
404
405    /// Not null constraint violation
406    #[error("NOT NULL constraint failed: {table}.{column}")]
407    NotNullViolation { table: String, column: String },
408
409    /// Foreign key constraint violation
410    #[error("Foreign key constraint failed: {details}")]
411    ForeignKeyViolation { details: String },
412
413    /// CHECK constraint violation
414    #[error("CHECK constraint failed on {table}.{column}: constraint '{constraint}' violated by value {value}")]
415    CheckViolation {
416        table: String,
417        column: String,
418        constraint: String,
419        value: String,
420    },
421
422    /// Transaction conflict (optimistic concurrency)
423    #[error("Transaction conflict detected, please retry")]
424    TransactionConflict,
425
426    /// Write-write conflict (first-committer-wins)
427    #[error("Write conflict on table '{table}': another transaction modified the same row and committed first")]
428    WriteConflict { table: String },
429
430    /// Transaction already committed or aborted
431    #[error("Transaction already ended")]
432    TransactionEnded,
433
434    /// Transaction timeout
435    #[error(
436        "Transaction {transaction_id} exceeded timeout of {timeout_ms}ms (elapsed: {elapsed_ms}ms)"
437    )]
438    TransactionTimeout {
439        transaction_id: u64,
440        elapsed_ms: u64,
441        timeout_ms: u64,
442    },
443
444    /// Deadlock detected
445    #[error("Deadlock detected: cycle {cycle:?}, transaction {victim} selected as victim")]
446    DeadlockDetected { cycle: Vec<u64>, victim: u64 },
447
448    /// Savepoint not found
449    #[error("Savepoint '{name}' not found")]
450    SavepointNotFound { name: String },
451
452    /// Table already exists
453    #[error("Table '{table}' already exists")]
454    TableAlreadyExists { table: String },
455
456    /// Index already exists
457    #[error("Index '{index}' already exists")]
458    IndexAlreadyExists { index: String },
459
460    /// Database is read-only
461    #[error("Database is read-only")]
462    ReadOnly,
463
464    /// Database is locked by another process
465    #[error("Database is locked by another process")]
466    DatabaseLocked,
467
468    /// Invalid SQL query
469    #[error("Invalid query: {message}")]
470    InvalidQuery { message: String },
471
472    /// Rich query error with full context
473    #[error("{0}")]
474    Query(#[from] QueryError),
475
476    /// Unsupported feature
477    #[error("Unsupported feature: {feature}")]
478    Unsupported { feature: String },
479
480    /// Configuration error
481    #[error("{0}")]
482    Config(#[from] ConfigError),
483
484    // ============ Internal errors ============
485    /// Page is corrupted (checksum mismatch or invalid structure)
486    #[error("Page {0:?} is corrupted")]
487    CorruptedPage(PageId),
488
489    /// WAL is corrupted
490    #[error("Write-ahead log is corrupted: {message}")]
491    CorruptedWal { message: String },
492
493    /// Invalid page type
494    #[error("Invalid page type {page_type} for page {page_id:?}")]
495    InvalidPageType { page_id: PageId, page_type: u8 },
496
497    /// Page not found in buffer pool
498    #[error("Page {0:?} not found")]
499    PageNotFound(PageId),
500
501    /// Double free error
502    #[error("Page {0:?} was already freed")]
503    DoubleFree(PageId),
504
505    /// Buffer pool is full and cannot evict any pages
506    #[error("Buffer pool is full ({pinned} of {capacity} pages pinned)")]
507    BufferPoolFull { capacity: usize, pinned: usize },
508
509    /// Invalid database file
510    #[error("Invalid database file: {message}")]
511    InvalidDatabaseFile { message: String },
512
513    /// Database version mismatch
514    #[error("Database format version {file_version} is not supported (expected {expected})")]
515    VersionMismatch { file_version: u32, expected: u32 },
516
517    /// Internal error (should not happen in production)
518    #[error("Internal error: {0}")]
519    Internal(String),
520
521    // ============ I/O errors ============
522    /// I/O error
523    #[error("I/O error: {0}")]
524    Io(#[from] std::io::Error),
525
526    /// File not found
527    #[error("Database file not found: {path}")]
528    FileNotFound { path: String },
529
530    /// Serialization error
531    #[error("Serialization error: {0}")]
532    Serialization(String),
533
534    // ============ Compression errors ============
535    /// Compression error
536    #[error("Compression failed: {message}")]
537    CompressionError { message: String },
538
539    /// Decompression error
540    #[error("Decompression failed: {message} (expected {expected_size} bytes)")]
541    DecompressionError {
542        message: String,
543        expected_size: usize,
544    },
545
546    // ============ Storage limit errors ============
547    /// Database size limit exceeded
548    #[error("Database size limit exceeded: current size {current_bytes} bytes, limit {limit_bytes} bytes, attempted to add {operation_bytes} bytes")]
549    StorageLimitExceeded {
550        current_bytes: u64,
551        limit_bytes: u64,
552        operation_bytes: u64,
553    },
554
555    /// WAL size limit exceeded
556    #[error(
557        "WAL size limit exceeded: current size {current_bytes} bytes, limit {limit_bytes} bytes"
558    )]
559    WalLimitExceeded {
560        current_bytes: u64,
561        limit_bytes: u64,
562    },
563
564    /// Insufficient disk space
565    #[error("Insufficient disk space: available {available_bytes} bytes, required {required_bytes} bytes")]
566    InsufficientDiskSpace {
567        available_bytes: u64,
568        required_bytes: u64,
569    },
570
571    // ============ Authentication errors ============
572    /// Authentication failed
573    #[error("Authentication failed: {reason}")]
574    AuthenticationFailed { reason: String },
575
576    /// Database requires authentication but no key provided
577    #[error("This database requires authentication. Provide an API key in config.")]
578    AuthenticationRequired,
579
580    /// API key not found
581    #[error("API key '{key_id}' not found")]
582    ApiKeyNotFound { key_id: String },
583
584    /// API key has expired
585    #[error("API key '{key_id}' has expired")]
586    ApiKeyExpired { key_id: String },
587
588    /// API key has been revoked
589    #[error("API key '{key_id}' has been revoked")]
590    ApiKeyRevoked { key_id: String },
591
592    // ============ Authorization errors ============
593    /// Permission denied for operation on table
594    #[error("Permission denied: {operation} on table '{table}' for API key '{api_key_id}'")]
595    PermissionDenied {
596        table: String,
597        operation: String,
598        api_key_id: String,
599    },
600
601    // ============ Session errors (v0.4.5) ============
602    /// Session not found
603    #[error("Session '{session_id}' not found")]
604    SessionNotFound { session_id: String },
605
606    /// Session has expired due to inactivity
607    #[error("Session '{session_id}' has expired (inactive for {inactive_secs}s, timeout: {timeout_secs}s)")]
608    SessionExpired {
609        session_id: String,
610        inactive_secs: u64,
611        timeout_secs: u64,
612    },
613
614    /// Session has been closed
615    #[error("Session '{session_id}' has been closed")]
616    SessionClosed { session_id: String },
617
618    /// Maximum number of sessions exceeded
619    #[error("Maximum number of sessions ({max}) exceeded")]
620    MaxSessionsExceeded { max: usize },
621
622    /// Cannot reopen an ephemeral session
623    #[error("Session '{session_id}' is not persistent and cannot be reopened")]
624    SessionNotPersistent { session_id: String },
625
626    // ============ Reliability errors (v0.5.0) ============
627    /// Database has been closed
628    #[error("Database is closed")]
629    DatabaseClosed,
630
631    /// Recovery failed
632    #[error("Recovery failed: {message}")]
633    RecoveryFailed { message: String },
634}
635
636impl Error {
637    /// Create a new internal error
638    pub fn internal(msg: impl Into<String>) -> Self {
639        Error::Internal(msg.into())
640    }
641
642    /// Create a new invalid query error
643    pub fn invalid_query(msg: impl Into<String>) -> Self {
644        Error::InvalidQuery {
645            message: msg.into(),
646        }
647    }
648
649    /// Create a column not found error with optional suggestion
650    pub fn column_not_found(
651        column: impl Into<String>,
652        table: impl Into<String>,
653        suggestion: Option<String>,
654    ) -> Self {
655        Error::ColumnNotFound {
656            column: column.into(),
657            table: table.into(),
658            suggestion,
659        }
660    }
661
662    /// Create a rich query error
663    pub fn query_error(
664        message: impl Into<String>,
665        sql: impl Into<String>,
666        line: usize,
667        column: usize,
668    ) -> Self {
669        Error::Query(QueryError::new(message, sql, line, column))
670    }
671
672    /// Create a query error with suggestion
673    pub fn query_error_with_suggestion(
674        message: impl Into<String>,
675        sql: impl Into<String>,
676        line: usize,
677        column: usize,
678        suggestion: impl Into<String>,
679    ) -> Self {
680        Error::Query(QueryError::new(message, sql, line, column).with_suggestion(suggestion))
681    }
682
683    /// Create a new unsupported feature error
684    pub fn unsupported(feature: impl Into<String>) -> Self {
685        Error::Unsupported {
686            feature: feature.into(),
687        }
688    }
689
690    /// Create a transaction timeout error
691    pub fn transaction_timeout(transaction_id: u64, elapsed_ms: u64, timeout_ms: u64) -> Self {
692        Error::TransactionTimeout {
693            transaction_id,
694            elapsed_ms,
695            timeout_ms,
696        }
697    }
698
699    /// Check if this error is retriable (e.g., transaction conflict)
700    pub fn is_retriable(&self) -> bool {
701        matches!(
702            self,
703            Error::TransactionConflict | Error::WriteConflict { .. } | Error::DatabaseLocked
704        )
705    }
706
707    /// Check if this error is a user error (vs internal/system error)
708    pub fn is_user_error(&self) -> bool {
709        matches!(
710            self,
711            Error::TableNotFound { .. }
712                | Error::ColumnNotFound { .. }
713                | Error::SyntaxError { .. }
714                | Error::TypeError { .. }
715                | Error::UniqueViolation { .. }
716                | Error::PrimaryKeyViolation { .. }
717                | Error::NotNullViolation { .. }
718                | Error::ForeignKeyViolation { .. }
719                | Error::CheckViolation { .. }
720                | Error::InvalidQuery { .. }
721                | Error::Query(_)
722        )
723    }
724
725    /// Try to get the QueryError if this is a query error
726    pub fn as_query_error(&self) -> Option<&QueryError> {
727        match self {
728            Error::Query(e) => Some(e),
729            _ => None,
730        }
731    }
732}
733
734#[cfg(test)]
735mod tests {
736    use super::*;
737
738    #[test]
739    fn test_levenshtein_distance() {
740        assert_eq!(levenshtein_distance("", ""), 0);
741        assert_eq!(levenshtein_distance("abc", "abc"), 0);
742        assert_eq!(levenshtein_distance("abc", "abd"), 1);
743        assert_eq!(levenshtein_distance("abc", "abcd"), 1);
744        assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
745        assert_eq!(levenshtein_distance("FORM", "FROM"), 2);
746        assert_eq!(levenshtein_distance("naem", "name"), 2);
747    }
748
749    #[test]
750    fn test_find_best_match() {
751        let candidates = vec!["name", "email", "age", "id"];
752
753        assert_eq!(find_best_match("naem", &candidates, 2), Some("name"));
754        assert_eq!(find_best_match("emai", &candidates, 2), Some("email"));
755        assert_eq!(find_best_match("NAME", &candidates, 2), Some("name")); // case insensitive
756        assert_eq!(find_best_match("xyz", &candidates, 2), None);
757    }
758
759    #[test]
760    fn test_suggest_keyword() {
761        assert_eq!(suggest_keyword("FORM"), Some("FROM"));
762        assert_eq!(suggest_keyword("SELEKT"), Some("SELECT"));
763        assert_eq!(suggest_keyword("WHRE"), Some("WHERE"));
764        assert_eq!(suggest_keyword("SELECT"), None); // correct keyword
765    }
766
767    #[test]
768    fn test_query_context_position() {
769        let ctx = QueryContext::new("SELECT *\nFROM users\nWHERE id = 1");
770
771        assert_eq!(ctx.position_from_offset(0), (1, 1)); // 'S'
772        assert_eq!(ctx.position_from_offset(7), (1, 8)); // '*'
773        assert_eq!(ctx.position_from_offset(9), (2, 1)); // 'F'
774        assert_eq!(ctx.position_from_offset(14), (2, 6)); // 'u'
775    }
776
777    #[test]
778    fn test_query_error_format() {
779        let err = QueryError::new("Column 'naem' not found", "SELECT naem FROM users", 1, 8)
780            .with_span(4)
781            .with_suggestion("Did you mean 'name'?");
782
783        let formatted = err.format_with_context();
784        assert!(formatted.contains("Column 'naem' not found"));
785        assert!(formatted.contains("query:1:8"));
786        assert!(formatted.contains("SELECT naem FROM users"));
787        assert!(formatted.contains("Did you mean 'name'?"));
788    }
789
790    #[test]
791    fn test_transaction_timeout_error() {
792        let err = Error::transaction_timeout(123, 6000, 5000);
793        let msg = err.to_string();
794        assert!(msg.contains("Transaction 123"));
795        assert!(msg.contains("exceeded timeout of 5000ms"));
796        assert!(msg.contains("elapsed: 6000ms"));
797    }
798
799    #[test]
800    fn test_transaction_timeout_error_display() {
801        let err = Error::TransactionTimeout {
802            transaction_id: 456,
803            elapsed_ms: 8000,
804            timeout_ms: 7000,
805        };
806        let msg = format!("{}", err);
807        assert!(msg.contains("456"));
808        assert!(msg.contains("8000"));
809        assert!(msg.contains("7000"));
810    }
811
812    #[test]
813    fn test_storage_limit_exceeded_error() {
814        let err = Error::StorageLimitExceeded {
815            current_bytes: 95_000_000,
816            limit_bytes: 100_000_000,
817            operation_bytes: 10_000_000,
818        };
819        let msg = err.to_string();
820        assert!(msg.contains("Database size limit exceeded"));
821        assert!(msg.contains("95000000"));
822        assert!(msg.contains("100000000"));
823        assert!(msg.contains("10000000"));
824    }
825
826    #[test]
827    fn test_wal_limit_exceeded_error() {
828        let err = Error::WalLimitExceeded {
829            current_bytes: 10_000_000,
830            limit_bytes: 10_485_760,
831        };
832        let msg = err.to_string();
833        assert!(msg.contains("WAL size limit exceeded"));
834        assert!(msg.contains("10000000"));
835        assert!(msg.contains("10485760"));
836    }
837
838    #[test]
839    fn test_insufficient_disk_space_error() {
840        let err = Error::InsufficientDiskSpace {
841            available_bytes: 50_000_000,
842            required_bytes: 100_000_000,
843        };
844        let msg = err.to_string();
845        assert!(msg.contains("Insufficient disk space"));
846        assert!(msg.contains("50000000"));
847        assert!(msg.contains("100000000"));
848    }
849}