8 breaking releases
Uses new Rust 2024
| new 0.15.0 | Nov 29, 2025 |
|---|---|
| 0.13.1 | Nov 25, 2025 |
| 0.10.0 | Jul 24, 2025 |
#99 in Procedural macros
Used in 2 crates
(via spacetimedsl_derive)
310KB
5K
SLoC
✨ SpacetimeDSL
SpacetimeDSL provides you a high-level Domain Specific Language (DSL) in Rust to interact in an ergonomic, more developer-friendly and type-safe way with the data in your SpacetimeDB instances.
🤖 For LLMs and AI-Assisted Development: See
llms.txtfor a concise, LLM-optimized reference with all key features, examples, and important rules in one place.
🚀 Try SpacetimeDSL for yourself, by adding it to your server modules Cargo.toml:
# https://crates.io/crates/spacetimedsl Ergonomic DSL for SpacetimeDB
spacetimedsl = { version = "*" }
📖 Get started by adding #[spacetimedsl::dsl] with required method() configuration, plus helper attributes #[create_wrapper], #[use_wrapper],
#[foreign_key] and #[referenced_by] to your structs with #[spacetimedb::table]!
💬 Need help?
- Consult the FAQ
- Join the SpacetimeDSL channel of the SpacetimeDB Discord Server
📑 Table of Contents
Core Unique Features
- 🔗 Foreign Keys / Referential Integrity - Enforce relationships between tables with automatic cascade operations
- 🏷️ Wrapper Types - Type-safe column identifiers that eliminate primitive obsession
- 🎲 Unique Multi-Column Indices - Enforce uniqueness across multiple columns (before SpacetimeDB native support)
- 🪝 Hooks System - Execute custom logic automatically before/after insert, update, and delete operations
- 🎯 Complete Method Coverage - DSL equivalents for all SpacetimeDB operations
Enhanced Developer Experience
- 🎨 Ergonomic DSL Methods - Cleaner syntax with smart defaults for creating, updating, and deleting rows
- 🎛️ Method Configuration - Explicit control over which operations are allowed on your tables
- 🚨 Rich Error Types - Detailed error information beyond what SpacetimeDB provides
- 📊 Deletion Results - Complete audit trails for delete operations with cascade tracking
- 🔄 Automatic Accessors - Generated getters, mut-getters and setters with visibility controls
Implementation Details
- 🎯 OnDelete Strategies - Control deletion behavior: Error, Delete, SetZero, or Ignore
- 📝 Plural Name Configuration - Customize generated method names
Additional Information
🔧 Vanilla SpacetimeDB
Let's start with a ordinary SpacetimeDB schema:
#[spacetimedb::table(name = entity, public)]
pub struct Entity {
#[primary_key]
#[auto_inc]
id: u128,
created_at: spacetimedb::Timestamp,
}
#[spacetimedb::table(name = position, public, index(name = x_y, btree(columns = [x, y])))]
pub struct Position {
#[primary_key]
#[auto_inc]
id: u128,
#[unique]
entity_id: u128,
x: i128,
y: i128,
modified_at: spacetimedb::Timestamp,
}
We have two tables:
- 📋 The
Entitytable - holds no data per row except an unique machine-readable identifier - 📍 The
Positiontable - holds anentity_idandx,yvalues per row
⚠️ Problems with Vanilla SpacetimeDB
Even with this small data model, there are fundamental issues:
Boilerplate Code:
- You must create an
Entityfirst, then pass it toinsert/try_insert - Must manually set sensible defaults:
0foridcolumn (for auto-increment)ctx.timestampfor timestamps
- Repetitive code that could be avoided ✂️
Data Integrity Issues:
- ❌ Can change
created_atafter creation and persist withupdate - ❌ Column name
entity_iddoesn't enforce it only acceptsEntityIDs - ❌ No guarantee referenced
Entitiesactually exist - ❌
Positionswon't auto-delete whenEntitiesare deleted
Missing Constraints:
- 🎮 For tile-based 2D games: each tile should contain max one
Entity - No unique multi-column indices - must check manually
- Must manually update
modified_atwithctx.timestamp
The Problem:
SpacetimeDB is great technology, but has weaknesses that prevent developers from utilizing its full potential — sometimes you work against the database.
⚡ SpacetimeDB with SpacetimeDSL
Let's see what happens when adding SpacetimeDSL:
#[spacetimedsl::dsl(plural_name = entities, method(update = false, delete = true))] // Added
#[spacetimedb::table(name = entity, public)]
pub struct Entity {
#[primary_key]
#[auto_inc]
#[create_wrapper] // Added
#[referenced_by(path = crate, table = position)] // Added
id: u128,
created_at: spacetimedb::Timestamp,
}
#[spacetimedsl::dsl(plural_name = positions, method(update = false, delete = true), unique_index(name = x_y))] // Added
#[spacetimedb::table(name = position, public, index(name = x_y, btree(columns = [x, y])))]
pub struct Position {
#[primary_key]
#[auto_inc]
#[create_wrapper] // Added
id: u128,
#[unique]
#[use_wrapper(EntityId)]
#[foreign_key(path = crate, table = entity, on_delete = Delete)] // Added
entity_id: u128,
x: i128,
y: i128,
modified_at: spacetimedb::Timestamp,
}
📝 Note: For clarity, DB = SpacetimeDB, DSL = SpacetimeDSL
Looks simple, but unlocks powerful capabilities! 🎯
🎨 The Create DSL method
#[spacetimedb::reducer]
pub fn create_example(ctx: &spacetimedb::ReducerContext) -> Result<(), String> {
// Vanilla SpacetimeDB
use spacetimedb::Table;
// Without the question mark it would return a
// Result<Entity, spacetimedb::TryInsertError<entity__TableHandle>>
let entity: Entity = ctx.db.entity().try_insert(
Entity {
id: 0,
created_at: ctx.timestamp,
}
)?;
// SpacetimeDB with SpacetimeDSL
let dsl: spacetimedsl::DSL<'_, spacetimedb::ReducerContext> = spacetimedsl::dsl(ctx);
// Without the question mark it would return a Result<Entity, spacetimedsl::SpacetimeDSLError>
let entity: Entity = dsl.create_entity()?;
Ok(())
}
✨ What's different?
Cleaner Code:
- 🎯 Much nicer to read and write!
- No manual
Entityconstruction required
Smart Defaults:
- 🤖
idcolumn: automatically set to0(DB generates ID) - ⏰
created_at: automatically set toctx.timestamp - 🔄
modified_at: supports bothTimestampandOption<Timestamp>types
Better API:
- SpacetimeDSL wraps
&spacetimedb::ReducerContext - Provides more ergonomic API with added capabilities
- Reduces boilerplate code significantly
💡 Best Practices:
- Create DSL instance once at reducer start:
spacetimedsl::dsl(ctx) - Pass only
&DSLto functions (not&ReducerContext) - Use
dsl.ctx()method if you really need the context
Here is the implementation:
pub trait CreateEntityRow<T: spacetimedsl::WriteContext>: spacetimedsl::DSLContext<T> {
fn create_entity<'a>(&'a self) -> Result<Entity, spacetimedsl::SpacetimeDSLError> {
use spacetimedsl::Wrapper;
use spacetimedb::{DbContext, Table};
let id = u128::default();
let created_at = self.ctx().timestamp;
let entity = Entity { id, created_at };
match self.ctx().db().entity().try_insert(entity) {
Ok(entity) => Ok(entity),
Err(error) => {
match error {
spacetimedb::TryInsertError::UniqueConstraintViolation(_) => {
Err(spacetimedsl::SpacetimeDSLError::UniqueConstraintViolation {
table_name: "entity".into(),
action: spacetimedsl::Action::Create,
error_from: spacetimedsl::ErrorFrom::SpacetimeDB,
one_or_multiple: spacetimedsl::OneOrMultiple::One,
column_names_and_row_values: format!(
"{{ entity : {:?} }}",
entity
).into(),
})
}
spacetimedb::TryInsertError::AutoIncOverflow(_) => {
Err(spacetimedsl::SpacetimeDSLError::AutoIncOverflow {
table_name: "entity".into(),
})
}
}
}
}
}
}
impl<T: spacetimedsl::WriteContext> CreateEntityRow<T> for spacetimedsl::DSL<'_, T> {}
🚨 The SpacetimeDSLError Type
Unlike DB errors, DSL errors include metadata for better debugging! 🔍
Transformation:
spacetimedb::TryInsertError→SpacetimeDSLErrorbool(Delete One) →SpacetimeDSLErrorOption(Get One) →SpacetimeDSLErroru64(Delete Many) →SpacetimeDSLError
Error Variants:
pub enum SpacetimeDSLError {
Error, // Not available in vanilla SpacetimeDB
NotFoundError, // Not available in vanilla SpacetimeDB
UniqueConstraintViolation,
AutoIncOverflow,
ReferenceIntegrityViolation, // Not available in vanilla SpacetimeDB
}
⚠️ The Error variant
- Used when DSL can't help with DB issues
- Never return
Ok(())in reducer if this occurs!
🔍 The NotFoundError variant
What DB gives: Simple Option<T>
What DSL gives: NotFound error with table name + column values
Example log:
Not Found Error while trying to find a row in the position table with {{ entity_id : 1 }}!
🔒 The UniqueConstraintViolation variant
Origins:
- 📊 DB: unique single-column indices
- 🎯 DSL: unique multi-column indices
Example log:
Unique Constraint Violation Error while trying to create a row in the entity table!
Unfortunately SpacetimeDB doesn't provide more information, so here are all columns and their values:
{{ entity : Entity { id: EntityId { id: 1 }, created_at: /* omitted */ } }}
📈 The AutoIncOverflow variant
Improvements over DB:
- DB: No column name
- DSL: Includes table name ✅
Example log:
Auto Inc Overflow Error on the entity table!
Unfortunately SpacetimeDB doesn't provide more information.
🔗 The ReferenceIntegrityViolation variant
Two scenarios:
1️⃣ Creating/Updating rows:
- Tables with foreign keys (
#[foreign_key]) - DSL checks if referenced rows exist
Example log:
Reference Integrity Violation Error while trying to create a row in the position table
because of {{ entity_id : 1 }}!
2️⃣ Deleting referenced rows:
- Tables with
#[referenced_by]on primary key - More complex - see DeletionResult
📊 The DeletionResult[Entry] Type
The Problem (DB Conversation):
Developer: "Hi DB! I need an audit log for every deletion. How?"
DB: "I can give you:
boolfor Delete One (deleted or not)u64for Delete Many (count of deleted rows)
Is that enough?"
DSL: "May I answer for you, developer?"
Developer: "Yes, please!"
DSL: "No, that's not enough! But don't worry, I have a solution:" 🎉
The Solution:
pub struct DeletionResult {
pub table_name: Box<str>,
pub one_or_multiple: OneOrMultiple,
pub entries: Vec<DeletionResultEntry>,
}
pub struct DeletionResultEntry {
pub table_name: Box<str>,
pub column_name: Box<str>,
pub strategy: OnDeleteStrategy,
pub row_value: Box<str>,
pub child_entries: Vec<DeletionResultEntry>,
}
Features:
- ✅ Get
DeletionResulton both success AND failure - 📝 Process programmatically or use
to_csv()method - 📊 Import into spreadsheet applications
- 🔍 Complete audit trail
See Foreign Keys and Referential Integrity for more details.
🎯 The OnDeleteStrategy Type
Found in #[foreign_key] attribute and DeletionResultEntry type.
Controls deletion behavior when referenced rows are deleted:
pub enum OnDeleteStrategy {
/**
* Available independent from the column type.
*
* If a row of a table should be deleted whose primary key value is referenced in foreign keys ...
* ... of other tables the deletion fails with a Reference Integrity Violation Error.
*/
Error,
/**
* Available independent from the column type.
*
* If a row of a table should be deleted whose primary key value is referenced in foreign keys ...
* ... of other tables, it's checked whether any primary key value of rows to delete is referenced
* in a foreign key with `OnDeleteStrategy::Error`.
*
* If true, the deletion fails with a Reference Integrity Violation Error and
* no other OnDeleteStrategy is executed (especially: no row is deleted).
*
* If false, the on delete strategies of all affected rows are executed and rows are deleted.
*/
Delete,
/**
* Available only for columns with a numeric type.
*
* If a row of a table should be deleted whose primary key value is referenced in foreign keys ...
* ... of other tables the value of the foreign key column is set to `0`.
*/
SetZero,
/**
* Available independent from the column type.
*
* If a row of a table should be deleted whose primary key value is referenced in foreign keys ...
* ... of other tables nothing happens, which means the referencing rows will reference a primary
* key value which doesn't exist anymore. The referential integrity is only enforced while creating
* a row or if a row is updated and the foreign key column value is changed.
*/
Ignore,
}
Strategies Explained:
🛑 Error
- Available for all column types
- Deletion fails if referenced by other tables
- Returns Reference Integrity Violation Error
🗑️ Delete
- Available for all column types
- Cascading delete of referencing rows
- Fails only if any reference uses
Errorstrategy - All-or-nothing: either all delete or none
0️⃣ SetZero
- Numeric types only
- Sets foreign key column to
0 - Referenced value becomes non-existent
🤷 Ignore
- Available for all column types
- Breaks referential integrity
- Creates dangling references
- Integrity only enforced on create/update
🏷️ The #[create_wrapper] and #[use_wrapper] attributes - aka Wrapper Types
Requirements:
Every column with these attributes needs a wrapper:
#[primary_key]✅#[unique]✅#[index]✅
Must have either:
#[create_wrapper]- creates new wrapper type#[use_wrapper]- uses existing wrapper type
Benefits:
🎯 Reduces Primitive Obsession:
- Unique, auto-generated alias types
- Wraps primitive column types
- Makes logical dependencies physical
🔒 Type Safety:
- Compile-time errors instead of runtime errors
- Field order mistakes caught at compilation
API:
pub trait Wrapper<WrappedType: Clone + Default, WrapperType>: Default +
Clone + PartialEq + PartialOrd + spacetimedb::SpacetimeType + Display
{
fn new(value: WrappedType) -> WrapperType;
fn value(&self) -> WrappedType;
}
Usage Examples:
#[spacetimedsl::dsl(plural_name = entities, method(update = false))]
#[spacetimedb::table(name = entity, public)]
pub struct Entity {
// Default Name Strategy: EntityId
// format!("{}{}", singular_table_name_pascal_case, column_name_pascal_case)
#[create_wrapper]
// Custom Name Strategy: EntityID
#[create_wrapper(EntityID)]
id: u128,
// Use a wrapper type from the same module
#[use_wrapper(EntityId)]
// Use a wrapper type from another module
#[use_wrapper(crate::entity::EntityId)]
parent_entity_id: u128,
}
⚠️ Common Error:
The trait bound `WrapperType: From<NumericType>` is not satisfied.
What this means:
- You provided
NumericType(likeu128) whereWrapperTypeis required - SpacetimeDB CLI and Admin Panel have primitive obsession limitations
- They lack feature-parity with server modules
Solution:
- Don't create Wrapper Types manually (
WrapperType::new(wrapped_type)) - Use Wrapper Types in your entire ecosystem API
- Include them in reducer arguments
- Embrace full SpacetimeDB capabilities! 💪
🔄 Accessors (Getters and Setters)
Getters (for all columns):
- 📖 Public getter for every column
- Returns reference for normal types
- Returns cloned Wrapper Type for wrapped types
Setters and Mut-Getters (for non-private columns):
- ✏️ Generated based on column visibility
- Visibility = field visibility
- Use field visibility to make fields immutable after creation
🔒 Automatic Field Privacy Enforcement: SpacetimeDSL automatically makes all struct fields private when processing DSL attributes.
Why?
- ✅ Prevents direct field access
- ✅ Forces use of wrapper types, getters, setters
- ✅ Prevents unauthorized modifications
- ✅ Enforces proper encapsulation
Use Cases for Private Columns: 🔑 Never-changing fields:
- Primary and foreign key columns
created_attimestamps- Event/audit table data
No Update Method:
- All columns private = no
UpdateDSL method - Indicates row should never change after insertion
Future Enhancement:
When rust-lang/rust#105077 releases, DSL will use field mutability restrictions instead of visibility.
🎲 Unique multi-column indices
Achievement Unlocked: 🏆
Example:
#[dsl(
plural_name = entity_relationships,
method(update = false),
unique_index(name = parent_child_entity_id)
)]
#[table(
name = entity_relationship,
public,
index(name = parent_child_entity_id, btree(columns = [parent_entity_id, child_entity_id]))
)]
pub struct EntityRelationship {
#[primary_key]
#[auto_inc]
id: u128,
parent_entity_id: u128,
child_entity_id: u128,
}
Setup:
- Add
unique_index(name = parent_child_entity_id)to#[spacetimedsl::dsl] - Have matching multi-column index in
#[spacetimedb::table]
You get:
- ✅
Get Oneinstead ofGet Many - ✅
Updatemethod - ✅
Delete Oneinstead ofDelete Many - ✅ Automatic uniqueness checks on create/update
⚠️ Important:
- Only enforced when using DSL methods
- Don't call DB state-mutating methods directly on
&ReducerContext - Always use
&spacetimedsl::DSLmethods
Status:
⚠️ Unstable - will be removed when SpacetimeDB implements native support
🔗 Foreign Keys / Referential Integrity
Add #[foreign_key] and #[referenced_by] to enforce referential integrity and apply on delete strategies.
Example:
pub mod entity {
#[dsl(plural_name = entities, method(update = false))]
#[table(name = entity, public)]
pub struct Entity {
#[primary_key]
#[auto_inc]
#[create_wrapper]
#[referenced_by(path = crate, table = identifier)] // Added
id: u128,
created_at: Timestamp,
}
}
pub mod identifier {
#[dsl(plural_name = identifiers, method(update = true))]
#[table(name = identifier, public)]
pub struct Identifier {
#[primary_key]
#[auto_inc]
#[create_wrapper]
#[referenced_by(path = crate, table = identifier_reference)] // Added
id: u128
#[unique]
#[use_wrapper(crate::EntityId)]
#[foreign_key(path = crate, table = entity, on_delete = Delete)] // Added
entity_id: u128
#[unique]
pub value: String
created_at: Timestamp
modified_at: Timestamp,
}
}
#[dsl(plural_name = identifier_references, method(update = true))]
#[table(name = identifier_reference, public)]
pub struct IdentifierReference {
#[primary_key]
#[use_wrapper(IdentifierId)]
#[foreign_key(path = crate, table = identifier, on_delete = Error)] // Added
id: u128,
#[unique]
#[use_wrapper(IdentifierId)]
#[foreign_key(path = crate, table = identifier, on_delete = Delete)] // Added
id2: u128,
#[unique]
#[use_wrapper(IdentifierId)]
#[foreign_key(path = crate, table = identifier, on_delete = SetZero)] // Added
pub id3: u128,
#[unique]
#[use_wrapper(IdentifierId)]
#[foreign_key(path = crate, table = identifier, on_delete = Ignore)] // Added
id4: u128,
}
📋 #[referenced_by] Attribute:
Requirements:
- Values for
pathandtablefields - Only on
#[primary_key]columns - Requires
#[create_wrapper]/#[use_wrapper]
Features:
- Multiple
#[referenced_by]per primary key allowed - One for each referencing table
- Controls Delete One/Many DSL methods
- Calls
OnDeleteStrategyof referencing tables
🔑 #[foreign_key] Attribute:
Requirements:
- Only on:
#[primary_key],#[index], or#[unique]columns - Requires
#[use_wrapper] - Values for:
path,table,column,on_delete - One per column maximum
Features:
- Referential integrity checks on create/update
- Executes
OnDeleteStrategywhen referenced row deleted
⚠️ Compatibility Requirements:
Foreign key strategies must be compatible with the table's method configuration and column visibility:
-
🔄
on_delete = Deleterequires the referencing table to havemethod(delete = true)- The table must support delete operations to allow cascading deletes
- Compilation will fail if delete methods are not enabled
-
0️⃣
on_delete = SetZerorequires:- The referencing table to have
method(update = true) - The foreign key column to be non-private (public or pub(in path))
- Both conditions are necessary because setting to zero is an update operation
- Compilation will fail if either requirement is not met
- The referencing table to have
-
⚠️
on_delete = Errorandon_delete = Ignorehave no special requirements
Example:
// ✅ Valid: table has delete methods enabled
#[dsl(plural_name = children, method(update = true, delete = true))]
#[table(name = child, public)]
pub struct Child {
#[primary_key]
#[auto_inc]
#[create_wrapper]
id: u128,
#[use_wrapper(ParentId)]
#[foreign_key(path = crate, table = parent, on_delete = Delete)]
parent_id: u128, // Can use Delete strategy
}
// ✅ Valid: table has update methods and column is public
#[dsl(plural_name = items, method(update = true, delete = false))]
#[table(name = item, public)]
pub struct Item {
#[primary_key]
#[auto_inc]
#[create_wrapper]
id: u128,
#[use_wrapper(OwnerId)]
#[foreign_key(path = crate, table = owner, on_delete = SetZero)]
pub owner_id: u128, // Must be public for SetZero
}
// ❌ Invalid: delete = false but using Delete strategy
#[dsl(plural_name = invalid, method(update = true, delete = false))]
pub struct Invalid {
#[foreign_key(path = crate, table = parent, on_delete = Delete)]
parent_id: u128, // Compile error!
}
// ❌ Invalid: private column with SetZero strategy
#[dsl(plural_name = invalid, method(update = true, delete = true))]
pub struct Invalid {
#[foreign_key(path = crate, table = owner, on_delete = SetZero)]
owner_id: u128, // Compile error! Must be public
}
⚠️ Important:
- Only enforced when using DSL methods
- Don't call DB methods directly on
&ReducerContext - Always use
&spacetimedsl::DSLmethods
Status:
⚠️ Unstable
- Will be removed when SpacetimeDB implements native support
- Tests exist but may not cover all cases
- Backup your data before testing!
- Found a bug? Create a GitHub issue! 🐛
📄 Here is the `Delete One` DSL method of the Entity table
pub trait DeleteEntityRowById<T: spacetimedsl::WriteContext>: spacetimedsl::DSLContext<T> {
fn delete_entity_by_id(
&self,
id: impl Into<EntityId> + Clone,
) -> Result<spacetimedsl::DeletionResult, spacetimedsl::SpacetimeDSLError> {
use spacetimedsl::Wrapper;
use spacetimedb::{DbContext, Table};
use spacetimedsl::itertools::Itertools;
let id = id.clone().into().value();
let row_to_delete = self.ctx().db().entity().id().find(id);
let primary_key_value_of_a_row_to_delete = match row_to_delete {
None => {
return Err(spacetimedsl::SpacetimeDSLError::NotFoundError {
table_name: "entity".into(),
column_names_and_row_values: format!("{{ , id : {0} }}", &id).into(),
});
}
Some(row_to_delete) => row_to_delete.id,
};
let mut deletion_result_entry = spacetimedsl::DeletionResultEntry {
table_name: "entity".into(),
column_name: "id".into(),
strategy: spacetimedsl::OnDeleteStrategy::Delete,
row_value: format!("{0}", EntityId::new(primary_key_value_of_a_row_to_delete.clone())).into(),
child_entries: vec![],
};
match spacetimedsl::internal::DSLInternals::execute_on_delete_strategies_of_referencing_tables_after_one_row_of_the_entity_table_was_deleted(
self.ctx(),
spacetimedsl::OnDeleteStrategy::Error,
&primary_key_value_of_a_row_to_delete,
) {
Err(mut child_entries) => {
deletion_result_entry.child_entries.append(&mut child_entries);
let error = spacetimedsl::DeletionResult {
table_name: "entity".into(),
one_or_multiple: spacetimedsl::OneOrMultiple::One,
entries: vec![deletion_result_entry],
};
return Err(
spacetimedsl::SpacetimeDSLError::ReferenceIntegrityViolation(
spacetimedsl::ReferenceIntegrityViolationError::OnDelete(
error,
),
),
);
}
Ok(mut child_entries) => {
deletion_result_entry.child_entries.append(&mut child_entries);
}
};
match self
.ctx()
.db()
.entity()
.id()
.delete(primary_key_value_of_a_row_to_delete)
{
false => return Err(spacetimedsl::SpacetimeDSLError::Error("Delete One Error: `count_of_rows_to_delete ( 1 ) != ( 0 ) count_of_deleted_rows`!".to_string())),
true => {}
};
match spacetimedsl::internal::DSLInternals::execute_on_delete_strategies_of_referencing_tables_after_one_row_of_the_entity_table_was_deleted(
self.ctx(),
spacetimedsl::OnDeleteStrategy::Delete,
&primary_key_value_of_a_row_to_delete,
) {
Err(mut child_entries) => {
deletion_result_entry.child_entries.append(&mut child_entries);
let error = spacetimedsl::DeletionResult {
table_name: "entity".into(),
one_or_multiple: spacetimedsl::OneOrMultiple::One,
entries: vec![deletion_result_entry],
};
return Err(
spacetimedsl::SpacetimeDSLError::Error(
format!("Delete One Error: An unknown error occurred after changing the database state! If the reducer running this doesn\'t return an error, the state changes are persisted and you have problems now! Here is the deletion result: {0}", error),
),
);
}
Ok(mut child_entries) => {
deletion_result_entry.child_entries.append(&mut child_entries);
}
};
match spacetimedsl::internal::DSLInternals::execute_on_delete_strategies_of_referencing_tables_after_one_row_of_the_entity_table_was_deleted(
self.ctx(),
spacetimedsl::OnDeleteStrategy::SetZero,
&primary_key_value_of_a_row_to_delete,
) {
Err(mut child_entries) => {
deletion_result_entry.child_entries.append(&mut child_entries);
let error = spacetimedsl::DeletionResult {
table_name: "entity".into(),
one_or_multiple: spacetimedsl::OneOrMultiple::One,
entries: vec![deletion_result_entry],
};
return Err(
spacetimedsl::SpacetimeDSLError::Error(
format!("Delete One Error: An unknown error occurred after changing the database state! If the reducer running this doesn\'t return an error, the state changes are persisted and you have problems now! Here is the deletion result: {0}", error),
),
);
}
Ok(mut child_entries) => {
deletion_result_entry.child_entries.append(&mut child_entries);
}
};
match spacetimedsl::internal::DSLInternals::execute_on_delete_strategies_of_referencing_tables_after_one_row_of_the_entity_table_was_deleted(
self.ctx(),
spacetimedsl::OnDeleteStrategy::Ignore,
&primary_key_value_of_a_row_to_delete,
) {
Err(mut child_entries) => {
deletion_result_entry.child_entries.append(&mut child_entries);
let error = spacetimedsl::DeletionResult {
table_name: "entity".into(),
one_or_multiple: spacetimedsl::OneOrMultiple::One,
entries: vec![deletion_result_entry],
};
return Err(
spacetimedsl::SpacetimeDSLError::Error(
format!("Delete One Error: An unknown error occurred after changing the database state! If the reducer running this doesn\'t return an error, the state changes are persisted and you have problems now! Here is the deletion result: {0}"error),
),
);
}
Ok(mut child_entries) => {
deletion_result_entry.child_entries.append(&mut child_entries);
}
};
return Ok(spacetimedsl::DeletionResult {
table_name: "entity".into(),
one_or_multiple: spacetimedsl::OneOrMultiple::One,
entries: vec![deletion_result_entry],
});
}
}
impl<T: spacetimedsl::WriteContext> DeleteEntityRowById<T> for spacetimedsl::DSL<'_, T> {}
Delete One/Many DSL methods call internal functions generated by tables with #[referenced_by].
🔧 Internals
💡 Optional reading - skip to plural name if not interested.
Execution:
- Called multiple times to ensure
OnDeleteStrategy::Errorprocesses first - Separate implementations for one vs. multiple rows
Key Differences (Multiple vs. One Row):
- Parameter:
&'a [PrimaryKeyType]vs.&PrimaryKeyType - Return:
HashMap<&'a u128, Vec<DeletionResultEntry>>vs.Vec<DeletionResultEntry>
📄 Function for multiple rows
pub trait ExecuteOnDeleteStrategiesOfReferencingTablesAfterMultipleRowsOfTheEntityTableWereDeleted {
fn execute_on_delete_strategies_of_referencing_tables_after_multiple_rows_of_the_entity_table_were_deleted<'a>(
ctx: &spacetimedb::ReducerContext,
strategy: spacetimedsl::OnDeleteStrategy,
primary_key_values_of_rows_to_delete: &'a [u128],
) -> Result<
std::collections::HashMap<&'a u128, Vec<spacetimedsl::DeletionResultEntry>>,
std::collections::HashMap<&'a u128, Vec<spacetimedsl::DeletionResultEntry>>,
> {
use spacetimedsl::Wrapper;
use spacetimedb::{DbContext, Table};
let mut entries = std::collections::HashMap::new();
for primary_key_value_of_a_row_to_delete in primary_key_values_of_rows_to_delete {
entries.insert(primary_key_value_of_a_row_to_delete, vec![]);
}
let mut error = false;
use crate::component::identifier::ExecuteOnDeleteStrategiesOfTheIdentifierTableAfterMultipleRowsOfTheEntityTableWereDeleted;
match spacetimedsl::internal::DSLInternals::execute_on_delete_strategies_of_the_identifier_table_after_multiple_rows_of_the_entity_table_were_deleted(
ctx,
&strategy,
primary_key_values_of_rows_to_delete,
) {
Err(child_entries_by_primary_key_value_of_a_row_to_delete) => {
for (primary_key_value_of_a_row_to_delete, mut child_entries) in child_entries_by_primary_key_value_of_a_row_to_delete {
entries.get_mut(&primary_key_value_of_a_row_to_delete).unwrap().append(&mut child_entries);
}
error = true;
}
Ok(child_entries_by_primary_key_value_of_a_row_to_delete) => {
for (primary_key_value_of_a_row_to_delete, mut child_entries) in child_entries_by_primary_key_value_of_a_row_to_delete {
entries.get_mut(&primary_key_value_of_a_row_to_delete).unwrap().append(&mut child_entries);
}
}
};
match error {
false => Ok(entries),
true => Err(entries),
}
}
}
impl ExecuteOnDeleteStrategiesOfReferencingTablesAfterMultipleRowsOfTheEntityTableWereDeleted for spacetimedsl::internal::DSLInternals {}
Calls another internal function generated by the Identifier table (has #[foreign_key]).
📄 Function created by Identifier table
pub trait ExecuteOnDeleteStrategiesOfTheIdentifierTableAfterMultipleRowsOfTheEntityTableWereDeleted {
fn execute_on_delete_strategies_of_the_identifier_table_after_multiple_rows_of_the_entity_table_were_deleted<'a>(
ctx: &spacetimedb::ReducerContext,
strategy: &spacetimedsl::OnDeleteStrategy,
primary_key_values_of_rows_of_another_table_to_delete: &'a [u128],
) -> Result<
std::collections::HashMap<&'a u128, Vec<spacetimedsl::DeletionResultEntry>>,
std::collections::HashMap<&'a u128, Vec<spacetimedsl::DeletionResultEntry>>,
>,
> {
use spacetimedsl::Wrapper;
use spacetimedb::{DbContext, Table};
use spacetimedsl::itertools::Itertools;
let mut entries = std::collections::HashMap::new();
for primary_key_value_of_a_row_of_another_table_to_delete in primary_key_values_of_rows_of_another_table_to_delete {
entries.insert(primary_key_value_of_a_row_of_another_table_to_delete, vec![]);
}
let mut error = false;
match &strategy {
spacetimedsl::OnDeleteStrategy::Ignore => {}
spacetimedsl::OnDeleteStrategy::Delete => {
let mut child_entries_by_primary_key_value_of_row_to_delete = std::collections::HashMap::new();
let mut primary_key_values_of_rows_to_delete_by_primary_key_value_of_a_row_of_another_table_to_delete = std::collections::HashMap::new();
for primary_key_value_of_a_row_of_another_table_to_delete in primary_key_values_of_rows_of_another_table_to_delete {
primary_key_values_of_rows_to_delete_by_primary_key_value_of_a_row_of_another_table_to_delete
.insert(primary_key_value_of_a_row_of_another_table_to_delete, vec![]);
}
for primary_key_value_of_a_row_of_another_table_to_delete in primary_key_values_of_rows_of_another_table_to_delete {
match ctx.db().identifier().entity_id().find(primary_key_value_of_a_row_of_another_table_to_delete)
{
None => {}
Some(row) => {
if !child_entries_by_primary_key_value_of_row_to_delete.contains_key(&row.id) {
primary_key_values_of_rows_to_delete_by_primary_key_value_of_a_row_of_another_table_to_delete
.get_mut(primary_key_value_of_a_row_of_another_table_to_delete)
.unwrap()
.push(row.id);
child_entries_by_primary_key_value_of_row_to_delete.insert(row.id, vec![]);
}
}
};
}
let primary_key_values_of_rows_to_delete = child_entries_by_primary_key_value_of_row_to_delete
.keys()
.cloned()
.collect_vec();
match spacetimedsl::internal::DSLInternals::execute_on_delete_strategies_of_referencing_tables_after_multiple_rows_of_the_identifier_table_were_deleted(
ctx,
spacetimedsl::OnDeleteStrategy::Error,
&primary_key_values_of_rows_to_delete[..],
) {
Err(
child_entries_by_primary_key_value_of_a_row_to_delete,
) => {
error = true;
for (
primary_key_value_of_a_row_to_delete,
mut child_entries,
) in child_entries_by_primary_key_value_of_a_row_to_delete {
child_entries_by_primary_key_value_of_a_row_to_delete
.get_mut(primary_key_value_of_a_row_to_delete)
.unwrap()
.append(&mut child_entries);
}
}
Ok(child_entries_by_primary_key_value_of_a_row_to_delete) => {
for (
primary_key_value_of_a_row_to_delete,
mut child_entries,
) in child_entries_by_primary_key_value_of_a_row_to_delete {
child_entries_by_primary_key_value_of_a_row_to_delete
.get_mut(primary_key_value_of_a_row_to_delete)
.unwrap()
.append(&mut child_entries);
}
}
};
match error {
false => {}
true => {
for (
primary_key_value_of_a_row_of_another_table_to_delete,
primary_key_values_of_rows_to_delete,
) in primary_key_values_of_rows_to_delete_by_primary_key_value_of_a_row_of_another_table_to_delete {
for id in &primary_key_values_of_rows_to_delete {
let child_entries = child_entries_by_primary_key_value_of_row_to_delete
.remove(&id)
.unwrap();
entries
.get_mut(
primary_key_value_of_a_row_of_another_table_to_delete,
)
.unwrap()
.push(spacetimedsl::DeletionResultEntry {
table_name: "identifier".into(),
column_name: "entity_id".into(),
strategy: spacetimedsl::OnDeleteStrategy::Delete,
row_value: format!("{0}", IdentifierId::new(id.clone())).into(),
child_entries,
});
}
}
return Err(entries);
}
};
for id in &primary_key_values_of_rows_to_delete {
ctx.db().identifier().id().delete(id);
}
match error {
false => {}
true => {
for (
primary_key_value_of_a_row_of_another_table_to_delete,
primary_key_values_of_rows_to_delete,
) in primary_key_values_of_rows_to_delete_by_primary_key_value_of_a_row_of_another_table_to_delete {
for id in &primary_key_values_of_rows_to_delete {
let child_entries = child_entries_by_primary_key_value_of_row_to_delete
.remove(&id)
.unwrap();
entries
.get_mut(
primary_key_value_of_a_row_of_another_table_to_delete,
)
.unwrap()
.push(spacetimedsl::DeletionResultEntry {
table_name: "identifier".into(),
column_name: "entity_id".into(),
strategy: spacetimedsl::OnDeleteStrategy::Delete,
row_value: format!("{0}", IdentifierId::new(id.clone())).into(),
child_entries,
});
}
}
return Err(entries);
}
};
match spacetimedsl::internal::DSLInternals::execute_on_delete_strategies_of_referencing_tables_after_multiple_rows_of_the_identifier_table_were_deleted(
ctx,
spacetimedsl::OnDeleteStrategy::Delete,
&primary_key_values_of_rows_to_delete[..],
) {
Err(
child_entries_by_primary_key_value_of_a_row_to_delete,
) => {
error = true;
for (
primary_key_value_of_a_row_to_delete,
mut child_entries,
) in child_entries_by_primary_key_value_of_a_row_to_delete {
child_entries_by_primary_key_value_of_a_row_to_delete
.get_mut(primary_key_value_of_a_row_to_delete)
.unwrap()
.append(&mut child_entries);
}
}
Ok(child_entries_by_primary_key_value_of_a_row_to_delete) => {
for (
primary_key_value_of_a_row_to_delete,
mut child_entries,
) in child_entries_by_primary_key_value_of_a_row_to_delete {
child_entries_by_primary_key_value_of_a_row_to_delete
.get_mut(primary_key_value_of_a_row_to_delete)
.unwrap()
.append(&mut child_entries);
}
}
};
match error {
false => {}
true => {
for (
primary_key_value_of_a_row_of_another_table_to_delete,
primary_key_values_of_rows_to_delete,
) in primary_key_values_of_rows_to_delete_by_primary_key_value_of_a_row_of_another_table_to_delete {
for id in &primary_key_values_of_rows_to_delete {
let child_entries = child_entries_by_primary_key_value_of_row_to_delete
.remove(&id)
.unwrap();
entries
.get_mut(
primary_key_value_of_a_row_of_another_table_to_delete,
)
.unwrap()
.push(spacetimedsl::DeletionResultEntry {
table_name: "identifier".into(),
column_name: "entity_id".into(),
strategy: spacetimedsl::OnDeleteStrategy::Delete,
row_value: format!("{0}", IdentifierId::new(id.clone())).into(),
child_entries,
});
}
}
return Err(entries);
}
};
match spacetimedsl::internal::DSLInternals::execute_on_delete_strategies_of_referencing_tables_after_multiple_rows_of_the_identifier_table_were_deleted(
ctx,
spacetimedsl::OnDeleteStrategy::SetZero,
&primary_key_values_of_rows_to_delete[..],
) {
Err(
child_entries_by_primary_key_value_of_a_row_to_delete,
) => {
error = true;
for (
primary_key_value_of_a_row_to_delete,
mut child_entries,
) in child_entries_by_primary_key_value_of_a_row_to_delete {
child_entries_by_primary_key_value_of_a_row_to_delete
.get_mut(primary_key_value_of_a_row_to_delete)
.unwrap()
.append(&mut child_entries);
}
}
Ok(child_entries_by_primary_key_value_of_a_row_to_delete) => {
for (
primary_key_value_of_a_row_to_delete,
mut child_entries,
) in child_entries_by_primary_key_value_of_a_row_to_delete {
child_entries_by_primary_key_value_of_a_row_to_delete
.get_mut(primary_key_value_of_a_row_to_delete)
.unwrap()
.append(&mut child_entries);
}
}
};
match error {
false => {}
true => {
for (
primary_key_value_of_a_row_of_another_table_to_delete,
primary_key_values_of_rows_to_delete,
) in primary_key_values_of_rows_to_delete_by_primary_key_value_of_a_row_of_another_table_to_delete {
for id in &primary_key_values_of_rows_to_delete {
let child_entries = child_entries_by_primary_key_value_of_row_to_delete
.remove(&id)
.unwrap();
entries
.get_mut(
primary_key_value_of_a_row_of_another_table_to_delete,
)
.unwrap()
.push(spacetimedsl::DeletionResultEntry {
table_name: "identifier".into(),
column_name: "entity_id".into(),
strategy: spacetimedsl::OnDeleteStrategy::Delete,
row_value: format!("{0}", IdentifierId::new(id.clone())).into(),
child_entries,
});
}
}
return Err(entries);
}
};
match spacetimedsl::internal::DSLInternals::execute_on_delete_strategies_of_referencing_tables_after_multiple_rows_of_the_identifier_table_were_deleted(
ctx,
spacetimedsl::OnDeleteStrategy::Ignore,
&primary_key_values_of_rows_to_delete[..],
) {
Err(
child_entries_by_primary_key_value_of_a_row_to_delete,
) => {
error = true;
for (
primary_key_value_of_a_row_to_delete,
mut child_entries,
) in child_entries_by_primary_key_value_of_a_row_to_delete {
child_entries_by_primary_key_value_of_a_row_to_delete
.get_mut(primary_key_value_of_a_row_to_delete)
.unwrap()
.append(&mut child_entries);
}
}
Ok(child_entries_by_primary_key_value_of_a_row_to_delete) => {
for (
primary_key_value_of_a_row_to_delete,
mut child_entries,
) in child_entries_by_primary_key_value_of_a_row_to_delete {
child_entries_by_primary_key_value_of_a_row_to_delete
.get_mut(primary_key_value_of_a_row_to_delete)
.unwrap()
.append(&mut child_entries);
}
}
};
for (
primary_key_value_of_a_row_of_another_table_to_delete,
primary_key_values_of_rows_to_delete,
) in primary_key_values_of_rows_to_delete_by_primary_key_value_of_a_row_of_another_table_to_delete {
for id in &primary_key_values_of_rows_to_delete {
let child_entries = child_entries_by_primary_key_value_of_row_to_delete
.remove(&id)
.unwrap();
entries
.get_mut(
primary_key_value_of_a_row_of_another_table_to_delete,
)
.unwrap()
.push(spacetimedsl::DeletionResultEntry {
table_name: "identifier".into(),
column_name: "entity_id".into(),
strategy: spacetimedsl::OnDeleteStrategy::Delete,
row_value: ::alloc::__export::must_use({
::alloc::fmt::format(
format_args!("{0}", IdentifierId::new(id.clone())),
)
})
.into(),
child_entries,
});
}
}
}
spacetimedsl::OnDeleteStrategy::Error => {}
spacetimedsl::OnDeleteStrategy::SetZero => {}
};
match error {
false => Ok(entries),
true => Err(entries),
}
}
}
impl ExecuteOnDeleteStrategiesOfTheIdentifierTableAfterMultipleRowsOfTheEntityTableWereDeleted for spacetimedsl::internal::DSLInternals {}
Because the Identifier table is referenced by the Identifier Reference table, it does much during execution of the OnDeleteStrategy::Delete strategy.
It calls it's own function generated because it has at least one #[referenced_by].
📄 Let's have a look into it (which doesn't do that much stuff in the OnDeleteStrategy::Delete match arm as the one for the Identifier table)
pub trait ExecuteOnDeleteStrategiesOfTheIdentifierReferenceTableAfterMultipleRowsOfTheIdentifierTableWereDeleted {
fn execute_on_delete_strategies_of_the_identifier_reference_table_after_multiple_rows_of_the_identifier_table_were_deleted<'a>(
ctx: &spacetimedb::ReducerContext,
strategy: &spacetimedsl::OnDeleteStrategy,
primary_key_values_of_rows_of_another_table_to_delete: &'a [u128],
) -> Result<
std::collections::HashMap<&'a u128, Vec<spacetimedsl::DeletionResultEntry>>,
std::collections::HashMap<&'a u128, Vec<spacetimedsl::DeletionResultEntry>>,
> {
use spacetimedsl::Wrapper;
use spacetimedb::{DbContext, Table};
use spacetimedsl::itertools::Itertools;
let mut entries = std::collections::HashMap::new();
for primary_key_value_of_a_row_of_another_table_to_delete in primary_key_values_of_rows_of_another_table_to_delete {
entries.insert(primary_key_value_of_a_row_of_another_table_to_delete, vec![]);
}
let mut error = false;
match &strategy {
spacetimedsl::OnDeleteStrategy::Error => {
for primary_key_value_of_a_row_of_another_table_to_delete in primary_key_values_of_rows_of_another_table_to_delete {
match ctx.db().identifier_reference().id().find(primary_key_value_of_a_row_of_another_table_to_delete) {
None => {}
Some(row) => {
error = true;
let child_entries = vec![];
let id = &row.id;
entries.get_mut(primary_key_value_of_a_row_of_another_table_to_delete).unwrap().push(spacetimedsl::DeletionResultEntry {
table_name: "identifier_reference".into(),
column_name: "id".into(),
strategy: spacetimedsl::OnDeleteStrategy::Error,
row_value: format!("{0}", IdentifierId::new(id.clone())).into(),
child_entries,
});
}
};
}
}
spacetimedsl::OnDeleteStrategy::SetZero => {
for primary_key_value_of_a_row_of_another_table_to_delete in primary_key_values_of_rows_of_another_table_to_delete {
match ctx.db().identifier_reference().id3().find(primary_key_value_of_a_row_of_another_table_to_delete) {
None => {}
Some(mut row) => {
row.id3 = 0;
let child_entries = vec![];
let id = &row.id;
entries.get_mut(primary_key_value_of_a_row_of_another_table_to_delete).unwrap().push(spacetimedsl::DeletionResultEntry {
table_name: "identifier_reference".into(),
column_name: "id3".into(),
strategy: spacetimedsl::OnDeleteStrategy::SetZero,
row_value: format!("{0}", IdentifierId::new(id.clone())).into(),
child_entries,
});
ctx.db().identifier_reference().id().update(row);
}
};
}
}
spacetimedsl::OnDeleteStrategy::Delete => {
for primary_key_value_of_a_row_of_another_table_to_delete in primary_key_values_of_rows_of_another_table_to_delete {
match ctx.db().identifier_reference().id2().find(primary_key_value_of_a_row_of_another_table_to_delete) {
None => {}
Some(row) => {
let child_entries = vec![];
let id = &row.id;
entries.get_mut(primary_key_value_of_a_row_of_another_table_to_delete).unwrap().push(spacetimedsl::DeletionResultEntry {
table_name: "identifier_reference".into(),
column_name: "id2".into(),
strategy: spacetimedsl::OnDeleteStrategy::Delete,
row_value: format!("{0}", IdentifierId::new(id.clone())).into(),
child_entries,
});
ctx.db().identifier_reference().id().delete(row.id);
}
};
}
}
spacetimedsl::OnDeleteStrategy::Ignore => {
for primary_key_value_of_a_row_of_another_table_to_delete in primary_key_values_of_rows_of_another_table_to_delete {
match ctx.db().identifier_reference().id4().find(primary_key_value_of_a_row_of_another_table_to_delete) {
None => {}
Some(row) => {
let child_entries = vec![];
let id = &row.id;
entries.get_mut(primary_key_value_of_a_row_of_another_table_to_delete).unwrap().push(spacetimedsl::DeletionResultEntry {
table_name: "identifier_reference".into(),
column_name: "id4".into(),
strategy: spacetimedsl::OnDeleteStrategy::Ignore,
row_value: format!("{0}", IdentifierId::new(id.clone())).into(),
child_entries,
});
}
};
}
}
};
match error {
false => Ok(entries),
true => Err(entries),
}
}
}
impl ExecuteOnDeleteStrategiesOfTheIdentifierReferenceTableAfterMultipleRowsOfTheIdentifierTableWereDeleted for spacetimedsl::internal::DSLInternals {}
📝 The plural name DSL attribute field
Found in: #[spacetimedsl::dsl(plural_name = entities)]
Required: ✅
Used in: DSL method names for #[index(btree)] columns
Get ManymethodsDelete Manymethods
🎯 Other DSL methods
Complete Method Coverage: Every DB method has an equivalent DSL method! 🎉


