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    /// RLS-protected updates cannot rewrite the tenant column.
128    RlsTenantColumnMutationDenied {
129        /// Target table being scoped.
130        table: String,
131        /// Tenant column that was assigned.
132        tenant_column: String,
133    },
134
135    /// RLS-protected MERGE query sources need a tenant projection for safe row classification.
136    RlsMergeSourceTenantProjectionRequired {
137        /// Target table being scoped.
138        table: String,
139        /// Tenant column that must be projected by the source query.
140        tenant_column: String,
141    },
142
143    /// Runtime relation registry lock failed.
144    RelationRegistryLock(String),
145
146    /// Relation metadata has more than one possible join edge.
147    AmbiguousRelation {
148        /// Source table.
149        from_table: String,
150        /// Related table.
151        to_table: String,
152        /// Number of registered foreign-key candidates.
153        foreign_key_count: usize,
154    },
155
156    /// No schema relation could be found for an implicit join.
157    RelationNotFound {
158        /// Current table.
159        from_table: String,
160        /// Requested related table.
161        to_table: String,
162    },
163}
164
165impl std::fmt::Display for QailBuildError {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        match self {
168            Self::RlsInsertRequiresExplicitColumns {
169                table,
170                tenant_column,
171            } => write!(
172                f,
173                "with_rls requires explicit columns for positional INSERT payloads on table '{table}' (tenant column '{tenant_column}')"
174            ),
175            Self::RlsTenantColumnMutationDenied {
176                table,
177                tenant_column,
178            } => write!(
179                f,
180                "with_rls rejects tenant column mutation on table '{table}' (tenant column '{tenant_column}')"
181            ),
182            Self::RlsMergeSourceTenantProjectionRequired {
183                table,
184                tenant_column,
185            } => write!(
186                f,
187                "with_rls requires MERGE query sources for table '{table}' to project tenant column '{tenant_column}'"
188            ),
189            Self::RelationRegistryLock(msg) => write!(f, "Relation registry lock error: {msg}"),
190            Self::AmbiguousRelation {
191                from_table,
192                to_table,
193                foreign_key_count,
194            } => write!(
195                f,
196                "Ambiguous relation between '{from_table}' and '{to_table}': {foreign_key_count} foreign keys registered. Use an explicit join condition."
197            ),
198            Self::RelationNotFound {
199                from_table,
200                to_table,
201            } => write!(
202                f,
203                "No relation found between '{from_table}' and '{to_table}'. Define a ref: in schema.qail or use load_schema_relations() first."
204            ),
205        }
206    }
207}
208
209impl std::error::Error for QailBuildError {}
210
211/// Result type alias for query-builder operations.
212pub type QailBuildResult<T> = Result<T, QailBuildError>;
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_error_display() {
220        let err = QailError::parse(5, "unexpected character");
221        assert_eq!(
222            err.to_string(),
223            "Parse error at position 5: unexpected character"
224        );
225    }
226}