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