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    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}