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