Skip to main content

radixdb_procedural/
diagnostic.rs

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}