Architecture
Architecture
Overview
ic-dbms is built as a layered architecture where each layer has specific responsibilities and builds upon the layer below. This design provides:
- Separation of concerns: Each layer focuses on one aspect
- Testability: Layers can be tested independently
- Flexibility: Internal implementations can change without affecting APIs
Three-Layer Architecture
┌─────────────────────────────────────────────────────────────┐
│ Layer 3: API Layer │
│ Canister endpoints, Candid interface, access control │
│ (DbmsCanister macro, ACL guards, request/response types) │
├─────────────────────────────────────────────────────────────┤
│ Layer 2: DBMS Layer │
│ Tables, CRUD operations, transactions, foreign keys │
│ (TableRegistry, TransactionManager, query execution) │
├─────────────────────────────────────────────────────────────┤
│ Layer 1: Memory Layer │
│ Stable memory management, encoding/decoding, page allocation│
│ (MemoryProvider, MemoryManager, Encode trait) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ IC Stable │
│ Memory │
└─────────────────┘
Layer 1: Memory Layer
Responsibilities:
- Manage stable memory allocation (64 KiB pages)
- Encode/decode data to/from binary format
- Track free space and handle fragmentation
- Provide abstraction for testing (heap vs stable memory)
Key components:
| Component | Purpose |
|---|---|
MemoryProvider |
Abstract interface for memory access |
MemoryManager |
Allocates and manages pages |
Encode trait |
Binary serialization for all stored types |
PageLedger |
Tracks which pages belong to which table |
FreeSegmentsLedger |
Tracks free space for reuse |
Memory layout:
Page 0: Schema Registry (table → page mapping)
Page 1: ACL (allowed principals)
Page 2+: Table data (Page Ledger, Free Segments, Records)
See Memory Documentation for detailed technical information.
Layer 2: DBMS Layer
Responsibilities:
- Implement CRUD operations
- Manage transactions with ACID properties
- Enforce foreign key constraints
- Handle sanitization and validation
- Execute queries with filters
Key components:
| Component | Purpose |
|---|---|
TableRegistry |
Manages records for a single table |
TransactionManager |
Handles transaction lifecycle |
Transaction |
Overlay for uncommitted changes |
QueryExecutor |
Executes queries with filters |
ForeignKeyHandler |
Validates and cascades foreign keys |
JoinEngine |
Executes cross-table join queries |
Transaction model:
┌──────────────────────────────────────────┐
│ Active Transactions │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Tx 1 │ │ Tx 2 │ │ Tx 3 │ │
│ │ (overlay)│ │(overlay)│ │(overlay)│ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └────────────┼────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Committed Data │ │
│ │ (in memory) │ │
│ └─────────────────┘ │
└──────────────────────────────────────────┘
Transactions use an overlay pattern:
- Changes are written to an overlay (in-memory)
- Reading checks overlay first, then committed data
- Commit merges overlay to committed data
- Rollback discards the overlay
Layer 3: API Layer
Responsibilities:
- Expose Candid interface
- Handle request/response encoding
- Enforce access control (ACL)
- Route requests to DBMS layer
- Generate table-specific endpoints
Key components:
| Component | Purpose |
|---|---|
DbmsCanister macro |
Generates canister API from schema |
| ACL guard | Checks caller authorization |
| Request types | InsertRequest, UpdateRequest, Query |
| Response types | Record, error handling |
Generated API structure:
// For each table "users":
insert_users(UserInsertRequest, Option<TxId>) -> Result<()>
select_users(Query, Option<TxId>) -> Result<Vec<UserRecord>>
update_users(UserUpdateRequest, Option<TxId>) -> Result<u64>
delete_users(DeleteBehavior, Option<Filter>, Option<TxId>) -> Result<u64>
// Untyped select (supports joins):
select(table: String, Query, Option<TxId>) -> Result<Vec<Vec<(CandidColumnDef, Value)>>>
// Global operations:
begin_transaction() -> TxId
commit(TxId) -> Result<()>
rollback(TxId) -> Result<()>
acl_add_principal(Principal) -> Result<()>
acl_remove_principal(Principal) -> Result<()>
acl_allowed_principals() -> Vec<Principal>
Crate Organization
ic-dbms/
├── ic-dbms-api/ # Shared types and traits
├── ic-dbms-canister/ # Core DBMS implementation
├── ic-dbms-macros/ # Procedural macros
└── ic-dbms-client/ # Client libraries
ic-dbms-api
Purpose: Shared types used across all crates
Contents:
- Data types (
Uint32,Text,DateTime, etc.) Valueenum for runtime values- Filter and Query types
- Sanitizer and Validator traits
- Error types
Tabletrait (marker for table types)
Dependencies: Minimal (candid, serde)
ic-dbms-canister
Purpose: Core database engine
Contents:
- Memory layer implementation
- DBMS layer implementation
- Transaction management
- Built-in sanitizers and validators
DbmsCanisterderive macro re-export
Dependencies: ic-dbms-api, ic-dbms-macros, ic-cdk
ic-dbms-macros
Purpose: Procedural macros for code generation
Macros:
#[derive(Encode)]- Binary serialization#[derive(Table)]- Table schema and related types#[derive(DbmsCanister)]- Complete canister API
Dependencies: syn, quote, proc-macro2
ic-dbms-client
Purpose: Client libraries for canister interaction
Implementations:
IcDbmsCanisterClient- Inter-canister callsIcDbmsAgentClient- External via ic-agent (feature-gated)IcDbmsPocketIcClient- Testing with PocketIC (feature-gated)
Dependencies: ic-dbms-api, ic-cdk (optional ic-agent, pocket-ic)
Data Flow
Insert Operation
1. Client calls insert_users(request, tx_id)
│
2. ACL guard checks caller authorization
│
3. API layer deserializes request
│
4. DBMS layer:
a. Apply sanitizers to values
b. Apply validators to values
c. Check primary key uniqueness
d. Validate foreign key references
e. If tx_id: write to transaction overlay
Else: write directly
│
5. Memory layer:
a. Encode record to bytes
b. Find space (free segment or new page)
c. Write to stable memory
│
6. Return Result<()>
Select Operation
1. Client calls select_users(query, tx_id)
│
2. ACL guard checks caller authorization
│
3. API layer deserializes query
│
4. DBMS layer:
a. Parse filters
b. Determine pages to scan
c. For each page:
- Read records from memory
- If tx_id: merge with overlay
- Apply filters
- Apply ordering
d. Apply limit/offset
e. Select requested columns
f. Handle eager loading
│
5. Memory layer:
a. Read pages
b. Decode records
│
6. Return Result<Vec<Record>>
Select with Join
1. Client calls select(table, query_with_joins, tx_id)
│
2. ACL guard checks caller authorization
│
3. API layer checks query.has_joins()
│ (true)
4. JoinEngine:
a. Read all rows from FROM table
b. For each JOIN clause:
- Read all rows from joined table
- Resolve column references
- Execute nested-loop join
c. Apply filter on combined rows
d. Apply ordering
e. Apply offset/limit
f. Flatten to output with CandidColumnDef
│
5. Return Result<Vec<Vec<(CandidColumnDef, Value)>>>
See Join Engine for implementation details.
Transaction Flow
begin_transaction():
1. Generate transaction ID
2. Create empty overlay
3. Record owner (caller principal)
4. Return transaction ID
Operation with tx_id:
1. Verify caller owns transaction
2. Read from: overlay first, then committed
3. Write to: overlay only
commit(tx_id):
1. Verify caller owns transaction
2. For each change in overlay:
- Write to committed data (stable memory)
3. Delete overlay
4. Transaction ID becomes invalid
rollback(tx_id):
1. Verify caller owns transaction
2. Delete overlay (discard all changes)
3. Transaction ID becomes invalid
Extension Points
ic-dbms provides several extension points for customization:
Custom Sanitizers
Implement the Sanitize trait:
pub trait Sanitize {
fn sanitize(&self, value: Value) -> IcDbmsResult<Value>;
}
Custom Validators
Implement the Validate trait:
pub trait Validate {
fn validate(&self, value: &Value) -> IcDbmsResult<()>;
}
Custom Data Types
While not directly extensible, you can use Json for custom structures:
pub metadata: Json, // Store any structure
Memory Provider
For testing, implement MemoryProvider:
pub trait MemoryProvider {
const PAGE_SIZE: u64;
fn size(&self) -> u64;
fn pages(&self) -> u64;
fn grow(&mut self, new_pages: u64) -> MemoryResult<u64>;
fn read(&self, offset: u64, buf: &mut [u8]) -> MemoryResult<()>;
fn write(&mut self, offset: u64, buf: &[u8]) -> MemoryResult<()>;
}
ic-dbms provides:
IcMemoryProvider- Uses IC stable memory (production)HeapMemoryProvider- Uses heap memory (testing)