1use std::{
5 fmt,
6 fmt::{Display, Formatter},
7};
8
9use reifydb_core::{
10 interface::catalog::config::{AcceptError, ConfigKey},
11 key::kind::KeyKind,
12};
13use reifydb_runtime::hash::Hash128;
14use reifydb_type::{
15 error::{Diagnostic, Error, IntoDiagnostic},
16 fragment::Fragment,
17 value::r#type::Type,
18};
19
20#[derive(Debug, Clone, PartialEq)]
21pub enum CatalogObjectKind {
22 Namespace,
23 Table,
24 View,
25 Flow,
26 RingBuffer,
27 Dictionary,
28 Enum,
29 Event,
30 VirtualTable,
31 Handler,
32 Series,
33 Tag,
34 Identity,
35 Role,
36 Policy,
37 Migration,
38 Column,
39 Source,
40 Sink,
41 Procedure,
42 TestProcedure,
43 Binding,
44}
45
46impl Display for CatalogObjectKind {
47 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
48 match self {
49 CatalogObjectKind::Namespace => f.write_str("namespace"),
50 CatalogObjectKind::Table => f.write_str("table"),
51 CatalogObjectKind::View => f.write_str("view"),
52 CatalogObjectKind::Flow => f.write_str("flow"),
53 CatalogObjectKind::RingBuffer => f.write_str("ring buffer"),
54 CatalogObjectKind::Dictionary => f.write_str("dictionary"),
55 CatalogObjectKind::Enum => f.write_str("enum"),
56 CatalogObjectKind::Event => f.write_str("event"),
57 CatalogObjectKind::VirtualTable => f.write_str("virtual table"),
58 CatalogObjectKind::Handler => f.write_str("handler"),
59 CatalogObjectKind::Series => f.write_str("series"),
60 CatalogObjectKind::Tag => f.write_str("tag"),
61 CatalogObjectKind::Identity => f.write_str("identity"),
62 CatalogObjectKind::Role => f.write_str("role"),
63 CatalogObjectKind::Policy => f.write_str("policy"),
64 CatalogObjectKind::Migration => f.write_str("migration"),
65 CatalogObjectKind::Column => f.write_str("column"),
66 CatalogObjectKind::Source => f.write_str("source"),
67 CatalogObjectKind::Sink => f.write_str("sink"),
68 CatalogObjectKind::Procedure => f.write_str("procedure"),
69 CatalogObjectKind::TestProcedure => f.write_str("test procedure"),
70 CatalogObjectKind::Binding => f.write_str("binding"),
71 }
72 }
73}
74
75#[derive(Debug, thiserror::Error)]
76pub enum CatalogError {
77 #[error("{kind} `{namespace}::{name}` already exists")]
78 AlreadyExists {
79 kind: CatalogObjectKind,
80 namespace: String,
81 name: String,
82 fragment: Fragment,
83 },
84
85 #[error("{kind} `{namespace}::{name}` not found")]
86 NotFound {
87 kind: CatalogObjectKind,
88 namespace: String,
89 name: String,
90 fragment: Fragment,
91 },
92
93 #[error("migration `{name}` has no rollback body")]
94 MigrationNoRollbackBody {
95 name: String,
96 fragment: Fragment,
97 },
98
99 #[error("migration `{name}` content has changed since registration: was {expected_hex}, now {actual_hex}")]
100 MigrationHashMismatch {
101 name: String,
102 expected: Hash128,
103 actual: Hash128,
104 expected_hex: String,
105 actual_hex: String,
106 fragment: Fragment,
107 },
108
109 #[error("column `{column}` already exists in {kind} `{namespace}::{name}`")]
110 ColumnAlreadyExists {
111 kind: CatalogObjectKind,
112 namespace: String,
113 name: String,
114 column: String,
115 fragment: Fragment,
116 },
117
118 #[error(
119 "column `{column}` type `{column_type}` does not match dictionary `{dictionary}` value type `{dictionary_value_type}`"
120 )]
121 DictionaryTypeMismatch {
122 column: String,
123 column_type: Type,
124 dictionary: String,
125 dictionary_value_type: Type,
126 fragment: Fragment,
127 },
128
129 #[error("auto increment is not supported for type `{ty}`")]
130 AutoIncrementInvalidType {
131 column: String,
132 ty: Type,
133 fragment: Fragment,
134 },
135
136 #[error("policy `{policy}` already exists for column `{column}`")]
137 ColumnPropertyAlreadyExists {
138 policy: String,
139 column: String,
140 },
141
142 #[error("{kind} `{namespace}` already has pending changes in this transaction")]
143 AlreadyPendingInTransaction {
144 kind: CatalogObjectKind,
145 namespace: String,
146 name: Option<String>,
147 fragment: Fragment,
148 },
149
150 #[error("cannot update {kind} as it is marked for deletion")]
151 CannotUpdateDeleted {
152 kind: CatalogObjectKind,
153 namespace: String,
154 name: Option<String>,
155 fragment: Fragment,
156 },
157
158 #[error("{kind} is already marked for deletion")]
159 CannotDeleteAlreadyDeleted {
160 kind: CatalogObjectKind,
161 namespace: String,
162 name: Option<String>,
163 fragment: Fragment,
164 },
165
166 #[error("primary key must contain at least one column")]
167 PrimaryKeyEmpty {
168 fragment: Fragment,
169 },
170
171 #[error("column with ID {column_id} not found for primary key")]
172 PrimaryKeyColumnNotFound {
173 fragment: Fragment,
174 column_id: u64,
175 },
176
177 #[error("subscription `{name}` already exists")]
178 SubscriptionAlreadyExists {
179 fragment: Fragment,
180 name: String,
181 },
182
183 #[error("subscription `{name}` not found")]
184 SubscriptionNotFound {
185 fragment: Fragment,
186 name: String,
187 },
188
189 #[error("column `{column}` not found in {kind} `{namespace}`.`{name}`")]
190 ColumnNotFound {
191 kind: CatalogObjectKind,
192 namespace: String,
193 name: String,
194 column: String,
195 fragment: Fragment,
196 },
197
198 #[error("cannot drop {kind} because it is in use")]
199 InUse {
200 kind: CatalogObjectKind,
201 namespace: String,
202 name: Option<String>,
203 dependents: String,
204 fragment: Fragment,
205 },
206
207 #[error(
208 "cannot drop {kind} procedure `{name}`: native/FFI/WASM procedures are managed by the runtime registry, not DDL"
209 )]
210 CannotDropEphemeralProcedure {
211 kind: String,
212 name: String,
213 fragment: Fragment,
214 },
215
216 #[error("cannot register {kind} procedure as ephemeral: only Native/FFI/WASM variants are accepted")]
217 CannotRegisterPersistentAsEphemeral {
218 kind: String,
219 },
220
221 #[error("unknown config key `{0}`")]
222 ConfigStorageKeyNotFound(String),
223
224 #[error("config value for key `{0}` cannot be none")]
225 ConfigValueInvalid(String),
226
227 #[error("config value for key `{key}` must be of type `{expected:?}`, got `{actual}`")]
228 ConfigTypeMismatch {
229 key: String,
230 expected: Vec<Type>,
231 actual: Type,
232 },
233
234 #[error("config value for key `{key}` is invalid: {reason}")]
235 ConfigInvalidValue {
236 key: String,
237 reason: String,
238 },
239
240 #[error("unknown operation `{operation}` for {target_type} policy")]
241 PolicyInvalidOperation {
242 target_type: &'static str,
243 operation: String,
244 valid: &'static [&'static str],
245 policy_name: Option<String>,
246 },
247
248 #[error("invalid binding config: {reason}")]
249 InvalidBindingConfig {
250 reason: String,
251 fragment: Fragment,
252 },
253}
254
255impl From<(ConfigKey, AcceptError)> for CatalogError {
256 fn from((key, err): (ConfigKey, AcceptError)) -> Self {
257 match err {
258 AcceptError::TypeMismatch {
259 expected,
260 actual,
261 } => CatalogError::ConfigTypeMismatch {
262 key: key.to_string(),
263 expected,
264 actual,
265 },
266 AcceptError::InvalidValue(reason) => CatalogError::ConfigInvalidValue {
267 key: key.to_string(),
268 reason,
269 },
270 }
271 }
272}
273
274impl IntoDiagnostic for CatalogError {
275 fn into_diagnostic(self) -> Diagnostic {
276 match self {
277 CatalogError::AlreadyExists {
278 kind,
279 namespace,
280 name,
281 fragment,
282 } => {
283 let (code, kind_str, help) = match kind {
284 CatalogObjectKind::Namespace => (
285 "CA_001",
286 "namespace",
287 "choose a different name or drop the existing namespace first",
288 ),
289 CatalogObjectKind::Table => (
290 "CA_003",
291 "table",
292 "choose a different name, drop the existing table or create table in a different namespace",
293 ),
294 CatalogObjectKind::View => (
295 "CA_003",
296 "view",
297 "choose a different name, drop the existing view or create view in a different namespace",
298 ),
299 CatalogObjectKind::Flow => (
300 "CA_030",
301 "flow",
302 "choose a different name, drop the existing flow or create flow in a different namespace",
303 ),
304 CatalogObjectKind::RingBuffer => (
305 "CA_005",
306 "ring buffer",
307 "choose a different name, drop the existing ring buffer or create ring buffer in a different namespace",
308 ),
309 CatalogObjectKind::Dictionary => (
310 "CA_006",
311 "dictionary",
312 "choose a different name, drop the existing dictionary or create dictionary in a different namespace",
313 ),
314 CatalogObjectKind::Enum => (
315 "CA_003",
316 "enum",
317 "choose a different name or drop the existing enum first",
318 ),
319 CatalogObjectKind::Event => (
320 "CA_003",
321 "event",
322 "choose a different name or drop the existing event first",
323 ),
324 CatalogObjectKind::VirtualTable => (
325 "CA_022",
326 "virtual table",
327 "choose a different name or unregister the existing virtual table first",
328 ),
329 CatalogObjectKind::Handler => (
330 "CA_003",
331 "handler",
332 "choose a different name or drop the existing handler first",
333 ),
334 CatalogObjectKind::Series => (
335 "CA_003",
336 "series",
337 "choose a different name or drop the existing series first",
338 ),
339 CatalogObjectKind::Tag => (
340 "CA_003",
341 "tag",
342 "choose a different name or drop the existing tag first",
343 ),
344 CatalogObjectKind::Identity => (
345 "CA_040",
346 "identity",
347 "choose a different name or drop the existing identity first",
348 ),
349 CatalogObjectKind::Role => (
350 "CA_041",
351 "role",
352 "choose a different name or drop the existing role first",
353 ),
354 CatalogObjectKind::Policy => (
355 "CA_042",
356 "policy",
357 "choose a different name or drop the existing policy first",
358 ),
359 CatalogObjectKind::Migration => {
360 ("CA_046", "migration", "choose a different name for the migration")
361 }
362 CatalogObjectKind::Column => {
363 ("CA_003", "column", "ensure the column exists in the definition")
364 }
365 CatalogObjectKind::Source => (
366 "CA_060",
367 "source",
368 "choose a different name, drop the existing source or create source in a different namespace",
369 ),
370 CatalogObjectKind::Sink => (
371 "CA_061",
372 "sink",
373 "choose a different name, drop the existing sink or create sink in a different namespace",
374 ),
375 CatalogObjectKind::Procedure => (
376 "CA_080",
377 "procedure",
378 "choose a different name, drop the existing procedure or create procedure in a different namespace",
379 ),
380 CatalogObjectKind::TestProcedure => (
381 "CA_081",
382 "test procedure",
383 "choose a different name or drop the existing test procedure first",
384 ),
385 CatalogObjectKind::Binding => (
386 "CA_087",
387 "binding",
388 "choose a different protocol key or drop the existing binding first",
389 ),
390 };
391 let message = if matches!(
392 kind,
393 CatalogObjectKind::Namespace | CatalogObjectKind::Migration
394 ) {
395 format!("{} `{}` already exists", kind_str, name)
396 } else {
397 format!("{} `{}::{}` already exists", kind_str, namespace, name)
398 };
399 Diagnostic {
400 code: code.to_string(),
401 rql: None,
402 message,
403 fragment,
404 label: Some(format!("duplicate {} definition", kind_str)),
405 help: Some(help.to_string()),
406 column: None,
407 notes: vec![],
408 cause: None,
409 operator_chain: None,
410 }
411 }
412
413 CatalogError::NotFound {
414 kind,
415 namespace,
416 name,
417 fragment,
418 } => {
419 let (code, kind_str, help) = match kind {
420 CatalogObjectKind::Namespace => (
421 "CA_002",
422 "namespace",
423 "make sure the namespace exists before using it or create it first".to_string(),
424 ),
425 CatalogObjectKind::Table => (
426 "CA_004",
427 "table",
428 "ensure the table exists or create it first using `CREATE TABLE`".to_string(),
429 ),
430 CatalogObjectKind::View => (
431 "CA_004",
432 "view",
433 "ensure the view exists or create it first using `CREATE VIEW`".to_string(),
434 ),
435 CatalogObjectKind::Flow => (
436 "CA_031",
437 "flow",
438 "ensure the flow exists or create it first using `CREATE FLOW`".to_string(),
439 ),
440 CatalogObjectKind::RingBuffer => (
441 "CA_006",
442 "ring buffer",
443 "ensure the ring buffer exists or create it first using `CREATE RING BUFFER`".to_string(),
444 ),
445 CatalogObjectKind::Dictionary => (
446 "CA_007",
447 "dictionary",
448 "ensure the dictionary exists or create it first using `CREATE DICTIONARY`".to_string(),
449 ),
450 CatalogObjectKind::Enum => (
451 "CA_002",
452 "type",
453 format!("create the enum first with `CREATE ENUM {}::{} {{ ... }}`", namespace, name),
454 ),
455 CatalogObjectKind::Event => (
456 "CA_002",
457 "event",
458 format!("create the event first with `CREATE EVENT {}::{} {{ ... }}`", namespace, name),
459 ),
460 CatalogObjectKind::VirtualTable => (
461 "CA_023",
462 "virtual table",
463 "ensure the virtual table is registered before using it".to_string(),
464 ),
465 CatalogObjectKind::Handler => (
466 "CA_004",
467 "handler",
468 "ensure the handler exists or create it first using `CREATE HANDLER`".to_string(),
469 ),
470 CatalogObjectKind::Series => (
471 "CA_024",
472 "series",
473 "ensure the series exists or create it first using `CREATE SERIES`".to_string(),
474 ),
475 CatalogObjectKind::Tag => (
476 "CA_002",
477 "tag",
478 format!("create the tag first with `CREATE TAG {}.{} {{ ... }}`", namespace, name),
479 ),
480 CatalogObjectKind::Identity => (
481 "CA_043",
482 "identity",
483 "ensure the identity exists or create it first using `CREATE IDENTITY`".to_string(),
484 ),
485 CatalogObjectKind::Role => (
486 "CA_044",
487 "role",
488 "ensure the role exists or create it first using `CREATE ROLE`".to_string(),
489 ),
490 CatalogObjectKind::Policy => (
491 "CA_045",
492 "policy",
493 "ensure the policy exists or create it first".to_string(),
494 ),
495 CatalogObjectKind::Migration => (
496 "CA_047",
497 "migration",
498 "ensure the migration exists or create it first using `CREATE MIGRATION`".to_string(),
499 ),
500 CatalogObjectKind::Column => (
501 "CA_004",
502 "column",
503 "ensure the column exists in the definition".to_string(),
504 ),
505 CatalogObjectKind::Source => (
506 "CA_062",
507 "source",
508 "ensure the source exists or create it first using `CREATE SOURCE`".to_string(),
509 ),
510 CatalogObjectKind::Sink => (
511 "CA_063",
512 "sink",
513 "ensure the sink exists or create it first using `CREATE SINK`".to_string(),
514 ),
515 CatalogObjectKind::Procedure => (
516 "CA_082",
517 "procedure",
518 "ensure the procedure exists or create it first using `CREATE PROCEDURE`".to_string(),
519 ),
520 CatalogObjectKind::TestProcedure => (
521 "CA_083",
522 "test procedure",
523 "ensure the test procedure exists or create it first using `CREATE TEST PROCEDURE`".to_string(),
524 ),
525 CatalogObjectKind::Binding => (
526 "CA_088",
527 "binding",
528 "ensure the binding exists or create it first using `CREATE <PROTOCOL> BINDING`".to_string(),
529 ),
530 };
531 let message = match kind {
532 CatalogObjectKind::Namespace => {
533 format!("{} `{}` not found", kind_str, namespace)
534 }
535 CatalogObjectKind::Migration => format!("{} `{}` not found", kind_str, name),
536 _ => format!("{} `{}::{}` not found", kind_str, namespace, name),
537 };
538 let label_str = match kind {
539 CatalogObjectKind::Namespace => "unknown namespace reference".to_string(),
540 CatalogObjectKind::Enum => "unknown type".to_string(),
541 CatalogObjectKind::Event => "unknown event reference".to_string(),
542 _ => format!("unknown {} reference", kind_str),
543 };
544 Diagnostic {
545 code: code.to_string(),
546 rql: None,
547 message,
548 fragment,
549 label: Some(label_str),
550 help: Some(help),
551 column: None,
552 notes: vec![],
553 cause: None,
554 operator_chain: None,
555 }
556 }
557
558 CatalogError::MigrationNoRollbackBody {
559 name,
560 fragment,
561 } => Diagnostic {
562 code: "CA_048".to_string(),
563 rql: None,
564 message: format!("migration `{}` has no rollback body", name),
565 fragment,
566 label: Some("no rollback body defined".to_string()),
567 help: Some("define a ROLLBACK clause when creating the migration".to_string()),
568 column: None,
569 notes: vec![],
570 cause: None,
571 operator_chain: None,
572 },
573
574 CatalogError::MigrationHashMismatch {
575 name,
576 expected: _,
577 actual: _,
578 expected_hex,
579 actual_hex,
580 fragment,
581 } => Diagnostic {
582 code: "CA_049".to_string(),
583 rql: None,
584 message: format!(
585 "migration `{}` content has changed since registration: was {}, now {}",
586 name, expected_hex, actual_hex
587 ),
588 fragment,
589 label: Some("modified migration".to_string()),
590 help: Some(
591 "applied migrations are immutable; revert the change or register a new migration with a different name"
592 .to_string(),
593 ),
594 column: None,
595 notes: vec![
596 "the registered hash in the catalog does not match the hash of the supplied body+rollback".to_string(),
597 ],
598 cause: None,
599 operator_chain: None,
600 },
601
602 CatalogError::ColumnAlreadyExists {
603 kind,
604 namespace,
605 name,
606 column,
607 fragment,
608 } => {
609 let kind_str = match kind {
610 CatalogObjectKind::Table => "table",
611 CatalogObjectKind::View => "view",
612 _ => "object",
613 };
614 Diagnostic {
615 code: "CA_005".to_string(),
616 rql: None,
617 message: format!(
618 "column `{}` already exists in {} `{}::{}`",
619 column, kind_str, namespace, name
620 ),
621 fragment,
622 label: Some("duplicate column definition".to_string()),
623 help: Some("choose a different column name or drop the existing one first"
624 .to_string()),
625 column: None,
626 notes: vec![],
627 cause: None,
628 operator_chain: None,
629 }
630 }
631
632 CatalogError::DictionaryTypeMismatch {
633 column,
634 column_type,
635 dictionary,
636 dictionary_value_type,
637 fragment,
638 } => Diagnostic {
639 code: "CA_008".to_string(),
640 rql: None,
641 message: format!(
642 "column `{}` type `{}` does not match dictionary `{}` value type `{}`",
643 column, column_type, dictionary, dictionary_value_type
644 ),
645 fragment,
646 label: Some("type mismatch".to_string()),
647 help: Some(format!(
648 "change the column type to `{}` to match the dictionary value type",
649 dictionary_value_type
650 )),
651 column: None,
652 notes: vec![],
653 cause: None,
654 operator_chain: None,
655 },
656
657 CatalogError::AutoIncrementInvalidType {
658 column,
659 ty,
660 fragment,
661 } => Diagnostic {
662 code: "CA_006".to_string(),
663 rql: None,
664 message: format!("auto increment is not supported for type `{}`", ty),
665 fragment,
666 label: Some("invalid auto increment usage".to_string()),
667 help: Some(format!(
668 "auto increment is only supported for integer types (int1-16, uint1-16), column `{}` has type `{}`",
669 column, ty
670 )),
671 column: None,
672 notes: vec![],
673 cause: None,
674 operator_chain: None,
675 },
676
677 CatalogError::ColumnPropertyAlreadyExists {
678 policy,
679 column,
680 } => Diagnostic {
681 code: "CA_008".to_string(),
682 rql: None,
683 message: format!("policy `{:?}` already exists for column `{}`", policy, column),
684 fragment: Fragment::None,
685 label: Some("duplicate column policy".to_string()),
686 help: Some("remove the existing policy first".to_string()),
687 column: None,
688 notes: vec![],
689 cause: None,
690 operator_chain: None,
691 },
692
693 CatalogError::AlreadyPendingInTransaction {
694 kind,
695 namespace,
696 name,
697 fragment,
698 } => {
699 let (code, message, label_str) = match kind {
700 CatalogObjectKind::Namespace => (
701 "CA_011",
702 format!(
703 "namespace `{}` already has pending changes in this transaction",
704 namespace
705 ),
706 "duplicate namespace modification in transaction",
707 ),
708 CatalogObjectKind::Table => (
709 "CA_012",
710 format!(
711 "table `{}::{}` already has pending changes in this transaction",
712 namespace,
713 name.as_deref().unwrap_or("")
714 ),
715 "duplicate table modification in transaction",
716 ),
717 CatalogObjectKind::View => (
718 "CA_013",
719 format!(
720 "view `{}::{}` already has pending changes in this transaction",
721 namespace,
722 name.as_deref().unwrap_or("")
723 ),
724 "duplicate view modification in transaction",
725 ),
726 _ => (
727 "CA_011",
728 format!("{} already has pending changes in this transaction", kind),
729 "duplicate modification in transaction",
730 ),
731 };
732 let kind_str = match kind {
733 CatalogObjectKind::Namespace => "namespace",
734 CatalogObjectKind::Table => "table",
735 CatalogObjectKind::View => "view",
736 _ => "object",
737 };
738 Diagnostic {
739 code: code.to_string(),
740 rql: None,
741 message,
742 fragment,
743 label: Some(label_str.to_string()),
744 help: Some(format!(
745 "a {} can only be created, updated, or deleted once per transaction",
746 kind_str
747 )),
748 column: None,
749 notes: vec![
750 "This usually indicates a programming error in transaction management"
751 .to_string(),
752 "Consider reviewing the transaction logic for duplicate operations"
753 .to_string(),
754 ],
755 cause: None,
756 operator_chain: None,
757 }
758 }
759
760 CatalogError::CannotUpdateDeleted {
761 kind,
762 namespace,
763 name,
764 fragment,
765 } => {
766 let (code, message, kind_str) = match kind {
767 CatalogObjectKind::Namespace => (
768 "CA_014",
769 format!(
770 "cannot update namespace `{}` as it is marked for deletion in this transaction",
771 namespace
772 ),
773 "namespace",
774 ),
775 CatalogObjectKind::Table => (
776 "CA_015",
777 format!(
778 "cannot update table `{}::{}` as it is marked for deletion in this transaction",
779 namespace,
780 name.as_deref().unwrap_or("")
781 ),
782 "table",
783 ),
784 CatalogObjectKind::View => (
785 "CA_016",
786 format!(
787 "cannot update view `{}::{}` as it is marked for deletion in this transaction",
788 namespace,
789 name.as_deref().unwrap_or("")
790 ),
791 "view",
792 ),
793 _ => (
794 "CA_014",
795 format!(
796 "cannot update {} as it is marked for deletion in this transaction",
797 kind
798 ),
799 "object",
800 ),
801 };
802 Diagnostic {
803 code: code.to_string(),
804 rql: None,
805 message,
806 fragment,
807 label: Some(format!("attempted update on deleted {}", kind_str)),
808 help: Some("remove the delete operation or skip the update".to_string()),
809 column: None,
810 notes: vec![format!(
811 "A {} marked for deletion cannot be updated in the same transaction",
812 kind_str
813 )],
814 cause: None,
815 operator_chain: None,
816 }
817 }
818
819 CatalogError::CannotDeleteAlreadyDeleted {
820 kind,
821 namespace,
822 name,
823 fragment,
824 } => {
825 let (code, message, kind_str) = match kind {
826 CatalogObjectKind::Namespace => (
827 "CA_017",
828 format!(
829 "namespace `{}` is already marked for deletion in this transaction",
830 namespace
831 ),
832 "namespace",
833 ),
834 CatalogObjectKind::Table => (
835 "CA_018",
836 format!(
837 "table `{}::{}` is already marked for deletion in this transaction",
838 namespace,
839 name.as_deref().unwrap_or("")
840 ),
841 "table",
842 ),
843 CatalogObjectKind::View => (
844 "CA_019",
845 format!(
846 "view `{}::{}` is already marked for deletion in this transaction",
847 namespace,
848 name.as_deref().unwrap_or("")
849 ),
850 "view",
851 ),
852 _ => (
853 "CA_017",
854 format!("{} is already marked for deletion in this transaction", kind),
855 "object",
856 ),
857 };
858 Diagnostic {
859 code: code.to_string(),
860 rql: None,
861 message,
862 fragment,
863 label: Some(format!("duplicate {} deletion", kind_str)),
864 help: Some("remove the duplicate delete operation".to_string()),
865 column: None,
866 notes: vec![format!("A {} can only be deleted once per transaction", kind_str)],
867 cause: None,
868 operator_chain: None,
869 }
870 }
871
872 CatalogError::PrimaryKeyEmpty {
873 fragment,
874 } => Diagnostic {
875 code: "CA_020".to_string(),
876 rql: None,
877 message: "primary key must contain at least one column".to_string(),
878 fragment,
879 label: Some("empty primary key definition".to_string()),
880 help: Some("specify at least one column for the primary key".to_string()),
881 column: None,
882 notes: vec![],
883 cause: None,
884 operator_chain: None,
885 },
886
887 CatalogError::PrimaryKeyColumnNotFound {
888 fragment,
889 column_id,
890 } => Diagnostic {
891 code: "CA_021".to_string(),
892 rql: None,
893 message: format!("column with ID {} not found for primary key", column_id),
894 fragment,
895 label: Some("invalid column reference in primary key".to_string()),
896 help: Some(
897 "ensure all columns referenced in the primary key exist in the table or view"
898 .to_string(),
899 ),
900 column: None,
901 notes: vec![],
902 cause: None,
903 operator_chain: None,
904 },
905
906 CatalogError::SubscriptionAlreadyExists {
907 fragment,
908 name,
909 } => Diagnostic {
910 code: "CA_010".to_string(),
911 rql: None,
912 message: format!("subscription `{}` already exists", name),
913 fragment,
914 label: Some("duplicate subscription definition".to_string()),
915 help: Some(
916 "choose a different name or close the existing subscription first".to_string()
917 ),
918 column: None,
919 notes: vec![],
920 cause: None,
921 operator_chain: None,
922 },
923
924 CatalogError::SubscriptionNotFound {
925 fragment,
926 name,
927 } => Diagnostic {
928 code: "CA_011".to_string(),
929 rql: None,
930 message: format!("subscription `{}` not found", name),
931 fragment,
932 label: Some("unknown subscription reference".to_string()),
933 help: Some(
934 "ensure the subscription exists or create it first using `CREATE SUBSCRIPTION`"
935 .to_string(),
936 ),
937 column: None,
938 notes: vec![],
939 cause: None,
940 operator_chain: None,
941 },
942
943 CatalogError::ColumnNotFound {
944 kind,
945 namespace,
946 name,
947 column,
948 fragment,
949 } => {
950 let kind_str = match kind {
951 CatalogObjectKind::Table => "table",
952 CatalogObjectKind::View => "view",
953 _ => "object",
954 };
955 Diagnostic {
956 code: "CA_039".to_string(),
957 rql: None,
958 message: format!(
959 "column `{}` not found in {} `{}`.`{}`",
960 column, kind_str, namespace, name
961 ),
962 fragment,
963 label: Some("unknown column reference".to_string()),
964 help: Some("ensure the column exists in the table".to_string()),
965 column: None,
966 notes: vec![],
967 cause: None,
968 operator_chain: None,
969 }
970 }
971
972 CatalogError::ConfigStorageKeyNotFound(key) => Diagnostic {
973 code: "CA_050".to_string(),
974 rql: None,
975 message: format!("unknown config key `{}`", key),
976 fragment: Fragment::None,
977 label: Some("unknown config key".to_string()),
978 help: Some("query system.config to see all registered configuration keys".to_string()),
979 column: None,
980 notes: vec![],
981 cause: None,
982 operator_chain: None,
983 },
984
985 CatalogError::ConfigValueInvalid(key) => Diagnostic {
986 code: "CA_051".to_string(),
987 rql: None,
988 message: format!("config value for key `{}` cannot be none", key),
989 fragment: Fragment::None,
990 label: Some("invalid config value".to_string()),
991 help: Some("provide a concrete value such as an integer or boolean".to_string()),
992 column: None,
993 notes: vec![],
994 cause: None,
995 operator_chain: None,
996 },
997
998 CatalogError::ConfigTypeMismatch {
999 key,
1000 expected,
1001 actual,
1002 } => {
1003 let expected_str =
1004 expected.iter().map(|t| format!("`{:?}`", t)).collect::<Vec<_>>().join(", ");
1005 Diagnostic {
1006 code: "CA_052".to_string(),
1007 rql: None,
1008 message: format!(
1009 "config value for key `{}` must be of type {}, got `{}`",
1010 key, expected_str, actual
1011 ),
1012 fragment: Fragment::None,
1013 label: Some("type mismatch".to_string()),
1014 help: Some(format!("provide a value of type {}", expected_str)),
1015 column: None,
1016 notes: vec![],
1017 cause: None,
1018 operator_chain: None,
1019 }
1020 }
1021
1022 CatalogError::ConfigInvalidValue {
1023 key,
1024 reason,
1025 } => Diagnostic {
1026 code: "CA_053".to_string(),
1027 rql: None,
1028 message: format!("config value for key `{}` is invalid: {}", key, reason),
1029 fragment: Fragment::None,
1030 label: Some("invalid config value".to_string()),
1031 help: None,
1032 column: None,
1033 notes: vec![],
1034 cause: None,
1035 operator_chain: None,
1036 },
1037
1038 CatalogError::InUse {
1039 kind,
1040 namespace,
1041 name,
1042 dependents,
1043 fragment,
1044 } => {
1045 let (code, label, help) = match kind {
1046 CatalogObjectKind::Dictionary => (
1047 "CA_032",
1048 "dictionary is in use",
1049 "drop or alter the dependent columns first, or use CASCADE to automatically drop all dependents",
1050 ),
1051 CatalogObjectKind::Enum => (
1052 "CA_033",
1053 "enum is in use",
1054 "drop or alter the dependent columns first, or use CASCADE to automatically drop all dependents",
1055 ),
1056 CatalogObjectKind::Event => (
1057 "CA_033",
1058 "event is in use",
1059 "drop or alter the dependent handlers first, or use CASCADE to automatically drop all dependents",
1060 ),
1061 CatalogObjectKind::Namespace => (
1062 "CA_034",
1063 "namespace contains referenced objects",
1064 "drop or alter the dependent columns in other namespaces first",
1065 ),
1066 CatalogObjectKind::Table => (
1067 "CA_035",
1068 "table is in use",
1069 "drop or alter the dependent flows first, or use CASCADE to automatically drop all dependents",
1070 ),
1071 CatalogObjectKind::View => (
1072 "CA_036",
1073 "view is in use",
1074 "drop or alter the dependent flows first, or use CASCADE to automatically drop all dependents",
1075 ),
1076 CatalogObjectKind::Flow => (
1077 "CA_037",
1078 "flow is in use",
1079 "drop or alter the dependent flows first, or use CASCADE to automatically drop all dependents",
1080 ),
1081 CatalogObjectKind::RingBuffer => (
1082 "CA_038",
1083 "ring buffer is in use",
1084 "drop or alter the dependent flows first, or use CASCADE to automatically drop all dependents",
1085 ),
1086 _ => (
1087 "CA_032",
1088 "object is in use",
1089 "drop or alter the dependents first, or use CASCADE to automatically drop all dependents",
1090 ),
1091 };
1092 let message = if matches!(kind, CatalogObjectKind::Namespace) {
1093 format!(
1094 "cannot drop namespace '{}' because it contains objects referenced from other namespaces: {}",
1095 namespace, dependents
1096 )
1097 } else {
1098 format!(
1099 "cannot drop {} '{}::{}' because it is referenced by: {}",
1100 kind,
1101 namespace,
1102 name.as_deref().unwrap_or(""),
1103 dependents
1104 )
1105 };
1106 Diagnostic {
1107 code: code.to_string(),
1108 rql: None,
1109 message,
1110 fragment,
1111 label: Some(label.to_string()),
1112 help: Some(help.to_string()),
1113 column: None,
1114 notes: vec![],
1115 cause: None,
1116 operator_chain: None,
1117 }
1118 }
1119
1120 CatalogError::CannotDropEphemeralProcedure {
1121 kind,
1122 name,
1123 fragment,
1124 } => Diagnostic {
1125 code: "CA_084".to_string(),
1126 rql: None,
1127 message: format!(
1128 "cannot drop {} procedure `{}`: native/FFI/WASM procedures are managed by the runtime registry, not DDL",
1129 kind, name
1130 ),
1131 fragment,
1132 label: Some("cannot drop system-managed procedure".to_string()),
1133 help: Some(
1134 "native, FFI, and WASM procedures are repopulated on every boot from the runtime registry — remove them from the binary or plugin directory instead"
1135 .to_string(),
1136 ),
1137 column: None,
1138 notes: vec![],
1139 cause: None,
1140 operator_chain: None,
1141 },
1142
1143 CatalogError::CannotRegisterPersistentAsEphemeral {
1144 kind,
1145 } => Diagnostic {
1146 code: "CA_085".to_string(),
1147 rql: None,
1148 message: format!(
1149 "cannot register {} procedure as ephemeral: only Native/FFI/WASM variants are accepted",
1150 kind
1151 ),
1152 fragment: Fragment::None,
1153 label: Some("variant not accepted by ephemeral registrar".to_string()),
1154 help: Some(
1155 "persistent Rql/Test procedures must be created via `CREATE PROCEDURE`, not via the ephemeral registrar"
1156 .to_string(),
1157 ),
1158 column: None,
1159 notes: vec![],
1160 cause: None,
1161 operator_chain: None,
1162 },
1163
1164 CatalogError::PolicyInvalidOperation {
1165 target_type,
1166 operation,
1167 valid,
1168 policy_name,
1169 } => {
1170 let where_clause = match policy_name {
1171 Some(name) => format!(" in policy `{}`", name),
1172 None => String::new(),
1173 };
1174 let help = if valid.is_empty() {
1175 format!(
1176 "{} policies currently have no enforceable operations — remove this policy or add an enforcement call site for it",
1177 target_type
1178 )
1179 } else {
1180 format!("valid operations for {} policy: {}", target_type, valid.join(", "))
1181 };
1182 Diagnostic {
1183 code: "CA_086".to_string(),
1184 rql: None,
1185 message: format!(
1186 "unknown operation `{}` for {} policy{}",
1187 operation, target_type, where_clause
1188 ),
1189 fragment: Fragment::None,
1190 label: Some("unknown policy operation".to_string()),
1191 help: Some(help),
1192 column: None,
1193 notes: vec![
1194 "operation names are matched by exact string equality at enforcement time; unknown keys are silently skipped and effectively dead code"
1195 .to_string(),
1196 ],
1197 cause: None,
1198 operator_chain: None,
1199 }
1200 }
1201
1202 CatalogError::InvalidBindingConfig {
1203 reason,
1204 fragment,
1205 } => Diagnostic {
1206 code: "CA_089".to_string(),
1207 rql: None,
1208 message: format!("invalid binding config: {}", reason),
1209 fragment,
1210 label: Some("invalid binding config".to_string()),
1211 help: Some("check the protocol's required WITH keys and value constraints".to_string()),
1212 column: None,
1213 notes: vec![],
1214 cause: None,
1215 operator_chain: None,
1216 },
1217 }
1218 }
1219}
1220
1221impl From<CatalogError> for Error {
1222 fn from(err: CatalogError) -> Self {
1223 Error(Box::new(err.into_diagnostic()))
1224 }
1225}
1226
1227#[derive(Debug, thiserror::Error)]
1228pub enum CatalogChangeError {
1229 #[error("failed to decode {kind:?} key while applying replicated catalog change")]
1230 KeyDecodeFailed {
1231 kind: KeyKind,
1232 },
1233
1234 #[error("unrecognized key kind (raw: {raw:?})")]
1235 UnrecognizedKey {
1236 raw: Vec<u8>,
1237 },
1238}
1239
1240impl IntoDiagnostic for CatalogChangeError {
1241 fn into_diagnostic(self) -> Diagnostic {
1242 match self {
1243 CatalogChangeError::KeyDecodeFailed {
1244 kind,
1245 } => Diagnostic {
1246 code: "CA_070".to_string(),
1247 rql: None,
1248 message: format!("failed to decode {:?} key while applying replicated catalog change", kind),
1249 fragment: Fragment::None,
1250 label: Some("key decode failure during replication".to_string()),
1251 help: Some(
1252 "this indicates a protocol mismatch between primary and replica — ensure both nodes are running the same version".to_string(),
1253 ),
1254 column: None,
1255 notes: vec![],
1256 cause: None,
1257 operator_chain: None,
1258 },
1259 CatalogChangeError::UnrecognizedKey {
1260 raw,
1261 } => Diagnostic {
1262 code: "CA_071".to_string(),
1263 rql: None,
1264 message: format!("unrecognized key kind (raw: {:?})", raw),
1265 fragment: Fragment::None,
1266 label: Some("unrecognized key kind during replication".to_string()),
1267 help: Some(
1268 "this indicates state inconsistency — ensure primary and replica are running the same version".to_string(),
1269 ),
1270 column: None,
1271 notes: vec![],
1272 cause: None,
1273 operator_chain: None,
1274 },
1275 }
1276 }
1277}
1278
1279impl From<CatalogChangeError> for Error {
1280 fn from(err: CatalogChangeError) -> Self {
1281 Error(Box::new(err.into_diagnostic()))
1282 }
1283}