| Crates.io | spacetimedsl |
| lib.rs | spacetimedsl |
| version | 0.15.0 |
| created_at | 2025-04-18 15:39:40.177161+00 |
| updated_at | 2025-11-29 20:46:34.242736+00 |
| description | Ergonomic DSL for SpacetimeDB |
| homepage | |
| repository | https://github.com/tamaro-skaljic/SpacetimeDSL |
| max_upload_size | |
| id | 1639567 |
| size | 105,134 |
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?
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:
Entity table - holds no data per row except an unique machine-readable identifierPosition table - holds an entity_id and x, y values per rowEven with this small data model, there are fundamental issues:
Boilerplate Code:
Entity first, then pass it to insert/try_insert0 for id column (for auto-increment)ctx.timestamp for timestampsData Integrity Issues:
created_at after creation and persist with updateentity_id doesn't enforce it only accepts Entity IDsEntities actually existPositions won't auto-delete when Entities are deletedMissing Constraints:
Entitymodified_at with ctx.timestampThe Problem:
SpacetimeDB is great technology, but has weaknesses that prevent developers from utilizing its full potential — sometimes you work against the database.
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! 🎯
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:
Entity construction requiredSmart Defaults:
id column: automatically set to 0 (DB generates ID)created_at: automatically set to ctx.timestampmodified_at: supports both Timestamp and Option<Timestamp> typesBetter API:
&spacetimedb::ReducerContext💡 Best Practices:
spacetimedsl::dsl(ctx)&DSL to functions (not &ReducerContext)dsl.ctx() method if you really need the contextHere 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> {}
SpacetimeDSLError TypeUnlike DB errors, DSL errors include metadata for better debugging! 🔍
Transformation:
spacetimedb::TryInsertError → SpacetimeDSLErrorbool (Delete One) → SpacetimeDSLErrorOption (Get One) → SpacetimeDSLErroru64 (Delete Many) → SpacetimeDSLErrorError Variants:
pub enum SpacetimeDSLError {
Error, // Not available in vanilla SpacetimeDB
NotFoundError, // Not available in vanilla SpacetimeDB
UniqueConstraintViolation,
AutoIncOverflow,
ReferenceIntegrityViolation, // Not available in vanilla SpacetimeDB
}
Error variantOk(()) in reducer if this occurs!NotFoundError variantWhat 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 }}!
UniqueConstraintViolation variantOrigins:
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 */ } }}
AutoIncOverflow variantImprovements over DB:
Example log:
Auto Inc Overflow Error on the entity table!
Unfortunately SpacetimeDB doesn't provide more information.
ReferenceIntegrityViolation variantTwo scenarios:
1️⃣ Creating/Updating rows:
#[foreign_key])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:
#[referenced_by] on primary keyDeletionResult[Entry] TypeThe Problem (DB Conversation):
Developer: "Hi DB! I need an audit log for every deletion. How?"
DB: "I can give you:
bool for Delete One (deleted or not)u64 for 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:
DeletionResult on both success AND failureto_csv() methodSee Foreign Keys and Referential Integrity for more details.
OnDeleteStrategy TypeFound 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
🗑️ Delete
Error strategy0️⃣ SetZero
0🤷 Ignore
#[create_wrapper] and #[use_wrapper] attributes - aka Wrapper TypesRequirements:
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 typeBenefits:
🎯 Reduces Primitive Obsession:
🔒 Type Safety:
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:
NumericType (like u128) where WrapperType is requiredSolution:
WrapperType::new(wrapped_type))Getters (for all columns):
Setters and Mut-Getters (for non-private columns):
🔒 Automatic Field Privacy Enforcement: SpacetimeDSL automatically makes all struct fields private when processing DSL attributes.
Why?
Use Cases for Private Columns: 🔑 Never-changing fields:
created_at timestampsNo Update Method:
Update DSL methodFuture Enhancement:
When rust-lang/rust#105077 releases, DSL will use field mutability restrictions instead of visibility.
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:
unique_index(name = parent_child_entity_id) to #[spacetimedsl::dsl]#[spacetimedb::table]You get:
Get One instead of Get ManyUpdate methodDelete One instead of Delete Many⚠️ Important:
&ReducerContext&spacetimedsl::DSL methodsStatus:
⚠️ Unstable - will be removed when SpacetimeDB implements native support
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:
path and table fields#[primary_key] columns#[create_wrapper]/#[use_wrapper]Features:
#[referenced_by] per primary key allowedOnDeleteStrategy of referencing tables🔑 #[foreign_key] Attribute:
Requirements:
#[primary_key], #[index], or #[unique] columns#[use_wrapper]path, table, column, on_deleteFeatures:
OnDeleteStrategy when referenced row deleted⚠️ Compatibility Requirements:
Foreign key strategies must be compatible with the table's method configuration and column visibility:
🔄 on_delete = Delete requires the referencing table to have method(delete = true)
0️⃣ on_delete = SetZero requires:
method(update = true)⚠️ on_delete = Error and on_delete = Ignore have 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:
&ReducerContext&spacetimedsl::DSL methodsStatus:
⚠️ 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! 🐛
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].
💡 Optional reading - skip to plural name if not interested.
Execution:
OnDeleteStrategy::Error processes firstKey Differences (Multiple vs. One Row):
&'a [PrimaryKeyType] vs. &PrimaryKeyTypeHashMap<&'a u128, Vec<DeletionResultEntry>> vs. Vec<DeletionResultEntry>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]).
Identifier tablepub 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].
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 {}
plural name DSL attribute fieldFound in: #[spacetimedsl::dsl(plural_name = entities)]
Required: ✅
Used in: DSL method names for #[index(btree)] columns
Get Many methodsDelete Many methodsComplete Method Coverage: Every DB method has an equivalent DSL method! 🎉


