Skip to main content

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}