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