Execute custom logic automatically during database operations!
SpacetimeDSL supports hooks that run before and after insert, update, and delete operations. This enables:
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 rowbefore(update) - Called before updating a rowbefore(delete) - Called before deleting a rowafter(insert) - Called after successfully inserting a rowafter(update) - Called after successfully updating a rowafter(delete) - Called after successfully deleting a rowImplementing 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:
before hooks run before the database operationafter hooks run after the database operation completes successfullyErr(SpacetimeDSLError), the operation is aborted and propagatedupdate hooks, both the old row (current state) and new row (updated values) are providedError Handling:
When a hook returns an error:
before hooks: the database operation is cancelled before any changesafter hooks: the row is already in the database, but the error is returned to the callerHook 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:
#[spacetimedsl::hook] attribute to mark hook implementationsOnDeleteStrategy🔒 Hook-Method Compatibility:
Hooks require compatible method configuration:
hook(before(update)) or hook(after(update)) requires method(update = true)hook(before(delete)) or hook(after(delete)) requires method(delete = true)// ❌ 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.
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 methodsdelete = true|false - Generate/skip delete DSL methodsWhy Explicit Configuration?
Making these decisions explicit helps you:
Automatic Restrictions:
SpacetimeDSL validates your configuration and ensures consistency:
For update = true:
modified_at or updated_at (even if all other columns are private)For delete = true:
on_delete = Delete strategyHook Constraints:
hook(before(update)) or hook(after(update)) requires method(update = true)hook(before(delete)) or hook(after(delete)) requires method(delete = true)Foreign Key Constraints:
#[foreign_key(on_delete = Delete)] requires the table to have method(delete = true)#[foreign_key(on_delete = SetZero)] requires:
method(update = true)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]SpacetimeDSL is not available in #[spacetimedb::view] functions until SpacetimeDB#3787 is merged and released.
(Anonymous)ViewContext type 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.❔ Why must #[primary_key] columns be private?
Currently, they are allowed to be public, until SpacetimeDB#3754 is resolved and released.
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.