Skip to main content

qail_core/
error.rs

1//! Error types for QAIL.
2
3/// Error types for QAIL operations.
4#[derive(Debug)]
5pub enum QailError {
6    /// Failed to parse the QAIL query string.
7    Parse {
8        /// Byte offset of the error.
9        position: usize,
10        /// Human-readable error message.
11        message: String,
12    },
13
14    /// Invalid action (must be get, set, del, or add).
15    InvalidAction(String),
16
17    /// Required syntax symbol is missing.
18    MissingSymbol {
19        /// The missing symbol.
20        symbol: &'static str,
21        /// Description of the expected symbol.
22        description: &'static str,
23    },
24
25    /// Invalid operator in expression.
26    InvalidOperator(String),
27
28    /// Invalid value in expression.
29    InvalidValue(String),
30
31    /// Database-layer error.
32    Database(String),
33
34    /// Connection-layer error.
35    Connection(String),
36
37    /// Execution-layer error.
38    Execution(String),
39
40    /// Validation error.
41    Validation(String),
42
43    /// Configuration error.
44    Config(String),
45
46    /// I/O error.
47    Io(std::io::Error),
48}
49
50impl QailError {
51    /// Create a parse error at the given position.
52    pub fn parse(position: usize, message: impl Into<String>) -> Self {
53        Self::Parse {
54            position,
55            message: message.into(),
56        }
57    }
58
59    /// Create a missing symbol error.
60    pub fn missing(symbol: &'static str, description: &'static str) -> Self {
61        Self::MissingSymbol {
62            symbol,
63            description,
64        }
65    }
66}
67
68impl std::fmt::Display for QailError {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        match self {
71            Self::Parse { position, message } => {
72                write!(f, "Parse error at position {position}: {message}")
73            }
74            Self::InvalidAction(action) => {
75                write!(
76                    f,
77                    "Invalid action: '{action}'. Expected: get, set, del, or add"
78                )
79            }
80            Self::MissingSymbol {
81                symbol,
82                description,
83            } => {
84                write!(f, "Missing required symbol: {symbol} ({description})")
85            }
86            Self::InvalidOperator(op) => write!(f, "Invalid operator: '{op}'"),
87            Self::InvalidValue(value) => write!(f, "Invalid value: {value}"),
88            Self::Database(msg) => write!(f, "Database error: {msg}"),
89            Self::Connection(msg) => write!(f, "Connection error: {msg}"),
90            Self::Execution(msg) => write!(f, "Execution error: {msg}"),
91            Self::Validation(msg) => write!(f, "Validation error: {msg}"),
92            Self::Config(msg) => write!(f, "Configuration error: {msg}"),
93            Self::Io(err) => write!(f, "IO error: {err}"),
94        }
95    }
96}
97
98impl std::error::Error for QailError {
99    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
100        match self {
101            Self::Io(err) => Some(err),
102            _ => None,
103        }
104    }
105}
106
107impl From<std::io::Error> for QailError {
108    fn from(value: std::io::Error) -> Self {
109        Self::Io(value)
110    }
111}
112
113/// Result type alias for QAIL operations.
114pub type QailResult<T> = Result<T, QailError>;
115
116/// Error type for query-builder operations.
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum QailBuildError {
119    /// RLS insertion cannot safely align positional values without columns.
120    RlsInsertRequiresExplicitColumns {
121        /// Target table being scoped.
122        table: String,
123        /// Tenant column that would be injected.
124        tenant_column: String,
125    },
126
127    /// Runtime relation registry lock failed.
128    RelationRegistryLock(String),
129
130    /// Relation metadata has more than one possible join edge.
131    AmbiguousRelation {
132        /// Source table.
133        from_table: String,
134        /// Related table.
135        to_table: String,
136        /// Number of registered foreign-key candidates.
137        foreign_key_count: usize,
138    },
139
140    /// No schema relation could be found for an implicit join.
141    RelationNotFound {
142        /// Current table.
143        from_table: String,
144        /// Requested related table.
145        to_table: String,
146    },
147}
148
149impl std::fmt::Display for QailBuildError {
150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151        match self {
152            Self::RlsInsertRequiresExplicitColumns {
153                table,
154                tenant_column,
155            } => write!(
156                f,
157                "with_rls requires explicit columns for positional INSERT payloads on table '{table}' (tenant column '{tenant_column}')"
158            ),
159            Self::RelationRegistryLock(msg) => write!(f, "Relation registry lock error: {msg}"),
160            Self::AmbiguousRelation {
161                from_table,
162                to_table,
163                foreign_key_count,
164            } => write!(
165                f,
166                "Ambiguous relation between '{from_table}' and '{to_table}': {foreign_key_count} foreign keys registered. Use an explicit join condition."
167            ),
168            Self::RelationNotFound {
169                from_table,
170                to_table,
171            } => write!(
172                f,
173                "No relation found between '{from_table}' and '{to_table}'. Define a ref: in schema.qail or use load_schema_relations() first."
174            ),
175        }
176    }
177}
178
179impl std::error::Error for QailBuildError {}
180
181/// Result type alias for query-builder operations.
182pub type QailBuildResult<T> = Result<T, QailBuildError>;
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_error_display() {
190        let err = QailError::parse(5, "unexpected character");
191        assert_eq!(
192            err.to_string(),
193            "Parse error at position 5: unexpected character"
194        );
195    }
196}