1#[derive(Debug)]
5pub enum QailError {
6 Parse {
8 position: usize,
10 message: String,
12 },
13
14 InvalidAction(String),
16
17 MissingSymbol {
19 symbol: &'static str,
21 description: &'static str,
23 },
24
25 InvalidOperator(String),
27
28 InvalidValue(String),
30
31 Database(String),
33
34 Connection(String),
36
37 Execution(String),
39
40 Validation(String),
42
43 Config(String),
45
46 Io(std::io::Error),
48}
49
50impl QailError {
51 pub fn parse(position: usize, message: impl Into<String>) -> Self {
53 Self::Parse {
54 position,
55 message: message.into(),
56 }
57 }
58
59 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
113pub type QailResult<T> = Result<T, QailError>;
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum QailBuildError {
119 RlsInsertRequiresExplicitColumns {
121 table: String,
123 tenant_column: String,
125 },
126
127 RlsTenantColumnMutationDenied {
129 table: String,
131 tenant_column: String,
133 },
134
135 RlsMergeSourceTenantProjectionRequired {
137 table: String,
139 tenant_column: String,
141 },
142
143 RlsRegistryUninitialized {
149 table: String,
151 },
152
153 RlsRegistryUnavailable {
159 table: String,
161 reason: String,
163 },
164
165 RlsScopeMissing {
170 table: String,
172 scope: &'static str,
174 column: String,
176 },
177
178 RlsJoinKindUnsupported {
183 table: String,
185 join_kind: String,
187 },
188
189 RlsOwnerMergeUnsupported {
191 table: String,
193 owner_column: String,
195 },
196
197 RelationRegistryLock(String),
199
200 AmbiguousRelation {
202 from_table: String,
204 to_table: String,
206 foreign_key_count: usize,
208 },
209
210 RelationNotFound {
212 from_table: String,
214 to_table: String,
216 },
217}
218
219impl std::fmt::Display for QailBuildError {
220 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 match self {
222 Self::RlsInsertRequiresExplicitColumns {
223 table,
224 tenant_column,
225 } => write!(
226 f,
227 "with_rls requires explicit columns for positional INSERT payloads on table '{table}' (tenant column '{tenant_column}')"
228 ),
229 Self::RlsTenantColumnMutationDenied {
230 table,
231 tenant_column,
232 } => write!(
233 f,
234 "with_rls rejects tenant column mutation on table '{table}' (tenant column '{tenant_column}')"
235 ),
236 Self::RlsRegistryUninitialized { table } => write!(
237 f,
238 "with_rls on table '{table}' before scope registries were declared — call qail_core::rls::init_scope_registries(&schema) or declare_policy_only_isolation(reason) at startup"
239 ),
240 Self::RlsRegistryUnavailable { table, reason } => write!(
241 f,
242 "with_rls on table '{table}' cannot read the scope registry ({reason}) — refusing to run unscoped"
243 ),
244 Self::RlsScopeMissing {
245 table,
246 scope,
247 column,
248 } => write!(
249 f,
250 "with_rls on table '{table}' requires a {scope} scope (column '{column}') but the context carries none — refusing to run unscoped"
251 ),
252 Self::RlsJoinKindUnsupported { table, join_kind } => write!(
253 f,
254 "with_rls cannot isolate RLS table '{table}' joined via {join_kind}; use INNER/LEFT/LATERAL or a CTE"
255 ),
256 Self::RlsOwnerMergeUnsupported {
257 table,
258 owner_column,
259 } => write!(
260 f,
261 "with_rls cannot owner-scope MERGE on table '{table}' (owner column '{owner_column}'); use with_rls_policy and a DB policy"
262 ),
263 Self::RlsMergeSourceTenantProjectionRequired {
264 table,
265 tenant_column,
266 } => write!(
267 f,
268 "with_rls requires MERGE query sources for table '{table}' to project tenant column '{tenant_column}'"
269 ),
270 Self::RelationRegistryLock(msg) => write!(f, "Relation registry lock error: {msg}"),
271 Self::AmbiguousRelation {
272 from_table,
273 to_table,
274 foreign_key_count,
275 } => write!(
276 f,
277 "Ambiguous relation between '{from_table}' and '{to_table}': {foreign_key_count} foreign keys registered. Use an explicit join condition."
278 ),
279 Self::RelationNotFound {
280 from_table,
281 to_table,
282 } => write!(
283 f,
284 "No relation found between '{from_table}' and '{to_table}'. Define a ref: in schema.qail or use load_schema_relations() first."
285 ),
286 }
287 }
288}
289
290impl std::error::Error for QailBuildError {}
291
292pub type QailBuildResult<T> = Result<T, QailBuildError>;
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn test_error_display() {
301 let err = QailError::parse(5, "unexpected character");
302 assert_eq!(
303 err.to_string(),
304 "Parse error at position 5: unexpected character"
305 );
306 }
307}