Skip to main content

systemprompt_extension/
error.rs

1//! Typed error enums raised by extension registration and configuration.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use 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}