CRUD Operations
CRUD Operations
Overview
ic-dbms provides four fundamental database operations:
| Operation | Description | Returns |
|---|---|---|
| Insert | Add a new record to a table | Result<()> |
| Select | Query records from a table | Result<Vec<Record>> |
| Update | Modify existing records | Result<u64> (affected rows) |
| Delete | Remove records from a table | Result<u64> (affected rows) |
All operations:
- Respect access control (caller must be in ACL)
- Support optional transaction IDs
- Validate and sanitize data according to schema rules
- Enforce foreign key constraints
Insert
Basic Insert
To insert a record, create an InsertRequest and call the insert method:
use ic_dbms_client::{IcDbmsCanisterClient, Client as _};
use my_schema::{User, UserInsertRequest};
use ic_dbms_api::prelude::*;
let client = IcDbmsCanisterClient::new(canister_id);
let user = UserInsertRequest {
id: 1.into(),
name: "Alice".into(),
email: "alice@example.com".into(),
created_at: DateTime::now(),
};
// Insert without transaction (None)
client
.insert::<User>(User::table_name(), user, None)
.await??;
Handling Primary Keys
Every table must have a primary key. Insert will fail if a record with the same primary key already exists:
// First insert succeeds
client.insert::<User>(User::table_name(), user1, None).await??;
// Second insert with same ID fails with PrimaryKeyConflict
let result = client.insert::<User>(User::table_name(), user2_same_id, None).await?;
assert!(matches!(result, Err(IcDbmsError::Query(QueryError::PrimaryKeyConflict))));
Nullable Fields
For fields wrapped in Nullable<T>, you can insert either a value or null:
#[derive(Debug, Table, CandidType, Deserialize, Clone, PartialEq, Eq)]
#[table = "profiles"]
pub struct Profile {
#[primary_key]
pub id: Uint32,
pub bio: Nullable<Text>, // Optional field
pub website: Nullable<Text>, // Optional field
}
// Insert with value
let profile = ProfileInsertRequest {
id: 1.into(),
bio: Nullable::Value("Hello world".into()),
website: Nullable::Null, // No website
};
client.insert::<Profile>(Profile::table_name(), profile, None).await??;
Insert with Transaction
To insert within a transaction, pass the transaction ID:
// Begin transaction
let tx_id = client.begin_transaction().await?;
// Insert within transaction
client.insert::<User>(User::table_name(), user, Some(tx_id)).await??;
// Commit or rollback
client.commit(tx_id).await??;
Select
Select All Records
Use Query::builder().all() to select all records:
use ic_dbms_api::prelude::*;
let query = Query::builder().all().build();
let users: Vec<UserRecord> = client
.select::<User>(User::table_name(), query, None)
.await??;
for user in users {
println!("User: {} ({})", user.name, user.email);
}
Select with Filter
Add filters to narrow down results:
// Select users with specific name
let query = Query::builder()
.filter(Filter::eq("name", Value::Text("Alice".into())))
.build();
let users = client.select::<User>(User::table_name(), query, None).await??;
See the Querying Guide for comprehensive filter documentation.
Select Specific Columns
Select only the columns you need:
let query = Query::builder()
.columns(vec!["id".to_string(), "name".to_string()])
.build();
let users = client.select::<User>(User::table_name(), query, None).await??;
// Only id and name are populated; other fields have default values
Select with Eager Loading
Load related records in a single query using with():
// Load posts with their authors
let query = Query::builder()
.all()
.with("users") // Eager load the related users table
.build();
let posts = client.select::<Post>(Post::table_name(), query, None).await??;
See the Relationships Guide for more on eager loading.
Update
Basic Update
Create an UpdateRequest to modify records:
use my_schema::UserUpdateRequest;
let update = UserUpdateRequest::builder()
.set_name("Alice Smith".into())
.filter(Filter::eq("id", Value::Uint32(1.into())))
.build();
let affected_rows = client
.update::<User>(User::table_name(), update, None)
.await??;
println!("Updated {} row(s)", affected_rows);
Partial Updates
Only specify the fields you want to change. Unspecified fields remain unchanged:
// Only update the email, keep everything else
let update = UserUpdateRequest::builder()
.set_email("new.email@example.com".into())
.filter(Filter::eq("id", Value::Uint32(1.into())))
.build();
client.update::<User>(User::table_name(), update, None).await??;
Update with Filter
The filter determines which records are updated:
// Update all users with a specific domain
let update = UserUpdateRequest::builder()
.set_verified(true.into())
.filter(Filter::like("email", "%@company.com"))
.build();
let affected = client.update::<User>(User::table_name(), update, None).await??;
println!("Verified {} company users", affected);
Update Return Value
Update returns the number of affected rows:
let affected = client.update::<User>(User::table_name(), update, None).await??;
if affected == 0 {
println!("No records matched the filter");
} else {
println!("Updated {} record(s)", affected);
}
Delete
Delete with Filter
Delete records matching a filter:
use ic_dbms_api::prelude::DeleteBehavior;
let filter = Filter::eq("id", Value::Uint32(1.into()));
let deleted = client
.delete::<User>(
User::table_name(),
DeleteBehavior::Restrict,
Some(filter),
None // No transaction
)
.await??;
println!("Deleted {} record(s)", deleted);
Delete Behaviors
When deleting records that are referenced by foreign keys, you must specify a behavior:
| Behavior | Description |
|---|---|
Restrict |
Fail if any foreign keys reference this record |
Cascade |
Delete all records that reference this record |
Restrict Example:
// Will fail if any posts reference this user
let result = client.delete::<User>(
User::table_name(),
DeleteBehavior::Restrict,
Some(Filter::eq("id", Value::Uint32(1.into()))),
None
).await?;
match result {
Ok(count) => println!("Deleted {} user(s)", count),
Err(IcDbmsError::Query(QueryError::ForeignKeyConstraintViolation)) => {
println!("Cannot delete: user has posts");
}
Err(e) => return Err(e.into()),
}
Cascade Example:
// Deletes the user AND all their posts
client.delete::<User>(
User::table_name(),
DeleteBehavior::Cascade,
Some(Filter::eq("id", Value::Uint32(1.into()))),
None
).await??;
Delete All Records
Pass None as the filter to delete all records (use with caution):
// Delete ALL users (respecting foreign key behavior)
let deleted = client
.delete::<User>(
User::table_name(),
DeleteBehavior::Cascade,
None, // No filter = all records
None
)
.await??;
println!("Deleted all {} users and their related records", deleted);
Operations with Transactions
All CRUD operations accept an optional transaction ID. When provided, the operation is performed within that transaction and won’t be visible to other callers until committed:
// Begin transaction
let tx_id = client.begin_transaction().await?;
// Perform operations within transaction
client.insert::<User>(User::table_name(), user1, Some(tx_id)).await??;
client.insert::<User>(User::table_name(), user2, Some(tx_id)).await??;
// Update within same transaction
let update = UserUpdateRequest::builder()
.set_verified(true.into())
.filter(Filter::all())
.build();
client.update::<User>(User::table_name(), update, Some(tx_id)).await??;
// Commit all changes atomically
client.commit(tx_id).await??;
See the Transactions Guide for comprehensive transaction documentation.
Error Handling
CRUD operations can fail for various reasons. Here are common errors:
| Error | Cause | Operation |
|---|---|---|
PrimaryKeyConflict |
Record with same primary key exists | Insert |
ForeignKeyConstraintViolation |
Referenced record doesn’t exist, or delete restricted | Insert, Update, Delete |
BrokenForeignKeyReference |
Foreign key points to non-existent record | Insert, Update |
UnknownColumn |
Invalid column name in filter or select | Select, Update, Delete |
MissingNonNullableField |
Required field not provided | Insert, Update |
RecordNotFound |
No record matches the criteria | Update, Delete |
TransactionNotFound |
Invalid transaction ID | All |
InvalidQuery |
Malformed query (e.g., invalid JSON path) | Select |
Example error handling:
use ic_dbms_api::prelude::{IcDbmsError, QueryError};
let result = client.insert::<User>(User::table_name(), user, None).await?;
match result {
Ok(()) => println!("Insert successful"),
Err(IcDbmsError::Query(QueryError::PrimaryKeyConflict)) => {
println!("User with this ID already exists");
}
Err(IcDbmsError::Query(QueryError::BrokenForeignKeyReference)) => {
println!("Referenced record does not exist");
}
Err(IcDbmsError::Validation(msg)) => {
println!("Validation failed: {}", msg);
}
Err(e) => {
println!("Unexpected error: {:?}", e);
}
}
See the Errors Reference for complete error documentation.