1use crate::PageId;
4use std::fmt;
5use thiserror::Error;
6
7pub type Result<T> = std::result::Result<T, Error>;
9
10#[derive(Debug, Clone, Default)]
12pub struct QueryContext {
13 pub sql: String,
15 pub line: usize,
17 pub column: usize,
19}
20
21impl QueryContext {
22 pub fn new(sql: impl Into<String>) -> Self {
24 QueryContext {
25 sql: sql.into(),
26 line: 1,
27 column: 1,
28 }
29 }
30
31 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 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#[derive(Debug, Clone)]
63pub struct QueryError {
64 pub message: String,
66 pub sql: String,
68 pub line: usize,
70 pub column: usize,
72 pub suggestion: Option<String>,
74 pub help: Option<String>,
76 pub span_length: usize,
78}
79
80impl QueryError {
81 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 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
101 self.suggestion = Some(suggestion.into());
102 self
103 }
104
105 pub fn with_help(mut self, help: impl Into<String>) -> Self {
107 self.help = Some(help.into());
108 self
109 }
110
111 pub fn with_span(mut self, length: usize) -> Self {
113 self.span_length = length;
114 self
115 }
116
117 pub fn format_with_context(&self) -> String {
119 let mut output = String::new();
120
121 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 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 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 if let Some(ref suggestion) = self.suggestion {
142 output.push_str(&format!(" = suggestion: {}\n", suggestion));
143 }
144
145 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
162pub 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
204pub 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 for &candidate in candidates {
214 if candidate.to_lowercase() == input_lower {
215 return Some(candidate);
216 }
217 }
218
219 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
238pub fn suggest_keyword(typo: &str) -> Option<&'static str> {
240 let typo_upper = typo.to_uppercase();
241 match typo_upper.as_str() {
242 "SELCT" | "SELEC" | "SLECT" | "SELET" | "SEELCT" | "SELEKT" => Some("SELECT"),
244 "FORM" | "FOMR" | "FRM" | "FRMO" | "FRON" => Some("FROM"),
246 "WHRE" | "WEHRE" | "WHER" | "WHEER" | "WAERE" => Some("WHERE"),
248 "INSRT" | "INSER" | "INSET" | "INSRET" | "ISERT" => Some("INSERT"),
250 "ITNO" | "INOT" | "INT" | "INTO" => Some("INTO"),
252 "UDPATE" | "UPDAE" | "UPDAT" | "UPDTE" | "UPADTE" => Some("UPDATE"),
254 "DELTE" | "DELEET" | "DELET" | "DELETEE" | "DELATE" => Some("DELETE"),
256 "CRAETE" | "CREAT" | "CRATE" | "CREAE" | "CRETAE" => Some("CREATE"),
258 "TABEL" | "TABL" | "TABLEE" | "TALBE" => Some("TABLE"),
260 "VALEUS" | "VLAUES" | "VALES" | "VALUESS" => Some("VALUES"),
262 "OREDR" | "ORDR" | "ODER" | "ORDEER" => Some("ORDER"),
264 "GRUOP" | "GOUP" | "GROPU" | "GROP" => Some("GROUP"),
266 "JION" | "JOUN" | "JON" | "JOINN" => Some("JOIN"),
268 "LFET" | "LETF" | "LEF" | "LEFTT" => Some("LEFT"),
270 "RIGTH" | "RIGT" | "RIHGT" | "RIGHTT" => Some("RIGHT"),
272 "INER" | "INNR" | "INEER" | "INNAR" => Some("INNER"),
274 "OTER" | "OUTR" | "OUUTER" | "OUTEER" => Some("OUTER"),
276 "LIMT" | "LIMI" | "LIMTI" | "LMIT" => Some("LIMIT"),
278 "OFFEST" | "OFFSE" | "OFSET" | "OFFET" => Some("OFFSET"),
280 "HAVNG" | "HAVIG" | "AHVING" | "HVAIN" => Some("HAVING"),
282 "DISTINT" | "DISTICT" | "DISTNCT" | "DISITINCT" => Some("DISTINCT"),
284 "AN" | "AD" | "ANN" | "ANDD" => Some("AND"),
286 "OOR" | "ORR" => Some("OR"),
288 "NOTT" | "NTO" | "NOO" => Some("NOT"),
290 "NOLL" | "NULLL" | "NUL" | "NUUL" => Some("NULL"),
292 "INDX" | "IDNEX" | "INEDX" | "INDXE" => Some("INDEX"),
294 "DRPO" | "DOP" | "DORP" | "DROOP" => Some("DROP"),
296 "ALTR" | "ALTRE" | "ATLER" | "ALETR" => Some("ALTER"),
298 "SE" | "SETT" | "STE" => Some("SET"),
300 "ASS" => Some("AS"),
302 "BYY" => Some("BY"),
304 "PRIMAY" | "PRIAMRY" | "PIRMARY" | "PRIMRY" => Some("PRIMARY"),
306 "KYE" | "KEE" | "KEYY" => Some("KEY"),
308 "UNIQE" | "UNQUE" | "UINQUE" | "UNIQUEE" => Some("UNIQUE"),
310 "DEFUALT" | "DFAULT" | "DEAFULT" | "DEFALT" => Some("DEFAULT"),
312 "CONSTRAIN" | "CONSTRAING" | "CONSTRIANT" => Some("CONSTRAINT"),
314 "FORIEGN" | "FOREGIN" | "FORIGN" | "FOERIGN" => Some("FOREIGN"),
316 "REFERNCES" | "REFERECES" | "REFERNECE" | "REFRENCES" => Some("REFERENCES"),
318 "CASCAD" | "CASCASE" | "CASCADEE" => Some("CASCADE"),
320 "TRANACTION" | "TRASACTION" | "TRANSCATION" => Some("TRANSACTION"),
322 "BEGINN" | "BGIN" | "BIGIN" | "BEGN" => Some("BEGIN"),
324 "COMIT" | "COMMITT" | "COMMMIT" | "COMMTI" => Some("COMMIT"),
326 "ROLBACK" | "ROLLBCK" | "ROOLBACK" | "ROLLBAK" => Some("ROLLBACK"),
328 _ => None,
329 }
330}
331
332#[derive(Debug, Clone, Error)]
334pub enum ConfigError {
335 #[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 #[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 #[error("Configuration error: {field} has invalid value: {reason}")]
353 InvalidValue { field: String, reason: String },
354}
355
356#[derive(Debug, Error)]
358pub enum Error {
359 #[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 #[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 #[error(
377 "Ambiguous column '{column}'. Could be from: {tables}. Please qualify with table name."
378 )]
379 AmbiguousColumn { column: String, tables: String },
380
381 #[error("Index '{index}' not found on table '{table}'")]
383 IndexNotFound { index: String, table: String },
384
385 #[error("Syntax error at line {line}, column {column}: {message}")]
387 SyntaxError {
388 message: String,
389 line: usize,
390 column: usize,
391 },
392
393 #[error("Type mismatch: expected {expected}, got {actual}")]
395 TypeError { expected: String, actual: String },
396
397 #[error("unique constraint violated: duplicate value in column '{column}' of table '{table}'")]
399 UniqueViolation { table: String, column: String },
400
401 #[error("Primary key violation: duplicate key in table '{table}'")]
403 PrimaryKeyViolation { table: String },
404
405 #[error("NOT NULL constraint failed: {table}.{column}")]
407 NotNullViolation { table: String, column: String },
408
409 #[error("Foreign key constraint failed: {details}")]
411 ForeignKeyViolation { details: String },
412
413 #[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 #[error("Transaction conflict detected, please retry")]
424 TransactionConflict,
425
426 #[error("Write conflict on table '{table}': another transaction modified the same row and committed first")]
428 WriteConflict { table: String },
429
430 #[error("Transaction already ended")]
432 TransactionEnded,
433
434 #[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 #[error("Deadlock detected: cycle {cycle:?}, transaction {victim} selected as victim")]
446 DeadlockDetected { cycle: Vec<u64>, victim: u64 },
447
448 #[error("Savepoint '{name}' not found")]
450 SavepointNotFound { name: String },
451
452 #[error("Table '{table}' already exists")]
454 TableAlreadyExists { table: String },
455
456 #[error("Index '{index}' already exists")]
458 IndexAlreadyExists { index: String },
459
460 #[error("Database is read-only")]
462 ReadOnly,
463
464 #[error("Database is locked by another process")]
466 DatabaseLocked,
467
468 #[error("Invalid query: {message}")]
470 InvalidQuery { message: String },
471
472 #[error("{0}")]
474 Query(#[from] QueryError),
475
476 #[error("Unsupported feature: {feature}")]
478 Unsupported { feature: String },
479
480 #[error("{0}")]
482 Config(#[from] ConfigError),
483
484 #[error("Page {0:?} is corrupted")]
487 CorruptedPage(PageId),
488
489 #[error("Write-ahead log is corrupted: {message}")]
491 CorruptedWal { message: String },
492
493 #[error("Invalid page type {page_type} for page {page_id:?}")]
495 InvalidPageType { page_id: PageId, page_type: u8 },
496
497 #[error("Page {0:?} not found")]
499 PageNotFound(PageId),
500
501 #[error("Page {0:?} was already freed")]
503 DoubleFree(PageId),
504
505 #[error("Buffer pool is full ({pinned} of {capacity} pages pinned)")]
507 BufferPoolFull { capacity: usize, pinned: usize },
508
509 #[error("Invalid database file: {message}")]
511 InvalidDatabaseFile { message: String },
512
513 #[error("Database format version {file_version} is not supported (expected {expected})")]
515 VersionMismatch { file_version: u32, expected: u32 },
516
517 #[error("Internal error: {0}")]
519 Internal(String),
520
521 #[error("I/O error: {0}")]
524 Io(#[from] std::io::Error),
525
526 #[error("Database file not found: {path}")]
528 FileNotFound { path: String },
529
530 #[error("Serialization error: {0}")]
532 Serialization(String),
533
534 #[error("Compression failed: {message}")]
537 CompressionError { message: String },
538
539 #[error("Decompression failed: {message} (expected {expected_size} bytes)")]
541 DecompressionError {
542 message: String,
543 expected_size: usize,
544 },
545
546 #[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 #[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 #[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 #[error("Authentication failed: {reason}")]
574 AuthenticationFailed { reason: String },
575
576 #[error("This database requires authentication. Provide an API key in config.")]
578 AuthenticationRequired,
579
580 #[error("API key '{key_id}' not found")]
582 ApiKeyNotFound { key_id: String },
583
584 #[error("API key '{key_id}' has expired")]
586 ApiKeyExpired { key_id: String },
587
588 #[error("API key '{key_id}' has been revoked")]
590 ApiKeyRevoked { key_id: String },
591
592 #[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 #[error("Session '{session_id}' not found")]
604 SessionNotFound { session_id: String },
605
606 #[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 #[error("Session '{session_id}' has been closed")]
616 SessionClosed { session_id: String },
617
618 #[error("Maximum number of sessions ({max}) exceeded")]
620 MaxSessionsExceeded { max: usize },
621
622 #[error("Session '{session_id}' is not persistent and cannot be reopened")]
624 SessionNotPersistent { session_id: String },
625
626 #[error("Database is closed")]
629 DatabaseClosed,
630
631 #[error("Recovery failed: {message}")]
633 RecoveryFailed { message: String },
634}
635
636impl Error {
637 pub fn internal(msg: impl Into<String>) -> Self {
639 Error::Internal(msg.into())
640 }
641
642 pub fn invalid_query(msg: impl Into<String>) -> Self {
644 Error::InvalidQuery {
645 message: msg.into(),
646 }
647 }
648
649 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 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 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 pub fn unsupported(feature: impl Into<String>) -> Self {
685 Error::Unsupported {
686 feature: feature.into(),
687 }
688 }
689
690 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 pub fn is_retriable(&self) -> bool {
701 matches!(
702 self,
703 Error::TransactionConflict | Error::WriteConflict { .. } | Error::DatabaseLocked
704 )
705 }
706
707 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 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")); 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); }
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)); assert_eq!(ctx.position_from_offset(7), (1, 8)); assert_eq!(ctx.position_from_offset(9), (2, 1)); assert_eq!(ctx.position_from_offset(14), (2, 6)); }
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}