Testnet is now LIVE at testnet.spacetimedb.com! NOTE: This is a testnet, and all data will be wiped periodically.

1.0 RC1

Login

SpacetimeDB Rust Modules

Rust clients of SpacetimeDB use the Rust SpacetimeDB module library to write modules which interact with the SpacetimeDB database.

First, the spacetimedb library provides a number of macros for creating tables and Rust structs corresponding to rows in those tables.

Then the client API allows interacting with the database inside special functions called reducers.

This guide assumes you are familiar with some basics of Rust. At the very least, you should be familiar with the idea of using attribute macros. An extremely common example is derive macros.

Derive macros look at the type they are attached to and generate some related code. In this example, #[derive(Debug)] generates the formatting code needed to print out a Location for debugging purposes.

#[derive(Debug)]
struct Location {
    x: u32,
    y: u32,
} 

SpacetimeDB Macro basics

Let's start with a highly commented example, straight from the demo. This Rust package defines a SpacetimeDB module, with types we can operate on and functions we can run.

// In this small example, we have two Rust imports:
// |spacetimedb::spacetimedb| is the most important attribute we'll be using.
// |spacetimedb::println| is like regular old |println|, but outputting to the module's logs.
use spacetimedb::{spacetimedb, println};

// This macro lets us interact with a SpacetimeDB table of Person rows.
// We can insert and delete into, and query, this table by the collection
// of functions generated by the macro.
#[table(name = person, public)]
pub struct Person {
    name: String,
}

// This is the other key macro we will be using. A reducer is a
// stored procedure that lives in the database, and which can
// be invoked remotely.
#[reducer]
pub fn add(ctx: &ReducerContext, name: String) {
    // |Person| is a totally ordinary Rust struct. We can construct
    // one from the given name as we typically would.
    let person = Person { name };

    // Here's our first generated function! Given a |Person| object,
    // we can insert it into the table:
    ctx.db.person().insert(person);
}

// Here's another reducer. Notice that this one doesn't take any arguments, while
// |add| did take one. Reducers can take any number of arguments, as long as
// SpacetimeDB recognizes their types. Reducers also have to be top level
// functions, not methods.
#[reducer]
pub fn say_hello(ctx: &ReducerContext) {
    // Here's the next of our generated functions: |iter()|. This
    // iterates over all the columns in the |Person| table in SpacetimeDB.
    for person in ctx.db.person().iter() {
        // Reducers run in a very constrained and sandboxed environment,
        // and in particular, can't do most I/O from the Rust standard library.
        // We provide an alternative |spacetimedb::println| which is just like
        // the std version, excepted it is redirected out to the module's logs.
        println!("Hello, {}!", person.name);
    }
    println!("Hello, World!");
}

// Reducers can't return values, but can return errors. To do so,
// the reducer must have a return type of `Result<(), T>`, for any `T` that
// implements `Debug`.  Such errors returned from reducers will be formatted and
// printed out to logs.
#[reducer]
pub fn add_person(ctx: &ReducerContext, name: String) -> Result<(), String> {
    if name.is_empty() {
        return Err("Name cannot be empty");
    }

    ctx.db.person().insert(Person { name })
} 

Macro API

Now we'll get into details on all the macro APIs SpacetimeDB provides, starting with all the variants of the spacetimedb attribute.

Defining tables

The #[table(name = table_name)] macro is applied to a Rust struct with named fields. By default, tables are considered private. This means that they are only readable by the table owner, and by server module code. The #[table(name = table_name, public)] macro makes a table public. Public tables are readable by all users, but can still only be modified by your server module code.

Coming soon: We plan to add much more robust access controls than just public or private. Stay tuned!

#[table(name = my_table, public)]
struct MyTable {
    field1: String,
    field2: u32,
} 

This attribute is applied to Rust structs in order to create corresponding tables in SpacetimeDB. Fields of the Rust struct correspond to columns of the database table.

The fields of the struct have to be types that SpacetimeDB knows how to encode into the database. This is captured in Rust by the SpacetimeType trait.

