1use super::{Diagnostic, IntoDiagnostic, util::value_range};
5use crate::{
6 error::{
7 AstErrorKind, AuthErrorKind, BinaryOp, BlobEncodingKind, ConstraintKind, FunctionErrorKind, LogicalOp,
8 NetworkErrorKind, OperandCategory, ProcedureErrorKind, RuntimeErrorKind, TemporalKind, TypeError,
9 },
10 fragment::Fragment,
11 value::value_type::ValueType,
12};
13
14fn temporal_unit_name(unit: char) -> &'static str {
15 match unit {
16 'Y' => "year",
17 'M' => "month/minute",
18 'W' => "week",
19 'D' => "day",
20 'H' => "hour",
21 'S' => "second",
22 _ => "unit",
23 }
24}
25
26impl IntoDiagnostic for TypeError {
27 fn into_diagnostic(self) -> Diagnostic {
28 match self {
29 TypeError::LogicalOperatorNotApplicable {
30 operator,
31 operand_category,
32 fragment,
33 } => {
34 let code = match (&operator, &operand_category) {
35 (LogicalOp::Not, OperandCategory::Number) => "OPERATOR_001",
36 (LogicalOp::Not, OperandCategory::Text) => "OPERATOR_002",
37 (LogicalOp::Not, OperandCategory::Temporal) => "OPERATOR_003",
38 (LogicalOp::Not, OperandCategory::Uuid) => "OPERATOR_004",
39 (LogicalOp::And, OperandCategory::Number) => "OPERATOR_005",
40 (LogicalOp::And, OperandCategory::Text) => "OPERATOR_006",
41 (LogicalOp::And, OperandCategory::Temporal) => "OPERATOR_007",
42 (LogicalOp::And, OperandCategory::Uuid) => "OPERATOR_008",
43 (LogicalOp::Or, OperandCategory::Number) => "OPERATOR_009",
44 (LogicalOp::Or, OperandCategory::Text) => "OPERATOR_010",
45 (LogicalOp::Or, OperandCategory::Temporal) => "OPERATOR_011",
46 (LogicalOp::Or, OperandCategory::Uuid) => "OPERATOR_012",
47 (LogicalOp::Xor, OperandCategory::Number) => "OPERATOR_013",
48 (LogicalOp::Xor, OperandCategory::Text) => "OPERATOR_014",
49 (LogicalOp::Xor, OperandCategory::Temporal) => "OPERATOR_015",
50 (LogicalOp::Xor, OperandCategory::Uuid) => "OPERATOR_016",
51 };
52
53 let message = format!("Cannot apply {} operator to {}", operator, operand_category);
54 let label = Some(format!("logical operator on {} type", match &operand_category {
55 OperandCategory::Number => "numeric",
56 OperandCategory::Text => "text",
57 OperandCategory::Temporal => "temporal",
58 OperandCategory::Uuid => "UUID",
59 }));
60
61 let help = format!(
62 "The {} operator can only be applied to boolean values. Consider using comparison operators{}first",
63 operator,
64 match &operator {
65 LogicalOp::Not => " or casting to boolean ",
66 _ => " ",
67 }
68 );
69
70 let mut notes = vec![];
71 match &operator {
72 LogicalOp::Not => {
73 notes.push(
74 "NOT is a logical operator that inverts boolean values (true becomes false, false becomes true)"
75 .to_string(),
76 );
77 }
78 LogicalOp::And => {
79 notes.push("AND is a logical operator that combines boolean values".to_string());
80 }
81 LogicalOp::Or => {
82 notes.push("OR is a logical operator that combines boolean values".to_string());
83 }
84 LogicalOp::Xor => {
85 notes.push(
86 "XOR is a logical operator that performs exclusive or on boolean values".to_string(),
87 );
88 }
89 }
90
91 match (&operator, &operand_category) {
92 (LogicalOp::Not, OperandCategory::Number) => {
93 notes.push("For numeric negation, use the minus (-) operator instead".to_string());
94 notes.push("To convert numbers to boolean, use comparison operators like: value != 0".to_string());
95 }
96 (LogicalOp::Not, OperandCategory::Text) => {
97 notes.push("To convert text to boolean, use comparison operators like: text != '...'".to_string());
98 notes.push("For string operations, use appropriate string functions instead".to_string());
99 }
100 (LogicalOp::Not, OperandCategory::Temporal) => {
101 notes.push("To convert temporal values to boolean, use comparison operators like: date > '2023-01-01'".to_string());
102 notes.push("Temporal types include Date, DateTime, Time, and Duration".to_string());
103 }
104 (LogicalOp::Not, OperandCategory::Uuid) => {
105 notes.push("To convert UUIDs to boolean, use comparison operators like: uuid == '...'".to_string());
106 notes.push("UUID types include Uuid4 and Uuid7".to_string());
107 }
108 (_, OperandCategory::Number) => {
109 notes.push("To convert numbers to boolean, use comparison operators like: value != 0".to_string());
110 match &operator {
111 LogicalOp::And => notes.push("For bitwise operations on integers, use the bitwise AND (&) operator instead".to_string()),
112 LogicalOp::Or => notes.push("For bitwise operations on integers, use the bitwise OR (|) operator instead".to_string()),
113 LogicalOp::Xor => notes.push("For bitwise operations on integers, use the bitwise XOR (^) operator instead".to_string()),
114 _ => {}
115 }
116 }
117 (_, OperandCategory::Text) => {
118 notes.push("To convert text to boolean, use comparison operators like: text != '...'".to_string());
119 match &operator {
120 LogicalOp::And => notes.push("For text concatenation, use the string concatenation operator (||) instead".to_string()),
121 LogicalOp::Or => notes.push("For text concatenation, use the string concatenation operator (+) instead".to_string()),
122 LogicalOp::Xor => notes.push("XOR returns true when exactly one operand is true".to_string()),
123 _ => {}
124 }
125 }
126 (_, OperandCategory::Temporal) => {
127 notes.push("To convert temporal values to boolean, use comparison operators like: date > '2023-01-01'".to_string());
128 notes.push("Temporal types include Date, DateTime, Time, and Duration".to_string());
129 }
130 (_, OperandCategory::Uuid) => {
131 notes.push("To convert UUIDs to boolean, use comparison operators like: uuid == '...'".to_string());
132 notes.push("UUID types include Uuid4 and Uuid7".to_string());
133 }
134 }
135
136 Diagnostic {
137 code: code.to_string(),
138 rql: None,
139 message,
140 column: None,
141 fragment,
142 label,
143 help: Some(help),
144 notes,
145 cause: None,
146 operator_chain: None,
147 }
148 }
149
150 TypeError::BinaryOperatorNotApplicable {
151 operator,
152 left,
153 right,
154 fragment,
155 } => {
156 let code = match &operator {
157 BinaryOp::Add => "OPERATOR_017",
158 BinaryOp::Sub => "OPERATOR_018",
159 BinaryOp::Mul => "OPERATOR_019",
160 BinaryOp::Div => "OPERATOR_020",
161 BinaryOp::Rem => "OPERATOR_021",
162 BinaryOp::Equal => "OPERATOR_022",
163 BinaryOp::NotEqual => "OPERATOR_023",
164 BinaryOp::LessThan => "OPERATOR_024",
165 BinaryOp::LessThanEqual => "OPERATOR_025",
166 BinaryOp::GreaterThan => "OPERATOR_026",
167 BinaryOp::GreaterThanEqual => "OPERATOR_027",
168 BinaryOp::Between => "OPERATOR_028",
169 };
170
171 let sym = operator.symbol();
172 let message = if matches!(&operator, BinaryOp::Between) {
173 format!("Cannot apply '{}' operator to {} with range of {}", sym, left, right)
174 } else {
175 format!("Cannot apply '{}' operator to {} and {}", sym, left, right)
176 };
177 let label = Some(format!("'{}' operator on incompatible types", sym));
178
179 let mut notes = if matches!(&operator, BinaryOp::Between) {
180 vec![
181 format!("Value is of type: {}", left),
182 format!("Range bounds are of type: {}", right),
183 ]
184 } else {
185 vec![
186 format!("Left operand is of type: {}", left),
187 format!("Right operand is of type: {}", right),
188 ]
189 };
190
191 let comparison_note = match &operator {
192 BinaryOp::Add
193 | BinaryOp::Sub
194 | BinaryOp::Mul
195 | BinaryOp::Div
196 | BinaryOp::Rem => {
197 Some("Consider converting operands to compatible numeric types first".to_string())
198 }
199 BinaryOp::Equal => {
200 Some("Equality comparison is only supported between compatible types".to_string())
201 }
202 BinaryOp::NotEqual => {
203 Some("Inequality comparison is only supported between compatible types".to_string())
204 }
205 BinaryOp::LessThan => {
206 Some("Less than comparison is only supported between compatible types".to_string())
207 }
208 BinaryOp::LessThanEqual => {
209 Some("Less than or equal comparison is only supported between compatible types".to_string())
210 }
211 BinaryOp::GreaterThan => {
212 Some("Greater than comparison is only supported between compatible types".to_string())
213 }
214 BinaryOp::GreaterThanEqual => {
215 Some("Greater than or equal comparison is only supported between compatible types".to_string())
216 }
217 BinaryOp::Between => {
218 Some("BETWEEN comparison is only supported between compatible types".to_string())
219 }
220 };
221 if let Some(note) = comparison_note {
222 notes.push(note);
223 }
224
225 Diagnostic {
226 code: code.to_string(),
227 rql: None,
228 message,
229 column: None,
230 fragment,
231 label,
232 help: None,
233 notes,
234 cause: None,
235 operator_chain: None,
236 }
237 }
238
239 TypeError::UnsupportedCast { from, to, fragment } => {
240 let label = Some(format!("cannot cast {} of type {} to {}", fragment.text(), from, to));
241 Diagnostic {
242 code: "CAST_001".to_string(),
243 rql: None,
244 message: format!("unsupported cast from {} to {}", from, to),
245 fragment,
246 label,
247 help: Some("ensure the source and target types are compatible for casting".to_string()),
248 notes: vec![
249 "supported casts include: numeric to numeric, string to temporal, boolean to numeric"
250 .to_string(),
251 ],
252 column: None,
253 cause: None,
254 operator_chain: None,
255 }
256 }
257
258 TypeError::CastToNumberFailed {
259 target,
260 fragment,
261 cause,
262 } => {
263 let label = Some(format!("failed to cast to {}", target));
264 Diagnostic {
265 code: "CAST_002".to_string(),
266 rql: None,
267 message: format!("failed to cast to {}", target),
268 fragment,
269 label,
270 help: None,
271 notes: vec![],
272 column: None,
273 cause: Some(Box::new((*cause).into_diagnostic())),
274 operator_chain: None,
275 }
276 }
277
278 TypeError::CastToTemporalFailed {
279 target,
280 fragment,
281 cause,
282 } => {
283 let label = Some(format!("failed to cast to {}", target));
284 Diagnostic {
285 code: "CAST_003".to_string(),
286 rql: None,
287 message: format!("failed to cast to {}", target),
288 fragment,
289 label,
290 help: None,
291 notes: vec![],
292 column: None,
293 cause: Some(Box::new((*cause).into_diagnostic())),
294 operator_chain: None,
295 }
296 }
297
298 TypeError::CastToBooleanFailed { fragment, cause } => {
299 let label = Some("failed to cast to bool".to_string());
300 Diagnostic {
301 code: "CAST_004".to_string(),
302 rql: None,
303 message: "failed to cast to bool".to_string(),
304 fragment,
305 label,
306 help: None,
307 notes: vec![],
308 column: None,
309 cause: Some(Box::new((*cause).into_diagnostic())),
310 operator_chain: None,
311 }
312 }
313
314 TypeError::CastToUuidFailed {
315 target,
316 fragment,
317 cause,
318 } => {
319 let label = Some(format!("failed to cast to {}", target));
320 Diagnostic {
321 code: "CAST_005".to_string(),
322 rql: None,
323 message: format!("failed to cast to {}", target),
324 fragment,
325 label,
326 help: None,
327 notes: vec![],
328 column: None,
329 cause: Some(Box::new((*cause).into_diagnostic())),
330 operator_chain: None,
331 }
332 }
333
334 TypeError::CastBlobToUtf8Failed { fragment, cause } => {
335 let label = Some("failed to cast BLOB to UTF8".to_string());
336 Diagnostic {
337 code: "CAST_006".to_string(),
338 rql: None,
339 message: "failed to cast BLOB to UTF8".to_string(),
340 fragment,
341 label,
342 help: Some(
343 "BLOB contains invalid UTF-8 bytes. Consider using to_utf8_lossy() function instead"
344 .to_string(),
345 ),
346 notes: vec![],
347 column: None,
348 cause: Some(Box::new((*cause).into_diagnostic())),
349 operator_chain: None,
350 }
351 }
352
353 TypeError::ConstraintViolation {
354 kind,
355 message,
356 fragment,
357 } => {
358 let (code, help) = match &kind {
359 ConstraintKind::Utf8MaxBytes { max, .. } => (
360 "CONSTRAINT_001",
361 format!(
362 "The UTF8 field is constrained to a maximum of {} bytes. Consider shortening the text or increasing the constraint.",
363 max
364 ),
365 ),
366 ConstraintKind::BlobMaxBytes { max, .. } => (
367 "CONSTRAINT_002",
368 format!(
369 "The BLOB field is constrained to a maximum of {} bytes. Consider reducing the data size or increasing the constraint.",
370 max
371 ),
372 ),
373 ConstraintKind::IntMaxBytes { max, .. } => (
374 "CONSTRAINT_003",
375 format!(
376 "The INT field is constrained to a maximum of {} bytes. Consider using a smaller value or increasing the constraint.",
377 max
378 ),
379 ),
380 ConstraintKind::UintMaxBytes { max, .. } => (
381 "CONSTRAINT_004",
382 format!(
383 "The UINT field is constrained to a maximum of {} bytes. Consider using a smaller value or increasing the constraint.",
384 max
385 ),
386 ),
387 ConstraintKind::DecimalPrecision { max, .. } => (
388 "CONSTRAINT_005",
389 format!(
390 "The DECIMAL field is constrained to a maximum precision of {} digits. Consider using a smaller number or increasing the precision constraint.",
391 max
392 ),
393 ),
394 ConstraintKind::DecimalScale { max, .. } => (
395 "CONSTRAINT_006",
396 format!(
397 "The DECIMAL field is constrained to a maximum of {} decimal places. Consider rounding the value or increasing the scale constraint.",
398 max
399 ),
400 ),
401 ConstraintKind::NoneNotAllowed { column_type } => (
402 "CONSTRAINT_007",
403 format!(
404 "The column type is {} which does not accept none. Use Option({}) if the column should be nullable.",
405 column_type, column_type
406 ),
407 ),
408 };
409
410 Diagnostic {
411 code: code.to_string(),
412 rql: None,
413 message,
414 column: None,
415 fragment,
416 label: Some("constraint violation".to_string()),
417 help: Some(help),
418 notes: vec![],
419 cause: None,
420 operator_chain: None,
421 }
422 }
423
424 TypeError::InvalidNumberFormat { target, fragment } => {
425 let label = Some(format!("'{}' is not a valid {} number", fragment.text(), target));
426 let (help, notes) = match target {
427 ValueType::Float4 | ValueType::Float8 => (
428 "use decimal format (e.g., 123.45, -67.89, 1.23e-4)".to_string(),
429 vec![
430 "valid: 123.45".to_string(),
431 "valid: -67.89".to_string(),
432 "valid: 1.23e-4".to_string(),
433 ],
434 ),
435 ValueType::Int1
436 | ValueType::Int2
437 | ValueType::Int4
438 | ValueType::Int8
439 | ValueType::Int16
440 | ValueType::Uint1
441 | ValueType::Uint2
442 | ValueType::Uint4
443 | ValueType::Uint8
444 | ValueType::Uint16 => (
445 "use integer format (e.g., 123, -456) or decimal that can be truncated".to_string(),
446 vec![
447 "valid: 123".to_string(),
448 "valid: -456".to_string(),
449 "truncated: 123.7 → 123".to_string(),
450 ],
451 ),
452 _ => (
453 "ensure the value is a valid number".to_string(),
454 vec!["use a proper number format".to_string()],
455 ),
456 };
457
458 Diagnostic {
459 code: "NUMBER_001".to_string(),
460 rql: None,
461 message: "invalid number format".to_string(),
462 fragment,
463 label,
464 help: Some(help),
465 notes,
466 column: None,
467 cause: None,
468 operator_chain: None,
469 }
470 }
471
472 TypeError::NumberOutOfRange {
473 target,
474 fragment,
475 descriptor,
476 } => {
477 let range = value_range(target.clone());
478
479 let label = if let Some(ref desc) = descriptor {
480 Some(format!(
481 "value '{}' exceeds the valid range for {} column {}",
482 fragment.text(),
483 desc.column_type.as_ref().unwrap_or(&target),
484 desc.location_string()
485 ))
486 } else {
487 Some(format!(
488 "value '{}' exceeds the valid range for type {} ({})",
489 fragment.text(),
490 target,
491 range
492 ))
493 };
494
495 let help = if let Some(ref desc) = descriptor {
496 if desc.namespace.is_some() && desc.table.is_some() {
497 Some(format!("use a value within range {} or modify column {}", range, desc.location_string()))
498 } else {
499 Some(format!("use a value within range {} or use a wider type", range))
500 }
501 } else {
502 Some(format!("use a value within range {} or use a wider type", range))
503 };
504
505 Diagnostic {
506 code: "NUMBER_002".to_string(),
507 rql: None,
508 message: "number out of range".to_string(),
509 fragment,
510 label,
511 help,
512 notes: vec![format!("valid range: {}", range)],
513 column: None,
514 cause: None,
515 operator_chain: None,
516 }
517 }
518
519 TypeError::DivisionByZero {
520 target,
521 fragment,
522 } => Diagnostic {
523 code: "NUMBER_007".to_string(),
524 rql: None,
525 message: "division by zero".to_string(),
526 fragment,
527 label: Some(format!("divisor evaluates to zero in {} arithmetic", target)),
528 help: Some("ensure the divisor is non-zero before evaluating this expression".to_string()),
529 notes: vec![],
530 column: None,
531 cause: None,
532 operator_chain: None,
533 },
534
535 TypeError::NanNotAllowed => Diagnostic {
536 code: "NUMBER_003".to_string(),
537 rql: None,
538 message: "NaN not allowed".to_string(),
539 fragment: Fragment::None,
540 label: Some("NaN (Not a Number) values are not permitted".to_string()),
541 help: Some("use a finite number or none instead".to_string()),
542 notes: vec![],
543 column: None,
544 cause: None,
545 operator_chain: None,
546 },
547
548 TypeError::IntegerPrecisionLoss {
549 shape_type,
550 target,
551 fragment,
552 } => {
553 let is_signed = shape_type.is_signed_integer();
554 let (min_limit, max_limit) = match target {
555 ValueType::Float4 => {
556 if is_signed {
557 ("-16_777_216 (-2^24)", "16_777_216 (2^24)")
558 } else {
559 ("0", "16_777_216 (2^24)")
560 }
561 }
562 ValueType::Float8 => {
563 if is_signed {
564 ("-9_007_199_254_740_992 (-2^53)", "9_007_199_254_740_992 (2^53)")
565 } else {
566 ("0", "9_007_199_254_740_992 (2^53)")
567 }
568 }
569 _ => unreachable!("IntegerPrecisionLoss should only be used for float targets"),
570 };
571
572 let label = Some(format!(
573 "converting '{}' from {} to {} would lose precision",
574 fragment.text(),
575 shape_type,
576 target
577 ));
578
579 Diagnostic {
580 code: "NUMBER_004".to_string(),
581 rql: None,
582 message: "too large for precise float conversion".to_string(),
583 fragment,
584 label,
585 help: None,
586 notes: vec![
587 format!("{} can only represent from {} to {} precisely", target, min_limit, max_limit),
588 "consider using a different numeric type if exact precision is required".to_string(),
589 ],
590 column: None,
591 cause: None,
592 operator_chain: None,
593 }
594 }
595
596 TypeError::DecimalScaleExceedsPrecision {
597 scale,
598 precision,
599 fragment,
600 } => {
601 let label = Some(format!("scale ({}) cannot be greater than precision ({})", scale, precision));
602 Diagnostic {
603 code: "NUMBER_005".to_string(),
604 rql: None,
605 message: "decimal scale exceeds precision".to_string(),
606 fragment,
607 label,
608 help: Some(format!("use a scale value between 0 and {} or increase precision", precision)),
609 notes: vec![
610 format!("current precision: {}", precision),
611 format!("current scale: {}", scale),
612 "scale represents the number of digits after the decimal point".to_string(),
613 "precision represents the total number of significant digits".to_string(),
614 ],
615 column: None,
616 cause: None,
617 operator_chain: None,
618 }
619 }
620
621 TypeError::DecimalPrecisionInvalid { precision } => {
622 let label = Some(format!("precision ({}) must be at least 1", precision));
623 Diagnostic {
624 code: "NUMBER_006".to_string(),
625 rql: None,
626 message: "invalid decimal precision".to_string(),
627 fragment: Fragment::None,
628 label,
629 help: Some("use a precision value of at least 1".to_string()),
630 notes: vec![
631 format!("current precision: {}", precision),
632 "precision represents the total number of significant digits".to_string(),
633 ],
634 column: None,
635 cause: None,
636 operator_chain: None,
637 }
638 }
639
640 TypeError::InvalidBooleanFormat { fragment } => {
641 let value = fragment.text().to_string();
642 let label = Some(format!("expected 'true' or 'false', found '{}'", value));
643 Diagnostic {
644 code: "BOOLEAN_001".to_string(),
645 rql: None,
646 message: "invalid boolean format".to_string(),
647 fragment,
648 label,
649 help: Some("use 'true' or 'false'".to_string()),
650 notes: vec!["valid: true, TRUE".to_string(), "valid: false, FALSE".to_string()],
651 column: None,
652 cause: None,
653 operator_chain: None,
654 }
655 }
656
657 TypeError::EmptyBooleanValue { fragment } => Diagnostic {
658 code: "BOOLEAN_002".to_string(),
659 rql: None,
660 message: "empty boolean value".to_string(),
661 fragment,
662 label: Some("boolean value cannot be empty".to_string()),
663 help: Some("provide either 'true' or 'false'".to_string()),
664 notes: vec!["valid: true".to_string(), "valid: false".to_string()],
665 column: None,
666 cause: None,
667 operator_chain: None,
668 },
669
670 TypeError::InvalidNumberBoolean { fragment } => {
671 let value = fragment.text().to_string();
672 let label =
673 Some(format!("number '{}' cannot be cast to boolean, only 1 or 0 are allowed", value));
674 Diagnostic {
675 code: "BOOLEAN_003".to_string(),
676 rql: None,
677 message: "invalid boolean".to_string(),
678 fragment,
679 label,
680 help: Some("use 1 for true or 0 for false".to_string()),
681 notes: vec![
682 "valid: 1 → true".to_string(),
683 "valid: 0 → false".to_string(),
684 "invalid: any other number".to_string(),
685 ],
686 column: None,
687 cause: None,
688 operator_chain: None,
689 }
690 }
691
692 TypeError::Temporal {
693 kind,
694 message,
695 fragment,
696 } => {
697 let (code, label, help, notes) = match &kind {
698 TemporalKind::InvalidDateFormat => (
699 "TEMPORAL_001",
700 Some(format!("expected YYYY-MM-DD format, found '{}'", fragment.text())),
701 Some("use the format YYYY-MM-DD (e.g., 2024-03-15)".to_string()),
702 vec!["dates must have exactly 3 parts separated by hyphens".to_string()],
703 ),
704 TemporalKind::InvalidDateTimeFormat => (
705 "TEMPORAL_002",
706 Some(format!("expected YYYY-MM-DDTHH:MM:SS format, found '{}'", fragment.text())),
707 Some("use the format YYYY-MM-DDTHH:MM:SS[.fff][Z|±HH:MM] (e.g., 2024-03-15T14:30:45)".to_string()),
708 vec!["datetime must contain 'T' separator between date and time parts".to_string()],
709 ),
710 TemporalKind::InvalidTimeFormat => (
711 "TEMPORAL_003",
712 Some(format!("expected HH:MM:SS format, found '{}'", fragment.text())),
713 Some("use the format HH:MM:SS[.fff][Z|±HH:MM] (e.g., 14:30:45)".to_string()),
714 vec!["time must have exactly 3 parts separated by colons".to_string()],
715 ),
716 TemporalKind::InvalidDurationFormat => (
717 "TEMPORAL_004",
718 Some(format!("expected P[n]Y[n]M[n]W[n]D[T[n]H[n]M[n]S] format, found '{}'", fragment.text())),
719 Some("use ISO 8601 duration format starting with 'P' (e.g., P1D, PT2H30M, P1Y2M3DT4H5M6S)".to_string()),
720 vec![
721 "duration must start with 'P' followed by duration components".to_string(),
722 "date part: P[n]Y[n]M[n]W[n]D (years, months, weeks, days)".to_string(),
723 "time part: T[n]H[n]M[n]S (hours, minutes, seconds)".to_string(),
724 ],
725 ),
726 TemporalKind::InvalidYear => (
727 "TEMPORAL_005",
728 Some(format!("year '{}' cannot be parsed as a number", fragment.text())),
729 Some("ensure the year is a valid 4-digit number".to_string()),
730 vec!["valid examples: 2024, 1999, 2000".to_string()],
731 ),
732 TemporalKind::InvalidTimeComponentFormat { component } => (
733 "TEMPORAL_005",
734 Some(format!("{} '{}' must be exactly 2 digits", component, fragment.text())),
735 Some(format!("ensure the {} is exactly 2 digits (e.g., 09, 14, 23)", component)),
736 vec![format!("{} must be exactly 2 digits in HH:MM:SS format", component)],
737 ),
738 TemporalKind::InvalidMonth => (
739 "TEMPORAL_006",
740 Some(format!("month '{}' cannot be parsed as a number (expected 1-12)", fragment.text())),
741 Some("ensure the month is a valid number between 1 and 12".to_string()),
742 vec!["valid examples: 01, 03, 12".to_string()],
743 ),
744 TemporalKind::InvalidDay => (
745 "TEMPORAL_007",
746 Some(format!("day '{}' cannot be parsed as a number (expected 1-31)", fragment.text())),
747 Some("ensure the day is a valid number between 1 and 31".to_string()),
748 vec!["valid examples: 01, 15, 31".to_string()],
749 ),
750 TemporalKind::InvalidHour => (
751 "TEMPORAL_008",
752 Some(format!("hour '{}' cannot be parsed as a number (expected 0-23)", fragment.text())),
753 Some("ensure the hour is a valid number between 0 and 23 (use 24-hour format)".to_string()),
754 vec![
755 "valid examples: 09, 14, 23".to_string(),
756 "hours must be in 24-hour format (00-23)".to_string(),
757 ],
758 ),
759 TemporalKind::InvalidMinute => (
760 "TEMPORAL_009",
761 Some(format!("minute '{}' cannot be parsed as a number (expected 0-59)", fragment.text())),
762 Some("ensure the minute is a valid number between 0 and 59".to_string()),
763 vec!["valid examples: 00, 30, 59".to_string()],
764 ),
765 TemporalKind::InvalidSecond => (
766 "TEMPORAL_010",
767 Some(format!("second '{}' cannot be parsed as a number (expected 0-59)", fragment.text())),
768 Some("ensure the second is a valid number between 0 and 59".to_string()),
769 vec!["valid examples: 00, 30, 59".to_string()],
770 ),
771 TemporalKind::InvalidFractionalSeconds => (
772 "TEMPORAL_011",
773 Some(format!("fractional seconds '{}' cannot be parsed as a number", fragment.text())),
774 Some("ensure fractional seconds contain only digits".to_string()),
775 vec!["valid examples: 123, 999999, 000001".to_string()],
776 ),
777 TemporalKind::InvalidDateValues => (
778 "TEMPORAL_012",
779 Some(format!("date '{}' represents an invalid calendar date", fragment.text())),
780 Some("ensure the date exists in the calendar (e.g., no February 30)".to_string()),
781 vec![
782 "check month has correct number of days".to_string(),
783 "consider leap years for February 29".to_string(),
784 ],
785 ),
786 TemporalKind::InvalidTimeValues => (
787 "TEMPORAL_013",
788 Some(format!("time '{}' contains out-of-range values", fragment.text())),
789 Some("ensure hours are 0-23, minutes and seconds are 0-59".to_string()),
790 vec!["use 24-hour format for hours".to_string()],
791 ),
792 TemporalKind::InvalidDurationCharacter => (
793 "TEMPORAL_014",
794 Some(format!("character '{}' is not valid in ISO 8601 duration", fragment.text())),
795 Some("use only valid duration units: Y, M, W, D, H, m, S".to_string()),
796 vec![
797 "date part units: Y (years), M (months), W (weeks), D (days)".to_string(),
798 "time part units: H (hours), m (minutes), S (seconds)".to_string(),
799 ],
800 ),
801 TemporalKind::IncompleteDurationSpecification => (
802 "TEMPORAL_015",
803 Some(format!("number '{}' is missing a unit specifier", fragment.text())),
804 Some("add a unit letter after the number (Y, M, W, D, H, M, or S)".to_string()),
805 vec!["example: P1D (not P1), PT2H (not PT2)".to_string()],
806 ),
807 TemporalKind::InvalidUnitInContext { unit, in_time_part } => {
808 let context = if *in_time_part {
809 "time part (after T)"
810 } else {
811 "date part (before T)"
812 };
813 let allowed = if *in_time_part { "H, M, S" } else { "Y, M, W, D" };
814 (
815 "TEMPORAL_016",
816 Some(format!("unit '{}' is not allowed in the {}", unit, context)),
817 Some(format!("use only {} in the {}", allowed, context)),
818 vec![
819 "date part (before T): Y, M, W, D".to_string(),
820 "time part (after T): H, M, S".to_string(),
821 ],
822 )
823 }
824 TemporalKind::InvalidDurationComponentValue { unit } => (
825 "TEMPORAL_017",
826 Some(format!("{} value '{}' cannot be parsed as a number", temporal_unit_name(*unit), fragment.text())),
827 Some(format!("ensure the {} value is a valid number", temporal_unit_name(*unit))),
828 vec![format!("valid examples: P1{}, P10{}", unit, unit)],
829 ),
830 TemporalKind::UnrecognizedTemporalPattern => (
831 "TEMPORAL_018",
832 Some(format!("value '{}' does not match any temporal format", fragment.text())),
833 Some("use one of the supported formats: date (YYYY-MM-DD), time (HH:MM:SS), datetime (YYYY-MM-DDTHH:MM:SS), or duration (P...)".to_string()),
834 vec![
835 "date: 2024-03-15".to_string(),
836 "time: 14:30:45".to_string(),
837 "datetime: 2024-03-15T14:30:45".to_string(),
838 "duration: P1Y2M3DT4H5M6S".to_string(),
839 ],
840 ),
841 TemporalKind::EmptyDateComponent => (
842 "TEMPORAL_019",
843 Some(format!("date component '{}' is empty", fragment.text())),
844 Some("ensure all date parts (year, month, day) are provided".to_string()),
845 vec!["date format: YYYY-MM-DD (e.g., 2024-03-15)".to_string()],
846 ),
847 TemporalKind::EmptyTimeComponent => (
848 "TEMPORAL_020",
849 Some(format!("time component '{}' is empty", fragment.text())),
850 Some("ensure all time parts (hour, minute, second) are provided".to_string()),
851 vec!["time format: HH:MM:SS (e.g., 14:30:45)".to_string()],
852 ),
853 TemporalKind::DuplicateDurationComponent { component } => (
854 "TEMPORAL_021",
855 Some(format!("duration component '{}' appears multiple times", component)),
856 Some("each duration component (Y, M, W, D, H, M, S) can only appear once".to_string()),
857 vec!["valid: P1Y2M3D".to_string(), "invalid: P1Y2Y (duplicate Y)".to_string()],
858 ),
859 TemporalKind::OutOfOrderDurationComponent { component } => (
860 "TEMPORAL_022",
861 Some(format!("duration component '{}' appears out of order", component)),
862 Some("duration components must appear in order: Y, M, W, D (before T), then H, M, S (after T)".to_string()),
863 vec![
864 "date part order: Y (years), M (months), W (weeks), D (days)".to_string(),
865 "time part order: H (hours), M (minutes), S (seconds)".to_string(),
866 "valid: P1Y2M3D, PT1H2M3S".to_string(),
867 "invalid: P1D1Y (D before Y), PT1S1H (S before H)".to_string(),
868 ],
869 ),
870 TemporalKind::DateTimeOutOfRange => (
871 "TEMPORAL_023",
872 Some("datetime value is outside the representable range".to_string()),
873 Some("use a datetime between 1970-01-01T00:00:00Z and 2554-07-21T23:34:33Z".to_string()),
874 vec!["DateTime is stored as nanoseconds since Unix epoch (u64)".to_string()],
875 ),
876 TemporalKind::DateTimeOverflow { message: msg } => (
877 "TEMPORAL_026",
878 Some(msg.clone()),
879 Some("ensure datetime values are within representable range".to_string()),
880 vec![
881 "DateTime is stored as nanoseconds since Unix epoch (u64)".to_string(),
882 "valid range: 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z".to_string(),
883 ],
884 ),
885 TemporalKind::DurationOverflow { message: msg } => (
886 "TEMPORAL_024",
887 Some(msg.clone()),
888 Some("ensure duration values are within representable range".to_string()),
889 vec![
890 "months and days are stored as i32 (max ±2,147,483,647)".to_string(),
891 "sub-day time is stored as nanoseconds in i64".to_string(),
892 ],
893 ),
894 TemporalKind::DurationMixedSign { days, nanos } => (
895 "TEMPORAL_025",
896 Some(format!("duration days and nanos have mixed signs: days={}, nanos={}", days, nanos)),
897 Some("days and nanos must share the same sign".to_string()),
898 vec![
899 "days and nanos are commensurable (1 day = 86400s) so they must agree in sign".to_string(),
900 "months may differ in sign from days/nanos (months have variable length)".to_string(),
901 ],
902 ),
903 TemporalKind::TimeOverflow { message: msg } => (
904 "TEMPORAL_027",
905 Some(msg.clone()),
906 Some("ensure time values are within representable range".to_string()),
907 vec![
908 "Time is stored as nanoseconds since midnight (u64)".to_string(),
909 "valid range: 00:00:00.000000000 to 23:59:59.999999999".to_string(),
910 ],
911 ),
912 TemporalKind::DateOverflow { message: msg } => (
913 "TEMPORAL_028",
914 Some(msg.clone()),
915 Some("ensure date values are within representable range".to_string()),
916 vec![
917 "Date is stored as days since epoch (i32)".to_string(),
918 "valid range: approximately ±1,000,000 years from 1970".to_string(),
919 ],
920 ),
921 };
922
923 Diagnostic {
924 code: code.to_string(),
925 rql: None,
926 message,
927 fragment,
928 label,
929 help,
930 notes,
931 column: None,
932 cause: None,
933 operator_chain: None,
934 }
935 }
936
937 TypeError::InvalidUuid4Format { fragment } => {
938 let label = Some(format!("'{}' is not a valid UUID v4", fragment.text()));
939 Diagnostic {
940 code: "UUID_001".to_string(),
941 rql: None,
942 message: "invalid UUID v4 format".to_string(),
943 fragment,
944 label,
945 help: Some("use UUID v4 format (e.g., 550e8400-e29b-41d4-a716-446655440000)".to_string()),
946 notes: vec![
947 "valid: 550e8400-e29b-41d4-a716-446655440000".to_string(),
948 "valid: f47ac10b-58cc-4372-a567-0e02b2c3d479".to_string(),
949 "UUID v4 uses random or pseudo-random numbers".to_string(),
950 ],
951 column: None,
952 cause: None,
953 operator_chain: None,
954 }
955 }
956
957 TypeError::InvalidUuid7Format { fragment } => {
958 let label = Some(format!("'{}' is not a valid UUID v7", fragment.text()));
959 Diagnostic {
960 code: "UUID_002".to_string(),
961 rql: None,
962 message: "invalid UUID v7 format".to_string(),
963 fragment,
964 label,
965 help: Some("use UUID v7 format (e.g., 017f22e2-79b0-7cc3-98c4-dc0c0c07398f)".to_string()),
966 notes: vec![
967 "valid: 017f22e2-79b0-7cc3-98c4-dc0c0c07398f".to_string(),
968 "valid: 01854d6e-bd60-7b28-a3c7-6b4ad2c4e2e8".to_string(),
969 "UUID v7 uses timestamp-based generation".to_string(),
970 ],
971 column: None,
972 cause: None,
973 operator_chain: None,
974 }
975 }
976
977 TypeError::BlobEncoding {
978 kind,
979 message,
980 fragment,
981 } => {
982 let (code, label, help) = match &kind {
983 BlobEncodingKind::InvalidHex => (
984 "BLOB_001",
985 Some("Invalid hex characters found".to_string()),
986 Some("Hex strings should only contain 0-9, a-f, A-F characters".to_string()),
987 ),
988 BlobEncodingKind::InvalidBase64 => (
989 "BLOB_002",
990 Some("Invalid base64 encoding found".to_string()),
991 Some("Base64 strings should only contain A-Z, a-z, 0-9, +, / and = padding".to_string()),
992 ),
993 BlobEncodingKind::InvalidBase64Url => (
994 "BLOB_003",
995 Some("Invalid base64url encoding found".to_string()),
996 Some("Base64url strings should only contain A-Z, a-z, 0-9, -, _ characters".to_string()),
997 ),
998 BlobEncodingKind::InvalidBase58 => (
999 "BLOB_005",
1000 Some("Invalid base58 encoding found".to_string()),
1001 Some("Base58 strings should only contain 1-9, A-H, J-N, P-Z, a-k, m-z characters".to_string()),
1002 ),
1003 BlobEncodingKind::InvalidUtf8Sequence { .. } => (
1004 "BLOB_004",
1005 Some("BLOB contains invalid UTF-8 bytes".to_string()),
1006 Some("Use to_utf8_lossy() if you want to replace invalid sequences with replacement characters".to_string()),
1007 ),
1008 };
1009
1010 Diagnostic {
1011 code: code.to_string(),
1012 rql: None,
1013 message,
1014 column: None,
1015 fragment,
1016 label,
1017 help,
1018 notes: vec![],
1019 cause: None,
1020 operator_chain: None,
1021 }
1022 }
1023
1024 TypeError::SerdeDeserialize { message } => Diagnostic {
1025 code: "SERDE_001".to_string(),
1026 rql: None,
1027 message: format!("Serde deserialization error: {}", message),
1028 column: None,
1029 fragment: Fragment::None,
1030 label: None,
1031 help: Some("Check data format and structure".to_string()),
1032 notes: vec![],
1033 cause: None,
1034 operator_chain: None,
1035 },
1036
1037 TypeError::SerdeSerialize { message } => Diagnostic {
1038 code: "SERDE_002".to_string(),
1039 rql: None,
1040 message: format!("Serde serialization error: {}", message),
1041 column: None,
1042 fragment: Fragment::None,
1043 label: None,
1044 help: Some("Check data format and structure".to_string()),
1045 notes: vec![],
1046 cause: None,
1047 operator_chain: None,
1048 },
1049
1050 TypeError::SerdeKeycode { message } => Diagnostic {
1051 code: "SERDE_003".to_string(),
1052 rql: None,
1053 message: format!("Keycode serialization error: {}", message),
1054 column: None,
1055 fragment: Fragment::None,
1056 label: None,
1057 help: Some("Check keycode data and format".to_string()),
1058 notes: vec![],
1059 cause: None,
1060 operator_chain: None,
1061 },
1062
1063 TypeError::ArrayConversion { message } => Diagnostic {
1064 code: "CONV_001".to_string(),
1065 rql: None,
1066 message: format!("Array conversion error: {}", message),
1067 column: None,
1068 fragment: Fragment::None,
1069 label: None,
1070 help: Some("Check array size requirements".to_string()),
1071 notes: vec![],
1072 cause: None,
1073 operator_chain: None,
1074 },
1075
1076 TypeError::Utf8Conversion { message } => Diagnostic {
1077 code: "CONV_002".to_string(),
1078 rql: None,
1079 message: format!("UTF-8 conversion error: {}", message),
1080 column: None,
1081 fragment: Fragment::None,
1082 label: None,
1083 help: Some("Check string encoding".to_string()),
1084 notes: vec![],
1085 cause: None,
1086 operator_chain: None,
1087 },
1088
1089 TypeError::IntegerConversion { message } => Diagnostic {
1090 code: "CONV_003".to_string(),
1091 rql: None,
1092 message: format!("Integer conversion error: {}", message),
1093 column: None,
1094 fragment: Fragment::None,
1095 label: None,
1096 help: Some("Check integer range limits".to_string()),
1097 notes: vec![],
1098 cause: None,
1099 operator_chain: None,
1100 },
1101
1102 TypeError::Network { kind, message } => {
1103 let (code, help) = match &kind {
1104 NetworkErrorKind::Connection { .. } => {
1105 ("NET_001", Some("Check network connectivity and server status".to_string()))
1106 }
1107 NetworkErrorKind::Engine { .. } => ("NET_002", None),
1108 NetworkErrorKind::Transport { .. } => {
1109 ("NET_003", Some("Check network connectivity".to_string()))
1110 }
1111 NetworkErrorKind::Status { .. } => {
1112 ("NET_004", Some("Check gRPC service status".to_string()))
1113 }
1114 };
1115
1116 Diagnostic {
1117 code: code.to_string(),
1118 rql: None,
1119 message,
1120 column: None,
1121 fragment: Fragment::None,
1122 label: None,
1123 help,
1124 notes: vec![],
1125 cause: None,
1126 operator_chain: None,
1127 }
1128 }
1129
1130 TypeError::Auth { kind, message } => {
1131 let (code, help) = match &kind {
1132 AuthErrorKind::AuthenticationFailed { .. } => {
1133 ("ASVTH_001", Some("Check your credentials and try again".to_string()))
1134 }
1135 AuthErrorKind::AuthorizationDenied { .. } => {
1136 ("ASVTH_002", Some("Check your permissions for this resource".to_string()))
1137 }
1138 AuthErrorKind::TokenExpired => {
1139 ("ASVTH_003", Some("Refresh your authentication token".to_string()))
1140 }
1141 AuthErrorKind::InvalidToken => {
1142 ("ASVTH_004", Some("Provide a valid authentication token".to_string()))
1143 }
1144 };
1145
1146 Diagnostic {
1147 code: code.to_string(),
1148 rql: None,
1149 message,
1150 column: None,
1151 fragment: Fragment::None,
1152 label: None,
1153 help,
1154 notes: vec![],
1155 cause: None,
1156 operator_chain: None,
1157 }
1158 }
1159
1160 TypeError::DictionaryCapacityExceeded {
1161 id_type,
1162 value,
1163 max_value,
1164 } => Diagnostic {
1165 code: "DICT_001".to_string(),
1166 rql: None,
1167 message: format!("dictionary entry ID {} exceeds maximum {} for type {}", value, max_value, id_type),
1168 column: None,
1169 fragment: Fragment::None,
1170 label: Some(format!("{} capacity exceeded", id_type)),
1171 help: Some(
1172 "use a larger ID type (e.g., Uint2 instead of Uint1) when creating the dictionary".to_string(),
1173 ),
1174 notes: vec![],
1175 cause: None,
1176 operator_chain: None,
1177 },
1178
1179 TypeError::AssertionFailed {
1180 fragment,
1181 message,
1182 expression,
1183 } => {
1184 let label = expression
1185 .as_ref()
1186 .map(|expr| format!("this expression is false: {}", expr))
1187 .or_else(|| Some("assertion failed".to_string()));
1188
1189 Diagnostic {
1190 code: "ASSERT".to_string(),
1191 rql: None,
1192 message,
1193 fragment,
1194 label,
1195 help: None,
1196 notes: vec![],
1197 column: None,
1198 cause: None,
1199 operator_chain: None,
1200 }
1201 }
1202
1203 TypeError::Function {
1204 kind,
1205 message,
1206 fragment,
1207 } => {
1208 let (code, label, help) = match &kind {
1209 FunctionErrorKind::UnknownFunction => (
1210 "FUNCTION_001",
1211 Some("unknown function".to_string()),
1212 Some("Check the function name and available functions".to_string()),
1213 ),
1214 FunctionErrorKind::ArityMismatch { expected, .. } => (
1215 "FUNCTION_002",
1216 Some("wrong number of arguments".to_string()),
1217 Some(format!("Provide exactly {} arguments to function {}", expected, fragment.text())),
1218 ),
1219 FunctionErrorKind::TooManyArguments { max_args, .. } => (
1220 "FUNCTION_003",
1221 Some("too many arguments".to_string()),
1222 Some(format!("Provide at most {} arguments to function {}", max_args, fragment.text())),
1223 ),
1224 FunctionErrorKind::InvalidArgumentType { expected, .. } => {
1225 let expected_types =
1226 expected.iter().map(|t| format!("{:?}", t)).collect::<Vec<_>>().join(", ");
1227 (
1228 "FUNCTION_004",
1229 Some("invalid argument type".to_string()),
1230 Some(format!("Provide an argument of type: {}", expected_types)),
1231 )
1232 }
1233 FunctionErrorKind::UndefinedArgument { .. } => (
1234 "FUNCTION_005",
1235 Some("none argument".to_string()),
1236 Some("Provide a defined value for this argument".to_string()),
1237 ),
1238 FunctionErrorKind::MissingInput => (
1239 "FUNCTION_006",
1240 Some("missing input".to_string()),
1241 Some("Provide input data to the function".to_string()),
1242 ),
1243 FunctionErrorKind::ExecutionFailed { .. } => (
1244 "FUNCTION_007",
1245 Some("execution failed".to_string()),
1246 Some("Check function arguments and data".to_string()),
1247 ),
1248 FunctionErrorKind::InternalError { .. } => (
1249 "FUNCTION_008",
1250 Some("internal error".to_string()),
1251 Some("This is an internal error - please report this issue".to_string()),
1252 ),
1253 FunctionErrorKind::GeneratorNotFound => (
1254 "FUNCTION_009",
1255 Some("unknown generator function".to_string()),
1256 Some("Check the generator function name and ensure it is registered".to_string()),
1257 ),
1258 };
1259
1260 Diagnostic {
1261 code: code.to_string(),
1262 rql: None,
1263 message,
1264 column: None,
1265 fragment,
1266 label,
1267 help,
1268 notes: vec![],
1269 cause: None,
1270 operator_chain: None,
1271 }
1272 }
1273
1274 TypeError::Ast {
1275 kind,
1276 message,
1277 fragment,
1278 } => {
1279 let (code, label, help) = match &kind {
1280 AstErrorKind::TokenizeError { .. } => (
1281 "AST_001",
1282 None,
1283 Some("Check syntax and token format".to_string()),
1284 ),
1285 AstErrorKind::UnexpectedEof => (
1286 "AST_002",
1287 None,
1288 Some("Complete the statement".to_string()),
1289 ),
1290 AstErrorKind::ExpectedIdentifier => (
1291 "AST_003",
1292 Some(format!("found `{}`", fragment.text())),
1293 Some("expected token of type `identifier`".to_string()),
1294 ),
1295 AstErrorKind::InvalidColumnProperty => (
1296 "AST_011",
1297 Some(format!("found `{}`", fragment.text())),
1298 Some("Expected one of: auto_increment, dictionary, saturation, default".to_string()),
1299 ),
1300 AstErrorKind::InvalidPolicy => (
1301 "AST_004",
1302 Some(format!("found `{}`", fragment.text())),
1303 Some("Expected a valid policy identifier".to_string()),
1304 ),
1305 AstErrorKind::UnexpectedToken { expected } => (
1306 "AST_005",
1307 Some(format!("found `{}`", fragment.text())),
1308 Some(format!("Use {} instead", expected)),
1309 ),
1310 AstErrorKind::UnsupportedToken => (
1311 "AST_006",
1312 Some(format!("found `{}`", fragment.text())),
1313 Some("This token is not supported in this context".to_string()),
1314 ),
1315 AstErrorKind::MultipleExpressionsWithoutBraces => {
1316 let keyword = fragment.text().to_string();
1317 (
1318 "AST_007",
1319 Some("missing `{ … }` around expressions".to_string()),
1320 Some(format!("wrap the expressions in curly braces:\n {} {{ expr1, expr2, … }}", keyword)),
1321 )
1322 }
1323 AstErrorKind::UnrecognizedType => (
1324 "AST_008",
1325 Some("type not found".to_string()),
1326 None,
1327 ),
1328 AstErrorKind::UnsupportedAstNode { .. } => (
1329 "AST_009",
1330 Some("not supported in this context".to_string()),
1331 Some("This syntax is not yet supported or may be invalid in this context".to_string()),
1332 ),
1333 AstErrorKind::EmptyPipeline => (
1334 "AST_010",
1335 None,
1336 Some("A query pipeline must contain at least one operation".to_string()),
1337 ),
1338 };
1339
1340 Diagnostic {
1341 code: code.to_string(),
1342 rql: None,
1343 message,
1344 column: None,
1345 fragment,
1346 label,
1347 help,
1348 notes: vec![],
1349 cause: None,
1350 operator_chain: None,
1351 }
1352 }
1353
1354 TypeError::Runtime { kind, message } => {
1355 let (code, help) = match &kind {
1356 RuntimeErrorKind::VariableNotFound { name } => (
1357 "RUNTIME_001",
1358 Some(format!("Define the variable using 'let {} = <value>' before using it", name)),
1359 ),
1360 RuntimeErrorKind::VariableIsDataframe { name } => (
1361 "RUNTIME_002",
1362 Some(format!(
1363 "Extract a scalar value from the dataframe using '${} | only()', '${} | first()', or '${} | first_or_none()'",
1364 name, name, name
1365 )),
1366 ),
1367 RuntimeErrorKind::VariableIsImmutable { .. } => (
1368 "RUNTIME_003",
1369 Some("Use 'let mut $name := value' to declare a mutable variable".to_string()),
1370 ),
1371 RuntimeErrorKind::BreakOutsideLoop => (
1372 "RUNTIME_004",
1373 Some("Use BREAK inside a LOOP, WHILE, or FOR block".to_string()),
1374 ),
1375 RuntimeErrorKind::ContinueOutsideLoop => (
1376 "RUNTIME_005",
1377 Some("Use CONTINUE inside a LOOP, WHILE, or FOR block".to_string()),
1378 ),
1379 RuntimeErrorKind::MaxIterationsExceeded { .. } => (
1380 "RUNTIME_006",
1381 Some("Add a BREAK condition or use WHILE with a terminating condition".to_string()),
1382 ),
1383 RuntimeErrorKind::UndefinedFunction { .. } => (
1384 "RUNTIME_007",
1385 Some("Define the function using 'DEF name [] { ... }' before calling it".to_string()),
1386 ),
1387 RuntimeErrorKind::FieldNotFound {
1388 variable, available, ..
1389 } => {
1390 let help = if available.is_empty() {
1391 format!("The variable '{}' has no fields", variable)
1392 } else {
1393 format!("Available fields: {}", available.join(", "))
1394 };
1395 ("RUNTIME_009", Some(help))
1396 }
1397 RuntimeErrorKind::AppendTargetNotFrame { .. } => (
1398 "RUNTIME_008",
1399 Some("APPEND can only target Frame variables. Use a new variable name or ensure the target was created by APPEND or FROM".to_string()),
1400 ),
1401 RuntimeErrorKind::AppendColumnMismatch { .. } => (
1402 "RUNTIME_011",
1403 Some("APPEND requires the new rows to have the same columns as the target.".to_string()),
1404 ),
1405 RuntimeErrorKind::ExpectedSingleColumn { actual } => (
1406 "RUNTIME_010",
1407 Some(format!(
1408 "Expected a single-column value but got {} columns. Use field access to select one column.",
1409 actual
1410 )),
1411 ),
1412 };
1413
1414 let notes = match &kind {
1415 RuntimeErrorKind::VariableIsImmutable { .. } => {
1416 vec!["Only mutable variables can be reassigned".to_string()]
1417 }
1418 RuntimeErrorKind::VariableIsDataframe { .. } => {
1419 vec![
1420 "Dataframes must be explicitly converted to scalar values before use in expressions".to_string(),
1421 "Use only() for exactly 1 row × 1 column dataframes".to_string(),
1422 "Use first() to take the first value from the first column".to_string(),
1423 ]
1424 }
1425 RuntimeErrorKind::AppendColumnMismatch { existing, incoming, .. } => vec![
1426 format!("existing columns: [{}]", existing.join(", ")),
1427 format!("incoming columns: [{}]", incoming.join(", ")),
1428 ],
1429 _ => vec![],
1430 };
1431
1432 let fragment = match &kind {
1433 RuntimeErrorKind::UndefinedFunction { name } => Fragment::internal(name.clone()),
1434 RuntimeErrorKind::AppendColumnMismatch { fragment, .. } => fragment.clone(),
1435 _ => Fragment::None,
1436 };
1437
1438 Diagnostic {
1439 code: code.to_string(),
1440 rql: None,
1441 message,
1442 column: None,
1443 fragment,
1444 label: None,
1445 help,
1446 notes,
1447 cause: None,
1448 operator_chain: None,
1449 }
1450 }
1451
1452 TypeError::Procedure { kind, message, fragment } => {
1453 let (code, help, label) = match &kind {
1454 ProcedureErrorKind::UndefinedProcedure { .. } => (
1455 "PROCEDURE_001",
1456 "Check the procedure name and available procedures",
1457 "unknown procedure",
1458 ),
1459 ProcedureErrorKind::NoRegisteredImplementation { .. } => (
1460 "PROCEDURE_002",
1461 "the catalog references a native/FFI/WASM binding that is not loaded in this binary — rebuild with the registration or drop/replace the catalog entry",
1462 "native binding not loaded",
1463 ),
1464 };
1465
1466 Diagnostic {
1467 code: code.to_string(),
1468 rql: None,
1469 message,
1470 column: None,
1471 fragment,
1472 label: Some(label.to_string()),
1473 help: Some(help.to_string()),
1474 notes: vec![],
1475 cause: None,
1476 operator_chain: None,
1477 }
1478 }
1479 }
1480 }
1481}