Skip to main content

reifydb_value/error/
diagnostic.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use 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}