This is automatically defined for built in numeric types:

  • bool
  • u8, u16, u32, u64, u128
  • i8, i16, i32, i64, i128
  • f32, f64

And common data structures:

  • String and &str, utf-8 string data
  • (), the unit type
  • Option<T> where T: SpacetimeType
  • Vec<T> where T: SpacetimeType

All #[table(..)] types are SpacetimeTypes, and accordingly, all of their fields have to be.

#[table(name = another_table, public)]
struct AnotherTable {
    // Fine, some builtin types.
    id: u64,
    name: Option<String>,

    // Fine, another table type.
    table: Table,

    // Fine, another type we explicitly make serializable.
    serial: Serial,
} 

If you want to have a field that is not one of the above primitive types, and not a table of its own, you can derive the SpacetimeType attribute on it.

We can derive SpacetimeType on structs and enums with members that are themselves SpacetimeTypes.

#[derive(SpacetimeType)]
enum Serial {
    Builtin(f64),
    Compound {
        s: String,
        bs: Vec<bool>,
    }
} 

Once the table is created via the macro, other attributes described below can control more aspects of the table. For instance, a particular column can be indexed, or take on values of an automatically incremented counter. These are described in detail below.

#[table(name = person, public)]
struct Person {
    #[unique]
    id: u64,

    name: String,
    address: String,
} 

Defining reducers

#[reducer] is always applied to top level Rust functions. They can take arguments of types known to SpacetimeDB (just like fields of structs must be known to SpacetimeDB), and either return nothing, or return a Result<(), E: Debug>.

#[reducer]
fn give_player_item(ctx: &ReducerContext, player_id: u64, item_id: u64) -> Result<(), GameErr> {
    // Notice how the exact name of the filter function derives from
    // the name of the field of the struct.
    let mut item = ctx.db.item().item_id().find(id).ok_or(GameErr::InvalidId)?;
    item.owner = Some(player_id);
    ctx.db.item().item_id().update(item);
    Ok(())
}

#[table(name = item, public)]
struct Item {
    #[primary_key]
    item_id: u64,
    owner: Option<u64>,
} 

Note that reducers can call non-reducer functions, including standard library functions.

There are several macros which modify the semantics of a column, which are applied to the members of the table struct. #[primary_key], #[unique] and #[autoinc] are covered below, describing how those attributes affect the semantics of inserting, filtering, and so on.

#[SpacetimeType]

#[sats]

Defining Scheduler Tables

Tables can be used to schedule a reducer calls either at a specific timestamp or at regular intervals.

// The `scheduled` attribute links this table to a reducer.
#[table(name = send_message_timer, scheduled(send_message)]
struct SendMessageTimer {
    text: String,
} 

The scheduled attribute adds a couple of default fields and expands as follows:

#[table(name = send_message_timer, scheduled(send_message)]
 struct SendMessageTimer {
    text: String,   // original field
    #[primary_key]
    #[autoinc]
    scheduled_id: u64, // identifier for internal purpose
    scheduled_at: ScheduleAt, //schedule details
}

pub enum ScheduleAt {
    /// A specific time at which the reducer is scheduled.
    /// Value is a UNIX timestamp in microseconds.
    Time(u64),
    /// A regular interval at which the repeated reducer is scheduled.
    /// Value is a duration in microseconds.
    Interval(u64),
} 

Managing timers with a scheduled table is as simple as inserting or deleting rows from the table.

#[reducer]
// Reducers linked to the scheduler table should have their first argument as `&ReducerContext`
// and the second as an instance of the table struct it is linked to.
fn send_message(ctx: &ReducerContext, arg: SendMessageTimer) -> Result<(), String> {
    // ...
}

