Skip to main content

reifydb_type/error/
diagnostic.rs

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