Errors Reference
- Errors Reference
Overview
wasm-dbms uses a structured error system to provide clear information about what went wrong. Errors are categorized by their source:
| Category | Description |
|---|---|
| Query | Database operation errors (constraints, missing data) |
| Transaction | Transaction state errors |
| Validation | Data validation failures |
| Sanitization | Data sanitization failures |
| Memory | Low-level memory errors |
| Migration | Schema migration / drift detection errors |
| Table | Schema/table definition errors |
| SQL | Errors of the SQL engine (SqlError, sql feature) |
Error Hierarchy
DbmsError
├── Query(QueryError)
│ ├── PrimaryKeyConflict
│ ├── UniqueConstraintViolation
│ ├── BrokenForeignKeyReference
│ ├── ForeignKeyConstraintViolation
│ ├── UnknownColumn
│ ├── MissingNonNullableField
│ ├── RecordNotFound
│ └── InvalidQuery
├── Transaction(TransactionError)
│ └── NotFound
├── Validation(String)
├── Sanitize(String)
├── Memory(MemoryError)
├── Migration(MigrationError)
│ ├── SchemaDrift
│ ├── IncompatibleType { table, column, old, new }
│ ├── DefaultMissing { table, column }
│ ├── ConstraintViolation { table, column, reason }
│ ├── DestructiveOpDenied { op }
│ ├── TransformAborted { table, column, reason }
│ ├── WideningIncompatible { table, column, old_type, new_type }
│ ├── TransformReturnedNone { table, column }
│ └── ForeignKeyViolation { table, column, target_table, value }
└── Table(TableError)
DbmsError
The top-level error enum:
#![allow(unused)]
fn main() {
use wasm_dbms_api::prelude::DbmsError;
pub enum DbmsError {
Memory(MemoryError),
Migration(MigrationError),
Query(QueryError),
Table(TableError),
Transaction(TransactionError),
Sanitize(String),
Validation(String),
}
}
Matching on error types:
#![allow(unused)]
fn main() {
match error {
DbmsError::Query(query_err) => {
// Handle query errors
}
DbmsError::Transaction(tx_err) => {
// Handle transaction errors
}
DbmsError::Validation(msg) => {
// Handle validation errors
println!("Validation failed: {}", msg);
}
DbmsError::Sanitize(msg) => {
// Handle sanitization errors
println!("Sanitization failed: {}", msg);
}
DbmsError::Memory(mem_err) => {
// Handle memory errors (rare)
}
DbmsError::Migration(mig_err) => {
// Handle schema migration errors
}
DbmsError::Table(table_err) => {
// Handle table errors (rare)
}
}
}
Migration Errors
MigrationError covers the schema migration pipeline: drift detection on boot, plan validation, and journaled apply. See the Migrations Reference for the full lifecycle.
#![allow(unused)]
fn main() {
use wasm_dbms_api::prelude::{DataTypeSnapshot, MigrationError};
pub enum MigrationError {
SchemaDrift,
IncompatibleType {
table: String,
column: String,
old: DataTypeSnapshot,
new: DataTypeSnapshot,
},
DefaultMissing {
table: String,
column: String,
},
ConstraintViolation {
table: String,
column: String,
reason: String,
},
DestructiveOpDenied {
op: String,
},
TransformAborted {
table: String,
column: String,
reason: String,
},
WideningIncompatible {
table: String,
column: String,
old_type: DataTypeSnapshot,
new_type: DataTypeSnapshot,
},
TransformReturnedNone {
table: String,
column: String,
},
ForeignKeyViolation {
table: String,
column: String,
target_table: String,
value: String,
},
}
}
SchemaDrift
Cause: A CRUD operation was attempted while the DBMS is in drift state — the compiled schema’s hash differs from the hash stored in the schema registry.
#![allow(unused)]
fn main() {
match database.insert::<User>(req) {
Err(DbmsError::Migration(MigrationError::SchemaDrift)) => {
// Stop accepting writes; call dbms.migrate(policy) first.
}
_ => {}
}
}
Solutions:
- Call
dbms.migrate(MigrationPolicy::default())from your boot path to clear the drift flag. - Inspect the diff first via
dbms.pending_migrations()to confirm the ops are safe.
IncompatibleType
Cause: A column changed to a type that is neither in the widening whitelist (e.g. Int32 → Int64) nor handled by Migrate::transform_column.
Solutions:
- If the change is conceptually a widen, double-check the from/to types match the whitelist.
- Otherwise mark the table with
#[migrate]and provide atransform_columnimpl that maps the oldValueto the new type.
DefaultMissing
Cause: Planning an AddColumn op for a non-nullable column that has neither a #[default = ...] attribute nor a Migrate::default_value override.
Solutions:
- Add
#[default = <expr>]to the field, or - Implement
Migrate::default_valuefor the table (after marking it#[migrate]), or - Make the column
Nullable<T>soNULLis the implicit default.
ConstraintViolation
Cause: Tightening an existing column (nullable: false, unique: true, add foreign key) on data that violates the new constraint.
Solutions:
- Clean the data before bumping the schema (e.g. backfill
NULLs, deduplicate). - Stage the change across two releases: relaxation + cleanup, then tightening.
DestructiveOpDenied
Cause: The planner emitted a DropTable or DropColumn op while MigrationPolicy::allow_destructive is false.
Solutions:
- Confirm the destruction is intentional and pass
MigrationPolicy { allow_destructive: true }. - Otherwise re-introduce the missing struct/field in the compiled schema.
TransformAborted
Cause: A user-supplied Migrate::transform_column impl returned Err. The journaled migration session rolls back; stored data and schema_hash are unchanged.
Solutions:
- Inspect the embedded
reasonstring to see which row failed. - Fix the offending data manually (or via a helper routine) before retrying
migrate.
WideningIncompatible
Cause: A WidenColumn op named a (old_type, new_type) pair that is not in the widening whitelist, and the table did not provide a Migrate::transform_column impl that handled it. The journaled session rolls back; stored data and schema_hash are unchanged.
Solutions:
- Pick a target type that fits the whitelist (e.g.
Uint32 → Uint64rather thanUint32 → Uint8). - Mark the table
#[migrate]and provide atransform_columnarm that maps the oldValueinto the new type. - Stage the change across two releases: convert via a transform first, then narrow as a separate widening with valid bounds.
TransformReturnedNone
Cause: Migrate::transform_column returned Ok(None) for a column that needed a transform (no widening rule applied). The migration aborts and rolls back.
Solutions:
- Implement a concrete
Ok(Some(_))arm for the column in the table’sMigrateimpl. - Or pick a target type that fits the widening whitelist so the framework converts automatically.
ForeignKeyViolation
Cause: An add-FK tightening (AlterColumn with foreign_key: Some(Some(_))) found a row whose value is absent from the target table’s referenced column. The journaled session rolls back; stored data and schema_hash are unchanged.
Solutions:
- Clean up the orphan rows in a prior release before adding the FK.
- Inspect
valuein the error to identify the offending record(s).
Query Errors
Query errors occur during database operations.
PrimaryKeyConflict
Cause: Attempting to insert a record with a primary key that already exists.
#![allow(unused)]
fn main() {
// Insert first user
database.insert::<User>(UserInsertRequest {
id: 1.into(),
name: "Alice".into(),
..
})?;
// Insert second user with same ID - FAILS
let result = database.insert::<User>(UserInsertRequest {
id: 1.into(), // Same ID!
name: "Bob".into(),
..
});
match result {
Err(DbmsError::Query(QueryError::PrimaryKeyConflict)) => {
println!("A user with this ID already exists");
}
_ => {}
}
}
Solutions:
- Use a unique primary key (e.g., UUID)
- Check if record exists before inserting
- Use upsert pattern (check, then insert or update)
UniqueConstraintViolation
Cause: Attempting to insert or update a record with a value that violates a #[unique] constraint.
#![allow(unused)]
fn main() {
// Insert first user
database.insert::<User>(UserInsertRequest {
id: 1.into(),
email: "alice@example.com".into(),
..
})?;
// Insert second user with same email - FAILS
let result = database.insert::<User>(UserInsertRequest {
id: 2.into(),
email: "alice@example.com".into(), // Duplicate!
..
});
match result {
Err(DbmsError::Query(QueryError::UniqueConstraintViolation { field })) => {
println!("Duplicate value on field: {}", field);
// field == "email"
}
_ => {}
}
}
Also triggered on update:
#![allow(unused)]
fn main() {
// Update user 2's email to match user 1's email - FAILS
let result = database.update::<User>(
UserUpdateRequest::from_values(
&[(email_col, Value::Text("alice@example.com".into()))],
Some(Filter::eq("id", Value::Uint32(2.into()))),
)?,
);
}
Solutions:
- Check if a record with the same value exists before inserting
- Use a different value
BrokenForeignKeyReference
Cause: Foreign key references a record that doesn’t exist.
#![allow(unused)]
fn main() {
// Insert post with non-existent author
let result = database.insert::<Post>(PostInsertRequest {
id: 1.into(),
title: "My Post".into(),
author_id: 999.into(), // User 999 doesn't exist!
..
});
match result {
Err(DbmsError::Query(QueryError::BrokenForeignKeyReference)) => {
println!("Referenced user does not exist");
}
_ => {}
}
}
Solutions:
- Ensure referenced record exists before inserting
- Create referenced record first in a transaction
ForeignKeyConstraintViolation
Cause: Attempting to delete a record that is referenced by other records (with Restrict behavior).
#![allow(unused)]
fn main() {
// User has posts - cannot delete with Restrict
let result = database.delete::<User>(
DeleteBehavior::Restrict,
Some(Filter::eq("id", Value::Uint32(1.into()))),
);
match result {
Err(DbmsError::Query(QueryError::ForeignKeyConstraintViolation)) => {
println!("Cannot delete: user has related records");
}
_ => {}
}
}
Solutions:
- Delete related records first
- Use
DeleteBehavior::Cascadeto delete related records automatically
UnknownColumn
Cause: Referencing a column that doesn’t exist in the table.
#![allow(unused)]
fn main() {
// Filter with wrong column name
let filter = Filter::eq("username", Value::Text("alice".into())); // Column is "name", not "username"
let result = database.select::<User>(
Query::builder().filter(filter).build(),
);
match result {
Err(DbmsError::Query(QueryError::UnknownColumn)) => {
println!("Column does not exist in table");
}
_ => {}
}
}
Solutions:
- Check column names in your schema
- Use IDE autocompletion with typed column names
MissingNonNullableField
Cause: Required field not provided in insert/update.
#![allow(unused)]
fn main() {
// This typically happens at compile time with the generated types,
// but can occur if manually constructing requests or using dynamic queries
}
Solutions:
- Provide all required fields
- Use
Nullable<T>for optional fields
RecordNotFound
Cause: Operation targets a record that doesn’t exist.
#![allow(unused)]
fn main() {
// Update non-existent record
let update = UserUpdateRequest::builder()
.set_name("New Name".into())
.filter(Filter::eq("id", Value::Uint32(999.into()))) // Doesn't exist
.build();
let affected = database.update::<User>(update)?;
// affected == 0 indicates no records matched
if affected == 0 {
println!("No records found to update");
}
}
Note: Update and delete operations return the count of affected rows. A count of 0 isn’t necessarily an error but indicates no matches.
InvalidQuery
Cause: Malformed query (invalid JSON path, bad filter syntax, etc.).
#![allow(unused)]
fn main() {
// Invalid JSON path
let filter = Filter::json("metadata", JsonFilter::has_key("user.")); // Trailing dot
let result = database.select::<User>(
Query::builder().filter(filter).build(),
);
match result {
Err(DbmsError::Query(QueryError::InvalidQuery)) => {
println!("Query is malformed");
}
_ => {}
}
}
Common causes:
- Invalid JSON paths (trailing dots, unclosed brackets)
- Applying JSON filter to non-JSON column
- Type mismatches in comparisons
- Update patch value whose type does not match its column, built with
UpdateRecord::from_valuesor sent through the dynamic schema update ("column '<col>' does not accept a value of type <type>");Nullis accepted only by nullable columns - Aggregate-specific:
SUMorAVGon non-numeric column ("aggregate requires numeric column: '<col>'")HAVINGreferences unknown column oragg{N}("HAVING references unknown column or aggregate: '<col>'")ORDER BYreferences unknownagg{N}("ORDER BY references unknown aggregate output: '<col>'")LIKEor JSON filter insideHAVING- Joins or eager relations on
Database::aggregate
JoinInsideTypedSelect
Cause: A typed Database::select::<T> was called with a query that
contains joins. Joins must go through select_join.
AggregateClauseInSelect
Cause: group_by or having was set on a non-aggregate select path
(select, select_raw, or select_join). Use
Database::aggregate instead — those clauses
have no meaning outside aggregation and are rejected to prevent silent data
loss.
#![allow(unused)]
fn main() {
let result = database.select::<User>(
Query::builder().group_by(&["role"]).build(),
);
match result {
Err(DbmsError::Query(QueryError::AggregateClauseInSelect)) => {
// call database.aggregate::<User>(query, &aggregates) instead
}
_ => {}
}
}
Transaction Errors
TransactionNotFound
Cause: Invalid transaction ID or transaction already completed.
#![allow(unused)]
fn main() {
use wasm_dbms_api::prelude::{DbmsError, TransactionError};
match database.commit() {
Err(DbmsError::Transaction(TransactionError::NoActiveTransaction)) => {
println!("No active transaction to commit");
}
_ => {}
}
}
Causes:
- Transaction ID never existed
- Transaction was already committed
- Transaction was already rolled back
Validation Errors
Cause: Data fails validation rules.
#![allow(unused)]
fn main() {
#[derive(Table, ...)]
#[table = "users"]
pub struct User {
#[validate(EmailValidator)]
pub email: Text,
}
// Insert with invalid email
let result = database.insert::<User>(UserInsertRequest {
id: 1.into(),
email: "not-an-email".into(), // Invalid!
..
});
match result {
Err(DbmsError::Validation(msg)) => {
println!("Validation failed: {}", msg);
// msg might be: "Invalid email format"
}
_ => {}
}
}
Common validation errors:
- String too long (
MaxStrlenValidator) - String too short (
MinStrlenValidator) - Invalid email format (
EmailValidator) - Invalid URL format (
UrlValidator) - Invalid phone format (
PhoneNumberValidator)
Sanitization Errors
Cause: Sanitizer fails to process the data.
#![allow(unused)]
fn main() {
// Sanitization errors are rare but can occur with malformed data
match result {
Err(DbmsError::Sanitize(msg)) => {
println!("Sanitization failed: {}", msg);
}
_ => {}
}
}
Sanitization errors are less common than validation errors since sanitizers typically transform data rather than reject it.
Memory Errors
Cause: Low-level memory errors.
#![allow(unused)]
fn main() {
pub enum MemoryError {
OutOfBounds, // Read/write outside allocated memory
ProviderError(String), // Memory provider error
InsufficientSpace, // Not enough space to allocate
}
}
Memory errors are rare and usually indicate:
- Running out of available memory
- Corrupted memory state
- Bug in wasm-dbms (please report!)
SQL Errors
Statements run through SqlEngine::execute (crate wasm-dbms-sql) fail with
a SqlError. The type lives in wasm-dbms-api behind the sql feature, which
wasm-dbms-sql turns on.
#![allow(unused)]
fn main() {
use wasm_dbms_api::prelude::SqlError;
pub enum SqlError {
Parse {
line: usize,
col: usize,
msg: String,
},
UnknownTable(String),
UnknownColumn {
table: String,
column: String,
},
AmbiguousColumn(String),
TypeMismatch {
column: String,
expected: CandidDataTypeKind,
got: String,
},
InvalidLiteral {
column: String,
expected: CandidDataTypeKind,
reason: String,
},
MissingWhereClause,
ParameterCountMismatch {
expected: usize,
got: usize,
},
TransactionAlreadyActive,
NoActiveTransaction,
Unsupported(String),
Runtime(DbmsError),
}
}
Every variant except Runtime is detected before the database is read or
written, so a statement that fails with one of them has no effect.
The SQL dialect is described in the SQL Reference.
Parse
Cause: The text is not a statement of the supported grammar. line and
col start at 1 and point at the first token that cannot be accepted. msg
says what was expected.
#![allow(unused)]
fn main() {
match engine.execute(&ctx, None, "SELECT name FORM users", &[]) {
Err(SqlError::Parse { line, col, msg }) => {
// line 1, column 13: expected `FROM`, found identifier `FORM`
println!("syntax error at {line}:{col}: {msg}");
}
_ => {}
}
}
Common causes:
- A typo in a keyword, or a missing comma or parenthesis
- A reserved word used as a name without double quotes
- Several statements in one call
- A feature that the dialect does not have, such as a subquery or an expression
UnknownTable
Cause: The statement names a table that is not in the schema, or qualifies a column with a name that is not a table or alias of the statement.
Names are case-sensitive. A table that has an alias must be referred to by the alias.
UnknownColumn (SQL)
Cause: The table has no column with that name.
#![allow(unused)]
fn main() {
match engine.execute(&ctx, None, "SELECT nickname FROM users", &[]) {
Err(SqlError::UnknownColumn { table, column }) => {
println!("table {table} has no column {column}");
}
_ => {}
}
}
AmbiguousColumn
Cause: In a join, a column name without a table qualifier exists in more than one table.
Solution: Qualify the column: users.id instead of id.
TypeMismatch
Cause: A literal or parameter of the wrong kind is used with a column, for
example a string compared with an integer column. got names the kind of the
literal (Integer, Float, String, Boolean, Null) or the type of the
parameter. Incompatible underlying types of columns in a join’s ON
condition also cause this error; got is the type of the joined column.
For example, users.name = posts.user_id compares Text with Uint32 and
reports column posts.user_id, expected Text, got Uint32.
#![allow(unused)]
fn main() {
// `age` is a Uint8 column
match engine.execute(&ctx, None, "SELECT * FROM users WHERE age = 'old'", &[]) {
Err(SqlError::TypeMismatch { column, expected, got }) => {
// column "age", expected Uint8, got "String"
}
_ => {}
}
}
A NULL parameter in a comparison is also reported this way: use IS NULL.
InvalidLiteral
Cause: A literal or parameter has the right kind but a value the column
cannot hold. reason describes the problem.
Examples:
300for aUint8column'2026-02-30'for aDatecolumn'xyz'for aBlobcolumn, which expects hexadecimal digits- A negative
LIMITparameter
MissingWhereClause
Cause: An UPDATE or DELETE statement has no WHERE clause.
The statement is rejected so that a forgotten condition cannot change or remove every row. To target every row on purpose, write a condition that is true for all of them.
ParameterCountMismatch
Cause: The number of values passed to execute differs from the number of
? placeholders in the statement.
TransactionAlreadyActive
Cause: BEGIN was sent together with a transaction id. A transaction
cannot be opened inside another one.
Solution: Send BEGIN without an id. If the earlier transaction is
finished, send COMMIT or ROLLBACK with its id first.
NoActiveTransaction
Cause: COMMIT or ROLLBACK was sent without a transaction id.
Solution: Pass the id returned by BEGIN.
Unsupported
Cause: The statement is valid SQL, but combines features that cannot run together. The message names the combination.
Examples:
DISTINCTor an aggregate function together with aJOIN- A column in the select list of an aggregate query that is not in
GROUP BY LIKEinHAVING- The same table twice in one statement
Runtime
Cause: The database rejected the operation while running it. The wrapped
DbmsError is one of the errors described in the sections above.
#![allow(unused)]
fn main() {
match engine.execute(&ctx, None, "INSERT INTO users (id, name) VALUES (1, 'A')", &[]) {
Err(SqlError::Runtime(DbmsError::Query(QueryError::PrimaryKeyConflict))) => {
println!("a user with this id already exists");
}
Err(SqlError::Runtime(error)) => println!("database error: {error}"),
_ => {}
}
}
COMMIT sent with the id of a transaction that is already closed fails with
Runtime(DbmsError::Query(QueryError::TransactionNotFound)), and so does any
statement sent with an id the database does not know.
Error Handling Examples
Basic error handling:
#![allow(unused)]
fn main() {
let result = database.insert::<User>(user);
match result {
Ok(()) => println!("Insert successful"),
Err(DbmsError::Query(QueryError::PrimaryKeyConflict)) => {
println!("User already exists");
}
Err(DbmsError::Query(QueryError::UniqueConstraintViolation { field })) => {
println!("Duplicate value on field: {}", field);
}
Err(DbmsError::Query(QueryError::BrokenForeignKeyReference)) => {
println!("Referenced record doesn't exist");
}
Err(DbmsError::Validation(msg)) => {
println!("Validation error: {}", msg);
}
Err(e) => {
println!("Database error: {:?}", e);
}
}
}
Helper function pattern:
#![allow(unused)]
fn main() {
fn handle_db_error(error: DbmsError) -> String {
match error {
DbmsError::Query(QueryError::PrimaryKeyConflict) => {
"Record with this ID already exists".to_string()
}
DbmsError::Query(QueryError::UniqueConstraintViolation { field }) => {
format!("Duplicate value on unique field: {}", field)
}
DbmsError::Query(QueryError::BrokenForeignKeyReference) => {
"Referenced record not found".to_string()
}
DbmsError::Query(QueryError::ForeignKeyConstraintViolation) => {
"Cannot delete: record has dependencies".to_string()
}
DbmsError::Validation(msg) => format!("Invalid data: {}", msg),
_ => format!("Unexpected error: {:?}", error),
}
}
}
For IC client-specific error handling (double-result pattern), see the IC Errors Reference.