wasm_dbms_api/dbms/database.rs
1use crate::error::DbmsResult;
2use crate::prelude::{
3 AggregateFunction, AggregatedRow, ColumnDef, DeleteBehavior, Filter, InsertRecord,
4 JoinResultSet, MigrationOp, MigrationPolicy, Query, TableSchema, UpdateRecord, Value,
5};
6
7/// CRUD, aggregate, and transaction operations exposed by a wasm-dbms session.
8///
9/// One implementation lives in `wasm-dbms` (`WasmDbmsDatabase`); IC adapters
10/// wrap that implementation behind canister endpoints. Methods that take a
11/// [`Query`] honour its `WHERE`, `DISTINCT`, `ORDER BY`, `OFFSET`, `LIMIT`,
12/// and (for [`aggregate`](Self::aggregate)) `GROUP BY` / `HAVING` clauses.
13///
14/// All methods return [`DbmsResult`]; the error variants worth handling at
15/// each call site are listed under each method's `# Errors` section.
16pub trait Database {
17 /// Runs a typed `SELECT` for table `T` and decodes each row into `T::Record`.
18 ///
19 /// The query's primary key column is always included in the returned
20 /// records even when the caller restricts `Select::Columns` to other
21 /// fields, so the `Record` shape is always reconstructible.
22 ///
23 /// # Arguments
24 ///
25 /// - `query` - The [`Query`] to execute. Must not contain joins; use
26 /// [`Database::select_join`] for joined queries.
27 ///
28 /// # Returns
29 ///
30 /// A `Vec<T::Record>` containing one entry per matching row, in the order
31 /// produced by the query pipeline (see the
32 /// [Query API reference](crate::prelude::Query)).
33 ///
34 /// # Errors
35 ///
36 /// - [`QueryError::JoinInsideTypedSelect`] — the query contains joins.
37 /// - [`QueryError::AggregateClauseInSelect`] — `group_by` or `having` is
38 /// set; call [`aggregate`](Self::aggregate) instead.
39 /// - [`QueryError::UnknownColumn`] — a referenced column does not exist
40 /// on `T`.
41 /// - [`QueryError::TableNotFound`] — `T::table_name()` was never registered.
42 ///
43 /// [`QueryError::JoinInsideTypedSelect`]: crate::prelude::QueryError::JoinInsideTypedSelect
44 /// [`QueryError::AggregateClauseInSelect`]: crate::prelude::QueryError::AggregateClauseInSelect
45 /// [`QueryError::UnknownColumn`]: crate::prelude::QueryError::UnknownColumn
46 /// [`QueryError::TableNotFound`]: crate::prelude::QueryError::TableNotFound
47 fn select<T>(&self, query: Query) -> DbmsResult<Vec<T::Record>>
48 where
49 T: TableSchema;
50
51 /// Runs a `SELECT` against a table identified by name, returning raw
52 /// column-value pairs instead of typed records.
53 ///
54 /// Useful when the caller does not know the table type at compile time
55 /// (for example, on the IC canister boundary). Same execution pipeline as
56 /// [`select`](Self::select); same restrictions apply.
57 ///
58 /// # Arguments
59 ///
60 /// - `table` - Table name as registered with the schema.
61 /// - `query` - The [`Query`] to execute. Must not contain joins.
62 ///
63 /// # Returns
64 ///
65 /// A `Vec` of rows, where each row is a `Vec<(ColumnDef, Value)>` with
66 /// one entry per selected column.
67 ///
68 /// # Errors
69 ///
70 /// - [`QueryError::TableNotFound`] — `table` is not registered.
71 /// - [`QueryError::AggregateClauseInSelect`] — `group_by` or `having` is set.
72 /// - [`QueryError::UnknownColumn`] — a referenced column does not exist.
73 ///
74 /// [`QueryError::TableNotFound`]: crate::prelude::QueryError::TableNotFound
75 /// [`QueryError::AggregateClauseInSelect`]: crate::prelude::QueryError::AggregateClauseInSelect
76 /// [`QueryError::UnknownColumn`]: crate::prelude::QueryError::UnknownColumn
77 fn select_raw(&self, table: &str, query: Query) -> DbmsResult<Vec<Vec<(ColumnDef, Value)>>>;
78
79 /// Runs a join query starting from `table`.
80 ///
81 /// Use `table.column` syntax in [`field`](crate::prelude::QueryBuilder::field),
82 /// [`and_where`](crate::prelude::QueryBuilder::and_where),
83 /// [`or_where`](crate::prelude::QueryBuilder::or_where), and `order_by_*`
84 /// to disambiguate columns that share names across joined tables.
85 /// Unqualified names default to the `table` argument.
86 ///
87 /// # Arguments
88 ///
89 /// - `table` - The driving (`FROM`) table for the join.
90 /// - `query` - The [`Query`] to execute. Must include at least one join via
91 /// [`QueryBuilder::inner_join`](crate::prelude::QueryBuilder::inner_join)
92 /// or its variants.
93 ///
94 /// # Returns
95 ///
96 /// A [`JoinResultSet`]: the [`JoinColumnDef`](crate::prelude::JoinColumnDef)
97 /// of every selected column once, each carrying its source table name,
98 /// followed by one
99 /// `Vec<Value>` per row in the same column order.
100 ///
101 /// # Errors
102 ///
103 /// - [`QueryError::TableNotFound`] — `table` or any joined table is not
104 /// registered.
105 /// - [`QueryError::AggregateClauseInSelect`] — `group_by` or `having` is set.
106 /// - [`QueryError::InvalidQuery`] — an ambiguous unqualified column appears
107 /// in multiple joined tables.
108 ///
109 /// [`QueryError::TableNotFound`]: crate::prelude::QueryError::TableNotFound
110 /// [`QueryError::AggregateClauseInSelect`]: crate::prelude::QueryError::AggregateClauseInSelect
111 /// [`QueryError::InvalidQuery`]: crate::prelude::QueryError::InvalidQuery
112 fn select_join(&self, table: &str, query: Query) -> DbmsResult<JoinResultSet>;
113
114 /// Runs an aggregate query for table `T`, computing the requested
115 /// aggregate functions per group.
116 ///
117 /// Pipeline: `WHERE` -> `DISTINCT` -> bucket rows by [`Query::group_by`]
118 /// -> compute each [`AggregateFunction`] per bucket -> apply
119 /// [`Query::having`] -> apply `ORDER BY` -> apply `OFFSET` / `LIMIT`. When
120 /// `group_by` is empty all matching rows form one group, producing at most
121 /// one [`AggregatedRow`].
122 ///
123 /// `HAVING` and `ORDER BY` may reference any column listed in `group_by`
124 /// or any aggregate output by its synthetic name `agg{N}` (`agg0` is the
125 /// first entry of `aggregates`, `agg1` the second, ...).
126 ///
127 /// # Arguments
128 ///
129 /// - `query` - The [`Query`] providing `WHERE`, `DISTINCT`, `GROUP BY`,
130 /// `HAVING`, `ORDER BY`, `LIMIT`, and `OFFSET`. Joins and eager
131 /// relations are rejected.
132 /// - `aggregates` - The aggregate functions to compute per group, in the
133 /// order they should appear in [`AggregatedRow::values`].
134 ///
135 /// # Returns
136 ///
137 /// One [`AggregatedRow`] per distinct grouping tuple. Empty when every
138 /// group is filtered out by `HAVING` or when no rows survive `WHERE`.
139 ///
140 /// # Errors
141 ///
142 /// - [`QueryError::UnknownColumn`] — `group_by` or an aggregate references
143 /// a column not on `T`.
144 /// - [`QueryError::InvalidQuery`] — `SUM` or `AVG` on a non-numeric column,
145 /// `HAVING` / `ORDER BY` references an unknown `agg{N}` or column,
146 /// `LIKE` or JSON filter inside `HAVING`, or query carries joins or
147 /// eager relations.
148 ///
149 /// [`QueryError::UnknownColumn`]: crate::prelude::QueryError::UnknownColumn
150 /// [`QueryError::InvalidQuery`]: crate::prelude::QueryError::InvalidQuery
151 fn aggregate<T>(
152 &self,
153 query: Query,
154 aggregates: &[AggregateFunction],
155 ) -> DbmsResult<Vec<AggregatedRow>>
156 where
157 T: TableSchema;
158
159 /// Inserts a single record into table `T`.
160 ///
161 /// Auto-increment columns left unset are filled before insertion.
162 /// Sanitizers run on each column before validators; insert-time integrity
163 /// checks (primary key uniqueness, `#[unique]` constraints, foreign-key
164 /// existence) are evaluated before the row is written.
165 ///
166 /// Outside a transaction the write is journaled and applied atomically
167 /// against stable storage; inside a transaction the write goes to the
168 /// transaction overlay and becomes visible to subsequent reads on the
169 /// same transaction.
170 ///
171 /// # Arguments
172 ///
173 /// - `record` - The insert payload, typically built from
174 /// `T::Insert::from_values(...)` or the `*InsertRequest` struct
175 /// generated by `#[derive(Table)]`.
176 ///
177 /// # Errors
178 ///
179 /// - [`QueryError::PrimaryKeyConflict`] — the row's PK already exists.
180 /// - [`QueryError::UniqueConstraintViolation`] — a `#[unique]` column
181 /// collides with an existing row.
182 /// - [`QueryError::BrokenForeignKeyReference`] — a foreign key points at
183 /// a row that does not exist.
184 /// - [`QueryError::MissingNonNullableField`] — a required column was
185 /// omitted.
186 /// - [`DbmsError::Validation`] / [`DbmsError::Sanitize`] — a column
187 /// validator or sanitizer rejected the value.
188 ///
189 /// [`QueryError::PrimaryKeyConflict`]: crate::prelude::QueryError::PrimaryKeyConflict
190 /// [`QueryError::UniqueConstraintViolation`]: crate::prelude::QueryError::UniqueConstraintViolation
191 /// [`QueryError::BrokenForeignKeyReference`]: crate::prelude::QueryError::BrokenForeignKeyReference
192 /// [`QueryError::MissingNonNullableField`]: crate::prelude::QueryError::MissingNonNullableField
193 /// [`DbmsError::Validation`]: crate::prelude::DbmsError
194 /// [`DbmsError::Sanitize`]: crate::prelude::DbmsError
195 fn insert<T>(&self, record: T::Insert) -> DbmsResult<()>
196 where
197 T: TableSchema,
198 T::Insert: InsertRecord<Schema = T>;
199
200 /// Updates rows of table `T` matching the patch's `where_clause`.
201 ///
202 /// The set of columns to write and the row predicate are both carried by
203 /// `patch` (see [`UpdateRecord`]); a missing `where_clause` updates every
204 /// row. Sanitizers and validators run on the patched values, and integrity
205 /// checks (unique, foreign-key, etc.) are re-evaluated for each updated
206 /// row.
207 ///
208 /// Updating a primary-key column cascades the new value to every
209 /// referencing row's foreign key.
210 ///
211 /// Outside a transaction the update is journaled and applied atomically;
212 /// inside a transaction the change is staged on the transaction overlay.
213 ///
214 /// # Arguments
215 ///
216 /// - `patch` - The update payload (typically a `*UpdateRequest` generated
217 /// by `#[derive(Table)]`) containing the new column values and the
218 /// `where_clause` filter.
219 ///
220 /// # Returns
221 ///
222 /// Number of rows updated. A return of `0` means no row matched the
223 /// filter — not an error.
224 ///
225 /// # Errors
226 ///
227 /// - [`QueryError::PrimaryKeyConflict`] — updating the PK collides with
228 /// an existing row.
229 /// - [`QueryError::UniqueConstraintViolation`] — the new value collides
230 /// with another row's `#[unique]` column.
231 /// - [`QueryError::BrokenForeignKeyReference`] — a new FK value points at
232 /// a non-existent parent row.
233 ///
234 /// [`QueryError::PrimaryKeyConflict`]: crate::prelude::QueryError::PrimaryKeyConflict
235 /// [`QueryError::UniqueConstraintViolation`]: crate::prelude::QueryError::UniqueConstraintViolation
236 /// [`QueryError::BrokenForeignKeyReference`]: crate::prelude::QueryError::BrokenForeignKeyReference
237 fn update<T>(&self, patch: T::Update) -> DbmsResult<u64>
238 where
239 T: TableSchema,
240 T::Update: UpdateRecord<Schema = T>;
241
242 /// Deletes rows of table `T` matching `filter`.
243 ///
244 /// `behaviour` controls the foreign-key handling:
245 /// [`DeleteBehavior::Restrict`] aborts the delete if any other row
246 /// references the target, while [`DeleteBehavior::Cascade`] also deletes
247 /// the referencing rows recursively.
248 ///
249 /// A `None` filter targets every row in the table.
250 ///
251 /// Outside a transaction the delete (and any cascade) is journaled and
252 /// applied atomically; inside a transaction the deletion is staged on the
253 /// overlay.
254 ///
255 /// # Arguments
256 ///
257 /// - `behaviour` - Foreign-key handling: [`DeleteBehavior::Restrict`] or
258 /// [`DeleteBehavior::Cascade`].
259 /// - `filter` - Predicate selecting rows to delete; `None` matches every
260 /// row.
261 ///
262 /// # Returns
263 ///
264 /// Total rows deleted, including rows removed by cascade. `0` means no
265 /// row matched the filter.
266 ///
267 /// # Errors
268 ///
269 /// - [`QueryError::ForeignKeyConstraintViolation`] — a referenced row
270 /// exists and `behaviour` is [`DeleteBehavior::Restrict`].
271 /// - [`QueryError::UnknownColumn`] — `filter` references a column not on
272 /// `T`.
273 ///
274 /// [`QueryError::ForeignKeyConstraintViolation`]: crate::prelude::QueryError::ForeignKeyConstraintViolation
275 /// [`QueryError::UnknownColumn`]: crate::prelude::QueryError::UnknownColumn
276 fn delete<T>(&self, behaviour: DeleteBehavior, filter: Option<Filter>) -> DbmsResult<u64>
277 where
278 T: TableSchema;
279
280 /// Commits the active transaction, replaying its operations against
281 /// stable storage under a single write-ahead journal.
282 ///
283 /// The transaction handle is consumed regardless of outcome. On success
284 /// every staged insert/update/delete is durably applied; on operation
285 /// failure the journal is rolled back, leaving stable storage untouched
286 /// before the error propagates to the caller.
287 ///
288 /// # Errors
289 ///
290 /// - [`TransactionError::NoActiveTransaction`] — no transaction was
291 /// started on this session.
292 /// - Any [`QueryError`] raised by the staged operations during replay
293 /// (constraint violations, missing FKs, etc.).
294 ///
295 /// # Panics
296 ///
297 /// Panics only if the rollback that follows a failed staged operation
298 /// itself fails — at that point stable memory is in an irrecoverable
299 /// state (M-PANIC-ON-BUG).
300 ///
301 /// [`TransactionError::NoActiveTransaction`]: crate::prelude::TransactionError::NoActiveTransaction
302 /// [`QueryError`]: crate::prelude::QueryError
303 fn commit(&mut self) -> DbmsResult<()>;
304
305 /// Discards every staged operation in the active transaction without
306 /// touching stable storage, and consumes the transaction handle.
307 ///
308 /// # Errors
309 ///
310 /// - [`TransactionError::NoActiveTransaction`] — no transaction was
311 /// started on this session.
312 ///
313 /// [`TransactionError::NoActiveTransaction`]: crate::prelude::TransactionError::NoActiveTransaction
314 fn rollback(&mut self) -> DbmsResult<()>;
315
316 /// Returns `true` iff the compiled schema differs from the snapshots
317 /// persisted in stable memory.
318 ///
319 /// `O(1)` after the first call thanks to a per-context cache. Implementors
320 /// gate every CRUD entry on this flag so callers can rely on the boolean
321 /// to decide whether a migration is required before doing any work.
322 ///
323 /// # Errors
324 ///
325 /// Propagates [`DbmsError::Memory`](crate::prelude::DbmsError::Memory) when
326 /// the persisted snapshots cannot be read.
327 fn has_drift(&self) -> DbmsResult<bool>;
328
329 /// Returns the migration ops needed to bring the on-disk schema in line
330 /// with the compiled schema, without applying anything.
331 ///
332 /// Always recomputes the diff — there is no cache; the call is rare
333 /// (typically once before [`Self::migrate`]) and the result depends on
334 /// runtime state the cache cannot track. Safe to call while drift is
335 /// active.
336 ///
337 /// # Errors
338 ///
339 /// Propagates the same [`DbmsError`](crate::prelude::DbmsError) variants
340 /// the migration diff produces, plus
341 /// [`DbmsError::Memory`](crate::prelude::DbmsError::Memory) when persisted
342 /// snapshots cannot be read.
343 fn pending_migrations(&self) -> DbmsResult<Vec<MigrationOp>>;
344
345 /// Applies a planned migration under `policy`.
346 ///
347 /// Plans the diff, sorts the ops into deterministic apply order, validates
348 /// against `policy`, then executes inside the implementation's journaled
349 /// atomic block. On success the drift cache is cleared so subsequent CRUD
350 /// calls pass; on failure the journal rolls back and the drift flag stays
351 /// set.
352 ///
353 /// # Errors
354 ///
355 /// - [`MigrationError::DestructiveOpDenied`](crate::prelude::MigrationError::DestructiveOpDenied)
356 /// when the planner emits a destructive op disallowed by `policy`.
357 /// - [`MigrationError::DefaultMissing`](crate::prelude::MigrationError::DefaultMissing)
358 /// when an `AddColumn` op cannot resolve a default for a non-nullable
359 /// column.
360 /// - [`MigrationError::WideningIncompatible`](crate::prelude::MigrationError::WideningIncompatible)
361 /// when a `WidenColumn` op falls outside the widening whitelist.
362 /// - Any other [`MigrationError`](crate::prelude::MigrationError) variant
363 /// raised by the diff or apply pipeline.
364 fn migrate(&mut self, policy: MigrationPolicy) -> DbmsResult<()>;
365}