// Scheduling reducers inside `init` reducer
#[reducer(init)]
fn init(ctx: &ReducerContext) {
    // Scheduling a reducer for a specific Timestamp
    ctx.db.send_message_timer().insert(SendMessageTimer {
        scheduled_id: 1,
        text:"bot sending a message".to_string(),
        //`spacetimedb::Timestamp` implements `From` trait to `ScheduleAt::Time`.
        scheduled_at: ctx.timestamp.plus(Duration::from_secs(10)).into()
    });

    // Scheduling a reducer to be called at fixed interval of 100 milliseconds.
    ctx.db.send_message_timer().insert(SendMessageTimer {
        scheduled_id: 0,
        text:"bot sending a message".to_string(),
        //`std::time::Duration` implements `From` trait to `ScheduleAt::Duration`.
        scheduled_at: duration!(100ms).into(),
    });
} 

Client API

Besides the macros for creating tables and reducers, there's two other parts of the Rust SpacetimeDB library. One is a collection of macros for logging, and the other is all the automatically generated functions for operating on those tables.

println! and friends

Because reducers run in a WASM sandbox, they don't have access to general purpose I/O from the Rust standard library. There's no filesystem or network access, and no input or output. This means no access to things like std::println!, which prints to standard output.

SpacetimeDB modules have access to logging output. These are exposed as macros, just like their std equivalents. The names, and all the Rust formatting machinery, work the same; just the location of the output is different.

Logs for a module can be viewed with the spacetime logs command from the CLI.

use spacetimedb::{
    println,
    print,
    eprintln,
    eprint,
    dbg,
};

#[reducer]
fn output(ctx: &ReducerContext, i: i32) {
    // These will be logged at log::Level::Info.
    println!("an int with a trailing newline: {i}");
    print!("some more text...\n");

    // These log at log::Level::Error.
    eprint!("Oops...");
    eprintln!(", we hit an error");

    // Just like std::dbg!, this prints its argument and returns the value,
    // as a drop-in way to print expressions. So this will print out |i|
    // before passing the value of |i| along to the calling function.
    //
    // The output is logged log::Level::Debug.
    ctx.db.outputted_number().insert(dbg!(i));
} 

Generated functions on a SpacetimeDB table

We'll work off these structs to see what functions SpacetimeDB generates:

This table has a plain old column.

#[table(name = ordinary, public)]
struct Ordinary {
    ordinary_field: u64,
} 

This table has a unique column. Every row in the Unique table must have distinct values of the unique_field column. Attempting to insert a row with a duplicate value will fail.

#[table(name = unique, public)]
struct Unique {
    // A unique column:
    #[unique]
    unique_field: u64,
} 

This table has an automatically incrementing column. SpacetimeDB automatically provides an incrementing sequence of values for this field, and sets the field to that value when you insert the row.

Only integer types can be #[unique]: u8, u16, u32, u64, u128, i8, i16, i32, i64 and i128.

#[table(name = autoinc, public)]
struct Autoinc {
    #[autoinc]
    autoinc_field: u64,
} 

These attributes can be combined, to create an automatically assigned ID usable for filtering.

#[table(name = identity, public)]
struct Identity {
    #[autoinc]
    #[unique]
    id_field: u64,
} 

Insertion

We'll talk about insertion first, as there a couple of special semantics to know about.

When we define |Ordinary| as a SpacetimeDB table, we get the ability to insert into it with the generated ctx.db.ordinary().insert(..) method.

Inserting takes a single argument, the row to insert. When there are no unique fields in the row, the return value is the inserted row.

#[reducer]
fn insert_ordinary(ctx: &ReducerContext, value: u64) {
    let ordinary = Ordinary { ordinary_field: value };
    let result = ctx.db.ordinary().insert(ordinary);
    assert_eq!(ordinary.ordinary_field, result.ordinary_field);
} 

When there is a unique column constraint on the table, insertion can fail if a uniqueness constraint is violated.

If we insert two rows which have the same value of a unique column, the second will fail.

#[reducer]
fn insert_unique(ctx: &ReducerContext, value: u64) {
    let result = ctx.db.unique().insert(Unique { unique_field: value });
    assert!(result.is_ok());

    let result = ctx.db.unique().insert(Unique { unique_field: value });
    assert!(result.is_err());
} 

