Skip to main content

reifydb_catalog/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use 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}