🪝 Hooks System
Execute custom logic automatically during database operations!
SpacetimeDSL supports hooks that run before and after insert, update, and delete operations. This enables:
- ✅ Custom validation beyond basic constraints
- 📊 Audit logging and tracking
- 🔔 Triggering side effects (notifications, related table updates)
- 🎯 Enforcing business rules across multiple tables
- Setting defaults before insert/update by mutating the row
Syntax:
Add hook() configuration to your #[spacetimedsl::dsl] attribute:
#[spacetimedsl::dsl(
plural_name = entities,
method(update = true, delete = true),
hook(before(update, delete), after(insert))
)]
#[spacetimedb::table(name = entity, public)]
pub struct Entity {
#[primary_key]
#[auto_inc]
#[create_wrapper]
id: u128,
pub value: String,
created_at: Timestamp,
modified_at: Option<Timestamp>,
}
Hook Types:
- 🔜
before(insert)- Called before inserting a row - 🔜
before(update)- Called before updating a row - 🔜
before(delete)- Called before deleting a row - ✅
after(insert)- Called after successfully inserting a row - ✅
after(update)- Called after successfully updating a row - ✅
after(delete)- Called after successfully deleting a row
Implementing Hook Functions:
Mark your hook functions with #[spacetimedsl::hook]:
use spacetimedsl::hook;
// After insert hook
#[hook]
pub fn after_entity_insert(dsl: &spacetimedsl::DSL<'_, T>, row: &Entity) -> Result<(), SpacetimeDSLError> {
log::info!("Inserted entity with id={}", row.id());
Ok(())
}
// Before update hook - has access to both old and new values and can Mutate the new row before the update occurs
#[hook]
pub fn before_entity_update(
dsl: &spacetimedsl::DSL<'_, T>,
old_row: &Entity,
mut new_row: Entity
) -> Result<Entity, SpacetimeDSLError> {
if new_row.get_value().is_empty() {
return Err(SpacetimeDSLError::Error("Value cannot be empty".to_string()));
}
log::info!("Updating entity {} from '{}' to '{}'",
old_row.get_id(), old_row.get_value(), new_row.get_value());
Ok(new_row)
}
// Before delete hook
#[hook]
pub fn before_entity_delete(dsl: &spacetimedsl::DSL<'_, T>, row: &Entity) -> Result<(), SpacetimeDSLError> {
log::info!("Deleting entity with id={}", row.id());
Ok(())
}
Hook Execution Timing:
- ⏰
beforehooks run before the database operation - ⏰
afterhooks run after the database operation completes successfully - 🚫 If a hook returns
Err(SpacetimeDSLError), the operation is aborted and propagated - 🔄 For
updatehooks, both the old row (current state) and new row (updated values) are provided
Error Handling:
When a hook returns an error:
- 🔙 For
beforehooks: the database operation is cancelled before any changes - ⚠️ For
afterhooks: the row is already in the database, but the error is returned to the caller
Hook Naming Convention:
SpacetimeDSL expects hook functions to follow this naming pattern:
{before|after}_{table_name}_{insert|update|delete}
For a table named Entity, the expected function names are:
before_entity_insert,after_entity_insertbefore_entity_update,after_entity_updatebefore_entity_delete,after_entity_delete
⚠️ Important Notes:
- Hook functions must be in the same module as the table definition
- Use the
#[spacetimedsl::hook]attribute to mark hook implementations - Hooks work seamlessly with foreign keys and
OnDeleteStrategy - For cascading deletes, delete hooks are called for each affected row
🔒 Hook-Method Compatibility:
Hooks require compatible method configuration:
- ❌
hook(before(update))orhook(after(update))requiresmethod(update = true) - ❌
hook(before(delete))orhook(after(delete))requiresmethod(delete = true) - ✅ Insert hooks always work (create methods are always generated)
// ❌ Invalid: update hook without update method
#[spacetimedsl::dsl(
plural_name = entities,
method(update = false, delete = true),
hook(after(update)) // Compile error!
)]
pub struct Entity { /* ... */ }
// ✅ Valid: update hook with update method enabled
#[spacetimedsl::dsl(
plural_name = entities,
method(update = true, delete = true),
hook(after(update)) // OK!
)]
pub struct Entity { /* ... */ }
See Method Configuration for details on enabling update/delete methods.
🎛️ Method Configuration
Explicit control over generated methods:
SpacetimeDSL requires you to explicitly specify which DSL methods to generate using the method() configuration:
#[spacetimedsl::dsl(
plural_name = entities,
method(update = true, delete = false) // Generate update methods, but not delete methods
)]
#[spacetimedb::table(name = entity, public)]
pub struct Entity {
// ... fields
}
Configuration Options:
- 🔄
update = true|false- Generate/skip update DSL methods - 🗑️
delete = true|false- Generate/skip delete DSL methods - ✨ Create and Get methods are always generated
Why Explicit Configuration?
Making these decisions explicit helps you:
- 🎯 Express intent clearly - Your code documents whether entities should be mutable
- 🔒 Prevent accidents - No accidental updates/deletes on immutable data
- 📊 Design better schemas - Think carefully about data lifecycle
Automatic Restrictions:
SpacetimeDSL validates your configuration and ensures consistency:
For update = true:
- ✅ Requires at least one non-private, updatable column OR
- ✅ A timestamp column named
modified_atorupdated_at(even if all other columns are private) - 🔄 This allows updates that only refresh timestamps
For delete = true:
- ✅ Compatible with all table configurations
- ⚠️ Must be enabled if any foreign key uses
on_delete = Deletestrategy
Hook Constraints:
- ❌
hook(before(update))orhook(after(update))requiresmethod(update = true) - ❌
hook(before(delete))orhook(after(delete))requiresmethod(delete = true) - 🎯 This ensures hooks are never orphaned
Foreign Key Constraints:
- ❌
#[foreign_key(on_delete = Delete)]requires the table to havemethod(delete = true) - ❌
#[foreign_key(on_delete = SetZero)]requires:- The table to have
method(update = true) - The foreign key column to be non-private (public or pub(crate))
- The table to have
See Foreign Keys / Referential Integrity for detailed examples.
Example Patterns:
// Immutable audit log - never changes after creation
#[spacetimedsl::dsl(
plural_name = audit_logs,
method(update = false, delete = false)
)]
pub struct AuditLog { /* ... */ }
// User profiles - can be updated but never deleted
#[spacetimedsl::dsl(
plural_name = user_profiles,
method(update = true, delete = false)
)]
pub struct UserProfile { /* ... */ }
// Temporary cache entries - can be both updated and deleted
#[spacetimedsl::dsl(
plural_name = cache_entries,
method(update = true, delete = true)
)]
pub struct CacheEntry { /* ... */ }
Ready to try? 🚀
Add to your server modules Cargo.toml:
# https://crates.io/crates/spacetimedsl Ergonomic DSL for SpacetimeDB
spacetimedsl = { version = "*" }
Get started with #[spacetimedsl::dsl] and helper attributes:
- ✨
#[create_wrapper] - 🔄
#[use_wrapper] - 🔗
#[foreign_key] - 📌
#[referenced_by]
⚠️ Current limitations
-
SpacetimeDSL is not available in
#[spacetimedb::view]functions until SpacetimeDB#3787 is merged and released.- If you encounter that you can't access a method on the
(Anonymous)ViewContexttype because it's private, please follow these instructions: https://github.com/tamaro-skaljic/SpacetimeDSL/issues/90#issuecomment-3573925117 until SpacetimeDB#3754 is resolved and released.
- If you encounter that you can't access a method on the
❓ FAQ
❔ Why must #[primary_key] columns be private?
Currently, they are allowed to be public, until SpacetimeDB#3754 is resolved and released.
- 🔒 They should never change after insertion
- DSL generates setters for non-private columns
- Making them public would:
- ❌ Allow changes after creation via setters
- ❌ Allow direct struct member access
- ❌ Bypass wrapped types
📜 Licensing
SpacetimeDSL is dual-licensed under:
Open Source ❤️
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Dependencies
~9.5MB
~172K SLoC