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::r#type::Type,
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 Type::Float4 | Type::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 Type::Int1
436 | Type::Int2
437 | Type::Int4
438 | Type::Int8
439 | Type::Int16
440 | Type::Uint1
441 | Type::Uint2
442 | Type::Uint4
443 | Type::Uint8
444 | Type::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::NanNotAllowed => Diagnostic {
520 code: "NUMBER_003".to_string(),
521 rql: None,
522 message: "NaN not allowed".to_string(),
523 fragment: Fragment::None,
524 label: Some("NaN (Not a Number) values are not permitted".to_string()),
525 help: Some("use a finite number or none instead".to_string()),
526 notes: vec![],
527 column: None,
528 cause: None,
529 operator_chain: None,
530 },
531
532 TypeError::IntegerPrecisionLoss {
533 shape_type,
534 target,
535 fragment,
536 } => {
537 let is_signed = shape_type.is_signed_integer();
538 let (min_limit, max_limit) = match target {
539 Type::Float4 => {
540 if is_signed {
541 ("-16_777_216 (-2^24)", "16_777_216 (2^24)")
542 } else {
543 ("0", "16_777_216 (2^24)")
544 }
545 }
546 Type::Float8 => {
547 if is_signed {
548 ("-9_007_199_254_740_992 (-2^53)", "9_007_199_254_740_992 (2^53)")
549 } else {
550 ("0", "9_007_199_254_740_992 (2^53)")
551 }
552 }
553 _ => unreachable!("IntegerPrecisionLoss should only be used for float targets"),
554 };
555
556 let label = Some(format!(
557 "converting '{}' from {} to {} would lose precision",
558 fragment.text(),
559 shape_type,
560 target
561 ));
562
563 Diagnostic {
564 code: "NUMBER_004".to_string(),
565 rql: None,
566 message: "too large for precise float conversion".to_string(),
567 fragment,
568 label,
569 help: None,
570 notes: vec![
571 format!("{} can only represent from {} to {} precisely", target, min_limit, max_limit),
572 "consider using a different numeric type if exact precision is required".to_string(),
573 ],
574 column: None,
575 cause: None,
576 operator_chain: None,
577 }
578 }
579
580 TypeError::DecimalScaleExceedsPrecision {
581 scale,
582 precision,
583 fragment,
584 } => {
585 let label = Some(format!("scale ({}) cannot be greater than precision ({})", scale, precision));
586 Diagnostic {
587 code: "NUMBER_005".to_string(),
588 rql: None,
589 message: "decimal scale exceeds precision".to_string(),
590 fragment,
591 label,
592 help: Some(format!("use a scale value between 0 and {} or increase precision", precision)),
593 notes: vec![
594 format!("current precision: {}", precision),
595 format!("current scale: {}", scale),
596 "scale represents the number of digits after the decimal point".to_string(),
597 "precision represents the total number of significant digits".to_string(),
598 ],
599 column: None,
600 cause: None,
601 operator_chain: None,
602 }
603 }
604
605 TypeError::DecimalPrecisionInvalid { precision } => {
606 let label = Some(format!("precision ({}) must be at least 1", precision));
607 Diagnostic {
608 code: "NUMBER_006".to_string(),
609 rql: None,
610 message: "invalid decimal precision".to_string(),
611 fragment: Fragment::None,
612 label,
613 help: Some("use a precision value of at least 1".to_string()),
614 notes: vec![
615 format!("current precision: {}", precision),
616 "precision represents the total number of significant digits".to_string(),
617 ],
618 column: None,
619 cause: None,
620 operator_chain: None,
621 }
622 }
623
624 TypeError::InvalidBooleanFormat { fragment } => {
625 let value = fragment.text().to_string();
626 let label = Some(format!("expected 'true' or 'false', found '{}'", value));
627 Diagnostic {
628 code: "BOOLEAN_001".to_string(),
629 rql: None,
630 message: "invalid boolean format".to_string(),
631 fragment,
632 label,
633 help: Some("use 'true' or 'false'".to_string()),
634 notes: vec!["valid: true, TRUE".to_string(), "valid: false, FALSE".to_string()],
635 column: None,
636 cause: None,
637 operator_chain: None,
638 }
639 }
640
641 TypeError::EmptyBooleanValue { fragment } => Diagnostic {
642 code: "BOOLEAN_002".to_string(),
643 rql: None,
644 message: "empty boolean value".to_string(),
645 fragment,
646 label: Some("boolean value cannot be empty".to_string()),
647 help: Some("provide either 'true' or 'false'".to_string()),
648 notes: vec!["valid: true".to_string(), "valid: false".to_string()],
649 column: None,
650 cause: None,
651 operator_chain: None,
652 },
653
654 TypeError::InvalidNumberBoolean { fragment } => {
655 let value = fragment.text().to_string();
656 let label =
657 Some(format!("number '{}' cannot be cast to boolean, only 1 or 0 are allowed", value));
658 Diagnostic {
659 code: "BOOLEAN_003".to_string(),
660 rql: None,
661 message: "invalid boolean".to_string(),
662 fragment,
663 label,
664 help: Some("use 1 for true or 0 for false".to_string()),
665 notes: vec![
666 "valid: 1 → true".to_string(),
667 "valid: 0 → false".to_string(),
668 "invalid: any other number".to_string(),
669 ],
670 column: None,
671 cause: None,
672 operator_chain: None,
673 }
674 }
675
676 TypeError::Temporal {
677 kind,
678 message,
679 fragment,
680 } => {
681 let (code, label, help, notes) = match &kind {
682 TemporalKind::InvalidDateFormat => (
683 "TEMPORAL_001",
684 Some(format!("expected YYYY-MM-DD format, found '{}'", fragment.text())),
685 Some("use the format YYYY-MM-DD (e.g., 2024-03-15)".to_string()),
686 vec!["dates must have exactly 3 parts separated by hyphens".to_string()],
687 ),
688 TemporalKind::InvalidDateTimeFormat => (
689 "TEMPORAL_002",
690 Some(format!("expected YYYY-MM-DDTHH:MM:SS format, found '{}'", fragment.text())),
691 Some("use the format YYYY-MM-DDTHH:MM:SS[.fff][Z|±HH:MM] (e.g., 2024-03-15T14:30:45)".to_string()),
692 vec!["datetime must contain 'T' separator between date and time parts".to_string()],
693 ),
694 TemporalKind::InvalidTimeFormat => (
695 "TEMPORAL_003",
696 Some(format!("expected HH:MM:SS format, found '{}'", fragment.text())),
697 Some("use the format HH:MM:SS[.fff][Z|±HH:MM] (e.g., 14:30:45)".to_string()),
698 vec!["time must have exactly 3 parts separated by colons".to_string()],
699 ),
700 TemporalKind::InvalidDurationFormat => (
701 "TEMPORAL_004",
702 Some(format!("expected P[n]Y[n]M[n]W[n]D[T[n]H[n]M[n]S] format, found '{}'", fragment.text())),
703 Some("use ISO 8601 duration format starting with 'P' (e.g., P1D, PT2H30M, P1Y2M3DT4H5M6S)".to_string()),
704 vec![
705 "duration must start with 'P' followed by duration components".to_string(),
706 "date part: P[n]Y[n]M[n]W[n]D (years, months, weeks, days)".to_string(),
707 "time part: T[n]H[n]M[n]S (hours, minutes, seconds)".to_string(),
708 ],
709 ),
710 TemporalKind::InvalidYear => (
711 "TEMPORAL_005",
712 Some(format!("year '{}' cannot be parsed as a number", fragment.text())),
713 Some("ensure the year is a valid 4-digit number".to_string()),
714 vec!["valid examples: 2024, 1999, 2000".to_string()],
715 ),
716 TemporalKind::InvalidTimeComponentFormat { component } => (
717 "TEMPORAL_005",
718 Some(format!("{} '{}' must be exactly 2 digits", component, fragment.text())),
719 Some(format!("ensure the {} is exactly 2 digits (e.g., 09, 14, 23)", component)),
720 vec![format!("{} must be exactly 2 digits in HH:MM:SS format", component)],
721 ),
722 TemporalKind::InvalidMonth => (
723 "TEMPORAL_006",
724 Some(format!("month '{}' cannot be parsed as a number (expected 1-12)", fragment.text())),
725 Some("ensure the month is a valid number between 1 and 12".to_string()),
726 vec!["valid examples: 01, 03, 12".to_string()],
727 ),
728 TemporalKind::InvalidDay => (
729 "TEMPORAL_007",
730 Some(format!("day '{}' cannot be parsed as a number (expected 1-31)", fragment.text())),
731 Some("ensure the day is a valid number between 1 and 31".to_string()),
732 vec!["valid examples: 01, 15, 31".to_string()],
733 ),
734 TemporalKind::InvalidHour => (
735 "TEMPORAL_008",
736 Some(format!("hour '{}' cannot be parsed as a number (expected 0-23)", fragment.text())),
737 Some("ensure the hour is a valid number between 0 and 23 (use 24-hour format)".to_string()),
738 vec![
739 "valid examples: 09, 14, 23".to_string(),
740 "hours must be in 24-hour format (00-23)".to_string(),
741 ],
742 ),
743 TemporalKind::InvalidMinute => (
744 "TEMPORAL_009",
745 Some(format!("minute '{}' cannot be parsed as a number (expected 0-59)", fragment.text())),
746 Some("ensure the minute is a valid number between 0 and 59".to_string()),
747 vec!["valid examples: 00, 30, 59".to_string()],
748 ),
749 TemporalKind::InvalidSecond => (
750 "TEMPORAL_010",
751 Some(format!("second '{}' cannot be parsed as a number (expected 0-59)", fragment.text())),
752 Some("ensure the second is a valid number between 0 and 59".to_string()),
753 vec!["valid examples: 00, 30, 59".to_string()],
754 ),
755 TemporalKind::InvalidFractionalSeconds => (
756 "TEMPORAL_011",
757 Some(format!("fractional seconds '{}' cannot be parsed as a number", fragment.text())),
758 Some("ensure fractional seconds contain only digits".to_string()),
759 vec!["valid examples: 123, 999999, 000001".to_string()],
760 ),
761 TemporalKind::InvalidDateValues => (
762 "TEMPORAL_012",
763 Some(format!("date '{}' represents an invalid calendar date", fragment.text())),
764 Some("ensure the date exists in the calendar (e.g., no February 30)".to_string()),
765 vec![
766 "check month has correct number of days".to_string(),
767 "consider leap years for February 29".to_string(),
768 ],
769 ),
770 TemporalKind::InvalidTimeValues => (
771 "TEMPORAL_013",
772 Some(format!("time '{}' contains out-of-range values", fragment.text())),
773 Some("ensure hours are 0-23, minutes and seconds are 0-59".to_string()),
774 vec!["use 24-hour format for hours".to_string()],
775 ),
776 TemporalKind::InvalidDurationCharacter => (
777 "TEMPORAL_014",
778 Some(format!("character '{}' is not valid in ISO 8601 duration", fragment.text())),
779 Some("use only valid duration units: Y, M, W, D, H, m, S".to_string()),
780 vec![
781 "date part units: Y (years), M (months), W (weeks), D (days)".to_string(),
782 "time part units: H (hours), m (minutes), S (seconds)".to_string(),
783 ],
784 ),
785 TemporalKind::IncompleteDurationSpecification => (
786 "TEMPORAL_015",
787 Some(format!("number '{}' is missing a unit specifier", fragment.text())),
788 Some("add a unit letter after the number (Y, M, W, D, H, M, or S)".to_string()),
789 vec!["example: P1D (not P1), PT2H (not PT2)".to_string()],
790 ),
791 TemporalKind::InvalidUnitInContext { unit, in_time_part } => {
792 let context = if *in_time_part {
793 "time part (after T)"
794 } else {
795 "date part (before T)"
796 };
797 let allowed = if *in_time_part { "H, M, S" } else { "Y, M, W, D" };
798 (
799 "TEMPORAL_016",
800 Some(format!("unit '{}' is not allowed in the {}", unit, context)),
801 Some(format!("use only {} in the {}", allowed, context)),
802 vec![
803 "date part (before T): Y, M, W, D".to_string(),
804 "time part (after T): H, M, S".to_string(),
805 ],
806 )
807 }
808 TemporalKind::InvalidDurationComponentValue { unit } => (
809 "TEMPORAL_017",
810 Some(format!("{} value '{}' cannot be parsed as a number", temporal_unit_name(*unit), fragment.text())),
811 Some(format!("ensure the {} value is a valid number", temporal_unit_name(*unit))),
812 vec![format!("valid examples: P1{}, P10{}", unit, unit)],
813 ),
814 TemporalKind::UnrecognizedTemporalPattern => (
815 "TEMPORAL_018",
816 Some(format!("value '{}' does not match any temporal format", fragment.text())),
817 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()),
818 vec![
819 "date: 2024-03-15".to_string(),
820 "time: 14:30:45".to_string(),
821 "datetime: 2024-03-15T14:30:45".to_string(),
822 "duration: P1Y2M3DT4H5M6S".to_string(),
823 ],
824 ),
825 TemporalKind::EmptyDateComponent => (
826 "TEMPORAL_019",
827 Some(format!("date component '{}' is empty", fragment.text())),
828 Some("ensure all date parts (year, month, day) are provided".to_string()),
829 vec!["date format: YYYY-MM-DD (e.g., 2024-03-15)".to_string()],
830 ),
831 TemporalKind::EmptyTimeComponent => (
832 "TEMPORAL_020",
833 Some(format!("time component '{}' is empty", fragment.text())),
834 Some("ensure all time parts (hour, minute, second) are provided".to_string()),
835 vec!["time format: HH:MM:SS (e.g., 14:30:45)".to_string()],
836 ),
837 TemporalKind::DuplicateDurationComponent { component } => (
838 "TEMPORAL_021",
839 Some(format!("duration component '{}' appears multiple times", component)),
840 Some("each duration component (Y, M, W, D, H, M, S) can only appear once".to_string()),
841 vec!["valid: P1Y2M3D".to_string(), "invalid: P1Y2Y (duplicate Y)".to_string()],
842 ),
843 TemporalKind::OutOfOrderDurationComponent { component } => (
844 "TEMPORAL_022",
845 Some(format!("duration component '{}' appears out of order", component)),
846 Some("duration components must appear in order: Y, M, W, D (before T), then H, M, S (after T)".to_string()),
847 vec![
848 "date part order: Y (years), M (months), W (weeks), D (days)".to_string(),
849 "time part order: H (hours), M (minutes), S (seconds)".to_string(),
850 "valid: P1Y2M3D, PT1H2M3S".to_string(),
851 "invalid: P1D1Y (D before Y), PT1S1H (S before H)".to_string(),
852 ],
853 ),
854 TemporalKind::DateTimeOutOfRange => (
855 "TEMPORAL_023",
856 Some("datetime value is outside the representable range".to_string()),
857 Some("use a datetime between 1970-01-01T00:00:00Z and 2554-07-21T23:34:33Z".to_string()),
858 vec!["DateTime is stored as nanoseconds since Unix epoch (u64)".to_string()],
859 ),
860 TemporalKind::DateTimeOverflow { message: msg } => (
861 "TEMPORAL_026",
862 Some(msg.clone()),
863 Some("ensure datetime values are within representable range".to_string()),
864 vec![
865 "DateTime is stored as nanoseconds since Unix epoch (u64)".to_string(),
866 "valid range: 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z".to_string(),
867 ],
868 ),
869 TemporalKind::DurationOverflow { message: msg } => (
870 "TEMPORAL_024",
871 Some(msg.clone()),
872 Some("ensure duration values are within representable range".to_string()),
873 vec![
874 "months and days are stored as i32 (max ±2,147,483,647)".to_string(),
875 "sub-day time is stored as nanoseconds in i64".to_string(),
876 ],
877 ),
878 TemporalKind::DurationMixedSign { days, nanos } => (
879 "TEMPORAL_025",
880 Some(format!("duration days and nanos have mixed signs: days={}, nanos={}", days, nanos)),
881 Some("days and nanos must share the same sign".to_string()),
882 vec![
883 "days and nanos are commensurable (1 day = 86400s) so they must agree in sign".to_string(),
884 "months may differ in sign from days/nanos (months have variable length)".to_string(),
885 ],
886 ),
887 TemporalKind::TimeOverflow { message: msg } => (
888 "TEMPORAL_027",
889 Some(msg.clone()),
890 Some("ensure time values are within representable range".to_string()),
891 vec![
892 "Time is stored as nanoseconds since midnight (u64)".to_string(),
893 "valid range: 00:00:00.000000000 to 23:59:59.999999999".to_string(),
894 ],
895 ),
896 TemporalKind::DateOverflow { message: msg } => (
897 "TEMPORAL_028",
898 Some(msg.clone()),
899 Some("ensure date values are within representable range".to_string()),
900 vec![
901 "Date is stored as days since epoch (i32)".to_string(),
902 "valid range: approximately ±1,000,000 years from 1970".to_string(),
903 ],
904 ),
905 };
906
907 Diagnostic {
908 code: code.to_string(),
909 rql: None,
910 message,
911 fragment,
912 label,
913 help,
914 notes,
915 column: None,
916 cause: None,
917 operator_chain: None,
918 }
919 }
920
921 TypeError::InvalidUuid4Format { fragment } => {
922 let label = Some(format!("'{}' is not a valid UUID v4", fragment.text()));
923 Diagnostic {
924 code: "UUID_001".to_string(),
925 rql: None,
926 message: "invalid UUID v4 format".to_string(),
927 fragment,
928 label,
929 help: Some("use UUID v4 format (e.g., 550e8400-e29b-41d4-a716-446655440000)".to_string()),
930 notes: vec![
931 "valid: 550e8400-e29b-41d4-a716-446655440000".to_string(),
932 "valid: f47ac10b-58cc-4372-a567-0e02b2c3d479".to_string(),
933 "UUID v4 uses random or pseudo-random numbers".to_string(),
934 ],
935 column: None,
936 cause: None,
937 operator_chain: None,
938 }
939 }
940
941 TypeError::InvalidUuid7Format { fragment } => {
942 let label = Some(format!("'{}' is not a valid UUID v7", fragment.text()));
943 Diagnostic {
944 code: "UUID_002".to_string(),
945 rql: None,
946 message: "invalid UUID v7 format".to_string(),
947 fragment,
948 label,
949 help: Some("use UUID v7 format (e.g., 017f22e2-79b0-7cc3-98c4-dc0c0c07398f)".to_string()),
950 notes: vec![
951 "valid: 017f22e2-79b0-7cc3-98c4-dc0c0c07398f".to_string(),
952 "valid: 01854d6e-bd60-7b28-a3c7-6b4ad2c4e2e8".to_string(),
953 "UUID v7 uses timestamp-based generation".to_string(),
954 ],
955 column: None,
956 cause: None,
957 operator_chain: None,
958 }
959 }
960
961 TypeError::BlobEncoding {
962 kind,
963 message,
964 fragment,
965 } => {
966 let (code, label, help) = match &kind {
967 BlobEncodingKind::InvalidHex => (
968 "BLOB_001",
969 Some("Invalid hex characters found".to_string()),
970 Some("Hex strings should only contain 0-9, a-f, A-F characters".to_string()),
971 ),
972 BlobEncodingKind::InvalidBase64 => (
973 "BLOB_002",
974 Some("Invalid base64 encoding found".to_string()),
975 Some("Base64 strings should only contain A-Z, a-z, 0-9, +, / and = padding".to_string()),
976 ),
977 BlobEncodingKind::InvalidBase64Url => (
978 "BLOB_003",
979 Some("Invalid base64url encoding found".to_string()),
980 Some("Base64url strings should only contain A-Z, a-z, 0-9, -, _ characters".to_string()),
981 ),
982 BlobEncodingKind::InvalidBase58 => (
983 "BLOB_005",
984 Some("Invalid base58 encoding found".to_string()),
985 Some("Base58 strings should only contain 1-9, A-H, J-N, P-Z, a-k, m-z characters".to_string()),
986 ),
987 BlobEncodingKind::InvalidUtf8Sequence { .. } => (
988 "BLOB_004",
989 Some("BLOB contains invalid UTF-8 bytes".to_string()),
990 Some("Use to_utf8_lossy() if you want to replace invalid sequences with replacement characters".to_string()),
991 ),
992 };
993
994 Diagnostic {
995 code: code.to_string(),
996 rql: None,
997 message,
998 column: None,
999 fragment,
1000 label,
1001 help,
1002 notes: vec![],
1003 cause: None,
1004 operator_chain: None,
1005 }
1006 }
1007
1008 TypeError::SerdeDeserialize { message } => Diagnostic {
1009 code: "SERDE_001".to_string(),
1010 rql: None,
1011 message: format!("Serde deserialization error: {}", message),
1012 column: None,
1013 fragment: Fragment::None,
1014 label: None,
1015 help: Some("Check data format and structure".to_string()),
1016 notes: vec![],
1017 cause: None,
1018 operator_chain: None,
1019 },
1020
1021 TypeError::SerdeSerialize { message } => Diagnostic {
1022 code: "SERDE_002".to_string(),
1023 rql: None,
1024 message: format!("Serde serialization error: {}", message),
1025 column: None,
1026 fragment: Fragment::None,
1027 label: None,
1028 help: Some("Check data format and structure".to_string()),
1029 notes: vec![],
1030 cause: None,
1031 operator_chain: None,
1032 },
1033
1034 TypeError::SerdeKeycode { message } => Diagnostic {
1035 code: "SERDE_003".to_string(),
1036 rql: None,
1037 message: format!("Keycode serialization error: {}", message),
1038 column: None,
1039 fragment: Fragment::None,
1040 label: None,
1041 help: Some("Check keycode data and format".to_string()),
1042 notes: vec![],
1043 cause: None,
1044 operator_chain: None,
1045 },
1046
1047 TypeError::ArrayConversion { message } => Diagnostic {
1048 code: "CONV_001".to_string(),
1049 rql: None,
1050 message: format!("Array conversion error: {}", message),
1051 column: None,
1052 fragment: Fragment::None,
1053 label: None,
1054 help: Some("Check array size requirements".to_string()),
1055 notes: vec![],
1056 cause: None,
1057 operator_chain: None,
1058 },
1059
1060 TypeError::Utf8Conversion { message } => Diagnostic {
1061 code: "CONV_002".to_string(),
1062 rql: None,
1063 message: format!("UTF-8 conversion error: {}", message),
1064 column: None,
1065 fragment: Fragment::None,
1066 label: None,
1067 help: Some("Check string encoding".to_string()),
1068 notes: vec![],
1069 cause: None,
1070 operator_chain: None,
1071 },
1072
1073 TypeError::IntegerConversion { message } => Diagnostic {
1074 code: "CONV_003".to_string(),
1075 rql: None,
1076 message: format!("Integer conversion error: {}", message),
1077 column: None,
1078 fragment: Fragment::None,
1079 label: None,
1080 help: Some("Check integer range limits".to_string()),
1081 notes: vec![],
1082 cause: None,
1083 operator_chain: None,
1084 },
1085
1086 TypeError::Network { kind, message } => {
1087 let (code, help) = match &kind {
1088 NetworkErrorKind::Connection { .. } => {
1089 ("NET_001", Some("Check network connectivity and server status".to_string()))
1090 }
1091 NetworkErrorKind::Engine { .. } => ("NET_002", None),
1092 NetworkErrorKind::Transport { .. } => {
1093 ("NET_003", Some("Check network connectivity".to_string()))
1094 }
1095 NetworkErrorKind::Status { .. } => {
1096 ("NET_004", Some("Check gRPC service status".to_string()))
1097 }
1098 };
1099
1100 Diagnostic {
1101 code: code.to_string(),
1102 rql: None,
1103 message,
1104 column: None,
1105 fragment: Fragment::None,
1106 label: None,
1107 help,
1108 notes: vec![],
1109 cause: None,
1110 operator_chain: None,
1111 }
1112 }
1113
1114 TypeError::Auth { kind, message } => {
1115 let (code, help) = match &kind {
1116 AuthErrorKind::AuthenticationFailed { .. } => {
1117 ("ASVTH_001", Some("Check your credentials and try again".to_string()))
1118 }
1119 AuthErrorKind::AuthorizationDenied { .. } => {
1120 ("ASVTH_002", Some("Check your permissions for this resource".to_string()))
1121 }
1122 AuthErrorKind::TokenExpired => {
1123 ("ASVTH_003", Some("Refresh your authentication token".to_string()))
1124 }
1125 AuthErrorKind::InvalidToken => {
1126 ("ASVTH_004", Some("Provide a valid authentication token".to_string()))
1127 }
1128 };
1129
1130 Diagnostic {
1131 code: code.to_string(),
1132 rql: None,
1133 message,
1134 column: None,
1135 fragment: Fragment::None,
1136 label: None,
1137 help,
1138 notes: vec![],
1139 cause: None,
1140 operator_chain: None,
1141 }
1142 }
1143
1144 TypeError::DictionaryCapacityExceeded {
1145 id_type,
1146 value,
1147 max_value,
1148 } => Diagnostic {
1149 code: "DICT_001".to_string(),
1150 rql: None,
1151 message: format!("dictionary entry ID {} exceeds maximum {} for type {}", value, max_value, id_type),
1152 column: None,
1153 fragment: Fragment::None,
1154 label: Some(format!("{} capacity exceeded", id_type)),
1155 help: Some(
1156 "use a larger ID type (e.g., Uint2 instead of Uint1) when creating the dictionary".to_string(),
1157 ),
1158 notes: vec![],
1159 cause: None,
1160 operator_chain: None,
1161 },
1162
1163 TypeError::AssertionFailed {
1164 fragment,
1165 message,
1166 expression,
1167 } => {
1168 let label = expression
1169 .as_ref()
1170 .map(|expr| format!("this expression is false: {}", expr))
1171 .or_else(|| Some("assertion failed".to_string()));
1172
1173 Diagnostic {
1174 code: "ASSERT".to_string(),
1175 rql: None,
1176 message,
1177 fragment,
1178 label,
1179 help: None,
1180 notes: vec![],
1181 column: None,
1182 cause: None,
1183 operator_chain: None,
1184 }
1185 }
1186
1187 TypeError::Function {
1188 kind,
1189 message,
1190 fragment,
1191 } => {
1192 let (code, label, help) = match &kind {
1193 FunctionErrorKind::UnknownFunction => (
1194 "FUNCTION_001",
1195 Some("unknown function".to_string()),
1196 Some("Check the function name and available functions".to_string()),
1197 ),
1198 FunctionErrorKind::ArityMismatch { expected, .. } => (
1199 "FUNCTION_002",
1200 Some("wrong number of arguments".to_string()),
1201 Some(format!("Provide exactly {} arguments to function {}", expected, fragment.text())),
1202 ),
1203 FunctionErrorKind::TooManyArguments { max_args, .. } => (
1204 "FUNCTION_003",
1205 Some("too many arguments".to_string()),
1206 Some(format!("Provide at most {} arguments to function {}", max_args, fragment.text())),
1207 ),
1208 FunctionErrorKind::InvalidArgumentType { expected, .. } => {
1209 let expected_types =
1210 expected.iter().map(|t| format!("{:?}", t)).collect::<Vec<_>>().join(", ");
1211 (
1212 "FUNCTION_004",
1213 Some("invalid argument type".to_string()),
1214 Some(format!("Provide an argument of type: {}", expected_types)),
1215 )
1216 }
1217 FunctionErrorKind::UndefinedArgument { .. } => (
1218 "FUNCTION_005",
1219 Some("none argument".to_string()),
1220 Some("Provide a defined value for this argument".to_string()),
1221 ),
1222 FunctionErrorKind::MissingInput => (
1223 "FUNCTION_006",
1224 Some("missing input".to_string()),
1225 Some("Provide input data to the function".to_string()),
1226 ),
1227 FunctionErrorKind::ExecutionFailed { .. } => (
1228 "FUNCTION_007",
1229 Some("execution failed".to_string()),
1230 Some("Check function arguments and data".to_string()),
1231 ),
1232 FunctionErrorKind::InternalError { .. } => (
1233 "FUNCTION_008",
1234 Some("internal error".to_string()),
1235 Some("This is an internal error - please report this issue".to_string()),
1236 ),
1237 FunctionErrorKind::GeneratorNotFound => (
1238 "FUNCTION_009",
1239 Some("unknown generator function".to_string()),
1240 Some("Check the generator function name and ensure it is registered".to_string()),
1241 ),
1242 };
1243
1244 Diagnostic {
1245 code: code.to_string(),
1246 rql: None,
1247 message,
1248 column: None,
1249 fragment,
1250 label,
1251 help,
1252 notes: vec![],
1253 cause: None,
1254 operator_chain: None,
1255 }
1256 }
1257
1258 TypeError::Ast {
1259 kind,
1260 message,
1261 fragment,
1262 } => {
1263 let (code, label, help) = match &kind {
1264 AstErrorKind::TokenizeError { .. } => (
1265 "AST_001",
1266 None,
1267 Some("Check syntax and token format".to_string()),
1268 ),
1269 AstErrorKind::UnexpectedEof => (
1270 "AST_002",
1271 None,
1272 Some("Complete the statement".to_string()),
1273 ),
1274 AstErrorKind::ExpectedIdentifier => (
1275 "AST_003",
1276 Some(format!("found `{}`", fragment.text())),
1277 Some("expected token of type `identifier`".to_string()),
1278 ),
1279 AstErrorKind::InvalidColumnProperty => (
1280 "AST_011",
1281 Some(format!("found `{}`", fragment.text())),
1282 Some("Expected one of: auto_increment, dictionary, saturation, default".to_string()),
1283 ),
1284 AstErrorKind::InvalidPolicy => (
1285 "AST_004",
1286 Some(format!("found `{}`", fragment.text())),
1287 Some("Expected a valid policy identifier".to_string()),
1288 ),
1289 AstErrorKind::UnexpectedToken { expected } => (
1290 "AST_005",
1291 Some(format!("found `{}`", fragment.text())),
1292 Some(format!("Use {} instead", expected)),
1293 ),
1294 AstErrorKind::UnsupportedToken => (
1295 "AST_006",
1296 Some(format!("found `{}`", fragment.text())),
1297 Some("This token is not supported in this context".to_string()),
1298 ),
1299 AstErrorKind::MultipleExpressionsWithoutBraces => {
1300 let keyword = fragment.text().to_string();
1301 (
1302 "AST_007",
1303 Some("missing `{ … }` around expressions".to_string()),
1304 Some(format!("wrap the expressions in curly braces:\n {} {{ expr1, expr2, … }}", keyword)),
1305 )
1306 }
1307 AstErrorKind::UnrecognizedType => (
1308 "AST_008",
1309 Some("type not found".to_string()),
1310 None,
1311 ),
1312 AstErrorKind::UnsupportedAstNode { .. } => (
1313 "AST_009",
1314 Some("not supported in this context".to_string()),
1315 Some("This syntax is not yet supported or may be invalid in this context".to_string()),
1316 ),
1317 AstErrorKind::EmptyPipeline => (
1318 "AST_010",
1319 None,
1320 Some("A query pipeline must contain at least one operation".to_string()),
1321 ),
1322 };
1323
1324 Diagnostic {
1325 code: code.to_string(),
1326 rql: None,
1327 message,
1328 column: None,
1329 fragment,
1330 label,
1331 help,
1332 notes: vec![],
1333 cause: None,
1334 operator_chain: None,
1335 }
1336 }
1337
1338 TypeError::Runtime { kind, message } => {
1339 let (code, help) = match &kind {
1340 RuntimeErrorKind::VariableNotFound { name } => (
1341 "RUNTIME_001",
1342 Some(format!("Define the variable using 'let {} = <value>' before using it", name)),
1343 ),
1344 RuntimeErrorKind::VariableIsDataframe { name } => (
1345 "RUNTIME_002",
1346 Some(format!(
1347 "Extract a scalar value from the dataframe using '${} | only()', '${} | first()', or '${} | first_or_none()'",
1348 name, name, name
1349 )),
1350 ),
1351 RuntimeErrorKind::VariableIsImmutable { .. } => (
1352 "RUNTIME_003",
1353 Some("Use 'let mut $name := value' to declare a mutable variable".to_string()),
1354 ),
1355 RuntimeErrorKind::BreakOutsideLoop => (
1356 "RUNTIME_004",
1357 Some("Use BREAK inside a LOOP, WHILE, or FOR block".to_string()),
1358 ),
1359 RuntimeErrorKind::ContinueOutsideLoop => (
1360 "RUNTIME_005",
1361 Some("Use CONTINUE inside a LOOP, WHILE, or FOR block".to_string()),
1362 ),
1363 RuntimeErrorKind::MaxIterationsExceeded { .. } => (
1364 "RUNTIME_006",
1365 Some("Add a BREAK condition or use WHILE with a terminating condition".to_string()),
1366 ),
1367 RuntimeErrorKind::UndefinedFunction { .. } => (
1368 "RUNTIME_007",
1369 Some("Define the function using 'DEF name [] { ... }' before calling it".to_string()),
1370 ),
1371 RuntimeErrorKind::FieldNotFound {
1372 variable, available, ..
1373 } => {
1374 let help = if available.is_empty() {
1375 format!("The variable '{}' has no fields", variable)
1376 } else {
1377 format!("Available fields: {}", available.join(", "))
1378 };
1379 ("RUNTIME_009", Some(help))
1380 }
1381 RuntimeErrorKind::AppendTargetNotFrame { .. } => (
1382 "RUNTIME_008",
1383 Some("APPEND can only target Frame variables. Use a new variable name or ensure the target was created by APPEND or FROM".to_string()),
1384 ),
1385 RuntimeErrorKind::AppendColumnMismatch { .. } => (
1386 "RUNTIME_011",
1387 Some("APPEND requires the new rows to have the same columns as the target.".to_string()),
1388 ),
1389 RuntimeErrorKind::ExpectedSingleColumn { actual } => (
1390 "RUNTIME_010",
1391 Some(format!(
1392 "Expected a single-column value but got {} columns. Use field access to select one column.",
1393 actual
1394 )),
1395 ),
1396 };
1397
1398 let notes = match &kind {
1399 RuntimeErrorKind::VariableIsImmutable { .. } => {
1400 vec!["Only mutable variables can be reassigned".to_string()]
1401 }
1402 RuntimeErrorKind::VariableIsDataframe { .. } => {
1403 vec![
1404 "Dataframes must be explicitly converted to scalar values before use in expressions".to_string(),
1405 "Use only() for exactly 1 row × 1 column dataframes".to_string(),
1406 "Use first() to take the first value from the first column".to_string(),
1407 ]
1408 }
1409 RuntimeErrorKind::AppendColumnMismatch { existing, incoming, .. } => vec![
1410 format!("existing columns: [{}]", existing.join(", ")),
1411 format!("incoming columns: [{}]", incoming.join(", ")),
1412 ],
1413 _ => vec![],
1414 };
1415
1416 let fragment = match &kind {
1417 RuntimeErrorKind::UndefinedFunction { name } => Fragment::internal(name.clone()),
1418 RuntimeErrorKind::AppendColumnMismatch { fragment, .. } => fragment.clone(),
1419 _ => Fragment::None,
1420 };
1421
1422 Diagnostic {
1423 code: code.to_string(),
1424 rql: None,
1425 message,
1426 column: None,
1427 fragment,
1428 label: None,
1429 help,
1430 notes,
1431 cause: None,
1432 operator_chain: None,
1433 }
1434 }
1435
1436 TypeError::Procedure { kind, message, fragment } => {
1437 let (code, help, label) = match &kind {
1438 ProcedureErrorKind::UndefinedProcedure { .. } => (
1439 "PROCEDURE_001",
1440 "Check the procedure name and available procedures",
1441 "unknown procedure",
1442 ),
1443 ProcedureErrorKind::NoRegisteredImplementation { .. } => (
1444 "PROCEDURE_002",
1445 "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",
1446 "native binding not loaded",
1447 ),
1448 };
1449
1450 Diagnostic {
1451 code: code.to_string(),
1452 rql: None,
1453 message,
1454 column: None,
1455 fragment,
1456 label: Some(label.to_string()),
1457 help: Some(help.to_string()),
1458 notes: vec![],
1459 cause: None,
1460 operator_chain: None,
1461 }
1462 }
1463 }
1464 }
1465}