1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum DiagnosticSeverity {
6 Warning,
8 Error,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14#[non_exhaustive]
15pub enum DiagnosticCode {
16 UnsupportedArrowType,
18 LossyConversionRequiresPolicy,
20 PolicyApplied,
22 IdentifierInvalid,
24 IdentifierTooLong,
26 DecimalOutOfRange,
28 IntegerOutOfRange,
30 TimestampOutOfRange,
32 TimezoneUnsupported,
37 SchemaMismatch,
39 BackendUnavailable,
41 ProfileDependentConversion,
43 ObservedDataRequired,
45 ValueConversionUnsupported,
47 ValueTypeMismatch,
49 NullInNonNullableColumn,
51 NonFiniteFloat,
53 ValueTooLong,
55 RowIndexOutOfBounds,
57 DirectEncodingInvalidPayload,
59 DirectEncodingUnsupportedMapping,
61 DirectEncodingUnsupportedBatch,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Hash)]
67pub struct FieldRef {
68 index: usize,
69 name: String,
70}
71
72impl FieldRef {
73 pub fn new(index: usize, name: impl Into<String>) -> Self {
75 Self {
76 index,
77 name: name.into(),
78 }
79 }
80
81 pub const fn index(&self) -> usize {
83 self.index
84 }
85
86 pub fn name(&self) -> &str {
88 &self.name
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct Diagnostic {
95 severity: DiagnosticSeverity,
96 code: DiagnosticCode,
97 message: String,
98 field: Option<FieldRef>,
99 row: Option<usize>,
100}
101
102impl Diagnostic {
103 pub fn new(
105 severity: DiagnosticSeverity,
106 code: DiagnosticCode,
107 message: impl Into<String>,
108 ) -> Self {
109 Self {
110 severity,
111 code,
112 message: message.into(),
113 field: None,
114 row: None,
115 }
116 }
117
118 pub fn warning(code: DiagnosticCode, message: impl Into<String>) -> Self {
120 Self::new(DiagnosticSeverity::Warning, code, message)
121 }
122
123 pub fn error(code: DiagnosticCode, message: impl Into<String>) -> Self {
125 Self::new(DiagnosticSeverity::Error, code, message)
126 }
127
128 #[must_use]
130 pub fn with_field(mut self, field: FieldRef) -> Self {
131 self.field = Some(field);
132 self
133 }
134
135 #[must_use]
137 pub const fn with_row(mut self, row: usize) -> Self {
138 self.row = Some(row);
139 self
140 }
141
142 pub const fn severity(&self) -> DiagnosticSeverity {
144 self.severity
145 }
146
147 pub const fn code(&self) -> DiagnosticCode {
149 self.code
150 }
151
152 pub fn message(&self) -> &str {
154 &self.message
155 }
156
157 pub fn field(&self) -> Option<&FieldRef> {
159 self.field.as_ref()
160 }
161
162 pub const fn row(&self) -> Option<usize> {
164 self.row
165 }
166
167 pub const fn is_error(&self) -> bool {
169 matches!(self.severity, DiagnosticSeverity::Error)
170 }
171}
172
173#[derive(Debug, Clone, Default, PartialEq, Eq)]
175pub struct DiagnosticSet {
176 diagnostics: Vec<Diagnostic>,
177}
178
179impl DiagnosticSet {
180 pub const fn new() -> Self {
182 Self {
183 diagnostics: Vec::new(),
184 }
185 }
186
187 pub fn push(&mut self, diagnostic: Diagnostic) {
189 self.diagnostics.push(diagnostic);
190 }
191
192 pub fn all(&self) -> &[Diagnostic] {
194 &self.diagnostics
195 }
196
197 pub fn is_empty(&self) -> bool {
199 self.diagnostics.is_empty()
200 }
201
202 pub fn has_errors(&self) -> bool {
204 self.diagnostics.iter().any(Diagnostic::is_error)
205 }
206
207 pub fn len(&self) -> usize {
209 self.diagnostics.len()
210 }
211}
212
213impl From<Vec<Diagnostic>> for DiagnosticSet {
214 fn from(diagnostics: Vec<Diagnostic>) -> Self {
215 Self { diagnostics }
216 }
217}
218
219impl IntoIterator for DiagnosticSet {
220 type Item = Diagnostic;
221 type IntoIter = std::vec::IntoIter<Diagnostic>;
222
223 fn into_iter(self) -> Self::IntoIter {
224 self.diagnostics.into_iter()
225 }
226}
227
228#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct PlanOutcome<T> {
231 value: T,
232 diagnostics: DiagnosticSet,
233}
234
235impl<T> PlanOutcome<T> {
236 pub const fn new(value: T, diagnostics: DiagnosticSet) -> Self {
238 Self { value, diagnostics }
239 }
240
241 pub const fn value(&self) -> &T {
243 &self.value
244 }
245
246 pub const fn diagnostics(&self) -> &DiagnosticSet {
248 &self.diagnostics
249 }
250
251 pub fn into_parts(self) -> (T, DiagnosticSet) {
253 (self.value, self.diagnostics)
254 }
255
256 pub fn into_value(self) -> T {
258 self.value
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::{
265 Diagnostic, DiagnosticCode, DiagnosticSet, DiagnosticSeverity, FieldRef, PlanOutcome,
266 };
267
268 #[test]
269 fn creates_field_diagnostic() {
270 let diagnostic = Diagnostic::warning(DiagnosticCode::PolicyApplied, "policy applied")
271 .with_field(FieldRef::new(2, "amount"));
272
273 assert_eq!(diagnostic.severity(), DiagnosticSeverity::Warning);
274 assert_eq!(diagnostic.code(), DiagnosticCode::PolicyApplied);
275 assert_eq!(diagnostic.message(), "policy applied");
276
277 let field = diagnostic.field().unwrap();
278 assert_eq!(field.index(), 2);
279 assert_eq!(field.name(), "amount");
280 assert_eq!(diagnostic.row(), None);
281 }
282
283 #[test]
284 fn creates_row_and_field_diagnostic() {
285 let diagnostic = Diagnostic::error(
286 DiagnosticCode::NullInNonNullableColumn,
287 "null value cannot be written",
288 )
289 .with_field(FieldRef::new(3, "name"))
290 .with_row(42);
291
292 assert_eq!(diagnostic.severity(), DiagnosticSeverity::Error);
293 assert_eq!(diagnostic.code(), DiagnosticCode::NullInNonNullableColumn);
294 assert_eq!(diagnostic.row(), Some(42));
295
296 let field = diagnostic.field().unwrap();
297 assert_eq!(field.index(), 3);
298 assert_eq!(field.name(), "name");
299 }
300
301 #[test]
302 fn detects_error_diagnostics() {
303 let mut diagnostics = DiagnosticSet::new();
304 diagnostics.push(Diagnostic::warning(
305 DiagnosticCode::PolicyApplied,
306 "policy applied",
307 ));
308
309 assert!(!diagnostics.has_errors());
310
311 diagnostics.push(Diagnostic::error(
312 DiagnosticCode::UnsupportedArrowType,
313 "unsupported",
314 ));
315
316 assert!(diagnostics.has_errors());
317 assert_eq!(diagnostics.len(), 2);
318 }
319
320 #[test]
321 fn empty_diagnostic_set_has_no_errors() {
322 let diagnostics = DiagnosticSet::new();
323
324 assert!(diagnostics.is_empty());
325 assert!(!diagnostics.has_errors());
326 assert_eq!(diagnostics.all(), &[]);
327 }
328
329 #[test]
330 fn converts_from_vec() {
331 let diagnostics = DiagnosticSet::from(vec![Diagnostic::error(
332 DiagnosticCode::IdentifierInvalid,
333 "invalid",
334 )]);
335
336 assert_eq!(diagnostics.len(), 1);
337 assert!(!diagnostics.is_empty());
338 }
339
340 #[test]
341 fn preserves_diagnostic_order_when_consumed() {
342 let diagnostics = DiagnosticSet::from(vec![
343 Diagnostic::warning(DiagnosticCode::PolicyApplied, "first"),
344 Diagnostic::error(DiagnosticCode::SchemaMismatch, "second"),
345 ]);
346
347 let messages = diagnostics
348 .into_iter()
349 .map(|diagnostic| diagnostic.message().to_owned())
350 .collect::<Vec<_>>();
351
352 assert_eq!(messages, ["first", "second"]);
353 }
354
355 #[test]
356 fn plan_outcome_exposes_value_and_diagnostics() {
357 let diagnostics = DiagnosticSet::from(vec![Diagnostic::warning(
358 DiagnosticCode::ProfileDependentConversion,
359 "policy needed",
360 )]);
361 let outcome = PlanOutcome::new("plan", diagnostics);
362
363 assert_eq!(outcome.value(), &"plan");
364 assert_eq!(outcome.diagnostics().len(), 1);
365
366 let (value, diagnostics) = outcome.into_parts();
367 assert_eq!(value, "plan");
368 assert_eq!(diagnostics.len(), 1);
369 }
370}