1use std::fmt;
2
3use radixdb_catalog::ObjectId;
4
5const MAX_SECONDARY_SPANS: usize = 16;
6const MAX_DIAGNOSTIC_FRAMES: usize = 64;
7const MAX_DIAGNOSTIC_DETAILS: usize = 32;
8const MAX_DIAGNOSTIC_TEXT_BYTES: usize = 4096;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum DiagnosticCategory {
12 Parse,
13 Bind,
14 Verify,
15 Runtime,
16 Security,
17 Resource,
18 Cardinality,
19 Trigger,
20 Job,
21}
22
23impl DiagnosticCategory {
24 pub const fn as_str(self) -> &'static str {
25 match self {
26 Self::Parse => "parse",
27 Self::Bind => "bind",
28 Self::Verify => "verify",
29 Self::Runtime => "runtime",
30 Self::Security => "security",
31 Self::Resource => "resource",
32 Self::Cardinality => "cardinality",
33 Self::Trigger => "trigger",
34 Self::Job => "job",
35 }
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
40pub enum DiagnosticKind {
41 ParseExpectedToken,
42 ParseUnsupportedSyntax,
43 ParseLimitExceeded,
44 BindUnknownLocal,
45 BindUnknownObject,
46 BindAmbiguousRoutine,
47 BindTypeMismatch,
48 BindDependencyCycle,
49 VerifyCapabilityDenied,
50 VerifyTransactionControlForbidden,
51 VerifyDynamicDdlNotSupported,
52 VerifyUnboundedResult,
53 RuntimeNullNotAllowed,
54 RuntimeArrayBounds,
55 RuntimeNumericOverflow,
56 RuntimeInvalidIr,
57 RuntimeInvalidArgument,
58 RuntimeConflict,
59 RuntimeNotFound,
60 RuntimeInvalidState,
61 RuntimeUniqueViolation,
62 CardinalityNoDataFound,
63 CardinalityTooManyRows,
64 SecurityExecuteDenied,
65 SecurityObjectDenied,
66 SecurityUnsafeSearchPath,
67 ResourceInstructions,
68 ResourceHeap,
69 ResourceRows,
70 ResourceBytes,
71 ResourceSqlStatements,
72 ResourceFrames,
73 ResourceDeadline,
74 ResourceCancelled,
75 TriggerCycle,
76 TriggerDepth,
77 TriggerInvalidReturn,
78 JobAttemptFailed,
79}
80
81impl DiagnosticKind {
82 pub const ALL: [Self; 38] = [
83 Self::ParseExpectedToken,
84 Self::ParseUnsupportedSyntax,
85 Self::ParseLimitExceeded,
86 Self::BindUnknownLocal,
87 Self::BindUnknownObject,
88 Self::BindAmbiguousRoutine,
89 Self::BindTypeMismatch,
90 Self::BindDependencyCycle,
91 Self::VerifyCapabilityDenied,
92 Self::VerifyTransactionControlForbidden,
93 Self::VerifyDynamicDdlNotSupported,
94 Self::VerifyUnboundedResult,
95 Self::RuntimeNullNotAllowed,
96 Self::RuntimeArrayBounds,
97 Self::RuntimeNumericOverflow,
98 Self::RuntimeInvalidIr,
99 Self::RuntimeInvalidArgument,
100 Self::RuntimeConflict,
101 Self::RuntimeNotFound,
102 Self::RuntimeInvalidState,
103 Self::RuntimeUniqueViolation,
104 Self::CardinalityNoDataFound,
105 Self::CardinalityTooManyRows,
106 Self::SecurityExecuteDenied,
107 Self::SecurityObjectDenied,
108 Self::SecurityUnsafeSearchPath,
109 Self::ResourceInstructions,
110 Self::ResourceHeap,
111 Self::ResourceRows,
112 Self::ResourceBytes,
113 Self::ResourceSqlStatements,
114 Self::ResourceFrames,
115 Self::ResourceDeadline,
116 Self::ResourceCancelled,
117 Self::TriggerCycle,
118 Self::TriggerDepth,
119 Self::TriggerInvalidReturn,
120 Self::JobAttemptFailed,
121 ];
122
123 pub const fn as_str(self) -> &'static str {
124 match self {
125 Self::ParseExpectedToken => "PL_PARSE_EXPECTED_TOKEN",
126 Self::ParseUnsupportedSyntax => "PL_PARSE_UNSUPPORTED_SYNTAX",
127 Self::ParseLimitExceeded => "PL_PARSE_LIMIT_EXCEEDED",
128 Self::BindUnknownLocal => "PL_BIND_UNKNOWN_LOCAL",
129 Self::BindUnknownObject => "PL_BIND_UNKNOWN_OBJECT",
130 Self::BindAmbiguousRoutine => "PL_BIND_AMBIGUOUS_ROUTINE",
131 Self::BindTypeMismatch => "PL_BIND_TYPE_MISMATCH",
132 Self::BindDependencyCycle => "PL_BIND_DEPENDENCY_CYCLE",
133 Self::VerifyCapabilityDenied => "PL_VERIFY_CAPABILITY_DENIED",
134 Self::VerifyTransactionControlForbidden => "PL_VERIFY_TRANSACTION_CONTROL_FORBIDDEN",
135 Self::VerifyDynamicDdlNotSupported => "PL_VERIFY_DYNAMIC_DDL_NOT_SUPPORTED",
136 Self::VerifyUnboundedResult => "PL_VERIFY_UNBOUNDED_RESULT",
137 Self::RuntimeNullNotAllowed => "PL_RUNTIME_NULL_NOT_ALLOWED",
138 Self::RuntimeArrayBounds => "PL_RUNTIME_ARRAY_BOUNDS",
139 Self::RuntimeNumericOverflow => "PL_RUNTIME_NUMERIC_OVERFLOW",
140 Self::RuntimeInvalidIr => "PL_RUNTIME_INVALID_IR",
141 Self::RuntimeInvalidArgument => "PL_RUNTIME_INVALID_ARGUMENT",
142 Self::RuntimeConflict => "PL_RUNTIME_CONFLICT",
143 Self::RuntimeNotFound => "PL_RUNTIME_NOT_FOUND",
144 Self::RuntimeInvalidState => "PL_RUNTIME_INVALID_STATE",
145 Self::RuntimeUniqueViolation => "PL_RUNTIME_UNIQUE_VIOLATION",
146 Self::CardinalityNoDataFound => "PL_CARDINALITY_NO_DATA_FOUND",
147 Self::CardinalityTooManyRows => "PL_CARDINALITY_TOO_MANY_ROWS",
148 Self::SecurityExecuteDenied => "PL_SECURITY_EXECUTE_DENIED",
149 Self::SecurityObjectDenied => "PL_SECURITY_OBJECT_DENIED",
150 Self::SecurityUnsafeSearchPath => "PL_SECURITY_UNSAFE_SEARCH_PATH",
151 Self::ResourceInstructions => "PL_RESOURCE_INSTRUCTIONS",
152 Self::ResourceHeap => "PL_RESOURCE_HEAP",
153 Self::ResourceRows => "PL_RESOURCE_ROWS",
154 Self::ResourceBytes => "PL_RESOURCE_BYTES",
155 Self::ResourceSqlStatements => "PL_RESOURCE_SQL_STATEMENTS",
156 Self::ResourceFrames => "PL_RESOURCE_FRAMES",
157 Self::ResourceDeadline => "PL_RESOURCE_DEADLINE",
158 Self::ResourceCancelled => "PL_RESOURCE_CANCELLED",
159 Self::TriggerCycle => "PL_TRIGGER_CYCLE",
160 Self::TriggerDepth => "PL_TRIGGER_DEPTH",
161 Self::TriggerInvalidReturn => "PL_TRIGGER_INVALID_RETURN",
162 Self::JobAttemptFailed => "PL_JOB_ATTEMPT_FAILED",
163 }
164 }
165
166 pub const fn category(self) -> DiagnosticCategory {
167 match self {
168 Self::ParseExpectedToken | Self::ParseUnsupportedSyntax | Self::ParseLimitExceeded => {
169 DiagnosticCategory::Parse
170 }
171 Self::BindUnknownLocal
172 | Self::BindUnknownObject
173 | Self::BindAmbiguousRoutine
174 | Self::BindTypeMismatch
175 | Self::BindDependencyCycle => DiagnosticCategory::Bind,
176 Self::VerifyCapabilityDenied
177 | Self::VerifyTransactionControlForbidden
178 | Self::VerifyDynamicDdlNotSupported
179 | Self::VerifyUnboundedResult => DiagnosticCategory::Verify,
180 Self::RuntimeNullNotAllowed
181 | Self::RuntimeArrayBounds
182 | Self::RuntimeNumericOverflow
183 | Self::RuntimeInvalidIr
184 | Self::RuntimeInvalidArgument
185 | Self::RuntimeConflict
186 | Self::RuntimeNotFound
187 | Self::RuntimeInvalidState
188 | Self::RuntimeUniqueViolation => DiagnosticCategory::Runtime,
189 Self::CardinalityNoDataFound | Self::CardinalityTooManyRows => {
190 DiagnosticCategory::Cardinality
191 }
192 Self::SecurityExecuteDenied
193 | Self::SecurityObjectDenied
194 | Self::SecurityUnsafeSearchPath => DiagnosticCategory::Security,
195 Self::ResourceInstructions
196 | Self::ResourceHeap
197 | Self::ResourceRows
198 | Self::ResourceBytes
199 | Self::ResourceSqlStatements
200 | Self::ResourceFrames
201 | Self::ResourceDeadline
202 | Self::ResourceCancelled => DiagnosticCategory::Resource,
203 Self::TriggerCycle | Self::TriggerDepth | Self::TriggerInvalidReturn => {
204 DiagnosticCategory::Trigger
205 }
206 Self::JobAttemptFailed => DiagnosticCategory::Job,
207 }
208 }
209
210 pub const fn retryable(self) -> bool {
211 matches!(
212 self,
213 Self::ResourceDeadline | Self::ResourceCancelled | Self::JobAttemptFailed
214 )
215 }
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub struct SourcePosition {
220 pub line: u32,
221 pub column: u32,
222}
223
224impl SourcePosition {
225 pub const fn new(line: u32, column: u32) -> Option<Self> {
226 if line == 0 || column == 0 {
227 None
228 } else {
229 Some(Self { line, column })
230 }
231 }
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct SourceSpan {
236 pub source_object_id: ObjectId,
237 pub definition_revision: u64,
238 pub start_byte: u32,
239 pub end_byte: u32,
240 pub start: SourcePosition,
241 pub end: SourcePosition,
242}
243
244impl SourceSpan {
245 #[allow(clippy::too_many_arguments)]
246 pub fn new(
247 source_object_id: ObjectId,
248 definition_revision: u64,
249 start_byte: u32,
250 end_byte: u32,
251 start_line: u32,
252 start_column: u32,
253 end_line: u32,
254 end_column: u32,
255 ) -> Option<Self> {
256 if definition_revision == 0 || start_byte > end_byte {
257 return None;
258 }
259 Some(Self {
260 source_object_id,
261 definition_revision,
262 start_byte,
263 end_byte,
264 start: SourcePosition::new(start_line, start_column)?,
265 end: SourcePosition::new(end_line, end_column)?,
266 })
267 }
268}
269
270#[derive(Debug, Clone, PartialEq, Eq)]
271pub struct SecondarySpan {
272 pub label: String,
273 pub span: SourceSpan,
274}
275
276#[derive(Debug, Clone, PartialEq, Eq)]
277pub struct DiagnosticFrame {
278 pub object_id: ObjectId,
279 pub definition_revision: u64,
280 pub name: String,
281 pub call_span: Option<SourceSpan>,
282}
283
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct DiagnosticDetail {
286 pub key: String,
287 pub value: String,
288}
289
290#[derive(Debug, Clone, PartialEq, Eq)]
291pub struct Diagnostic {
292 inner: Box<DiagnosticData>,
293}
294
295#[derive(Debug, Clone, PartialEq, Eq)]
296struct DiagnosticData {
297 kind: DiagnosticKind,
298 message: String,
299 primary_span: Option<SourceSpan>,
300 secondary_spans: Vec<SecondarySpan>,
301 frames: Vec<DiagnosticFrame>,
302 details: Vec<DiagnosticDetail>,
303 cause: Option<DiagnosticKind>,
304}
305
306impl Diagnostic {
307 pub fn new(kind: DiagnosticKind, message: impl Into<String>) -> Self {
308 Self {
309 inner: Box::new(DiagnosticData {
310 kind,
311 message: bounded_text(message.into()),
312 primary_span: None,
313 secondary_spans: Vec::new(),
314 frames: Vec::new(),
315 details: Vec::new(),
316 cause: None,
317 }),
318 }
319 }
320
321 pub fn with_primary_span(mut self, span: Option<SourceSpan>) -> Self {
322 self.inner.primary_span = span;
323 self
324 }
325
326 pub fn with_secondary_span(mut self, label: impl Into<String>, span: SourceSpan) -> Self {
327 if self.inner.secondary_spans.len() < MAX_SECONDARY_SPANS {
328 self.inner.secondary_spans.push(SecondarySpan {
329 label: bounded_text(label.into()),
330 span,
331 });
332 }
333 self
334 }
335
336 pub fn with_frame(mut self, mut frame: DiagnosticFrame) -> Self {
337 if self.inner.frames.len() < MAX_DIAGNOSTIC_FRAMES {
338 frame.name = bounded_text(frame.name);
339 self.inner.frames.push(frame);
340 }
341 self
342 }
343
344 pub fn with_outer_frame(mut self, mut frame: DiagnosticFrame) -> Self {
345 if self.inner.frames.len() < MAX_DIAGNOSTIC_FRAMES {
346 frame.name = bounded_text(frame.name);
347 self.inner.frames.insert(0, frame);
348 }
349 self
350 }
351
352 pub fn with_detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
353 if self.inner.details.len() < MAX_DIAGNOSTIC_DETAILS {
354 self.inner.details.push(DiagnosticDetail {
355 key: bounded_text(key.into()),
356 value: bounded_text(value.into()),
357 });
358 }
359 self
360 }
361
362 pub const fn kind(&self) -> DiagnosticKind {
363 self.inner.kind
364 }
365 pub const fn category(&self) -> DiagnosticCategory {
366 self.inner.kind.category()
367 }
368 pub const fn retryable(&self) -> bool {
369 self.inner.kind.retryable()
370 }
371 pub fn message(&self) -> &str {
372 &self.inner.message
373 }
374 pub fn primary_span(&self) -> Option<&SourceSpan> {
375 self.inner.primary_span.as_ref()
376 }
377 pub fn secondary_spans(&self) -> &[SecondarySpan] {
378 &self.inner.secondary_spans
379 }
380 pub fn frames(&self) -> &[DiagnosticFrame] {
381 &self.inner.frames
382 }
383 pub fn details(&self) -> &[DiagnosticDetail] {
384 &self.inner.details
385 }
386 pub const fn cause(&self) -> Option<DiagnosticKind> {
387 self.inner.cause
388 }
389 pub fn with_cause(mut self, cause: DiagnosticKind) -> Self {
390 self.inner.cause = Some(cause);
391 self
392 }
393}
394
395impl fmt::Display for Diagnostic {
396 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
397 write!(
398 formatter,
399 "{}: {}",
400 self.inner.kind.as_str(),
401 self.inner.message
402 )
403 }
404}
405
406impl std::error::Error for Diagnostic {}
407
408fn bounded_text(mut value: String) -> String {
409 if value.len() <= MAX_DIAGNOSTIC_TEXT_BYTES {
410 return value;
411 }
412 let mut boundary = MAX_DIAGNOSTIC_TEXT_BYTES;
413 while !value.is_char_boundary(boundary) {
414 boundary -= 1;
415 }
416 value.truncate(boundary);
417 value
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn every_kind_has_the_stable_prefix_and_category() {
426 let mut spellings = std::collections::BTreeSet::new();
427 for kind in DiagnosticKind::ALL {
428 assert!(kind.as_str().starts_with("PL_"));
429 assert!(!kind.category().as_str().is_empty());
430 assert!(spellings.insert(kind.as_str()), "duplicate diagnostic kind");
431 assert!(Diagnostic::new(kind, "failure")
432 .to_string()
433 .starts_with(kind.as_str()));
434 }
435 assert_eq!(spellings.len(), DiagnosticKind::ALL.len());
436 }
437
438 #[test]
439 fn retryable_registry_is_explicit_and_closed() {
440 let retryable = DiagnosticKind::ALL
441 .into_iter()
442 .filter(|kind| kind.retryable())
443 .collect::<Vec<_>>();
444 assert_eq!(
445 retryable,
446 vec![
447 DiagnosticKind::ResourceDeadline,
448 DiagnosticKind::ResourceCancelled,
449 DiagnosticKind::JobAttemptFailed,
450 ]
451 );
452 }
453
454 #[test]
455 fn diagnostic_text_and_lists_are_bounded() {
456 let mut diagnostic = Diagnostic::new(DiagnosticKind::RuntimeInvalidIr, "x".repeat(10_000));
457 for index in 0..100 {
458 diagnostic = diagnostic.with_detail(format!("key-{index}"), "value");
459 }
460 assert_eq!(diagnostic.message().len(), MAX_DIAGNOSTIC_TEXT_BYTES);
461 assert_eq!(diagnostic.details().len(), MAX_DIAGNOSTIC_DETAILS);
462 assert_eq!(
463 std::mem::size_of::<Diagnostic>(),
464 std::mem::size_of::<usize>()
465 );
466 }
467}