diff --git a/crates/bindings-macro/src/lib.rs b/crates/bindings-macro/src/lib.rs index 8357650559c..3e38c251397 100644 --- a/crates/bindings-macro/src/lib.rs +++ b/crates/bindings-macro/src/lib.rs @@ -1,6 +1,13 @@ //! Defines procedural macros like `#[spacetimedb::table]`, //! simplifying writing SpacetimeDB modules in Rust. +// DO NOT WRITE (public) DOCS IN THIS MODULE. +// Docs should be written in the `spacetimedb` crate (i.e. `bindings/`) at reexport sites +// using `#[doc(inline)]`. +// We do this so that links to library traits, structs, etc can resolve correctly. +// +// (private documentation for the macro authors is totally fine here and you SHOULD write that!) + mod filter; mod reducer; mod sats; @@ -81,76 +88,6 @@ mod sym { } } -/// Marks a function as a spacetimedb reducer. -/// -/// A reducer is a function which traverses and updates the database, -/// a sort of stored procedure that lives in the database, and which can be invoked remotely. -/// Each reducer call runs in its own transaction, -/// and its updates to the database are only committed if the reducer returns successfully. -/// -/// A reducer may take no arguments, like so: -/// -/// ```rust,ignore -/// #[spacetimedb::reducer] -/// pub fn hello_world() { -/// println!("Hello, World!"); -/// } -/// ``` -/// -/// But it may also take some: -/// ```rust,ignore -/// #[spacetimedb::reducer] -/// pub fn add_person(name: String, age: u16) { -/// // Logic to add a person with `name` and `age`. -/// } -/// ``` -/// -/// Reducers cannot return values, but can return errors. -/// To do so, a reducer must have a return type of `Result<(), impl Debug>`. -/// When such an error occurs, it will be formatted and printed out to logs, -/// resulting in an aborted transaction. -/// -/// # Lifecycle Reducers -/// -/// You can specify special lifecycle reducers that are run at set points in -/// the module's lifecycle. You can have one each per module. -/// -/// ## `#[spacetimedb::reducer(init)]` -/// -/// This reducer is run the first time a module is published -/// and anytime the database is cleared. -/// -/// The reducer cannot be called manually -/// and may not have any parameters except for `ReducerContext`. -/// If an error occurs when initializing, the module will not be published. -/// -/// ## `#[spacetimedb::reducer(client_connected)]` -/// -/// This reducer is run when a client connects to the SpacetimeDB module. -/// Their identity can be found in the sender value of the `ReducerContext`. -/// -/// The reducer cannot be called manually -/// and may not have any parameters except for `ReducerContext`. -/// If an error occurs in the reducer, the client will be disconnected. -/// -/// -/// ## `#[spacetimedb::reducer(client_disconnected)]` -/// -/// This reducer is run when a client disconnects from the SpacetimeDB module. -/// Their identity can be found in the sender value of the `ReducerContext`. -/// -/// The reducer cannot be called manually -/// and may not have any parameters except for `ReducerContext`. -/// If an error occurs in the disconnect reducer, -/// the client is still recorded as disconnected. -/// -/// ## `#[spacetimedb::reducer(update)]` -/// -/// This reducer is run when the module is updated, -/// i.e., when publishing a module for a database that has already been initialized. -/// -/// The reducer cannot be called manually and may not have any parameters. -/// If an error occurs when initializing, the module will not be published. #[proc_macro_attribute] pub fn reducer(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream { cvt_attr::(args, item, quote!(), |args, original_function| { @@ -159,77 +96,7 @@ pub fn reducer(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream { }) } -/// Generates code for treating this struct type as a table. -/// -/// Among other things, this derives `Serialize`, `Deserialize`, -/// `SpacetimeType`, and `Table` for our type. -/// -/// # Example -/// -/// ```ignore -/// #[spacetimedb::table(name = users, public)] -/// pub struct User { -/// #[auto_inc] -/// #[primary_key] -/// pub id: u32, -/// #[unique] -/// pub username: String, -/// #[index(btree)] -/// pub popularity: u32, -/// } -/// ``` -/// -/// # Macro arguments -/// -/// * `public` and `private` -/// -/// Tables are private by default. If you'd like to make your table publically -/// accessible by anyone, put `public` in the macro arguments (e.g. -/// `#[spacetimedb::table(public)]`). You can also specify `private` if -/// you'd like to be specific. This is fully separate from Rust's module visibility -/// system; `pub struct` or `pub(crate) struct` do not affect the table visibility, only -/// the visibility of the items in your own source code. -/// -/// * `index(name = my_index, btree(columns = [a, b, c]))` -/// -/// You can specify an index on 1 or more of the table's columns with the above syntax. -/// You can also just put `#[index(btree)]` on the field itself if you only need -/// a single-column attribute; see column attributes below. -/// -/// * `name = my_table` -/// -/// Specify the name of the table in the database, if you want it to be different from -/// the name of the struct. -/// -/// # Column (field) attributes -/// -/// * `#[auto_inc]` -/// -/// Creates a database sequence. -/// -/// When a row is inserted with the annotated field set to `0` (zero), -/// the sequence is incremented, and this value is used instead. -/// Can only be used on numeric types and may be combined with indexes. -/// -/// Note that using `#[auto_inc]` on a field does not also imply `#[primary_key]` or `#[unique]`. -/// If those semantics are desired, those attributes should also be used. -/// -/// * `#[unique]` -/// -/// Creates an index and unique constraint for the annotated field. -/// -/// * `#[primary_key]` -/// -/// Similar to `#[unique]`, but generates additional CRUD methods. -/// -/// * `#[index(btree)]` -/// -/// Creates a single-column index with the specified algorithm. -/// -/// [`Serialize`]: https://docs.rs/spacetimedb/latest/spacetimedb/trait.Serialize.html -/// [`Deserialize`]: https://docs.rs/spacetimedb/latest/spacetimedb/trait.Deserialize.html -/// [`SpacetimeType`]: https://docs.rs/spacetimedb/latest/spacetimedb/trait.SpacetimeType.html -/// [`TableType`]: https://docs.rs/spacetimedb/latest/spacetimedb/trait.TableType.html + #[proc_macro_attribute] pub fn table(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream { // put this on the struct so we don't get unknown attribute errors @@ -316,29 +183,6 @@ pub fn schema_type(input: StdTokenStream) -> StdTokenStream { }) } -/// Generates code for registering a row-level security `SQL` function. -/// -/// A row-level security function takes a `SQL` query expression that is used to filter rows. -/// -/// The query follows the same syntax as a subscription query. -/// -/// **Example:** -/// -/// ```rust,ignore -/// /// Players can only see what's in their chunk -/// spacetimedb::filter!(" -/// SELECT * FROM LocationState WHERE chunk_index IN ( -/// SELECT chunk_index FROM LocationState WHERE entity_id IN ( -/// SELECT entity_id FROM UserState WHERE identity = @sender -/// ) -/// ) -/// "); -/// ``` -/// -/// **NOTE:** The `SQL` query expression is pre-parsed at compile time, but only check is a valid -/// subscription query *syntactically*, not that the query is valid when executed. -/// -/// For example, it could refer to a non-existent table. #[proc_macro] pub fn filter(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let arg = syn::parse_macro_input!(input); diff --git a/crates/bindings/README.md b/crates/bindings/README.md new file mode 100644 index 00000000000..d2d6b02b03e --- /dev/null +++ b/crates/bindings/README.md @@ -0,0 +1,519 @@ +# SpacetimeDB Rust Module Library + + + +[SpacetimeDB](https://spacetimedb.com/) allows using the Rust language to write server-side applications called **modules**. Modules run **inside** a SQL database. They have direct access to database tables, and expose public functions called **reducers** that can be invoked over the network. Clients connect directly to the database to read data. + +```text + Client Application SpacetimeDB +┌───────────────────────┐ ┌───────────────────────┐ +│ │ │ │ +│ ┌─────────────────┐ │ SQL Query │ ┌─────────────────┐ │ +│ │ Subscribed Data │<─────────────────────│ Database │ │ +│ └─────────────────┘ │ │ └─────────────────┘ │ +│ │ │ │ ^ │ +│ │ │ │ │ │ +│ v │ │ v │ +│ +─────────────────┐ │ call_reducer() │ ┌─────────────────┐ │ +│ │ Client Code │─────────────────────>│ Module Code │ │ +│ └─────────────────┘ │ │ └─────────────────┘ │ +│ │ │ │ +└───────────────────────┘ └───────────────────────┘ +``` + +Rust modules are written with the the Rust Module Library (this crate). They are built using [cargo](https://doc.rust-lang.org/cargo/) and deployed using the [`spacetime` CLI tool](https://spacetimedb.com/install). Rust modules can import any Rust [crate](https://crates.io/) that supports being compiled to WebAssembly. + +(Note: Rust can also be used to write **clients** of SpacetimeDB databases, but this requires using a completely different library, the SpacetimeDB Rust Client SDK. See the documentation on [clients] for more information.) + +This reference assumes you are familiar with the basics of Rust. If you aren't, check out Rust's [excellent documentation](https://www.rust-lang.org/learn). For a guided introduction to Rust Modules, see the [Rust Module Quickstart](https://spacetimedb.com/docs/modules/rust/quickstart). + +## Overview + +SpacetimeDB modules have two ways to interact with the outside world: tables and reducers. + +- [Tables](#tables) store data and optionally make it readable by [clients]. + +- [Reducers](#reducers) modify data and can be invoked by [clients] over the network. They can read and write data in tables, and write to a private debug log. + +These are the only ways for a SpacetimeDB module to interact with the outside world. Calling functions from `std::net` or `std::fs` inside a module will result in runtime errors. + +Reducers don't return data directly; they can only modify the database. Clients connect directly to the database and use SQL to query [public](#public-and-private-tables) tables. Clients can also open subscriptions to receive streaming updates as the results of a SQL query change. + +Tables and reducers in Rust modules can use any type that implements the [`SpacetimeType`] trait. + + + + + + + +## Setup + +To create a Rust module, install [`spacetime` CLI tool](https://spacetimedb.com/install) in your preferred shell. Navigate to your work directory and run the following command: + +```text +spacetime init --lang rust my-project-directory +``` + +This creates a Cargo project in `my-project-directory` with the following `Cargo.toml`: + +```text +[package] +name = "spacetime-module" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +spacetimedb = "1.0.0-rc2" +log = "0.4" +``` + + +This is a standard `Cargo.toml`, with the exception of the line `crate-type = ["cdylib"]`. +This line is important: it allows the project to be compiled to a WebAssembly module. + +The project's `lib.rs` will contain the following skeleton: + +```rust +use spacetimedb::{ReducerContext, Table}; + +#[spacetimedb::table(name = person)] +pub struct Person { + name: String +} + +#[spacetimedb::reducer(init)] +pub fn init(_ctx: &ReducerContext) { + // Called when the module is initially published +} + +#[spacetimedb::reducer(client_connected)] +pub fn identity_connected(_ctx: &ReducerContext) { + // Called everytime a new client connects +} + +#[spacetimedb::reducer(client_disconnected)] +pub fn identity_disconnected(_ctx: &ReducerContext) { + // Called everytime a client disconnects +} + +#[spacetimedb::reducer] +pub fn add(ctx: &ReducerContext, name: String) { + ctx.db.person().insert(Person { name }); +} + +#[spacetimedb::reducer] +pub fn say_hello(ctx: &ReducerContext) { + for person in ctx.db.person().iter() { + log::info!("Hello, {}!", person.name); + } + log::info!("Hello, World!"); +} +``` + +This skeleton declares a [table](#tables), some [reducers](#reducers), and some [lifecycle reducers](#lifecycle-annotations). + +To compile the project, run the following command: + +```text +spacetime build +``` + +SpacetimeDB requires a WebAssembly-compatible Rust toolchain. If the `spacetime` cli finds a compatible version of [`rustup`](https://rustup.rs/) that it can run, it will automatically install the `wasm32-unknown-unknown` target and use it to build your application. This can also be done manually using the command: + +```text +rustup target add wasm32-unknown-unknown +``` + +If you are managing your Rust installation in some other way, you will need to install the `wasm32-unknown-unknown` target yourself. + +To build your application and upload it to the public SpacetimeDB network, run: + +```text +spacetime login +spacetime publish +``` + +When you publish your module, a database will be created with the requested tables, and the module will be installed inside it. + +The output of `spacetime publish` will end with a line: +```text +Created new database with identity: +``` + +This hex string is the [`Identity`] of the created database. It distinguishes the created database from the other databases running on the SpacetimeDB network. It is used when administering the module, for example using the [`spacetime logs `](#the-log-crate) command. You should save it to a text file so that you can remember it. + +After modifying your project, you can run: + +`spacetime publish ` + +to update the module attached to your database. Note that SpacetimeDB tries to [automatically migrate](#automatic-migrations) your database schema whenever you run `spacetime publish`. + +You can also generate code for clients of your module using the `spacetime generate` command. See the [client SDK documentation] for more information. + +## How it works + +Under the hood, SpacetimeDB modules are WebAssembly modules that import a [specific WebAssembly ABI](https://spacetimedb.com/docs/webassembly-abi) and export a small number of special functions. This is automatically configured when you add the `spacetime` crate as a dependency of your application. + +The SpacetimeDB host is an application that hosts SpacetimeDB databases. It is [source available](https://github.com/clockworklabs/SpacetimeDB). You can run your own host, or you can upload your module to the public SpacetimeDB network. The SpacetimeDB host knows how to load SpacetimeDB modules and install them into databases. + +#### In More Detail: Publishing a Module + +The `spacetime publish [DATABASE_IDENTITY]` command compiles a module and uploads it to a SpacetimeDB host. After this: +- The host finds the database with the requested `DATABASE_IDENTITY`. + - (Or creates a fresh database and identity, if no identity was provided). +- The host loads the new module and inspects its requested database schema. If there are changes to the schema, the host tries perform an [automatic migration](#automatic-migrations). If the migration fails, publishing fails. +- The host terminates the old module attached to the database. +- The host installs the new module into the database. It begins running the module's [lifecycle reducers](#lifecycle-annotations) and [scheduled reducers](#scheduled-reducers). +- The host begins allowing clients to call the module's reducers. + +From the perspective of clients, this process is mostly seamless. Open connections are maintained and subscriptions continue functioning. [Automatic migrations](#automatic-migrations) forbid most table changes except for adding new tables, so client code does not need to be recompiled. +However: +- Clients may witness a brief interruption in the execution of scheduled reducers (for example, game loops.) +- New versions of a module may remove or change reducers that were previously present. Client code calling those reducers will receive runtime errors. + + +## Tables + +Tables are declared using the [`#[table(name = table_name)]` macro](https://docs.rs/spacetimedb/latest/spacetimedb/attr.table.html). + +This macro is applied to a Rust struct with named fields. All of the fields of the table must implement [`SpacetimeType`]. + +The resulting type is used to store rows of the table. It is normal struct type. Row values are not special -- operations on row types do not, by themselves, modify the table. Instead, a [`ReducerContext`](#reducercontext) is needed to get a handle to the table. + +```rust +use spacetimedb::{table, reducer, ReducerContext, Table}; + +/// A `Person` is a row of the table `people`. +#[table(name = people, public)] +pub struct Person { + #[unique] + #[auto_inc] + id: u64, + #[index(btree)] + name: String, +} + +// `Person` is a normal Rust struct type. +// Operations on a `Person` do not, by themselves, do anything. +// The following function does not interact with the database at all. +fn do_nothing() { + // Creating a `Person` DOES NOT modify the database. + let mut person = Person { id: 0, name: "Joe Average".to_string() }; + // Updating a `Person` DOES NOT modify the database. + person.name = "Joanna Average".to_string(); + // Dropping a `Person` DOES NOT modify the database. + drop(person); +} + +// To interact with the database, you need a `ReducerContext`. +// The first argument of a reducer is always a `ReducerContext`. +#[reducer] +fn do_something(ctx: &ReducerContext) { + // `ctx.db.{table_name}()` gets a handle to a database table. + let people = ctx.db.people(); + + // The following inserts a row into the table: + let mut person = people.insert(Person { id: 0, name: "Joe Average".to_string() }); + + // `person` is a COPY of the row stored in the database. + // If we update it: + person.name = "Joanna Average".to_string(); + // Our copy is now updated, but the database's copy is UNCHANGED. + // To push our change through, we can call an `update_by_...` function: + person = people.update_by_id(person); + // Now the database and our copy are in sync again. + + // We can also delete the row in the database using a `delete_by_...`. + people.delete_by_id(person.id); +} +``` + +See the [reducers](#reducers) section for more information on the `#[reducer]` macro. +See the [table macro documentation](`table`) for more information on declaring and using tables. + +#### Public and Private tables + +By default, tables are considered **private**. This means that they are only readable by the table owner and by reducers. Reducers run inside the database, so clients cannot see private tables at all. + +The `#[table(name = table_name, public)]` macro makes a table public. **Public** tables are readable by all clients. They can still only be modified by reducers. + +```rust +use spacetimedb::table; + +// The `players` table can be read by all connected clients. +#[table(name = players, public)] +pub struct Player { + /* ... */ +} + +// The `loot_items` table is invisible to clients, but not to reducers. +#[table(name = loot_items)] +pub struct LootItem { + /* ... */ +} +``` + + + +To learn how to subscribe to a public table, see the [client SDK documentation](https://spacetimedb.com/docs/sdks). + +## Reducers + +Reducers are declared using the [`#[reducer]` macro](`reducer`). + +`#[reducer]` is always applied to top level Rust functions. Arguments of reducers must implement [`SpacetimeType`]. Reducers can either return nothing, or return a `Result<(), E>`, where `E` implements `Debug`. + +```rust +use spacetimedb::{reducer, ReducerContext}; + +#[derive(Debug)] +enum GiveItemError { + NoSuchItem(u64), + NonTransferable(u64) +} + +#[reducer] +fn give_player_item(ctx: &ReducerContext, player_id: u64, item_id: u64) -> Result<(), GiveItemError> { + /* ... */ +} +``` + +Every reducer runs inside a [database transaction](https://en.wikipedia.org/wiki/Database_transaction). This means that reducers will not observe the effects of other reducers modifying the database while they run. Also, if a reducer fails, all of its changes to the database will automatically be rolled back. Reducers can fail by [panicking](`std::panic`) or by returning an `Err`. + +#### The `ReducerContext` Type + +Reducers have access to a special [`ReducerContext`] argument. This argument allows reading and writing the database attached to a module. It also provides some additional functionality, like generating random numbers and scheduling future operations. + +The most important field of [`ReducerContext`] is `.db`. This field provides a [local view](`Local`) of the module's database. The `#[table]` macro generates traits that add accessor methods to this field. + +To see all of the available methods on `ctx.db`, run `cargo doc` in your module's directory, and navigate to the `spacetimedb::Local` struct in the generated documentation. This will be at the path: +- `[your_project_directory]/target/doc/spacetimedb/struct.Local.html` (non-Windows) +- `[your_project_directory]\target\doc\spacetimedb\struct.Local.html` (Windows) + +#### The `log` crate + +SpacetimeDB Rust modules have built-in support for the [log crate](https://docs.rs/log/latest/log/index.html). All modules automatically install a suitable logger when they are first loaded by SpacetimeDB. (At time of writing, this happens [here](https://github.com/clockworklabs/SpacetimeDB/blob/e9e287b8aab638ba6e8bf9c5d41d632db041029c/crates/bindings/src/logger.rs)). Log macros can be used anywhere in module code, and log outputs of a running module can be inspected using the `spacetime logs` command: + +```text +spacetime logs +``` + + +#### Lifecycle annotations + +#### Scheduled reducers + +In addition to life cycle annotations, reducers can be made **scheduled**. +This allows calling the reducers at a particular time, or in a loop. +This can be used for game loops. + +The scheduling information for a reducer is stored in a table. +This table has two mandatory fields: +- A primary key that identifies scheduled reducer calls. +- A [`ScheduleAt`] field that says when to call the reducer. +Managing timers with a scheduled table is as simple as inserting or deleting rows from the table. + +A [`ScheduleAt`] can be created from a [`Timestamp`], in which case the reducer will be scheduled once, +or from a [`std::time::Duration`], in which case the reducer will be scheduled in a loop. In either case the conversion can be performed using [`Into::into`]. + +```rust +use spacetime::{table, reducer, Timestamp, ScheduleAt, Table} +use std::time::Duration; +use log::debug; + +// First, we declare the table with scheduling information. + +#[table(name = send_message_timer, scheduled(send_message))] +struct SendMessageSchedule { + // Mandatory fields: + // ============================ + + /// An identifier for the scheduled reducer call. + #[primary_key] + #[autoinc] + scheduled_id: u64, + + /// Information about when the reducer should be called. + #[scheduled_at] + scheduled_at: ScheduleAt, + + // After the mandatory fields, any number of fields can be added. + // These can be used to provide extra information to the scheduled reducer. + + // Custom fields: + // ============================ + + /// The text of the scheduled message to send. + text: String, +} + +// Then, we declare the scheduled reducer. +// The first argument of the reducer should be, as always, a `&ReducerContext`. +// The second argument should be a row of the scheduling information table. + +#[reducer] +fn send_message(ctx: &ReducerContext, arg: SendMessageSchedule) -> Result<(), String> { + let message_to_send = arg.text; + + // ... send the message .. +} + +// Now, we want to actually start scheduling reducers. +// It's convenient to do this inside the `init` reducer. +#[reducer(init)] +fn init(ctx: &ReducerContext) { + + let current_time = ctx.timestamp; + + let future_timestamp: Timestamp = ctx.timestamp.plus(Duration::from_secs(10)).into(); + ctx.db.send_message_timer().insert(SendMessageTimer { + scheduled_id: 1, + text:"I'm a bot sending a message one time".to_string(), + scheduled_at: future_timestamp.into() + }); + + let loop_duration: Duration = Duration::from_secs(10); + ctx.db.send_message_timer().insert(SendMessageTimer { + scheduled_id: 0, + text:"I'm a bot sending a message every 10 seconds".to_string(), + scheduled_at: loop_duration.into() + }); +} +``` + +## Automatic migrations + + + + +[macro library]: https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/bindings-macro +[module library]: https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/lib +[demo]: /#demo +[clients]: https://spacetimedb.com/docs/#client +[client SDK documentation]: https://spacetimedb.com/docs/#client +[host]: https://spacetimedb.com/docs/#host \ No newline at end of file diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index 7228aab8c87..fe7b0b2ad54 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -1,5 +1,4 @@ -//! Provides safe abstractions around `bindings-sys` -//! and re-exports `#[spacetimedb]` and `#[duration]`. +#![doc = include_str!("../README.md")] pub mod log_stopwatch; mod logger; @@ -24,7 +23,6 @@ pub use rng::StdbRng; pub use sats::SpacetimeType; #[doc(hidden)] pub use spacetimedb_bindings_macro::__TableHelper; -pub use spacetimedb_bindings_macro::{duration, filter, reducer, table}; pub use spacetimedb_bindings_sys as sys; pub use spacetimedb_lib; pub use spacetimedb_lib::de::{Deserialize, DeserializeOwned}; @@ -41,13 +39,627 @@ pub use timestamp::Timestamp; pub type ReducerResult = core::result::Result<(), Box>; -/// A context that any reducer is provided with. +pub use spacetimedb_bindings_macro::duration; + +/// Generates code for registering a row-level security `SQL` function. +/// +/// A row-level security function takes a `SQL` query expression that is used to filter rows. +/// +/// +/// The query follows the same syntax as a subscription query. +/// +/// **Example:** +/// +/// ```rust,ignore +/// /// Players can only see what's in their chunk +/// spacetimedb::filter!(" +/// SELECT * FROM LocationState WHERE chunk_index IN ( +/// SELECT chunk_index FROM LocationState WHERE entity_id IN ( +/// SELECT entity_id FROM UserState WHERE identity = @sender +/// ) +/// ) +/// "); +/// ``` +/// +/// **NOTE:** The `SQL` query expression is pre-parsed at compile time, but this only checks +/// that it is a valid subscription query *syntactically*, not that the query is valid when executed. +/// +/// For example, it could refer to a non-existent table. +#[doc(inline)] +pub use spacetimedb_bindings_macro::filter; + +/* +### Generated table functions + + + +We'll work off these structs to see what functions SpacetimeDB generates: + +This table has a plain old column. + +```rust +#[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. + +```rust +#[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`. + +```rust +#[table(name = autoinc, public)] +struct Autoinc { + #[autoinc] + autoinc_field: u64, +} +``` + +These attributes can be combined, to create an automatically assigned ID usable for filtering. + +```rust +#[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. + +```rust +#[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. + +```rust +#[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. + +```rust +#[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. + +```rust +#[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: + +```rust +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. + +```rust +# #[table(name = person, public)] +# struct Person { +# #[unique] +# id: u64, +# +# #[index(btree)] +# age: u32, +# name: String, +# address: String, +# } +#[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` 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. + +```rust +#[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` rather than an `Option`. + +```rust +#[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: +> ```text +> error[E0271]: type mismatch resolving `::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::::filter` +> | +> 410 | pub fn filter(&self, b: B) -> impl Iterator +> | ------ required by a bound in this associated function +> 411 | where +> 412 | B: BTreeIndexBounds, +> | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BTreeIndex::::filter` +> ``` + +#### Deleting + +Like filtering, we can delete by an indexed or unique column instead of the entire row. + +```rust +#[reducer] +fn delete_id(ctx: &ReducerContext, id: u64) { + ctx.db.person().id().delete(id) +} +``` + + */ + +/// Declares a table with a particular row format. +/// +/// This attribute is applied to a struct type. +/// This derives [`Serialize`], [`Deserialize`], [`SpacetimeType`], and [`Debug`] for the annotated type. +/// +/// Elements of the struct type are NOT automatically inserted into any global table. +/// They are regular structs, with no special behavior. +/// In particular, modifying them does not automatically modify the database! +/// +/// Instead, a struct implementing [`Table`] is generated. This can be looked up in a [`ReducerContext`] +/// using `ctx.db().table_name()`. This method represents a handle to a database table, and can be used to +/// iterate and modify the table's elements. It is a view of the entire table -- the entire set of rows at the time of the reducer call. +/// +/// # Example +/// +/// ```ignore +/// use spacetimedb::{table, ReducerCtx}; +/// use log::debug; +/// +/// #[table(name = users, public, +/// index(name = id_and_username, btree(id, username)))] +/// pub struct User { +/// #[auto_inc] +/// #[primary_key] +/// pub id: u32, +/// #[unique] +/// pub username: String, +/// #[index(btree)] +/// pub popularity: u32, +/// } +/// +/// fn demo(ctx: &ReducerCtx) { +/// // Use the *name* of the table to get a struct +/// // implementing `spacetimedb::Table`. +/// let users = ctx.db().users(); +/// +/// // You can use methods from `spacetimedb::Table` +/// // on the table. +/// debug!("User count: {}", users.count()); +/// for user in users.iter() { +/// debug!("{:?}", user); +/// } +/// +/// // For every named `index`, the table has an extra method +/// // for getting a corresponding `spacetimedb::BTreeIndex`. +/// let by_id_and_username: spacetimedb::BTreeIndex<_, (u32, String), _> = +/// users.id_and_username(); +/// by_id_and_username.delete((&57, &"Billy".to_string())); +/// +/// // For every `#[unique]` or `#[primary_key]` field, +/// // the table has an extra method that allows getting a +/// // corresponding `spacetimedb::UniqueColumn`. +/// let by_username: spacetimedb::UniqueColumn<_, String, _> = users.id(); +/// by_username.delete(&"test_user".to_string()); +/// } +/// ``` +/// +/// # Macro arguments +/// +/// The `#[table(...)]` attribute accepts any number of the following arguments, separated by commas: +/// +/// * `name = my_table` +/// +/// Specify the name of the table in the database, if you want it to be different from +/// the name of the struct. +/// Multiple `table` annotations can be present on the same type. This will generate +/// multiple tables of the same row type, but with different names. +/// +/// * `public` and `private` +/// +/// Tables are private by default. If you'd like to make your table publically +/// accessible by anyone, put `public` in the macro arguments (e.g. +/// `#[spacetimedb::table(public)]`). You can also specify `private` if +/// you'd like to be specific. This is fully separate from Rust's module visibility +/// system; `pub struct` or `pub(crate) struct` do not affect the table visibility, only +/// the visibility of the items in your own source code. +/// +/// * `index(name = my_index, btree(columns = [a, b, c]))` +/// +/// You can specify an index on one or more of the table's columns with the above syntax. +/// You can also just put `#[index(btree)]` on the field itself if you only need +/// a single-column attribute; see column attributes below. +/// Multiple indexes are permitted. +/// +/// * `scheduled(reducer_name)` +/// +/// Scheduled [reducers](macro@crate::reducer) need a table storing scheduling information. +/// The rows of this table store all information needed when invoking a scheduled reducer. +/// This can be any information you want, but we require that the tables store at least an +/// invocation ID field and timestamp field. +/// +/// The corresponding reducer should accept a single argument +/// +/// These can be declared like so: +/// +/// ```ignore +/// #[table(name = train_schedule, scheduled(run_train))] +/// pub struct TrainSchedule { +/// // Required fields. +/// #[primary_key] +/// #[auto_inc] +/// scheduled_id: u64, +/// #[scheduled_at] +/// scheduled_at: spacetimedb::ScheduleAt, +/// +/// // Any other fields needed. +/// train: TrainID, +/// source_station: StationID, +/// target_station: StationID +/// } +/// +/// #[reducer] +/// pub fn run_train(ctx: &ReducerCtx, schedule: TrainSchedule) { +/// /* ... */ +/// } +/// ``` +/// +/// # Column (field) attributes +/// +/// * `#[auto_inc]` +/// +/// Creates an auto-increment constraint. +/// +/// When a row is inserted with the annotated field set to `0` (zero), +/// the sequence is incremented, and this value is used instead. +/// Can only be used on numeric types and may be combined with indexes. +/// +/// Note that using `#[auto_inc]` on a field does not also imply `#[primary_key]` or `#[unique]`. +/// If those semantics are desired, those attributes should also be used. +/// +/// * `#[unique]` +/// +/// Creates an index and unique constraint for the annotated field. +/// +/// * `#[primary_key]` +/// +/// Similar to `#[unique]`, but generates additional CRUD methods. +/// +/// * `#[index(btree)]` +/// +/// Creates a single-column index with the specified algorithm. +/// +/// * `#[scheduled_at]` +/// +/// Used in scheduled reducer tables, see above. +/// +/// * `#[scheduled_id]` +/// +/// Used in scheduled reducer tables, see above. +/// +/// # Generated code +/// +/// For each `[table(name = {name})]` annotation on a type `{T}`, generates a struct +/// `{name}Handle` implementing `Table`, and a trait that allows looking up such a +/// `{name}Handle` in a `ReducerContext`. +/// +/// The struct `{name}Handle` is hidden in an anonymous scope and cannot be accessed. +/// +/// For each named index declaration, add a method to `{name}Handle` for getting a corresponding +/// `BTreeIndex`. +/// +/// For each field with a `#[unique]` or `#[primary_key]` annotation, +/// add a method to `{name}Handle` for getting a corresponding `UniqueColumn`. +/// +/// The following pseudocode illustrates the general idea. Curly braces are used to indicate templated +/// names. +/// +/// ```ignore +/// use spacetimedb::{BTreeIndex, UniqueColumn, Table, DbView}; +/// +/// // This generated struct is hidden and cannot be directly accessed. +/// struct {name}Handle { /* ... */ }; +/// +/// // It is a table handle. +/// impl Table for {name}Handle { +/// type Row = {T}; +/// /* ... */ +/// } +/// +/// // It can be looked up in a `ReducerContext`, +/// // using `ctx.db().{name}()`. +/// trait {name} { +/// fn {name}(&self) -> Row = {T}>; +/// } +/// impl {name} for ::DbView { /* ... */ } +/// +/// // Once looked up, it can be used to look up indexes. +/// impl {name}Handle { +/// // For each `#[unique]` or `#[primary_key]` field `{field}` of type `{F}`: +/// fn {field}(&self) -> UniqueColumn<_, {F}, _> { /* ... */ }; +/// +/// // For each named index `{index}` on fields of type `{(F1, ..., FN)}`: +/// fn {index}(&self) -> BTreeIndex<_, {(F1, ..., FN)}, _>; +/// } +/// ``` +/// +/// [`Table`]: `Table` +#[doc(inline)] +pub use spacetimedb_bindings_macro::table; + +/// Marks a function as a spacetimedb reducer. +/// +/// A reducer is a function with read/write access to the database +/// that can be invoked remotely by [clients]. +/// +/// Each reducer call runs in its own database transaction, +/// and its updates to the database are only committed if the reducer returns successfully. +/// +/// The first argument of a reducer is always a [`&ReducerContext`]. This context object +/// allows accessing the database and viewing information about the caller, among other things. +/// +/// After this, a reducer can take any number of arguments. +/// These arguments must implement the [`SpacetimeType`], [`Serialize`], and [`Deserialize`] traits. +/// All of these traits can be derived at once by marking a type with `#[derive(SpacetimeType)]`. +/// +/// Reducers may return either `()` or `Result<(), E>` where `E: Debug`. +/// +/// ```rust,ignore +/// use spacetimedb::reducer; +/// use log::info; +/// +/// #[reducer] +/// pub fn hello_world(context: &ReducerContext) { +/// info!("Hello, World!"); +/// } +/// +/// #[reducer] +/// pub fn add_person(context: &ReducerContext, name: String, age: u16) { +/// // add a "person" to the database. +/// } +/// +/// #[derive(SpacetimeType)] +/// struct Coordinates { +/// x: f32, +/// y: f32, +/// } +/// +/// #[derive(Debug)] +/// enum AddPlaceError { +/// InvalidCoordinates(Coordinates), +/// InvalidName(String), +/// } +/// +/// #[reducer] +/// pub fn add_place( +/// context: &ReducerContext, +/// name: String, +/// x: f32, +/// y: f32, +/// area: f32, +/// ) -> Result<(), AddPlaceError> { +/// // add a "place" to the database. +/// } +/// ``` +/// +/// Reducers may fail by returning a [`Result::Err`](`Result`) or by [panicking](`panic`). +/// Such a failure will be printed to the module logs and abort the active database transaction. +/// Any changes to the database will be rolled back. +/// +/// Reducers are limited in their ability to interact with the outside world. +/// They do not directly return data aside from errors, and have no access to any +/// network or filesystem interfaces. +/// Calling methods from [`std::io`], [`std::net`], or [`std::fs`] +/// inside a reducer will result in runtime errors. +/// +/// Reducers can communicate information to the outside world in two ways: +/// - They can modify tables in the database. +/// See the `#[table]`(#table) macro documentation for information on how to declare and use tables. +/// - They can call logging macros from the [`log`] crate. +/// This writes to a private debug log attached to the database. +/// Run `spacetime logs ` to browse these. +/// +/// Reducers are permitted to call other reducers, simply by passing their `ReducerContext` as the first argument. +/// This is a regular function call, and does not involve any network communication. The callee will run within the +/// caller's transaction, and any changes made by the callee will be committed or rolled back with the caller. +/// +/// # Lifecycle Reducers +/// +/// You can specify special lifecycle reducers that are run at set points in +/// the module's lifecycle. You can have one of each per module. +/// +/// ## `#[spacetimedb::reducer(init)]` +/// +/// This reducer is run the first time a module is published +/// and anytime the database is cleared. +/// +/// The reducer cannot be called manually +/// and may not have any parameters except for `ReducerContext`. +/// If an error occurs when initializing, the module will not be published. +/// +/// ## `#[spacetimedb::reducer(client_connected)]` +/// +/// This reducer is run when a client connects to the SpacetimeDB module. +/// Their identity can be found in the sender value of the `ReducerContext`. +/// +/// The reducer cannot be called manually +/// and may not have any parameters except for `ReducerContext`. +/// If an error occurs in the reducer, the client will be disconnected. +/// +/// ## `#[spacetimedb::reducer(client_disconnected)]` +/// +/// This reducer is run when a client disconnects from the SpacetimeDB module. +/// Their identity can be found in the sender value of the `ReducerContext`. +/// +/// The reducer cannot be called manually +/// and may not have any parameters except for `ReducerContext`. +/// If an error occurs in the disconnect reducer, +/// the client is still recorded as disconnected. +/// +/// ## `#[spacetimedb::reducer(update)]` +/// +/// This reducer is run when the module is updated, +/// i.e., when publishing a module for a database that has already been initialized. +/// +/// The reducer cannot be called manually and may not have any parameters. +/// If an error occurs when initializing, the module will not be published, +/// and the previous version of the module attached to the database will continue executing. +/// +/// # Scheduled reducers +/// +/// Reducers can be scheduled to run repeatedly. +/// +/// +/// [`&ReducerContext`]: `ReducerContext` +/// [clients]: https://spacetimedb.com/docs/#client +#[doc(inline)] +pub use spacetimedb_bindings_macro::reducer; + +#[doc(inline)] +/// Trait that allows looking up methods on a table. +/// +/// This trait associates a [table handle](`Table`) type to a table row type. Code like: +/// +/// ```rust +/// #[spacetimedb::table(name = people)] +/// struct Person { +/// #[unique] +/// #[auto_inc] +/// id: u64, +/// name: String, +/// } +/// ``` +/// +/// will generate accessors that allow looking up the `people` table in a `ReducerContext`. +/// +pub use table::__MapRowTypeToTable; + +/// The context that any reducer is provided with. +/// +/// This must be the first argument of the reducer. Clients of the module will +/// only see arguments after the `ReducerContext`. +/// +/// Includes information about the client calling the reducer and the time of invocation, +/// as well as a view into the module's database. +/// +/// If the crate was compiled with the `rand` feature, also includes faculties for random +/// number generation. +/// +/// Implements the `DbContext` trait for accessing views into a database. +/// Currently, being this generic is only meaningful in clients, +/// as `ReducerContext` is the only implementor of `DbContext` within modules. #[non_exhaustive] pub struct ReducerContext { /// The `Identity` of the client that invoked the reducer. pub sender: Identity, + /// The time at which the reducer was started. pub timestamp: Timestamp, + /// The `Address` of the client that invoked the reducer. /// /// `None` if no `Address` was supplied to the `/database/call` HTTP endpoint, @@ -56,6 +668,15 @@ pub struct ReducerContext { /// For automatic reducers, i.e. `init`, `update` and scheduled reducers, /// this will be the module's `Address`. pub address: Option
, + + /// Allows accessing the local database attached to a module. + /// + /// This slightly strange type appears to have no methods, but that is misleading. + /// The `#[table]` macro uses the trait system to add table accessors to this type. + /// These are generated methods that allow you to access specific tables. + /// + /// Run `cargo doc` in your SpacetimeDB module project and browse the generated documentation + /// to see the methods have been automatically added to this type. pub db: Local, #[cfg(feature = "rand")] @@ -89,7 +710,7 @@ pub trait DbContext { /// This method is provided for times when a programmer wants to be generic over the `DbContext` type. /// Concrete-typed code is expected to read the `.db` field off the particular `DbContext` implementor. /// Currently, being this generic is only meaningful in clients, - /// as modules have only a single implementor of `DbContext`. + /// as `ReducerContext` is the only implementor of `DbContext` within modules. fn db(&self) -> &Self::DbView; } @@ -101,6 +722,17 @@ impl DbContext for ReducerContext { } } +/// Allows accessing the local database attached to the module. +/// +/// This slightly strange type appears to have no methods, but that is misleading. +/// The `#[table]` macro uses the trait system to add table accessors to this type. +/// These are generated methods that allow you to access specific tables. +/// +/// Run `cargo doc` in your Rust module project and navigate to this type +/// to see the methods have been automatically added. It will be at the path: +/// `[your_project_directory]/target/doc/spacetimedb/struct.Local.html`. +/// (or, `[your_project_directory]\target\doc\spacetimedb\struct.Local.html` on Windows.) +/// #[non_exhaustive] pub struct Local {} diff --git a/crates/bindings/src/table.rs b/crates/bindings/src/table.rs index c6c1e23b255..99780d7b61e 100644 --- a/crates/bindings/src/table.rs +++ b/crates/bindings/src/table.rs @@ -149,7 +149,6 @@ pub struct ScheduleDesc<'a> { pub scheduled_at_column: u16, } -#[doc(hidden)] pub trait __MapRowTypeToTable { type Table: Table; } @@ -268,6 +267,34 @@ pub trait Column { fn get_field(row: &Self::Row) -> &Self::ColType; } +/// A handle to a unique index on a column. +/// Only available for `#[unique]` or `#[primary_key]` columns. +/// To get one of these, use the accessors generated by the `#[table]` macro. +/// +/// Example: +/// +/// ```no_run +/// use spacetimedb::{table, UniqueColumn, ReducerContext, DbContext}; +/// +/// #[table(name = users)] +/// struct User { +/// #[primary_key] +/// id: u32, +/// #[unique] +/// username: String, +/// dog_count: u128 +/// } +/// +/// fn demo(ctx: &ReducerContext) { +/// let users = ctx.db().users(); +/// +/// let by_id: UniqueColumn<_, u32, _> = users.id(); +/// by_id.delete(&69); +/// +/// let by_username: UniqueColumn<_, String, _> = users.username(); +/// by_username.delete(&"Evil Bob".to_string()); +/// } +/// ``` pub struct UniqueColumn where ColType: SpacetimeType + Serialize + DeserializeOwned, @@ -379,6 +406,28 @@ pub trait Index { fn index_id() -> IndexId; } +/// A handle to a `btree` index on a table. +/// To get one of these, use the accessors generated by the `#[table]` macro. +/// +/// Example: +/// +/// ```no_run +/// use spacetimedb::{table, BTreeIndex, ReducerContext, DbContext}; +/// +/// #[table(name = users, +/// index(name = id_and_username, btree(columns = [id, username])))] +/// struct User { +/// id: u32, +/// username: String, +/// dog_count: u128 +/// } +/// +/// fn demo(ctx: &ReducerContext) { +/// let users = ctx.db().users(); +/// let by_id_and_username: BTreeIndex<_, (u32, String), _> = users.id_and_username(); +/// by_id_and_username.delete((&69u32, "Evil Bob")); +/// } +/// ``` pub struct BTreeIndex { _marker: PhantomData<(Tbl, IndexType, Idx)>, } diff --git a/crates/sats/src/de.rs b/crates/sats/src/de.rs index 974c959812a..cc4d821f748 100644 --- a/crates/sats/src/de.rs +++ b/crates/sats/src/de.rs @@ -542,7 +542,7 @@ pub trait DeserializeSeed<'de> { use crate::de::impls::BorrowedSliceVisitor; pub use spacetimedb_bindings_macro::Deserialize; -/// A **datastructure** that can be deserialized from any data format supported by SATS. +/// A **data structure** that can be deserialized from any data format supported by the SpacetimeDB Algebraic Type System. /// /// In most cases, implementations of `Deserialize` may be `#[derive(Deserialize)]`d. /// @@ -551,6 +551,9 @@ pub use spacetimedb_bindings_macro::Deserialize; /// /// The lifetime `'de` allows us to deserialize lifetime-generic types in a zero-copy fashion. /// +/// Do not manually implement this trait unless you know what you are doing. +/// Incorrect implementations are safe, but can result in data loss. +/// /// [`serde::Deserialize`]: ::serde::Deserialize /// [`serde`]: https://crates.io/crates/serde pub trait Deserialize<'de>: Sized { diff --git a/crates/sats/src/ser.rs b/crates/sats/src/ser.rs index e3a89ae0abc..20c95541c30 100644 --- a/crates/sats/src/ser.rs +++ b/crates/sats/src/ser.rs @@ -203,13 +203,17 @@ pub use spacetimedb_bindings_macro::Serialize; use crate::AlgebraicType; -/// A **data structure** that can be serialized into any data format supported by SATS. +/// A **data structure** that can be serialized into any data format supported by +/// the SpacetimeDB Algebraic Type System. /// /// In most cases, implementations of `Serialize` may be `#[derive(Serialize)]`d. /// /// The `Serialize` trait in SATS performs the same function as [`serde::Serialize`] in [`serde`]. /// See the documentation of [`serde::Serialize`] for more information of the data model. /// +/// Do not manually implement this trait unless you know what you are doing. +/// Incorrect implementations are safe, but can result in data loss. +/// /// [`serde::Serialize`]: ::serde::Serialize /// [`serde`]: https://crates.io/crates/serde pub trait Serialize { diff --git a/crates/sats/src/typespace.rs b/crates/sats/src/typespace.rs index cbddf79620a..1db566461a5 100644 --- a/crates/sats/src/typespace.rs +++ b/crates/sats/src/typespace.rs @@ -232,14 +232,65 @@ pub trait GroundSpacetimeType { fn get_type() -> AlgebraicType; } -/// A trait for Rust types that can be represented as an [`AlgebraicType`] -/// provided a typing context `typespace`. +/// This trait makes types self-describing, allowing them to automatically register their structure +/// with SpacetimeDB. This is used to tell SpacetimeDB about the structure of a module's tables and +/// reducers. +/// +/// Deriving this trait also derives [`Serialize`](crate::ser::Serialize), [`Deserialize`](crate::de::Deserialize), +/// and [`Debug`](std::fmt::Debug). (There are currently no trait bounds on `SpacetimeType` documenting this fact.) +/// These traits are used to convert Rust data structures to other formats, suitable for storing on disk or passing over the network. `Debug` is simply for debugging convenience. +/// +/// Any Rust type implementing `SpacetimeType` can be used in table and reducer declarations. A derive macro is provided, and can be used on both structs and enums: +/// +/// ```rust +/// use spacetimedb::SpacetimeType; +/// +/// #[derive(SpacetimeType)] +/// struct Location { +/// x: u64, +/// y: u64 +/// } +/// +/// #[derive(SpacetimeType)] +/// enum FruitCrate { +/// Apples { variety: String, count: u32, freshness: u32 }, +/// Plastic { count: u32 } +/// } +/// ``` +/// +/// The fields of the struct/enum must also implement `SpacetimeType`. +/// +/// Any type annotated with `#[table(..)]` automatically derives `SpacetimeType`. +/// +/// SpacetimeType is implemented for many of the primitive types in the standard library: +/// +/// - `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 where T: SpacetimeType` +/// - `Vec where T: SpacetimeType` +/// +/// (Storing collections in rows of a database table is a form of [denormalization](https://en.wikipedia.org/wiki/Denormalization).) +/// +/// Do not manually implement this trait unless you are VERY sure you know what you're doing. Incorrect implementations can result in data loss! +/// +/// N.B.: It's `SpacetimeType`, not `SpaceTimeType`. // TODO: we might want to have a note about what to do if you're trying to use a type from another crate in your table. // keep this note in sync with the ones on spacetimedb::rt::{ReducerArg, TableColumn} #[diagnostic::on_unimplemented(note = "if you own the type, try adding `#[derive(SpacetimeType)]` to its definition")] pub trait SpacetimeType { /// Returns an `AlgebraicType` representing the type for `Self` in SATS - /// and in the typing context in `typespace`. + /// and in the typing context in `typespace`. This is used by the + /// automatic type registration system in Rust modules. + /// + /// The resulting `AlgebraicType` may contain `Ref`s that only make sense + /// within the context of this particular `typespace`. fn make_type(typespace: &mut S) -> AlgebraicType; }