1use thiserror::Error;
7
8#[derive(Debug, Error)]
9pub enum LoaderError {
10 #[error("Extension '{extension}' requires dependency '{dependency}' which is not registered")]
11 MissingDependency {
12 extension: String,
13 dependency: String,
14 },
15
16 #[error("Extension with ID '{0}' is already registered")]
17 DuplicateExtension(String),
18
19 #[error("Extension '{0}' is required and cannot be disabled")]
20 RequiredExtensionDisabled(String),
21
22 #[error(
23 "Extension '{dependency}' is disabled but extension '{extension}' depends on it; disable \
24 '{extension}' as well or re-enable '{dependency}'"
25 )]
26 DisabledDependency {
27 extension: String,
28 dependency: String,
29 },
30
31 #[error("Failed to initialize extension '{extension}': {message}")]
32 InitializationFailed { extension: String, message: String },
33
34 #[error("Failed to install schema for extension '{extension}': {message}")]
35 SchemaInstallationFailed { extension: String, message: String },
36
37 #[error("Migration failed for extension '{extension}': {message}")]
38 MigrationFailed { extension: String, message: String },
39
40 #[error(
41 "Migration {version} for extension '{extension}' is not reversible (no down SQL provided)"
42 )]
43 MigrationNotReversible { extension: String, version: u32 },
44
45 #[error(
46 "Extension '{extension}' migration slot {version} was applied as '{stored_name}' but the \
47 tree now names it '{current_name}'. A migration file was deleted and its number reused; \
48 established databases have already spent that slot, so the new SQL would be skipped, \
49 never executed. Renumber it above every used slot and leave a \
50 `{version:03}_{stored_name}.tombstone` behind so the number cannot be claimed again."
51 )]
52 MigrationSlotReused {
53 extension: String,
54 version: u32,
55 stored_name: String,
56 current_name: String,
57 },
58
59 #[error("Configuration validation failed for extension '{extension}': {message}")]
60 ConfigValidationFailed { extension: String, message: String },
61
62 #[error("Extension '{extension}' uses reserved API path '{path}'")]
63 ReservedPathCollision { extension: String, path: String },
64
65 #[error(
66 "Extension '{extension}' has invalid base path '{path}': must be / or start with /api/"
67 )]
68 InvalidBasePath { extension: String, path: String },
69
70 #[error("Dependency cycle detected while ordering extensions: {chain}")]
71 DependencyCycle { chain: String },
72
73 #[error(
74 "Extension '{extension}' migration ALTERs table '{table}' but does not create it in its \
75 schemas() nor declare it in cross_extension_tables(); cross-extension table mutations \
76 must be declared explicitly"
77 )]
78 CrossExtensionAlterUndeclared { extension: String, table: String },
79
80 #[error(
81 "Extension '{extension}' migration {migration} references {kind} '{object}' via {how}, \
82 which only a declarative schema file creates; the dependent phase runs after \
83 migrations, so a database that has not booted on that schema fails here. Create it in \
84 a migration, guard the reference in a DO $$ block that tests pg_trigger/pg_views, or \
85 leave it to the declarative schema"
86 )]
87 MigrationReferencesDeclarativeObject {
88 extension: String,
89 migration: String,
90 kind: String,
91 object: String,
92 how: String,
93 },
94
95 #[error(
96 "Extension '{extension}' migration {migration} toggles trigger '{trigger}' on '{table}' \
97 by name; that statement fails on any database where the trigger has since been retired. \
98 The runner already suspends every row trigger on the tables a migration writes, so \
99 delete the toggle, or guard it in a DO $$ block that tests pg_trigger first"
100 )]
101 MigrationTogglesTriggerByName {
102 extension: String,
103 migration: String,
104 table: String,
105 trigger: String,
106 },
107
108 #[error(
109 "Trigger '{trigger}' on '{table}' runs {function}, which uses '{relation}', and \
110 '{relation}' no longer exists: every write to '{table}' would fail. Retire the trigger in \
111 its extension's `retirements()` (it runs before any migration), or restore the relation"
112 )]
113 DanglingTriggerRoutine {
114 trigger: String,
115 table: String,
116 function: String,
117 relation: String,
118 },
119
120 #[error(
121 "Extension '{extension}' migration {version} ('{name}') has been edited since it was \
122 applied (stored checksum {stored_checksum}, current {current_checksum}). Refusing to \
123 proceed. If the database schema already matches the edited file, run `systemprompt \
124 infra db migrate-repair --reconcile-only --apply` to rewrite the stored checksum \
125 without executing any SQL. To re-execute the edited migration, run `systemprompt infra \
126 db migrate-repair --apply`. Passing --allow-checksum-drift bypasses the check without \
127 fixing it."
128 )]
129 MigrationChecksumDrift {
130 extension: String,
131 version: u32,
132 name: String,
133 stored_checksum: String,
134 current_checksum: String,
135 },
136
137 #[error(
138 "Table '{table}' is created by both extension '{extension_a}' and '{extension_b}'; every \
139 table must be declared by exactly one extension"
140 )]
141 DuplicateTableOwner {
142 table: String,
143 extension_a: String,
144 extension_b: String,
145 },
146
147 #[error(
148 "Extension '{extension}' declares cross_extension_tables() entry '{table}', which is not \
149 a table created by any other loaded extension"
150 )]
151 CrossExtensionTableNotOwned { extension: String, table: String },
152
153 #[error(
154 "Extension '{extension}' seed '{seed}' contains forbidden statement '{statement}'; seeds \
155 may only contain INSERT … ON CONFLICT, UPDATE, MERGE, or WITH … INSERT"
156 )]
157 InvalidSeedStatement {
158 extension: String,
159 seed: String,
160 statement: String,
161 },
162
163 #[error(
164 "Extension '{extension}' seed '{seed}' contains a bare INSERT with no ON CONFLICT clause; \
165 seeds run on every boot and must be idempotent — add ON CONFLICT … DO NOTHING/UPDATE"
166 )]
167 SeedInsertNotIdempotent { extension: String, seed: String },
168
169 #[error("Extension '{extension}' seed '{seed}' failed to parse or apply: {message}")]
170 SeedFailed {
171 extension: String,
172 seed: String,
173 message: String,
174 },
175}
176
177#[derive(Debug, Error)]
178pub enum ExtensionConfigError {
179 #[error("Configuration key '{0}' not found")]
180 NotFound(String),
181
182 #[error("Invalid configuration value for '{key}': {message}")]
183 InvalidValue { key: String, message: String },
184
185 #[error("Failed to parse configuration: {message}")]
186 ParseError { message: String },
187
188 #[error("Schema validation failed: {0}")]
189 SchemaValidation(String),
190}