1use super::{Diagnostic, IntoDiagnostic, util::value_range};
5use crate::{
6 error::{
7 AstErrorKind, BinaryOp, BlobEncodingKind, ConstraintKind, FunctionErrorKind, LogicalOp,
8 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
259
260
261
262
263 TypeError::ConstraintViolation {
264 kind,
265 message,
266 fragment,
267 } => {
268 let (code, help) = match &kind {
269 ConstraintKind::Utf8MaxBytes { max, .. } => (
270 "CONSTRAINT_001",
271 format!(
272 "The UTF8 field is constrained to a maximum of {} bytes. Consider shortening the text or increasing the constraint.",
273 max
274 ),
275 ),
276 ConstraintKind::BlobMaxBytes { max, .. } => (
277 "CONSTRAINT_002",
278 format!(
279 "The BLOB field is constrained to a maximum of {} bytes. Consider reducing the data size or increasing the constraint.",
280 max
281 ),
282 ),
283 ConstraintKind::DecimalPrecision { max, .. } => (
284 "CONSTRAINT_005",
285 format!(
286 "The DECIMAL field is constrained to a maximum precision of {} digits. Consider using a smaller number or increasing the precision constraint.",
287 max
288 ),
289 ),
290 ConstraintKind::DecimalScale { max, .. } => (
291 "CONSTRAINT_006",
292 format!(
293 "The DECIMAL field has a scale of {} fraction digits and a write is never rounded. Round the value explicitly or increase the scale.",
294 max
295 ),
296 ),
297 ConstraintKind::NoneNotAllowed { column_type } => (
298 "CONSTRAINT_007",
299 format!(
300 "The column type is {} which does not accept none. Use Option({}) if the column should be optional.",
301 column_type, column_type
302 ),
303 ),
304 };
305
306 Diagnostic {
307 code: code.to_string(),
308 rql: None,
309 message,
310 column: None,
311 fragment,
312 label: Some("constraint violation".to_string()),
313 help: Some(help),
314 notes: vec![],
315 cause: None,
316 operator_chain: None,
317 }
318 }
319
320 TypeError::InvalidNumberFormat { target, fragment } => {
321 let label = Some(format!("'{}' is not a valid {} number", fragment.text(), target));
322 let (help, notes) = match target {
323 ValueType::Float4 | ValueType::Float8 => (
324 "use decimal format (e.g., 123.45, -67.89, 1.23e-4)".to_string(),
325 vec![
326 "valid: 123.45".to_string(),
327 "valid: -67.89".to_string(),
328 "valid: 1.23e-4".to_string(),
329 ],
330 ),
331 ValueType::Int1
332 | ValueType::Int2
333 | ValueType::Int4
334 | ValueType::Int8
335 | ValueType::Int16
336 | ValueType::Uint1
337 | ValueType::Uint2
338 | ValueType::Uint4
339 | ValueType::Uint8
340 | ValueType::Uint16 => (
341 "use integer format (e.g., 123, -456) or decimal that can be truncated".to_string(),
342 vec![
343 "valid: 123".to_string(),
344 "valid: -456".to_string(),
345 "truncated: 123.7 → 123".to_string(),
346 ],
347 ),
348 _ => (
349 "ensure the value is a valid number".to_string(),
350 vec!["use a proper number format".to_string()],
351 ),
352 };
353
354 Diagnostic {
355 code: "NUMBER_001".to_string(),
356 rql: None,
357 message: "invalid number format".to_string(),
358 fragment,
359 label,
360 help: Some(help),
361 notes,
362 column: None,
363 cause: None,
364 operator_chain: None,
365 }
366 }
367
368 TypeError::NumberOutOfRange {
369 target,
370 fragment,
371 descriptor,
372 } => {
373 let range = value_range(target.clone());
374
375 let label = if let Some(ref desc) = descriptor {
376 Some(format!(
377 "value '{}' exceeds the valid range for {} column {}",
378 fragment.text(),
379 desc.column_type.as_ref().unwrap_or(&target),
380 desc.location_string()
381 ))
382 } else {
383 Some(format!(
384 "value '{}' exceeds the valid range for type {} ({})",
385 fragment.text(),
386 target,
387 range
388 ))
389 };
390
391 let help = if let Some(ref desc) = descriptor {
392 if desc.namespace.is_some() && desc.table.is_some() {
393 Some(format!("use a value within range {} or modify column {}", range, desc.location_string()))
394 } else {
395 Some(format!("use a value within range {} or use a wider type", range))
396 }
397 } else {
398 Some(format!("use a value within range {} or use a wider type", range))
399 };
400
401 Diagnostic {
402 code: "NUMBER_002".to_string(),
403 rql: None,
404 message: "number out of range".to_string(),
405 fragment,
406 label,
407 help,
408 notes: vec![format!("valid range: {}", range)],
409 column: None,
410 cause: None,
411 operator_chain: None,
412 }
413 }
414
415 TypeError::DivisionByZero {
416 target,
417 fragment,
418 } => Diagnostic {
419 code: "NUMBER_007".to_string(),
420 rql: None,
421 message: "division by zero".to_string(),
422 fragment,
423 label: Some(format!("divisor evaluates to zero in {} arithmetic", target)),
424 help: Some("ensure the divisor is non-zero before evaluating this expression".to_string()),
425 notes: vec![],
426 column: None,
427 cause: None,
428 operator_chain: None,
429 },
430
431 TypeError::NanNotAllowed => Diagnostic {
432 code: "NUMBER_003".to_string(),
433 rql: None,
434 message: "NaN not allowed".to_string(),
435 fragment: Fragment::None,
436 label: Some("NaN (Not a Number) values are not permitted".to_string()),
437 help: Some("use a finite number or none instead".to_string()),
438 notes: vec![],
439 column: None,
440 cause: None,
441 operator_chain: None,
442 },
443
444 TypeError::IntegerPrecisionLoss {
445 object_type,
446 target,
447 fragment,
448 } => {
449 let is_signed = object_type.is_signed_integer();
450 let (min_limit, max_limit) = match target {
451 ValueType::Float4 => {
452 if is_signed {
453 ("-16_777_216 (-2^24)", "16_777_216 (2^24)")
454 } else {
455 ("0", "16_777_216 (2^24)")
456 }
457 }
458 ValueType::Float8 => {
459 if is_signed {
460 ("-9_007_199_254_740_992 (-2^53)", "9_007_199_254_740_992 (2^53)")
461 } else {
462 ("0", "9_007_199_254_740_992 (2^53)")
463 }
464 }
465 _ => unreachable!("IntegerPrecisionLoss should only be used for float targets"),
466 };
467
468 let label = Some(format!(
469 "converting '{}' from {} to {} would lose precision",
470 fragment.text(),
471 object_type,
472 target
473 ));
474
475 Diagnostic {
476 code: "NUMBER_004".to_string(),
477 rql: None,
478 message: "too large for precise float conversion".to_string(),
479 fragment,
480 label,
481 help: None,
482 notes: vec![
483 format!("{} can only represent from {} to {} precisely", target, min_limit, max_limit),
484 "consider using a different numeric type if exact precision is required".to_string(),
485 ],
486 column: None,
487 cause: None,
488 operator_chain: None,
489 }
490 }
491
492 TypeError::DecimalScaleExceedsPrecision {
493 scale,
494 precision,
495 fragment,
496 } => {
497 let label = Some(format!("scale ({}) cannot be greater than precision ({})", scale, precision));
498 Diagnostic {
499 code: "NUMBER_005".to_string(),
500 rql: None,
501 message: "decimal scale exceeds precision".to_string(),
502 fragment,
503 label,
504 help: Some(format!("use a scale value between 0 and {} or increase precision", precision)),
505 notes: vec![
506 format!("current precision: {}", precision),
507 format!("current scale: {}", scale),
508 "scale represents the number of digits after the decimal point".to_string(),
509 "precision represents the total number of significant digits".to_string(),
510 ],
511 column: None,
512 cause: None,
513 operator_chain: None,
514 }
515 }
516
517 TypeError::DecimalPrecisionInvalid { precision } => {
518 let label = Some(format!("precision ({}) must be between 1 and 76", precision));
519 Diagnostic {
520 code: "NUMBER_006".to_string(),
521 rql: None,
522 message: "invalid decimal precision".to_string(),
523 fragment: Fragment::None,
524 label,
525 help: Some("use a precision value between 1 and 76".to_string()),
526 notes: vec![
527 format!("current precision: {}", precision),
528 "precision represents the total number of significant digits".to_string(),
529 ],
530 column: None,
531 cause: None,
532 operator_chain: None,
533 }
534 }
535
536 TypeError::InvalidBooleanFormat { fragment } => {
537 let value = fragment.text().to_string();
538 let label = Some(format!("expected 'true' or 'false', found '{}'", value));
539 Diagnostic {
540 code: "BOOLEAN_001".to_string(),
541 rql: None,
542 message: "invalid boolean format".to_string(),
543 fragment,
544 label,
545 help: Some("use 'true' or 'false'".to_string()),
546 notes: vec!["valid: true, TRUE".to_string(), "valid: false, FALSE".to_string()],
547 column: None,
548 cause: None,
549 operator_chain: None,
550 }
551 }
552
553 TypeError::EmptyBooleanValue { fragment } => Diagnostic {
554 code: "BOOLEAN_002".to_string(),
555 rql: None,
556 message: "empty boolean value".to_string(),
557 fragment,
558 label: Some("boolean value cannot be empty".to_string()),
559 help: Some("provide either 'true' or 'false'".to_string()),
560 notes: vec!["valid: true".to_string(), "valid: false".to_string()],
561 column: None,
562 cause: None,
563 operator_chain: None,
564 },
565
566 TypeError::InvalidNumberBoolean { fragment } => {
567 let value = fragment.text().to_string();
568 let label =
569 Some(format!("number '{}' cannot be cast to boolean, only 1 or 0 are allowed", value));
570 Diagnostic {
571 code: "BOOLEAN_003".to_string(),
572 rql: None,
573 message: "invalid boolean".to_string(),
574 fragment,
575 label,
576 help: Some("use 1 for true or 0 for false".to_string()),
577 notes: vec![
578 "valid: 1 → true".to_string(),
579 "valid: 0 → false".to_string(),
580 "invalid: any other number".to_string(),
581 ],
582 column: None,
583 cause: None,
584 operator_chain: None,
585 }
586 }
587
588 TypeError::Temporal {
589 kind,
590 message,
591 fragment,
592 } => {
593 let (code, label, help, notes) = match &kind {
594 TemporalKind::InvalidDateFormat => (
595 "TEMPORAL_001",
596 Some(format!("expected YYYY-MM-DD format, found '{}'", fragment.text())),
597 Some("use the format YYYY-MM-DD (e.g., 2024-03-15)".to_string()),
598 vec!["dates must have exactly 3 parts separated by hyphens".to_string()],
599 ),
600 TemporalKind::InvalidDateTimeFormat => (
601 "TEMPORAL_002",
602 Some(format!("expected YYYY-MM-DDTHH:MM:SS format, found '{}'", fragment.text())),
603 Some("use the format YYYY-MM-DDTHH:MM:SS[.fff][Z|±HH:MM] (e.g., 2024-03-15T14:30:45)".to_string()),
604 vec!["datetime must contain 'T' separator between date and time parts".to_string()],
605 ),
606 TemporalKind::InvalidTimeFormat => (
607 "TEMPORAL_003",
608 Some(format!("expected HH:MM:SS format, found '{}'", fragment.text())),
609 Some("use the format HH:MM:SS[.fff][Z|±HH:MM] (e.g., 14:30:45)".to_string()),
610 vec!["time must have exactly 3 parts separated by colons".to_string()],
611 ),
612 TemporalKind::InvalidDurationFormat => (
613 "TEMPORAL_004",
614 Some(format!("expected P[n]Y[n]M[n]W[n]D[T[n]H[n]M[n]S] format, found '{}'", fragment.text())),
615 Some("use ISO 8601 duration format starting with 'P' (e.g., P1D, PT2H30M, P1Y2M3DT4H5M6S)".to_string()),
616 vec![
617 "duration must start with 'P' followed by duration components".to_string(),
618 "date part: P[n]Y[n]M[n]W[n]D (years, months, weeks, days)".to_string(),
619 "time part: T[n]H[n]M[n]S (hours, minutes, seconds)".to_string(),
620 ],
621 ),
622 TemporalKind::InvalidYear => (
623 "TEMPORAL_005",
624 Some(format!("year '{}' cannot be parsed as a number", fragment.text())),
625 Some("ensure the year is a valid 4-digit number".to_string()),
626 vec!["valid examples: 2024, 1999, 2000".to_string()],
627 ),
628 TemporalKind::InvalidTimeComponentFormat { component } => (
629 "TEMPORAL_029",
630 Some(format!("{} '{}' must be exactly 2 digits", component, fragment.text())),
631 Some(format!("ensure the {} is exactly 2 digits (e.g., 09, 14, 23)", component)),
632 vec![format!("{} must be exactly 2 digits in HH:MM:SS format", component)],
633 ),
634 TemporalKind::InvalidMonth => (
635 "TEMPORAL_006",
636 Some(format!("month '{}' cannot be parsed as a number (expected 1-12)", fragment.text())),
637 Some("ensure the month is a valid number between 1 and 12".to_string()),
638 vec!["valid examples: 01, 03, 12".to_string()],
639 ),
640 TemporalKind::InvalidDay => (
641 "TEMPORAL_007",
642 Some(format!("day '{}' cannot be parsed as a number (expected 1-31)", fragment.text())),
643 Some("ensure the day is a valid number between 1 and 31".to_string()),
644 vec!["valid examples: 01, 15, 31".to_string()],
645 ),
646 TemporalKind::InvalidHour => (
647 "TEMPORAL_008",
648 Some(format!("hour '{}' cannot be parsed as a number (expected 0-23)", fragment.text())),
649 Some("ensure the hour is a valid number between 0 and 23 (use 24-hour format)".to_string()),
650 vec![
651 "valid examples: 09, 14, 23".to_string(),
652 "hours must be in 24-hour format (00-23)".to_string(),
653 ],
654 ),
655 TemporalKind::InvalidMinute => (
656 "TEMPORAL_009",
657 Some(format!("minute '{}' cannot be parsed as a number (expected 0-59)", fragment.text())),
658 Some("ensure the minute is a valid number between 0 and 59".to_string()),
659 vec!["valid examples: 00, 30, 59".to_string()],
660 ),
661 TemporalKind::InvalidSecond => (
662 "TEMPORAL_010",
663 Some(format!("second '{}' cannot be parsed as a number (expected 0-59)", fragment.text())),
664 Some("ensure the second is a valid number between 0 and 59".to_string()),
665 vec!["valid examples: 00, 30, 59".to_string()],
666 ),
667 TemporalKind::InvalidFractionalSeconds => (
668 "TEMPORAL_011",
669 Some(format!("fractional seconds '{}' cannot be parsed as a number", fragment.text())),
670 Some("ensure fractional seconds contain only digits".to_string()),
671 vec!["valid examples: 123, 999999, 000001".to_string()],
672 ),
673 TemporalKind::InvalidDateValues => (
674 "TEMPORAL_012",
675 Some(format!("date '{}' represents an invalid calendar date", fragment.text())),
676 Some("ensure the date exists in the calendar (e.g., no February 30)".to_string()),
677 vec![
678 "check month has correct number of days".to_string(),
679 "consider leap years for February 29".to_string(),
680 ],
681 ),
682 TemporalKind::InvalidTimeValues => (
683 "TEMPORAL_013",
684 Some(format!("time '{}' contains out-of-range values", fragment.text())),
685 Some("ensure hours are 0-23, minutes and seconds are 0-59".to_string()),
686 vec!["use 24-hour format for hours".to_string()],
687 ),
688 TemporalKind::InvalidDurationCharacter => (
689 "TEMPORAL_014",
690 Some(format!("character '{}' is not valid in ISO 8601 duration", fragment.text())),
691 Some("use only valid duration units: Y, M, W, D, H, m, S".to_string()),
692 vec![
693 "date part units: Y (years), M (months), W (weeks), D (days)".to_string(),
694 "time part units: H (hours), m (minutes), S (seconds)".to_string(),
695 ],
696 ),
697 TemporalKind::IncompleteDurationSpecification => (
698 "TEMPORAL_015",
699 Some(format!("number '{}' is missing a unit specifier", fragment.text())),
700 Some("add a unit letter after the number (Y, M, W, D, H, M, or S)".to_string()),
701 vec!["example: P1D (not P1), PT2H (not PT2)".to_string()],
702 ),
703 TemporalKind::InvalidUnitInContext { unit, in_time_part } => {
704 let context = if *in_time_part {
705 "time part (after T)"
706 } else {
707 "date part (before T)"
708 };
709 let allowed = if *in_time_part { "H, M, S" } else { "Y, M, W, D" };
710 (
711 "TEMPORAL_016",
712 Some(format!("unit '{}' is not allowed in the {}", unit, context)),
713 Some(format!("use only {} in the {}", allowed, context)),
714 vec![
715 "date part (before T): Y, M, W, D".to_string(),
716 "time part (after T): H, M, S".to_string(),
717 ],
718 )
719 }
720 TemporalKind::InvalidDurationComponentValue { unit } => (
721 "TEMPORAL_017",
722 Some(format!("{} value '{}' cannot be parsed as a number", temporal_unit_name(*unit), fragment.text())),
723 Some(format!("ensure the {} value is a valid number", temporal_unit_name(*unit))),
724 vec![format!("valid examples: P1{}, P10{}", unit, unit)],
725 ),
726 TemporalKind::UnrecognizedTemporalPattern => (
727 "TEMPORAL_018",
728 Some(format!("value '{}' does not match any temporal format", fragment.text())),
729 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()),
730 vec![
731 "date: 2024-03-15".to_string(),
732 "time: 14:30:45".to_string(),
733 "datetime: 2024-03-15T14:30:45".to_string(),
734 "duration: P1Y2M3DT4H5M6S".to_string(),
735 ],
736 ),
737 TemporalKind::EmptyDateComponent => (
738 "TEMPORAL_019",
739 Some(format!("date component '{}' is empty", fragment.text())),
740 Some("ensure all date parts (year, month, day) are provided".to_string()),
741 vec!["date format: YYYY-MM-DD (e.g., 2024-03-15)".to_string()],
742 ),
743 TemporalKind::EmptyTimeComponent => (
744 "TEMPORAL_020",
745 Some(format!("time component '{}' is empty", fragment.text())),
746 Some("ensure all time parts (hour, minute, second) are provided".to_string()),
747 vec!["time format: HH:MM:SS (e.g., 14:30:45)".to_string()],
748 ),
749 TemporalKind::DuplicateDurationComponent { component } => (
750 "TEMPORAL_021",
751 Some(format!("duration component '{}' appears multiple times", component)),
752 Some("each duration component (Y, M, W, D, H, M, S) can only appear once".to_string()),
753 vec!["valid: P1Y2M3D".to_string(), "invalid: P1Y2Y (duplicate Y)".to_string()],
754 ),
755 TemporalKind::OutOfOrderDurationComponent { component } => (
756 "TEMPORAL_022",
757 Some(format!("duration component '{}' appears out of order", component)),
758 Some("duration components must appear in order: Y, M, W, D (before T), then H, M, S (after T)".to_string()),
759 vec![
760 "date part order: Y (years), M (months), W (weeks), D (days)".to_string(),
761 "time part order: H (hours), M (minutes), S (seconds)".to_string(),
762 "valid: P1Y2M3D, PT1H2M3S".to_string(),
763 "invalid: P1D1Y (D before Y), PT1S1H (S before H)".to_string(),
764 ],
765 ),
766 TemporalKind::DateTimeOutOfRange => (
767 "TEMPORAL_023",
768 Some("datetime value is outside the representable range".to_string()),
769 Some("use a datetime between 1677-09-21T00:12:43.145224192Z and 2262-04-11T23:47:16.854775807Z".to_string()),
770 vec!["DateTime is stored as nanoseconds since Unix epoch (i64)".to_string()],
771 ),
772 TemporalKind::DateTimeOverflow { message: msg } => (
773 "TEMPORAL_026",
774 Some(msg.clone()),
775 Some("ensure datetime values are within representable range".to_string()),
776 vec![
777 "DateTime is stored as nanoseconds since Unix epoch (i64)".to_string(),
778 "valid range: 1677-09-21T00:12:43.145224192Z to 2262-04-11T23:47:16.854775807Z".to_string(),
779 ],
780 ),
781 TemporalKind::DurationOverflow { message: msg } => (
782 "TEMPORAL_024",
783 Some(msg.clone()),
784 Some("ensure duration values are within representable range".to_string()),
785 vec![
786 "months and days are stored as i32 (max ±2,147,483,647)".to_string(),
787 "sub-day time is stored as nanoseconds in i64".to_string(),
788 ],
789 ),
790 TemporalKind::DurationMixedSign { days, nanos } => (
791 "TEMPORAL_025",
792 Some(format!("duration days and nanos have mixed signs: days={}, nanos={}", days, nanos)),
793 Some("days and nanos must share the same sign".to_string()),
794 vec![
795 "days and nanos are commensurable (1 day = 86400s) so they must agree in sign".to_string(),
796 "months may differ in sign from days/nanos (months have variable length)".to_string(),
797 ],
798 ),
799 TemporalKind::TimeOverflow { message: msg } => (
800 "TEMPORAL_027",
801 Some(msg.clone()),
802 Some("ensure time values are within representable range".to_string()),
803 vec![
804 "Time is stored as nanoseconds since midnight (u64)".to_string(),
805 "valid range: 00:00:00.000000000 to 23:59:59.999999999".to_string(),
806 ],
807 ),
808 TemporalKind::DateOverflow { message: msg } => (
809 "TEMPORAL_028",
810 Some(msg.clone()),
811 Some("ensure date values are within representable range".to_string()),
812 vec![
813 "Date is stored as days since epoch (i32)".to_string(),
814 "valid range: approximately ±1,000,000 years from 1970".to_string(),
815 ],
816 ),
817 };
818
819 Diagnostic {
820 code: code.to_string(),
821 rql: None,
822 message,
823 fragment,
824 label,
825 help,
826 notes,
827 column: None,
828 cause: None,
829 operator_chain: None,
830 }
831 }
832
833 TypeError::InvalidUuid4Format { fragment } => {
834 let label = Some(format!("'{}' is not a valid UUID v4", fragment.text()));
835 Diagnostic {
836 code: "UUID_001".to_string(),
837 rql: None,
838 message: "invalid UUID v4 format".to_string(),
839 fragment,
840 label,
841 help: Some("use UUID v4 format (e.g., 550e8400-e29b-41d4-a716-446655440000)".to_string()),
842 notes: vec![
843 "valid: 550e8400-e29b-41d4-a716-446655440000".to_string(),
844 "valid: f47ac10b-58cc-4372-a567-0e02b2c3d479".to_string(),
845 "UUID v4 uses random or pseudo-random numbers".to_string(),
846 ],
847 column: None,
848 cause: None,
849 operator_chain: None,
850 }
851 }
852
853 TypeError::InvalidUuid7Format { fragment } => {
854 let label = Some(format!("'{}' is not a valid UUID v7", fragment.text()));
855 Diagnostic {
856 code: "UUID_002".to_string(),
857 rql: None,
858 message: "invalid UUID v7 format".to_string(),
859 fragment,
860 label,
861 help: Some("use UUID v7 format (e.g., 017f22e2-79b0-7cc3-98c4-dc0c0c07398f)".to_string()),
862 notes: vec![
863 "valid: 017f22e2-79b0-7cc3-98c4-dc0c0c07398f".to_string(),
864 "valid: 01854d6e-bd60-7b28-a3c7-6b4ad2c4e2e8".to_string(),
865 "UUID v7 uses timestamp-based generation".to_string(),
866 ],
867 column: None,
868 cause: None,
869 operator_chain: None,
870 }
871 }
872
873 TypeError::BlobEncoding {
874 kind,
875 message,
876 fragment,
877 } => {
878 let (code, label, help) = match &kind {
879 BlobEncodingKind::InvalidHex => (
880 "BLOB_001",
881 Some("Invalid hex characters found".to_string()),
882 Some("Hex strings should only contain 0-9, a-f, A-F characters".to_string()),
883 ),
884 BlobEncodingKind::InvalidBase64 => (
885 "BLOB_002",
886 Some("Invalid base64 encoding found".to_string()),
887 Some("Base64 strings should only contain A-Z, a-z, 0-9, +, / and = padding".to_string()),
888 ),
889 BlobEncodingKind::InvalidBase64Url => (
890 "BLOB_003",
891 Some("Invalid base64url encoding found".to_string()),
892 Some("Base64url strings should only contain A-Z, a-z, 0-9, -, _ characters".to_string()),
893 ),
894 BlobEncodingKind::InvalidBase58 => (
895 "BLOB_005",
896 Some("Invalid base58 encoding found".to_string()),
897 Some("Base58 strings should only contain 1-9, A-H, J-N, P-Z, a-k, m-z characters".to_string()),
898 ),
899 BlobEncodingKind::InvalidUtf8Sequence { .. } => (
900 "BLOB_004",
901 Some("BLOB contains invalid UTF-8 bytes".to_string()),
902 Some("Use to_utf8_lossy() if you want to replace invalid sequences with replacement characters".to_string()),
903 ),
904 };
905
906 Diagnostic {
907 code: code.to_string(),
908 rql: None,
909 message,
910 column: None,
911 fragment,
912 label,
913 help,
914 notes: vec![],
915 cause: None,
916 operator_chain: None,
917 }
918 }
919
920 TypeError::SerdeDeserialize { message } => Diagnostic {
921 code: "SERDE_001".to_string(),
922 rql: None,
923 message: format!("Serde deserialization error: {}", message),
924 column: None,
925 fragment: Fragment::None,
926 label: None,
927 help: Some("Check data format and structure".to_string()),
928 notes: vec![],
929 cause: None,
930 operator_chain: None,
931 },
932
933 TypeError::SerdeSerialize { message } => Diagnostic {
934 code: "SERDE_002".to_string(),
935 rql: None,
936 message: format!("Serde serialization error: {}", message),
937 column: None,
938 fragment: Fragment::None,
939 label: None,
940 help: Some("Check data format and structure".to_string()),
941 notes: vec![],
942 cause: None,
943 operator_chain: None,
944 },
945
946 TypeError::SerdeKeycode { message } => Diagnostic {
947 code: "SERDE_003".to_string(),
948 rql: None,
949 message: format!("Keycode serialization error: {}", message),
950 column: None,
951 fragment: Fragment::None,
952 label: None,
953 help: Some("Check keycode data and format".to_string()),
954 notes: vec![],
955 cause: None,
956 operator_chain: None,
957 },
958
959 TypeError::ArrayConversion { message } => Diagnostic {
960 code: "CONV_001".to_string(),
961 rql: None,
962 message: format!("Array conversion error: {}", message),
963 column: None,
964 fragment: Fragment::None,
965 label: None,
966 help: Some("Check array size requirements".to_string()),
967 notes: vec![],
968 cause: None,
969 operator_chain: None,
970 },
971
972 TypeError::Utf8Conversion { message } => Diagnostic {
973 code: "CONV_002".to_string(),
974 rql: None,
975 message: format!("UTF-8 conversion error: {}", message),
976 column: None,
977 fragment: Fragment::None,
978 label: None,
979 help: Some("Check string encoding".to_string()),
980 notes: vec![],
981 cause: None,
982 operator_chain: None,
983 },
984
985 TypeError::IntegerConversion { message } => Diagnostic {
986 code: "CONV_003".to_string(),
987 rql: None,
988 message: format!("Integer conversion error: {}", message),
989 column: None,
990 fragment: Fragment::None,
991 label: None,
992 help: Some("Check integer range limits".to_string()),
993 notes: vec![],
994 cause: None,
995 operator_chain: None,
996 },
997
998 TypeError::ColumnRead {
999 column_type,
1000 target,
1001 reason,
1002 } => Diagnostic {
1003 code: "CONV_004".to_string(),
1004 rql: None,
1005 message: format!("cannot read {} as {}: {}", column_type, target, reason),
1006 column: None,
1007 fragment: Fragment::None,
1008 label: None,
1009 help: Some("Read the column as a type that holds its values".to_string()),
1010 notes: vec![],
1011 cause: None,
1012 operator_chain: None,
1013 },
1014
1015
1016
1017 TypeError::DictionaryCapacityExceeded {
1018 id_type,
1019 value,
1020 max_value,
1021 } => Diagnostic {
1022 code: "DICT_001".to_string(),
1023 rql: None,
1024 message: format!("dictionary entry ID {} exceeds maximum {} for type {}", value, max_value, id_type),
1025 column: None,
1026 fragment: Fragment::None,
1027 label: Some(format!("{} capacity exceeded", id_type)),
1028 help: Some(
1029 "use a larger ID type (e.g., Uint2 instead of Uint1) when creating the dictionary".to_string(),
1030 ),
1031 notes: vec![],
1032 cause: None,
1033 operator_chain: None,
1034 },
1035
1036
1037 TypeError::Function {
1038 kind,
1039 message,
1040 fragment,
1041 } => {
1042 let (code, label, help) = match &kind {
1043 FunctionErrorKind::ArityMismatch { expected, .. } => (
1044 "FUNCTION_002",
1045 Some("wrong number of arguments".to_string()),
1046 Some(format!("Provide exactly {} arguments to function {}", expected, fragment.text())),
1047 ),
1048 FunctionErrorKind::TooManyArguments { max_args, .. } => (
1049 "FUNCTION_003",
1050 Some("too many arguments".to_string()),
1051 Some(format!("Provide at most {} arguments to function {}", max_args, fragment.text())),
1052 ),
1053 };
1054
1055 Diagnostic {
1056 code: code.to_string(),
1057 rql: None,
1058 message,
1059 column: None,
1060 fragment,
1061 label,
1062 help,
1063 notes: vec![],
1064 cause: None,
1065 operator_chain: None,
1066 }
1067 }
1068
1069 TypeError::Ast {
1070 kind,
1071 message,
1072 fragment,
1073 } => {
1074 let (code, label, help) = match &kind {
1075 AstErrorKind::TokenizeError { .. } => (
1076 "AST_001",
1077 None,
1078 Some("Check syntax and token format".to_string()),
1079 ),
1080 AstErrorKind::InvalidColumnProperty => (
1081 "AST_011",
1082 Some(format!("found `{}`", fragment.text())),
1083 Some("Expected one of: auto_increment, dictionary, saturation, default".to_string()),
1084 ),
1085 AstErrorKind::InvalidPolicy => (
1086 "AST_004",
1087 Some(format!("found `{}`", fragment.text())),
1088 Some("Expected a valid policy identifier".to_string()),
1089 ),
1090 AstErrorKind::UnexpectedToken { expected } => (
1091 "AST_005",
1092 Some(format!("found `{}`", fragment.text())),
1093 Some(format!("Use {} instead", expected)),
1094 ),
1095 };
1096
1097 Diagnostic {
1098 code: code.to_string(),
1099 rql: None,
1100 message,
1101 column: None,
1102 fragment,
1103 label,
1104 help,
1105 notes: vec![],
1106 cause: None,
1107 operator_chain: None,
1108 }
1109 }
1110
1111 TypeError::Runtime { kind, message } => {
1112 let (code, help) = match &kind {
1113 RuntimeErrorKind::VariableNotFound { fragment } => {
1114 let name = fragment.text().strip_prefix('$').unwrap_or(fragment.text());
1115 (
1116 "RUNTIME_001",
1117 Some(format!("Define the variable using 'let {} = <value>' before using it", name)),
1118 )
1119 }
1120 RuntimeErrorKind::VariableIsClosure { fragment } => {
1121 let name = fragment.text().strip_prefix('$').unwrap_or(fragment.text());
1122 (
1123 "RUNTIME_013",
1124 Some(format!("Call the closure, like '{}()', a closure value cannot be read as rows", name)),
1125 )
1126 }
1127 RuntimeErrorKind::VariableIsDataframe { name } => (
1128 "RUNTIME_002",
1129 Some(format!(
1130 "Extract a scalar value from the dataframe using '${} | only()', '${} | first()', or '${} | first_or_none()'",
1131 name, name, name
1132 )),
1133 ),
1134 RuntimeErrorKind::VariableIsImmutable { .. } => (
1135 "RUNTIME_003",
1136 Some("Use 'let mut $name := value' to declare a mutable variable".to_string()),
1137 ),
1138 RuntimeErrorKind::MaxIterationsExceeded { .. } => (
1139 "RUNTIME_006",
1140 Some("Add a BREAK condition or use WHILE with a terminating condition".to_string()),
1141 ),
1142 RuntimeErrorKind::FieldNotFound {
1143 variable, available, ..
1144 } => {
1145 let help = if available.is_empty() {
1146 format!("The variable '{}' has no fields", variable)
1147 } else {
1148 format!("Available fields: {}", available.join(", "))
1149 };
1150 ("RUNTIME_009", Some(help))
1151 }
1152 RuntimeErrorKind::AppendTargetNotFrame { .. } => (
1153 "RUNTIME_008",
1154 Some("APPEND can only target Frame variables. Use a new variable name or ensure the target was created by APPEND or FROM".to_string()),
1155 ),
1156 RuntimeErrorKind::AppendColumnMismatch { .. } => (
1157 "RUNTIME_011",
1158 Some("APPEND requires the new rows to have the same columns as the target.".to_string()),
1159 ),
1160 RuntimeErrorKind::ConditionalBranchMismatch { .. } => (
1161 "RUNTIME_012",
1162 Some("Every branch of a conditional must produce the same columns with the same types.".to_string()),
1163 ),
1164 RuntimeErrorKind::ColumnLengthMismatch { left, right, .. } => (
1165 "RUNTIME_014",
1166 Some(format!(
1167 "Both sides must cover the same rows; got {} and {}. A single row value is repeated to match, any other length is not.",
1168 left, right
1169 )),
1170 ),
1171 RuntimeErrorKind::ExpectedSingleColumn { actual } => (
1172 "RUNTIME_010",
1173 Some(format!(
1174 "Expected a single-column value but got {} columns. Use field access to select one column.",
1175 actual
1176 )),
1177 ),
1178 };
1179
1180 let notes = match &kind {
1181 RuntimeErrorKind::VariableIsImmutable { .. } => {
1182 vec!["Only mutable variables can be reassigned".to_string()]
1183 }
1184 RuntimeErrorKind::VariableIsDataframe { .. } => {
1185 vec![
1186 "Dataframes must be explicitly converted to scalar values before use in expressions".to_string(),
1187 "Use only() for exactly 1 row × 1 column dataframes".to_string(),
1188 "Use first() to take the first value from the first column".to_string(),
1189 ]
1190 }
1191 RuntimeErrorKind::AppendColumnMismatch { existing, incoming, .. } => vec![
1192 format!("existing columns: [{}]", existing.join(", ")),
1193 format!("incoming columns: [{}]", incoming.join(", ")),
1194 ],
1195 RuntimeErrorKind::ConditionalBranchMismatch { expected, actual, .. } => vec![
1196 format!("first branch yields: [{}]", expected.join(", ")),
1197 format!("this branch yields: [{}]", actual.join(", ")),
1198 ],
1199 _ => vec![],
1200 };
1201
1202 let fragment = match &kind {
1203 RuntimeErrorKind::VariableNotFound { fragment } => fragment.clone(),
1204 RuntimeErrorKind::VariableIsClosure { fragment } => fragment.clone(),
1205 RuntimeErrorKind::AppendColumnMismatch { fragment, .. } => fragment.clone(),
1206 RuntimeErrorKind::ConditionalBranchMismatch { fragment, .. } => fragment.clone(),
1207 RuntimeErrorKind::ColumnLengthMismatch { fragment, .. } => fragment.clone(),
1208 _ => Fragment::None,
1209 };
1210
1211 Diagnostic {
1212 code: code.to_string(),
1213 rql: None,
1214 message,
1215 column: None,
1216 fragment,
1217 label: None,
1218 help,
1219 notes,
1220 cause: None,
1221 operator_chain: None,
1222 }
1223 }
1224
1225 TypeError::Procedure { kind, message, fragment } => {
1226 let (code, help, label) = match &kind {
1227 ProcedureErrorKind::UndefinedProcedure { .. } => (
1228 "PROCEDURE_004",
1229 "Check the procedure name and available procedures",
1230 "unknown procedure",
1231 ),
1232 ProcedureErrorKind::NoRegisteredImplementation { .. } => (
1233 "PROCEDURE_005",
1234 "the catalog references an extern binding that is not loaded in this binary - rebuild with the registration or drop/replace the catalog entry",
1235 "extern binding not loaded",
1236 ),
1237 };
1238
1239 Diagnostic {
1240 code: code.to_string(),
1241 rql: None,
1242 message,
1243 column: None,
1244 fragment,
1245 label: Some(label.to_string()),
1246 help: Some(help.to_string()),
1247 notes: vec![],
1248 cause: None,
1249 operator_chain: None,
1250 }
1251 }
1252 }
1253 }
1254}