When inserting a table with an #[autoinc] column, the database will automatically overwrite whatever we give it with an atomically increasing value.

The returned row has the autoinc column set to the value that was actually written into the database.

#[reducer]
fn insert_autoinc(ctx: &ReducerContext) {
    for i in 1..=10 {
        // These will have values of 1, 2, ..., 10
        // at rest in the database, regardless of
        // what value is actually present in the
        // insert call.
        let actual = ctx.db.autoinc().insert(Autoinc { autoinc_field: 23 })
        assert_eq!(actual.autoinc_field, i);
    }
}

#[reducer]
fn insert_id(ctx: &ReducerContext) {
    for _ in 0..10 {
        // These also will have values of 1, 2, ..., 10.
        // There's no collision and silent failure to insert,
        // because the value of the field is ignored and overwritten
        // with the automatically incremented value.
        ctx.db.identity().insert(Identity { id_field: 23 })
    }
} 

Iterating

Given a table, we can iterate over all the rows in it.

#[table(name = person, public)]
struct Person {
    #[unique]
    id: u64,

    #[index(btree)]
    age: u32,
    name: String,
    address: String,
} 

// Every table structure has a generated iter function, like:

ctx.db.my_table().iter() 

iter() returns a regular old Rust iterator, giving us a sequence of Person. The database sends us over rows, one at a time, for each time through the loop. This means we get them by value, and own the contents of String fields and so on.

#[reducer]
fn iteration(ctx: &ReducerContext) {
    let mut addresses = HashSet::new();

    for person in ctx.db.person().iter() {
        addresses.insert(person.address);
    }

    for address in addresses.iter() {
        println!("{address}");
    }
} 

Filtering

Often, we don't need to look at the entire table, and instead are looking for rows with specific values in certain columns.

Our Person table has a unique id column, so we can filter for a row matching that ID. Since it is unique, we will find either 0 or 1 matching rows in the database. This gets represented naturally as an Option<Person> in Rust. SpacetimeDB automatically creates and uses indexes for filtering on unique columns, so it is very efficient.

The name of the filter method just corresponds to the column name.

#[reducer]
fn filtering(ctx: &ReducerContext, id: u64) {
    match ctx.db.person().id().find(id) {
        Some(person) => println!("Found {person}"),
        None => println!("No person with id {id}"),
    }
} 

Our Person table also has an index on its age column. Unlike IDs, ages aren't unique. Filtering for every person who is 21, then, gives us an Iterator<Item = Person> rather than an Option<Person>.

#[reducer]
fn filtering_non_unique(ctx: &ReducerContext) {
    for person in ctx.db.person().age().filter(21u32) {
        println!("{} has turned 21", person.name);
    }
} 

NOTE: An unfortunate interaction between Rust's trait solver and integer literal defaulting rules means that you must specify the types of integer literals passed to filter and find methods via the suffix syntax, like 21u32. If you don't, you'll see a compiler error like:

error[E0271]: type mismatch resolving `<i32 as FilterableValue>::Column == u32`
   --> modules/rust-wasm-test/src/lib.rs:356:48
    |
356 |     for person in ctx.db.person().age().filter(21) {
    |                                         ------ ^^ expected `u32`, found `i32`
    |                                         |
    |                                         required by a bound introduced by this call
    |
    = note: required for `i32` to implement `BTreeIndexBounds<(u32,), SingleBound>`
note: required by a bound in `BTreeIndex::<Tbl, IndexType, Idx>::filter`
    |
410 |     pub fn filter<B, K>(&self, b: B) -> impl Iterator<Item = Tbl::Row>
    |            ------ required by a bound in this associated function
411 |     where
412 |         B: BTreeIndexBounds<IndexType, K>,
    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BTreeIndex::<Tbl, IndexType, Idx>::filter` 

Deleting

Like filtering, we can delete by an indexed or unique column instead of the entire row.

#[reducer]
fn delete_id(ctx: &ReducerContext, id: u64) {
    ctx.db.person().id().delete(id)
} 
Edit On Github