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