Skip to main content

reifydb_value/error/
diagnostic.rs

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