diff --git a/crates/bindings-macro/src/lib.rs b/crates/bindings-macro/src/lib.rs index 1dddf12834b..1ebc56593c5 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 reducer; mod sats; mod table; @@ -96,76 +103,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| { @@ -190,77 +127,6 @@ fn derive_table_helper_attr() -> Attribute { .unwrap() } -/// 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 @@ -376,37 +242,6 @@ pub fn schema_type(input: StdTokenStream) -> StdTokenStream { }) } -/// Generates code for registering a row-level security rule. -/// -/// This attribute must be applied to a `const` binding of type [`Filter`]. -/// It will be interpreted as a filter on the table to which it applies, for all client queries. -/// If a module contains multiple `client_visibility_filter`s for the same table, -/// they will be unioned together as if by SQL `OR`, -/// so that any row permitted by at least one filter is visible. -/// -/// The `const` binding's identifier must be unique within the module. -/// -/// The query follows the same syntax as a subscription query. -/// -/// ## Example: -/// -/// ```rust,ignore -/// /// Players can only see what's in their chunk -/// #[spacetimedb::client_visibility_filter] -/// const PLAYERS_SEE_ENTITIES_IN_SAME_CHUNK: Filter = Filter::Sql(" -/// SELECT * FROM LocationState WHERE chunk_index IN ( -/// SELECT chunk_index FROM LocationState WHERE entity_id IN ( -/// SELECT entity_id FROM UserState WHERE identity = @sender -/// ) -/// ) -/// "); -/// ``` -/// -/// Queries are not checked for syntactic or semantic validity -/// until they are processed by the SpacetimeDB host. -/// This means that errors in queries, such as syntax errors, type errors or unknown tables, -/// will be reported during `spacetime publish`, not at compile time. -#[doc(hidden)] // TODO: RLS filters are currently unimplemented, and are not enforced. #[proc_macro_attribute] pub fn client_visibility_filter(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream { ok_or_compile_error(|| { diff --git a/crates/bindings-sys/src/lib.rs b/crates/bindings-sys/src/lib.rs index 7ce6e3b98fe..2ff32345211 100644 --- a/crates/bindings-sys/src/lib.rs +++ b/crates/bindings-sys/src/lib.rs @@ -520,7 +520,7 @@ pub mod raw { pub fn bytes_source_read(source: BytesSource, buffer_ptr: *mut u8, buffer_len_ptr: *mut usize) -> i16; /// Logs at `level` a `message` message occuring in `filename:line_number` - /// with [`target`](target) being the module path at the `log!` invocation site. + /// with `target` being the module path at the `log!` invocation site. /// /// These various pointers are interpreted lossily as UTF-8 strings with a corresponding `_len`. /// @@ -592,7 +592,7 @@ pub mod raw { /// What strategy does the database index use? /// - /// See also: https://www.postgresql.org/docs/current/sql-createindex.html + /// See also: #[repr(u8)] #[non_exhaustive] pub enum IndexType { @@ -885,7 +885,7 @@ pub fn datastore_delete_all_by_eq_bsatn(table_id: TableId, relation: &[u8]) -> R /// Starts iteration on each row, as BSATN-encoded, of a table identified by `table_id`. /// Returns iterator handle is written to the `out` pointer. -/// This handle can be advanced by [`row_iter_bsatn_advance`]. +/// This handle can be advanced by [`RowIter::read`]. /// /// # Errors /// @@ -920,7 +920,7 @@ pub fn datastore_table_scan_bsatn(table_id: TableId) -> Result { /// which is unique for the module. /// /// On success, the iterator handle is written to the `out` pointer. -/// This handle can be advanced by [`row_iter_bsatn_advance`]. +/// This handle can be advanced by [`RowIter::read`]. /// /// # Non-obvious queries /// diff --git a/crates/bindings/README.md b/crates/bindings/README.md new file mode 100644 index 00000000000..443e082086b --- /dev/null +++ b/crates/bindings/README.md @@ -0,0 +1,570 @@ +# SpacetimeDB Rust Module Library + + + +[SpacetimeDB](https://spacetimedb.com/) allows using the Rust language to write server-side applications called **modules**. Modules run inside a relational 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) are functions that 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 reducer will result in runtime errors. + +Declaring tables and reducers is straightforward: + +```no_run +# #[cfg(target_arch = "wasm32")] mod demo { +use spacetimedb::{table, reducer, ReducerContext, Table}; + +#[table(name = player)] +pub struct Player { + id: u32, + name: String +} + +#[reducer] +fn add_person(ctx: &ReducerContext, id: u32, name: String) { + log::debug!("Inserting {name} with id {id}"); + ctx.db.player().insert(Player { id, name }); +} +# } +``` + + +Note that 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 the [`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: + +```no_run +# #[cfg(target_arch = "wasm32")] mod demo { +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-reducers). + +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 +``` + +And then: + +```text +spacetime publish [MY_DATABASE_NAME] +``` + +For example: + +```text +spacetime publish silly_demo_app +``` + +When you publish your module, a database named 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 name: , identity: +``` + +This name is the human-readable name of the created database, and the hex string is its [`Identity`]. These distinguish the created database from the other databases running on the SpacetimeDB network. They are used when administering the application, for example using the [`spacetime logs `](#the-log-crate) command. You should probably write the database name down in 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. [Its source code is available](https://github.com/clockworklabs/SpacetimeDB) under [the Business Source License with an Additional Use Grant](https://github.com/clockworklabs/SpacetimeDB/blob/master/LICENSE.txt). You can run your own host, or you can upload your module to the public SpacetimeDB network. The network will create a database for you and install your module in it to serve client requests. + +#### 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-reducers) and [scheduled reducers](#scheduled-reducers), starting with the [`#[init]` reducer](macro@crate::reducer#scheduled-reducers). +- The host begins allowing clients to call the module's reducers. + +From the perspective of clients, this process is 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](macro@crate::table). + +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. + +```no_run +# #[cfg(target_arch = "wasm32")] mod demo { +use spacetimedb::{table, reducer, ReducerContext, Table, UniqueColumn}; + +/// A `Person` is a row of the table `person`. +#[table(name = person, public)] +pub struct Person { + #[primary_key] + #[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 person: &person__TableHandle = ctx.db.person(); + + // The following inserts a row into the table: + let mut example_person = person.insert(Person { id: 0, name: "Joe Average".to_string() }); + + // `person` is a COPY of the row stored in the database. + // If we update it: + example_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 `UniqueColumn::update()`: + example_person = person.id().update(example_person); + // Now the database and our copy are in sync again. + + // We can also delete the row in the database using `UniqueColumn::delete()`. + person.id().delete(&example_person.id); +} +# } +``` + +(See [reducers](#reducers) for more information on declaring reducers.) + +This library generates a custom API for each table, depending on the table's name and structure. + +All tables support getting a handle implementing the [`Table`] trait from a [`ReducerContext`], using: + +```text +ctx.db.{table_name}() +``` + +For example, + +```no_build +ctx.db.person() +``` + +The [`Table`] trait provides: +- [`Table::insert`] +- [`Table::try_insert`] +- [`Table::delete`] +- [`Table::iter`] +- [`Table::count`] + +Tables' [constraints](#unique-and-primary-key-columns) and [indexes](#indexes) generate additional accessors. + + + +#### Public and Private Tables + +By default, tables are considered **private**. This means that they are only readable by the database owner and by reducers. Reducers run inside the database, so clients cannot see private tables at all. + +Using the [`#[table(name = table_name, public)]`](macro@crate::table) flag makes a table public. **Public** tables are readable by all clients. They can still only be modified by reducers. + +```no_run +# #[cfg(target_arch = "wasm32")] mod demo { +use spacetimedb::table; + +// The `enemies` table can be read by all connected clients. +#[table(name = enemy, public)] +pub struct Enemy { + /* ... */ +} + +// The `loot_items` table is invisible to clients, but not to reducers. +#[table(name = loot_item)] +pub struct LootItem { + /* ... */ +} +# } +``` + +(Note that, when run by the module owner, the `spacetime sql ` command can also read private tables. This is for debugging convenience. Only the module owner can see these tables. This is determined by the `Identity` stored by the `spacetime login` command. Run `spacetime login show` to print your current logged-in `Identity`.) + +To learn how to subscribe to a public table, see the [client SDK documentation](https://spacetimedb.com/docs/sdks). + +#### Unique and Primary Key Columns + +Columns of a table (that is, fields of a [`#[table]`](macro@crate::table) struct) can be annotated with [`#[unique]`](macro@crate::table#unique) or [`#[primary_key]`](macro@crate::table#primary_key). Multiple columns can be `#[unique]`, but only one can be `#[primary_key]`. For example: + +```no_run +# #[cfg(target_arch = "wasm32")] mod demo { +use spacetimedb::table; + +type SSN = String; +type Email = String; + +#[table(name = citizen)] +pub struct Citizen { + #[primary_key] + id: u64, + #[unique] + ssn: SSN, + #[unique] + email: Email, + name: String, +} +# } +``` + +Every row in the table `Person` must have unique entries in the `id`, `ssn`, and `email` columns. Attempting to insert multiple `Person`s with the same `id`, `ssn`, or `email` will fail. (Either via panic, with [`Table::insert`], or via a `Result::Err`, with [`Table::try_insert`].) + +Any `#[unique]` or `#[primary_key]` column supports getting a [`UniqueColumn`] from a [`ReducerContext`] using: + +```text +ctx.db.{table}().{unique_column}() +``` + +For example, + +```no_build +ctx.db.person().ssn() +``` + +[`UniqueColumn`] provides: +- [`UniqueColumn::find`] +- [`UniqueColumn::delete`] +- [`UniqueColumn::update`] + + +Notice that updating a row is only possible if a row has a unique column -- there is no `update` method in the base [`Table`] trait. SpacetimeDB has no notion of rows having an "identity" aside from their unique / primary keys. + +The `#[primary_key]` annotation is similar to the `#[unique]` annotation, except that it leads to additional methods being made available in the [client]-side SDKs. + +It is not currently possible to mark a group of fields as collectively unique. You can, however, derive `#[SpacetimeType]` on another struct, include that struct as a column, and mark it unique. + +Filtering on unique columns is only supported for a limited number of types. + +#### Auto-inc columns + +Columns can be marked [`#[auto_inc]`](macro@crate::table#auto_inc). This can only be used on integer types (`i32`, `u8`, etc.) + +When inserting into a table with an `#[auto_inc]` column, if the annotated column is set to zero (`0`), the database will automatically overwrite that zero with an atomically increasing value. + +[`Table::insert`] and [`Table::try_insert`] return rows with `#[auto_inc]` columns set to the values that were actually written into the database. + +```no_run +# #[cfg(target_arch = "wasm32")] mod demo { +use spacetimedb::{table, reducer, ReducerContext, Table}; + +#[table(name = example)] +struct Example { + #[auto_inc] + field: u32 +} + +#[reducer] +fn insert_auto_inc_example(ctx: &ReducerContext) { + for i in 1..=10 { + // These will have distinct, unique values + // at rest in the database, since they + // are inserted with the sentinel value 0. + let actual = ctx.db.example().insert(Example { field: 0 }); + assert!(actual.field != 0); + } +} +# } +``` + +`auto_inc` is often combined with `unique` or `primary_key` to automatically assign unique integer identifiers to rows. + +#### Indexes + +SpacetimeDB supports both single- and multi-column [B-Tree](https://en.wikipedia.org/wiki/B-tree) indexes. + +Indexes are declared using the syntax: + +[`#[table(..., index(name = my_index, btree(columns = [a, b, c]))]`](macro@crate::table#index). + +For example: + +```no_run +# #[cfg(target_arch = "wasm32")] mod demo { + +use spacetimedb::table; + +#[table(name = paper, index(name = url_and_country, btree(columns = [url, country])))] +struct Paper { + url: String, + country: String, + venue: String +} + +# } +``` + +Multiple indexes can be declared, separated by commas. + +Single-column indexes can also be declared using the + +[`#[index(btree)]`](macro@crate::table#indexbtree) + +column attribute. + +Any index supports getting a [`RangedIndex`] using [`ctx`](crate::ReducerContext)`.db.{table}().{index}()`. For example, `ctx.db.person().name()`. + +[`RangedIndex`] provides: + - [`RangedIndex::filter`] + - [`RangedIndex::delete`] + +## Reducers + +Reducers are declared using the [`#[reducer]` macro](macro@crate::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 [`std::fmt::Display`]. + +```no_run +# #[cfg(target_arch = "wasm32")] mod demo { +use spacetimedb::{reducer, ReducerContext}; +use std::fmt; + +#[reducer] +fn give_player_item( + ctx: &ReducerContext, + player_id: u64, + item_id: u64 +) -> Result<(), String> { + /* ... */ + # Ok(()) +} +# } +``` + +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. + +[`ReducerContext`] provides access to the database tables via [the `.db` field](ReducerContext#structfield.db). The [`#[table]`](macro@crate::table) macro generates traits that add accessor methods to this field. + + + +#### The `log` crate + +SpacetimeDB Rust modules have built-in support for the [log crate](log). 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 Reducers + +A small group of reducers are called at set points in the module lifecycle. These are used to initialize +the database and respond to client connections. See [Lifecycle Reducers](macro@crate::reducer#lifecycle-reducers). + +#### Scheduled Reducers + +Reducers can be scheduled to run repeatedly. This can be used to implement timers, game loops, and +maintenance tasks. See [Scheduled Reducers](macro@crate::reducer#scheduled-reducers). + +## Automatic migrations + +When you `spacetime publish` a module that has already been published using `spacetime publish `, +SpacetimeDB attempts to automatically migrate your existing database to the new schema. (The "schema" is just the collection +of tables and reducers you've declared in your code, together with the types they depend on.) This form of migration is very limited and only supports a few kinds of changes. +On the plus side, automatic migrations usually don't break clients. The situations that may break clients are documented below. + +The following changes are always allowed and never breaking: + + + +- ✅ **Adding tables**. Non-updated clients will not be able to see the new tables. +- ✅ **Adding indexes**. +- ✅ **Adding or removing `#[auto_inc]` annotations.** +- ✅ **Changing tables from private to public**. +- ✅ **Adding reducers**. +- ✅ **Removing `#[unique]` annotations.** + +The following changes are allowed, but may break clients: + +- ⚠️ **Changing or removing reducers**. Clients that attempt to call the old version of a changed reducer will receive runtime errors. +- ⚠️ **Changing tables from public to private**. Clients that are subscribed to a newly-private table will receive runtime errors. +- ⚠️ **Removing `#[primary_key]` annotations**. Non-updated clients will still use the old `#[primary_key]` as a unique key in their local cache, which can result in non-deterministic behavior when updates are received. +- ⚠️ **Removing indexes**. This is only breaking in some situtations. + The specific problem is subscription queries involving semijoins, such as: + ```sql + SELECT Employee.* + FROM Employee JOIN Dept + ON Employee.DeptName = Dept.DeptName + ) + ``` + For performance reasons, SpacetimeDB will only allow this kind of subscription query if there are indexes on `Employee.DeptName` and `Dept.DeptName`. Removing either of these indexes will invalidate this subscription query, resulting in client-side runtime errors. + +The following changes are forbidden without a manual migration: + +- ❌ **Removing tables**. +- ❌ **Changing the columns of a table**. This includes changing the order of columns of a table. +- ❌ **Changing whether a table is used for [scheduling](#scheduled-reducers).** +- ❌ **Adding `#[unique]` or `#[primary_key]` constraints.** This could result in existing tables being in an invalid state. + +Currently, manual migration support is limited. The `spacetime publish --clear-database ` command can be used to **COMPLETELY DELETE** and reinitialize your database, but naturally it should be used with EXTREME CAUTION. + +[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 +[client]: https://spacetimedb.com/docs/#client +[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/bindings-doctests.sh b/crates/bindings/bindings-doctests.sh new file mode 100644 index 00000000000..73f539ed1c9 --- /dev/null +++ b/crates/bindings/bindings-doctests.sh @@ -0,0 +1,15 @@ +#!/bin/env bash + +# Script to run doctests. +# Note: if you get `cannot find type thing__TableHandle in this scope`, that +# means you forgot to properly wrap your doctest. +# See the top comment of README.md. + +set -exo pipefail + +# Test doctests +rustup run nightly cargo test --doc --target wasm32-unknown-unknown -Zdoctest-xcompile +# Make sure they also work outside wasm (use the proper boilerplate) +cargo test --doc +# And look for broken links +RUSTDOCFLAGS="-D warnings" cargo doc \ No newline at end of file diff --git a/crates/bindings/src/client_visibility_filter.rs b/crates/bindings/src/client_visibility_filter.rs index 96feda95f7d..2e2deb94e76 100644 --- a/crates/bindings/src/client_visibility_filter.rs +++ b/crates/bindings/src/client_visibility_filter.rs @@ -1,5 +1,5 @@ /// A row-level security filter, -/// which can be registered using the [`crate::client_visibility_filter`] attribute. +/// which can be registered using the [`macro@crate::client_visibility_filter`] attribute. #[non_exhaustive] pub enum Filter { /// A SQL query. Rows that match this query will be made visible to clients. @@ -11,7 +11,7 @@ pub enum Filter { /// /// SQL queries are not checked for syntactic or semantic validity /// until they are processed by the SpacetimeDB host. - /// This means that errors in queries used as [`crate::client_visibility_filter`] rules + /// This means that errors in queries used as [`macro@crate::client_visibility_filter`] rules /// will be reported during `spacetime publish`, not at compile time. Sql(&'static str), } diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index 29985c02d73..8aac9c20730 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -1,5 +1,5 @@ -//! Provides safe abstractions around `bindings-sys` -//! and re-exports `#[spacetimedb]` and `#[duration]`. +#![doc = include_str!("../README.md")] +// ^ if you are working on docs, go read the top comment of README.md please. mod client_visibility_filter; pub mod log_stopwatch; @@ -24,9 +24,7 @@ pub use client_visibility_filter::Filter; pub use rng::StdbRng; pub use sats::SpacetimeType; #[doc(hidden)] -// TODO: move `client_visibility_filter` out of `doc(hidden)` once RLS is implemented. -pub use spacetimedb_bindings_macro::{__TableHelper, client_visibility_filter}; -pub use spacetimedb_bindings_macro::{duration, reducer, table}; +pub use spacetimedb_bindings_macro::__TableHelper; pub use spacetimedb_bindings_sys as sys; pub use spacetimedb_lib; pub use spacetimedb_lib::de::{Deserialize, DeserializeOwned}; @@ -44,17 +42,670 @@ pub use table::{AutoIncOverflow, RangedIndex, Table, TryInsertError, UniqueColum 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 rule. +/// +/// This attribute must be applied to a `const` binding of type [`Filter`]. +/// It will be interpreted as a filter on the table to which it applies, for all client queries. +/// If a module contains multiple `client_visibility_filter`s for the same table, +/// they will be unioned together as if by SQL `OR`, +/// so that any row permitted by at least one filter is visible. +/// +/// The `const` binding's identifier must be unique within the module. +/// +/// The query follows the same syntax as a subscription query. +/// +/// ## Example: +/// +/// ```no_run +/// # #[cfg(target_arch = "wasm32")] mod demo { +/// use spacetimedb::{client_visibility_filter, Filter}; +/// +/// /// Players can only see what's in their chunk +/// #[client_visibility_filter] +/// const PLAYERS_SEE_ENTITIES_IN_SAME_CHUNK: Filter = Filter::Sql(" +/// SELECT * FROM LocationState WHERE chunk_index IN ( +/// SELECT chunk_index FROM LocationState WHERE entity_id IN ( +/// SELECT entity_id FROM UserState WHERE identity = @sender +/// ) +/// ) +/// "); +/// # } +/// ``` +/// +/// Queries are not checked for syntactic or semantic validity +/// until they are processed by the SpacetimeDB host. +/// This means that errors in queries, such as syntax errors, type errors or unknown tables, +/// will be reported during `spacetime publish`, not at compile time. +#[doc(inline, hidden)] // TODO: RLS filters are currently unimplemented, and are not enforced. +pub use spacetimedb_bindings_macro::client_visibility_filter; + +/// Declares a table with a particular row type. +/// +/// This attribute is applied to a struct type with named fields. +/// This derives [`Serialize`], [`Deserialize`], [`SpacetimeType`], and [`Debug`] for the annotated struct. +/// +/// 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 type implementing [`Table`] is generated. This can be looked up in a [`ReducerContext`] +/// using `ctx.db.{table_name}()`. This type 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, ReducerContext}; +/// +/// #[table(name = user, public, +/// index(name = popularity_and_username, btree(columns = [popularity, username])), +/// )] +/// pub struct User { +/// #[auto_inc] +/// #[primary_key] +/// pub id: u32, +/// #[unique] +/// pub username: String, +/// #[index(btree)] +/// pub popularity: u32, +/// } +/// +/// fn demo(ctx: &ReducerContext) { +/// // Use the name of the table to get a struct +/// // implementing `spacetimedb::Table`. +/// let user: user__TableHandle = ctx.db.user(); +/// +/// // You can use methods from `spacetimedb::Table` +/// // on the table. +/// log::debug!("User count: {}", user.count()); +/// for user in user.iter() { +/// log::debug!("{:?}", user); +/// } +/// +/// // For every `#[index(btree)]`, the table has an extra method +/// // for getting a corresponding `spacetimedb::BTreeIndex`. +/// let by_popularity: RangedIndex<_, (u32,), _> = +/// user.popularity(); +/// for popular_user in by_popularity.filter(95..) { +/// log::debug!("Popular user: {:?}", popular_user); +/// } +/// +/// // There are similar methods for multi-column indexes. +/// let by_popularity_and_username: RangedIndex<_, (u32, String), _> = user.popularity_and_username(); +/// for popular_user in by_popularity.filter((100, "a"..)) { +/// log::debug!("Popular user whose name starts with 'a': {:?}", popular_user); +/// } +/// +/// // 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, _> = user.id(); +/// by_username.delete(&"test_user".to_string()); +/// } +/// ``` +/// +/// See [`Table`], [`RangedIndex`], and [`UniqueColumn`] for more information on the methods available on these types. +/// +/// # Browsing generated documentation +/// +/// The `#[table]` macro generates different APIs depending on the contents of your table. +/// +/// To browse the complete generated API for your tables, run `cargo doc` in your SpacetimeDB module project. Navigate to `[YOUR PROJECT/target/doc/spacetime_module/index.html` in your file explorer, and right click -> open it in a web browser. +/// +/// For the example above, we would see three items: +/// - A struct `User`. This is the struct you declared. It stores rows of the table `user`. +/// - A struct `user__TableHandle`. This is an opaque handle that allows you to interact with the table `user`. +/// - A trait `user` containing a single `fn user(&self) -> user__TableHandle`. +/// This trait is implemented for the `db` field of a [`ReducerContext`], allowing you to get a +/// `user__TableHandle` using `ctx.db.user()`. +/// +/// # Macro arguments +/// +/// The `#[table(...)]` attribute accepts any number of the following arguments, separated by commas. +/// +/// Multiple `table` annotations can be present on the same type. This will generate +/// multiple tables of the same row type, but with different names. +/// +/// ### `name` +/// +/// Specify the name of the table in the database. The name can be any valid Rust identifier. +/// +/// The table name is used to get a handle to the table from a [`ReducerContext`]. +/// For a table *table*, use `ctx.db.{table}()` to do this. +/// For example: +/// ```ignore +/// #[table(name = user)] +/// pub struct User { +/// #[auto_inc] +/// #[primary_key] +/// pub id: u32, +/// #[unique] +/// pub username: String, +/// #[index(btree)] +/// pub popularity: u32, +/// } +/// #[reducer] +/// fn demo(ctx: &ReducerContext) { +/// let user: user__TableHandle = ctx.db.user(); +/// } +/// ``` +/// +/// ### `public` and `private` +/// +/// Tables are private by default. This means that clients cannot read their contents +/// or see that they exist. +/// +/// If you'd like to make your table publically accessible by clients, +/// 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(...)` +/// +/// You can specify an index on one or more of the table's columns with the syntax: +/// `index(name = my_index, btree(columns = [a, b, c]))` +/// +/// You can also just put `#[index(btree)]` on the field itself if you only need +/// a single-column index; see column attributes below. +/// +/// A table may declare any number of indexes. +/// +/// You can use indexes to efficiently [`filter`](crate::RangedIndex::filter) and +/// [`delete`](crate::RangedIndex::delete) rows. This is encapsulated in the struct [`RangedIndex`]. +/// +/// For a table *table* and an index *index*, use: +/// ```text +/// ctx.db.{table}().{index}() +/// ``` +/// to get a [`RangedIndex`] for a [`ReducerContext`]. +/// +/// For example: +/// ```ignore +/// let by_id_and_username: spacetimedb::RangedIndex<_, (u32, String), _> = +/// ctx.db.user().by_id_and_username(); +/// ``` +/// +/// ### `scheduled(reducer_name)` +/// +/// Used to declare a [scheduled reducer](macro@crate::reducer#scheduled-reducers). +/// +/// The annotated struct type must have at least the following fields: +/// - `scheduled_id: u64` +/// - [`scheduled_at: ScheduleAt`](crate::ScheduleAt) +/// +/// # 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. +/// +/// May be combined with indexes or unique constraints. +/// +/// 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 unique constraint and index for the annotated field. +/// +/// You can [`find`](crate::UniqueColumn::find), [`update`](crate::UniqueColumn::update), +/// and [`delete`](crate::UniqueColumn::delete) rows by their unique columns. +/// This is encapsulated in the struct [`UniqueColumn`]. +/// +/// For a table *table* and a column *column*, use: +/// ```text +/// ctx.db.{table}().{column}()` +/// ``` +/// to get a [`UniqueColumn`] from a [`ReducerContext`]. +/// +/// For example: +/// ```ignore +/// let by_username: spacetimedb::UniqueColumn<_, String, _> = ctx.db.user().username(); +/// ``` +/// +/// 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. +/// This will be via a panic with [`Table::insert`] or via a `Result::Err` with [`Table::try_insert`]. +/// +/// For example: +/// ```no_run +/// # #[cfg(target_arch = "wasm32")] mod demo { +/// use spacetimedb::{ +/// table, +/// reducer, +/// ReducerContext, +/// // Make sure to import the `Table` trait to use `insert` or `try_insert`. +/// Table +/// }; +/// +/// type CountryCode = String; +/// +/// #[table(name = country)] +/// struct Country { +/// #[unique] +/// code: CountryCode, +/// national_bird: String +/// } +/// +/// #[reducer] +/// fn insert_unique_demo(ctx: &ReducerContext) { +/// let result = ctx.db.country().try_insert(Country { +/// code: "AU".into(), national_bird: "Emu".into() +/// }); +/// assert!(result.is_ok()); +/// +/// let result = ctx.db.country().try_insert(Country { +/// code: "AU".into(), national_bird: "Great Egret".into() +/// // Whoops, this was Austria's national bird, not Australia's. +/// // We should have used the country code "AT", not "AU". +/// }); +/// // since there's already a country in the database with the code "AU", +/// // SpacetimeDB gives us an error. +/// assert!(result.is_err()); +/// +/// // The following line would panic, since we use `insert` rather than `try_insert`. +/// // let result = ctx.db.country().insert(Country { code: "CN".into(), national_bird: "Blue Magpie".into() }); +/// +/// // If we wanted to *update* the row for Australia, we can use the `update` method of `UniqueIndex`. +/// // The following line will succeed: +/// ctx.db.country().code().update(Country { +/// code: "AU".into(), national_bird: "Australian Emu".into() +/// }); +/// } +/// # } +/// ``` +/// +/// ### `#[primary_key]` +/// +/// Implies `#[unique]`. Also generates additional methods client-side for handling updates to the table. +/// +/// +/// ### `#[index(btree)]` +/// +/// Creates a single-column index with the specified algorithm. +/// +/// It is an error to specify this attribute together with `#[unique]`. +/// Unique constraints implicitly create a unique index, which is accessed using the [`UniqueColumn`] struct instead of the +/// [`RangedIndex`] struct. +/// +/// The created index has the same name as the column. +/// +/// For a table *table* and an indexed *column*, use: +/// ```text +/// ctx.db.{table}().{column}() +/// ``` +/// to get a [`RangedIndex`] from a [`ReducerContext`]. +/// +/// For example: +/// +/// ```ignore +/// ctx.db.cities().latitude() +/// ``` +/// +/// # Generated code +/// +/// For each `[table(name = {name})]` annotation on a type `{T}`, generates a struct +/// `{name}__TableHandle` implementing [`Table`](crate::Table), and a trait that allows looking up such a +/// `{name}Handle` in a [`ReducerContext`]. +/// +/// The struct `{name}__TableHandle` is public and lives next to the row struct. +/// +/// For each named index declaration, add a method to `{name}__TableHandle` for getting a corresponding +/// [`RangedIndex`]. +/// +/// 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::{RangedIndex, UniqueColumn, Table, DbView}; +/// +/// // This generated struct is hidden and cannot be directly accessed. +/// struct {name}__TableHandle { /* ... */ }; +/// +/// // It is a table handle. +/// impl Table for {name}__TableHandle { +/// 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) -> RangedIndex<_, {(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: std::fmt::Display`](std::fmt::Display). +/// +/// ```no_run +/// # #[cfg(target_arch = "wasm32")] mod demo { +/// use spacetimedb::{reducer, SpacetimeType, ReducerContext}; +/// use log::info; +/// use std::fmt; +/// +/// #[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, Debug)] +/// struct Coordinates { +/// x: f32, +/// y: f32, +/// } +/// +/// enum AddPlaceError { +/// InvalidCoordinates(Coordinates), +/// InvalidName(String), +/// } +/// +/// impl fmt::Display for AddPlaceError { +/// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { +/// match self { +/// AddPlaceError::InvalidCoordinates(coords) => { +/// write!(f, "invalid coordinates: {coords:?}") +/// }, +/// AddPlaceError::InvalidName(name) => { +/// write!(f, "invalid name: {name:?}") +/// }, +/// } +/// } +/// } +/// +/// #[reducer] +/// pub fn add_place( +/// context: &ReducerContext, +/// name: String, +/// x: f32, +/// y: f32, +/// area: f32, +/// ) -> Result<(), AddPlaceError> { +/// // ... add a place to the database... +/// # Ok(()) +/// } +/// # } +/// ``` +/// +/// Reducers may fail by returning a [`Result::Err`](std::result::Result) or by [panicking](std::panic!). +/// Failures will abort the active database transaction. +/// Any changes to the database made by the failed reducer call 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. +/// +/// These reducers cannot be called manually +/// and may not have any parameters except for `ReducerContext`. +/// +/// ### The `init` reducer +/// +/// This reducer is marked with `#[spacetimedb::reducer(init)]`. It is run the first time a module is published +/// and any time the database is cleared. (It does not have to be named `init`.) +/// +/// If an error occurs when initializing, the module will not be published. +/// +/// This reducer can be used to configure any static data tables used by your module. It can also be used to start running [scheduled reducers](#scheduled-reducers). +/// +/// ### The `client_connected` reducer +/// +/// This reducer is marked with `#[spacetimedb::reducer(client_connected)]`. It is run when a client connects to the SpacetimeDB module. +/// Their identity can be found in the sender value of the `ReducerContext`. +/// +/// If an error occurs in the reducer, the client will be disconnected. +/// +/// ### The `client_disconnected` reducer +/// +/// This reducer is marked with `#[spacetimedb::reducer(client_disconnected)]`. It is run when a client disconnects from the SpacetimeDB module. +/// Their identity can be found in the sender value of the `ReducerContext`. +/// +/// If an error occurs in the disconnect reducer, +/// the client is still recorded as disconnected. +/// +/// ### The `update` reducer +/// +/// This reducer is marked with `#[spacetimedb::reducer(update)]`. It is run when the module is updated, +/// i.e., when publishing a module for a database that has already been initialized. +/// +/// If an error occurs when updating, the module will not be published, +/// and the previous version of the module attached to the database will continue executing. +/// +/// # 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 [`spacetimedb::Timestamp`](crate::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`]. +/// +/// ```no_run +/// # #[cfg(target_arch = "wasm32")] mod demo { +/// use spacetimedb::{table, reducer, ReducerContext, Timestamp, TimeDuration, ScheduleAt, Table}; +/// use log::debug; +/// +/// // First, we declare the table with scheduling information. +/// +/// #[table(name = send_message_schedule, scheduled(send_message))] +/// struct SendMessageSchedule { +/// // Mandatory fields: +/// // ============================ +/// +/// /// An identifier for the scheduled reducer call. +/// #[primary_key] +/// #[auto_inc] +/// scheduled_id: u64, +/// +/// /// Information about when the reducer should be called. +/// scheduled_at: ScheduleAt, +/// +/// // In addition to 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 ... +/// +/// Ok(()) +/// } +/// +/// // 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 ten_seconds = TimeDuration::from_micros(10_000_000); +/// +/// let future_timestamp: Timestamp = ctx.timestamp + ten_seconds; +/// ctx.db.send_message_schedule().insert(SendMessageSchedule { +/// scheduled_id: 1, +/// text:"I'm a bot sending a message one time".to_string(), +/// +/// // Creating a `ScheduleAt` from a `Timestamp` results in the reducer +/// // being called once, at exactly the time `future_timestamp`. +/// scheduled_at: future_timestamp.into() +/// }); +/// +/// let loop_duration: TimeDuration = ten_seconds; +/// ctx.db.send_message_schedule().insert(SendMessageSchedule { +/// scheduled_id: 0, +/// text:"I'm a bot sending a message every 10 seconds".to_string(), +/// +/// // Creating a `ScheduleAt` from a `Duration` results in the reducer +/// // being called in a loop, once every `loop_duration`. +/// scheduled_at: loop_duration.into() +/// }); +/// } +/// # } +/// ``` +/// +/// Scheduled reducers are called on a best-effort basis and may be slightly delayed in their execution +/// when a database is under heavy load. +/// +/// +/// +/// [`&ReducerContext`]: `ReducerContext` +/// [clients]: https://spacetimedb.com/docs/#client +#[doc(inline)] +pub use spacetimedb_bindings_macro::reducer; + +/// 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 `ConnectionId` of the client that invoked the reducer. /// - /// Will be `None` for scheduled reducers. + /// `None` if no `ConnectionId` was supplied to the `/database/call` HTTP endpoint, + /// or via the CLI's `spacetime call` subcommand. + /// + /// For automatic reducers, i.e. `init`, `update` and scheduled reducers, + /// this will be the module's `Address`. pub connection_id: 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. + /// + /// For a table named *table*, use `ctx.db.{table}()` to get a handle. + /// For example: + /// ```no_run + /// # mod demo { // work around doctest+index issue + /// # #![cfg(target_arch = "wasm32")] + /// use spacetimedb::{table, reducer, ReducerContext}; + /// + /// #[table(name = book)] + /// #[derive(Debug)] + /// struct Book { + /// #[primary_key] + /// id: u64, + /// isbn: String, + /// name: String, + /// #[index(btree)] + /// author: String + /// } + /// + /// #[reducer] + /// fn find_books_by(ctx: &ReducerContext, author: String) { + /// let book: &book__TableHandle = ctx.db.book(); + /// + /// log::debug!("looking up books by {author}..."); + /// for book in book.author().filter(&author) { + /// log::debug!("- {book:?}"); + /// } + /// } + /// # } + /// ``` + /// See the [`#[table]`](macro@crate::table) macro for more information. pub db: Local, #[cfg(feature = "rand")] @@ -101,7 +752,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; } @@ -113,6 +764,11 @@ 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. #[non_exhaustive] pub struct Local {} diff --git a/crates/bindings/src/log_stopwatch.rs b/crates/bindings/src/log_stopwatch.rs index a7f0c7ece01..91eebf46650 100644 --- a/crates/bindings/src/log_stopwatch.rs +++ b/crates/bindings/src/log_stopwatch.rs @@ -1,3 +1,4 @@ +/// TODO(this PR): docs pub struct LogStopwatch { stopwatch_id: u32, } diff --git a/crates/bindings/src/rng.rs b/crates/bindings/src/rng.rs index cdccbcdc7c6..1a7707ef324 100644 --- a/crates/bindings/src/rng.rs +++ b/crates/bindings/src/rng.rs @@ -27,17 +27,20 @@ impl ReducerContext { /// /// # Examples /// - /// ``` - /// # #[spacetimedb::reducer] - /// # fn rng_demo(ctx: &spacetimedb::ReducerContext) { + /// ```no_run + /// # #[cfg(target_arch = "wasm32")] mod demo { + /// use spacetimedb::{reducer, ReducerContext}; /// use rand::Rng; + /// + /// #[spacetimedb::reducer] + /// fn rng_demo(ctx: &spacetimedb::ReducerContext) { + /// // Can be used in method chaining style: + /// let digit = ctx.rng().gen_range(0..=9); /// - /// // Can be used in method chaining style: - /// let digit = ctx.rng().gen_range(0..=9); - /// - /// // Or, cache locally for reuse: - /// let mut rng = ctx.rng(); - /// let floats: Vec = rng.sample_iter(rand::distributions::Standard).collect(); + /// // Or, cache locally for reuse: + /// let mut rng = ctx.rng(); + /// let floats: Vec = rng.sample_iter(rand::distributions::Standard).collect(); + /// } /// # } /// ``` /// diff --git a/crates/bindings/src/rt.rs b/crates/bindings/src/rt.rs index e329cd896f5..5b2ec0ea1dc 100644 --- a/crates/bindings/src/rt.rs +++ b/crates/bindings/src/rt.rs @@ -420,7 +420,7 @@ static REDUCERS: OnceLock> = OnceLock::new(); /// For the time being, the definition of `ModuleDef` is not stabilized, /// as it is being changed by the schema proposal. /// -/// The `ModuleDef` is used to define tables, constraints, indices, reducers, etc. +/// The `ModuleDef` is used to define tables, constraints, indexes, reducers, etc. /// This affords the module the opportunity /// to define and, to a limited extent, alter the schema at initialization time, /// including when modules are updated (re-publishing). diff --git a/crates/bindings/src/table.rs b/crates/bindings/src/table.rs index d59bb6ce58a..0cad5fcd1de 100644 --- a/crates/bindings/src/table.rs +++ b/crates/bindings/src/table.rs @@ -12,23 +12,29 @@ pub use spacetimedb_primitives::{ColId, IndexId}; use crate::{bsatn, sys, ConnectionId, DeserializeOwned, Identity, IterBuf, Serialize, SpacetimeType, TableId}; -/// Implemented for every `TableHandle` struct generated in the client `module_bindings` -/// and the module macroexpansion. +/// Implemented for every `TableHandle` struct generated by the [`table`](macro@crate::table) macro. +/// Contains methods that are present for every table, regardless of what unique constraints +/// and indexes are present. +/// +/// To get a `TableHandle` +// TODO: should we rename this `TableHandle`? Documenting this, I think that's much clearer. pub trait Table: TableInternal { /// The type of rows stored in this table. type Row: SpacetimeType + Serialize + DeserializeOwned + Sized + 'static; - /// Returns the number of rows of this table in the TX state, - /// i.e. num(committed_state) + num(insert_table) - num(delete_table). + /// Returns the number of rows of this table. /// - /// This API is new to modules (though it previously existed in the Rust SDK) - /// and will require a new host function in the ABI. + /// This takes into account modifications by the current transaction, + /// even though those modifications have not yet been committed or broadcast to clients. fn count(&self) -> u64 { sys::datastore_table_row_count(Self::table_id()).expect("datastore_table_row_count() call failed") } - /// Iterate over all rows in the TX state, - /// i.e. committed_state ∪ insert_table ∖ delete_table. + /// Iterate over all rows of the table. + /// + /// For large tables, this can be a very slow operation! + /// Prefer [filtering](RangedIndex::filter) a [`RangedIndex`] or [finding](UniqueColumn::find) a [`UniqueColumn`] if + /// possible. #[inline] fn iter(&self) -> impl Iterator { let table_id = Self::table_id(); @@ -36,25 +42,20 @@ pub trait Table: TableInternal { TableIter::new(iter) } - /// Inserts `row` into the TX state, - /// i.e. removes it from the delete table or adds it to the insert table as appropriate. + /// Inserts `row` into the table. /// /// The return value is the inserted row, with any auto-incrementing columns replaced with computed values. /// The `insert` method always returns the inserted row, /// even when the table contains no auto-incrementing columns. /// + /// (The returned row is a copy of the row in the database. + /// Modifying this copy does not directly modify the database. + /// See [`UniqueColumn::update`] if you want to update the row.) + /// /// May panic if inserting the row violates any constraints. /// Callers which intend to handle constraint violation errors should instead use [`Self::try_insert`]. /// - /// Note that, in languages where error handling is based on exceptions, - /// no distinction is provided between `Table::insert` and `Table::try_insert`. - /// A single method `insert` is defined which throws an exception on failure, - /// and callers may either install handlers around it or allow the exception to bubble up. - /// - /// Note on MVCC: because callers have no way to determine if the row was previously present, - /// two concurrent transactions which delete the same row - /// may be ordered arbitrarily with respect to one another - /// while maintaining sequential consistency, assuming no other conflicts. + /// #[track_caller] fn insert(&self, row: Self::Row) -> Self::Row { self.try_insert(row).unwrap_or_else(|e| panic!("{e}")) @@ -64,6 +65,7 @@ pub trait Table: TableInternal { /// [`UniqueConstraintViolation`] if the table has any unique constraints, or [`Infallible`] /// otherwise. type UniqueConstraintViolation: MaybeError; + /// The error type for this table for auto-increment overflows. Will either be /// [`AutoIncOverflow`] if the table has any auto-incrementing columns, or [`Infallible`] /// otherwise. @@ -71,21 +73,15 @@ pub trait Table: TableInternal { /// Counterpart to [`Self::insert`] which allows handling failed insertions. /// - /// For tables without any constraints, [`Self::TryInsertError`] will be [`std::convert::Infallible`], - /// and this will be a more-verbose [`Self::insert`]. /// For tables with constraints, this method returns an `Err` when the insertion fails rather than panicking. - /// - /// Note that, in languages where error handling is based on exceptions, - /// no distinction is provided between `Table::insert` and `Table::try_insert`. - /// A single method `insert` is defined which throws an exception on failure, - /// and callers may either install handlers around it or allow the exception to bubble up. + /// For tables without any constraints, [`Self::UniqueConstraintViolation`] and [`Self::AutoIncOverflow`] + /// will be [`std::convert::Infallible`], and this will be a more-verbose [`Self::insert`]. #[track_caller] fn try_insert(&self, row: Self::Row) -> Result> { insert::(row, IterBuf::take()) } - /// Deletes a row equal to `row` from the TX state, - /// i.e. deletes it from the insert table or adds it to the delete table as appropriate. + /// Deletes a row equal to `row` from the table. /// /// Returns `true` if the row was present and has been deleted, /// or `false` if the row was not present and therefore the tables have not changed. @@ -95,18 +91,13 @@ pub trait Table: TableInternal { /// No analogue to auto-increment placeholders exists for deletions. /// /// May panic if deleting the row violates any constraints. - /// Note that as of writing deletion is infallible, but future work may define new constraints, - /// e.g. foreign keys, which cause deletion to fail in some cases. - /// If and when these new constraints are added, - /// we should define `Self::try_delete` and `Self::TryDeleteError`, - /// analogous to [`Self::try_insert`] and [`Self::TryInsertError`]. - /// - /// Note on MVCC: the return value means that logically a `delete` performs a query - /// to see if the row is present. - /// As such, two concurrent transactions which delete the same row - /// cannot be placed in a sequentially-consistent ordering, - /// and one of them must be retried. fn delete(&self, row: Self::Row) -> bool { + // Note that as of writing deletion is infallible, but future work may define new constraints, + // e.g. foreign keys, which cause deletion to fail in some cases. + // If and when these new constraints are added, + // we should define `Self::ForeignKeyViolation`, + // analogous to [`Self::UniqueConstraintViolation`]. + let relation = std::slice::from_ref(&row); let buf = IterBuf::serialize(relation).unwrap(); let count = sys::datastore_delete_all_by_eq_bsatn(Self::table_id(), &buf).unwrap(); @@ -150,7 +141,7 @@ pub struct ScheduleDesc<'a> { pub scheduled_at_column: u16, } -/// A UNIQUE constraint violation on a table was attempted. +/// A row operation was attempted that would violate a unique constraint. // TODO: add column name for better error message #[derive(Debug)] #[non_exhaustive] @@ -191,7 +182,7 @@ pub enum TryInsertError { /// An [`AutoIncOverflow`]. /// - /// Returned from [`TableHandle::try_insert`] if an attempted insertion + /// Returned from [`Table::try_insert`] if an attempted insertion /// advances an auto-inc sequence past the bounds of the column type. /// /// This variant is only possible if the table has at least one auto-inc column, @@ -264,6 +255,43 @@ pub trait Column { fn get_field(row: &::Row) -> &Self::ColType; } +/// A handle to a unique index on a column. +/// Available for `#[unique]` and `#[primary_key]` columns. +/// +/// For a table *table* with a column *column*, use `ctx.db.{table}().{column}()` +/// to get a `UniqueColumn` from a [`ReducerContext`](crate::ReducerContext). +/// +/// Example: +/// +/// ```no_run +/// # #[cfg(target_arch = "wasm32")] mod demo { +/// use spacetimedb::{table, UniqueColumn, ReducerContext, DbContext}; +/// +/// #[table(name = user)] +/// struct User { +/// #[primary_key] +/// id: u32, +/// #[unique] +/// username: String, +/// dog_count: u64 +/// } +/// +/// fn demo(ctx: &ReducerContext) { +/// let user = ctx.db().user(); +/// +/// let by_id: UniqueColumn<_, u32, _> = user.id(); +/// +/// let mut example_user: User = by_id.find(357).unwrap(); +/// example_user.dog_count += 5; +/// by_id.update(example_user); +/// +/// let by_username: UniqueColumn<_, String, _> = user.username(); +/// by_username.delete(&"Evil Bob".to_string()); +/// } +/// # } +/// ``` +/// +/// pub struct UniqueColumn { _marker: PhantomData<(Tbl, ColType, Col)>, } @@ -362,6 +390,55 @@ pub trait Index { fn index_id() -> IndexId; } +/// A handle to a B-Tree index on a table. +/// +/// To get one of these from a `ReducerContext`, use: +/// ```text +/// ctx.db.{table}().{index}() +/// ``` +/// for a table *table* and an index *index*. +/// +/// Example: +/// +/// ```no_run +/// # #[cfg(target_arch = "wasm32")] mod demo { +/// use spacetimedb::{table, RangedIndex, ReducerContext, DbContext}; +/// +/// #[table(name = user, +/// index(name = dogs_and_name, btree(columns = [dogs, name])))] +/// struct User { +/// id: u32, +/// name: String, +/// /// Number of dogs owned by the user. +/// dogs: u64 +/// } +/// +/// fn demo(ctx: &ReducerContext) { +/// let by_dogs_and_name: RangedIndex<_, (u64, String), _> = ctx.db.user().dogs_and_name(); +/// } +/// # } +/// ``` +/// +/// For single-column indexes, use the name of the column: +/// +/// ```no_run +/// # #[cfg(target_arch = "wasm32")] mod demo { +/// use spacetimedb::{table, RangedIndex, ReducerContext, DbContext}; +/// +/// #[table(name = user)] +/// struct User { +/// id: u32, +/// username: String, +/// #[index(btree)] +/// dogs: u64 +/// } +/// +/// fn demo(ctx: &ReducerContext) { +/// let by_dogs: RangedIndex<_, (u64,), _> = ctx.db().user().dogs(); +/// } +/// # } +/// ``` +/// pub struct RangedIndex { _marker: PhantomData<(Tbl, IndexType, Idx)>, } @@ -372,10 +449,79 @@ impl RangedIndex { /// Returns an iterator over all rows in the database state where the indexed column(s) match the bounds `b`. /// - /// `b` may be: + /// This method accepts a variable numbers of arguments using the [`IndexScanRangeBounds`] trait. + /// This depends on the type of the B-Tree index. `b` may be: /// - A value for the first indexed column. /// - A range of values for the first indexed column. /// - A tuple of values for any prefix of the indexed columns, optionally terminated by a range for the next. + /// + /// For example: + /// + /// ```no_run + /// # #[cfg(target_arch = "wasm32")] mod demo { + /// use spacetimedb::{table, ReducerContext, RangedIndex}; + /// + /// #[table(name = user, + /// index(name = dogs_and_name, btree(columns = [dogs, name])))] + /// struct User { + /// id: u32, + /// name: String, + /// dogs: u64 + /// } + /// + /// fn demo(ctx: &ReducerContext) { + /// let by_dogs_and_name: RangedIndex<_, (u64, String), _> = ctx.db.user().dogs_and_name(); + /// + /// // Find user with exactly 25 dogs. + /// for user in by_dogs_and_name.filter(25u64) { // The `u64` is required, see below. + /// /* ... */ + /// } + /// + /// // Find user with at least 25 dogs. + /// for user in by_dogs_and_name.filter(25u64..) { + /// /* ... */ + /// } + /// + /// // Find user with exactly 25 dogs, and a name beginning with "J". + /// for user in by_dogs_and_name.filter((25u64, "J".."K")) { + /// /* ... */ + /// } + /// + /// // Find user with exactly 25 dogs, and exactly the name "Joseph". + /// for user in by_dogs_and_name.filter((25u64, "Joseph")) { + /// /* ... */ + /// } + /// + /// // You can also pass arguments by reference if desired. + /// for user in by_dogs_and_name.filter((&25u64, &"Joseph".to_string())) { + /// /* ... */ + /// } + /// } + /// # } + /// ``` + /// + /// **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 `IndexScanRangeBounds<(u32,), SingleBound>` + /// > note: required by a bound in `RangedIndex::::filter` + /// > | + /// > 410 | pub fn filter(&self, b: B) -> impl Iterator + /// > | ------ required by a bound in this associated function + /// > 411 | where + /// > 412 | B: IndexScanRangeBounds, + /// > | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `RangedIndex::::filter` + /// > ``` + /// pub fn filter(&self, b: B) -> impl Iterator where B: IndexScanRangeBounds, @@ -390,13 +536,71 @@ impl RangedIndex { /// Deletes all rows in the database state where the indexed column(s) match the bounds `b`. /// - /// `b` may be: + /// This method accepts a variable numbers of arguments using the [`IndexScanRangeBounds`] trait. + /// This depends on the type of the B-Tree index. `b` may be: /// - A value for the first indexed column. /// - A range of values for the first indexed column. /// - A tuple of values for any prefix of the indexed columns, optionally terminated by a range for the next. /// + /// For example: + /// + /// ```no_run + /// # #[cfg(target_arch = "wasm32")] mod demo { + /// use spacetimedb::{table, ReducerContext, RangedIndex}; + /// + /// #[table(name = user, + /// index(name = dogs_and_name, btree(columns = [dogs, name])))] + /// struct User { + /// id: u32, + /// name: String, + /// dogs: u64 + /// } + /// + /// fn demo(ctx: &ReducerContext) { + /// let by_dogs_and_name: RangedIndex<_, (u64, String), _> = ctx.db.user().dogs_and_name(); + /// + /// // Delete users with exactly 25 dogs. + /// by_dogs_and_name.delete(25u64); // The `u64` is required, see below. + /// + /// // Delete users with at least 25 dogs. + /// by_dogs_and_name.delete(25u64..); + /// + /// // Delete users with exactly 25 dogs, and a name beginning with "J". + /// by_dogs_and_name.delete((25u64, "J".."K")); + /// + /// // Delete users with exactly 25 dogs, and exactly the name "Joseph". + /// by_dogs_and_name.delete((25u64, "Joseph")); + /// + /// // You can also pass arguments by reference if desired. + /// by_dogs_and_name.delete((&25u64, &"Joseph".to_string())); + /// } + /// # } + /// ``` + /// + /// **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 `IndexScanRangeBounds<(u32,), SingleBound>` + /// > note: required by a bound in `RangedIndex::::filter` + /// > | + /// > 410 | pub fn filter(&self, b: B) -> impl Iterator + /// > | ------ required by a bound in this associated function + /// > 411 | where + /// > 412 | B: IndexScanRangeBounds, + /// > | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `RangedIndex::::filter` + /// > ``` + /// /// May panic if deleting any one of the rows would violate a constraint, - /// though as of proposing no such constraints exist. + /// though at present no such constraints exist. pub fn delete(&self, b: B) -> u64 where B: IndexScanRangeBounds, @@ -418,7 +622,7 @@ mod private_filtrable_value { /// for a column of type `Column`. /// /// Types which can appear specifically as a terminating bound in a BTree index, -/// which may be a range, instead use [`IndexRangeScanBoundsTerminator`]. +/// which may be a range, instead use [`IndexScanRangeBoundsTerminator`]. /// /// General rules for implementors of this type: /// - It should only be implemented for types that have @@ -489,6 +693,8 @@ impl_filterable_value! { // &[u8] => Vec, } +/// Trait used for overloading methods on [`RangedIndex`]. +/// See [`RangedIndex`] for more information. pub trait IndexScanRangeBounds { #[doc(hidden)] fn get_args(&self) -> IndexScanRangeArgs; @@ -528,16 +734,16 @@ macro_rules! impl_index_scan_range_bounds { // the first of which we use for the column types in the index, // and the second for the arguments supplied to the filter function. // We do our "outer recursion" to visit the sublists of these two lists, - // at each step implementing the trait for indices of that many columns. + // at each step implementing the trait for indexes of that many columns. // // There's also an "inner recursion" later on, which, given a fixed number of columns, // implements the trait with the arguments being all the prefixes of that list. (($ColTerminator:ident $(, $ColPrefix:ident)*), ($ArgTerminator:ident $(, $ArgPrefix:ident)*)) => { - // Implement the trait for all arguments N-column indices. + // Implement the trait for all arguments N-column indexes. // The "inner recursion" described above happens in here. impl_index_scan_range_bounds!(@inner_recursion (), ($ColTerminator $(, $ColPrefix)*), ($ArgTerminator $(, $ArgPrefix)*)); - // Recurse on the suffix of the two lists, to implement the trait for all arguments to (N - 1)-column indices. + // Recurse on the suffix of the two lists, to implement the trait for all arguments to (N - 1)-column indexes. impl_index_scan_range_bounds!(($($ColPrefix),*), ($($ArgPrefix),*)); }; // Base case for the previous "outer recursion." @@ -546,14 +752,14 @@ macro_rules! impl_index_scan_range_bounds { // The recursive case for the inner loop. // // When we start this recursion, `$ColUnused` will be empty, - // so we'll implement N-element queries on N-column indices. + // so we'll implement N-element queries on N-column indexes. // The next call will move one type name from `($ColTerminator, $ColPrefix)` into `$ColUnused`, - // so we'll implement (N - 1)-element queries on N-column indices. + // so we'll implement (N - 1)-element queries on N-column indexes. // And so on. (@inner_recursion ($($ColUnused:ident),*), ($ColTerminator:ident $(, $ColPrefix:ident)+), ($ArgTerminator:ident $(, $ArgPrefix:ident)+)) => { - // Emit the actual `impl IndexScanRangeBounds` form for M-element queries on N-column indices. + // Emit the actual `impl IndexScanRangeBounds` form for M-element queries on N-column indexes. impl_index_scan_range_bounds!(@emit_impl ($($ColUnused),*), ($ColTerminator $(,$ColPrefix)*), ($ArgTerminator $(, $ArgPrefix)*)); - // Recurse, to implement for (M - 1)-element queries on N-column indices. + // Recurse, to implement for (M - 1)-element queries on N-column indexes. impl_index_scan_range_bounds!(@inner_recursion ($($ColUnused,)* $ColTerminator), ($($ColPrefix),*), ($($ArgPrefix),*)); }; // Base case for the inner recursive loop, when there is only one column remaining. @@ -716,17 +922,17 @@ impl_terminator!( (ops::Bound, ops::Bound), ); -// Single-column indices +// Single-column indexes // impl IndexScanRangeBounds<(T,)> for Range {} // impl IndexScanRangeBounds<(T,)> for T {} -// // Two-column indices +// // Two-column indexes // impl IndexScanRangeBounds<(T, U)> for Range {} // impl IndexScanRangeBounds<(T, U)> for T {} // impl IndexScanRangeBounds<(T, U)> for (T, Range) {} // impl IndexScanRangeBounds<(T, U)> for (T, U) {} -// // Three-column indices +// // Three-column indexes // impl IndexScanRangeBounds<(T, U, V)> for Range {} // impl IndexScanRangeBounds<(T, U, V)> for T {} // impl IndexScanRangeBounds<(T, U, V)> for (T, Range) {} @@ -740,8 +946,9 @@ impl_terminator!( pub trait SequenceTrigger: Sized { /// Is this value one that will trigger a sequence, if any, /// when used as a column value. + /// For numeric types, this is `0`. fn is_sequence_trigger(&self) -> bool; - /// BufReader::get_[< self >] + /// Should invoke `BufReader::get_{Self}`, for example `BufReader::get_u32`. fn decode(reader: &mut &[u8]) -> Result; /// Read a generated column from the slice, if this row was a sequence trigger. #[inline(always)] diff --git a/crates/data-structures/src/slim_slice.rs b/crates/data-structures/src/slim_slice.rs index 2c1142125da..68202d7efec 100644 --- a/crates/data-structures/src/slim_slice.rs +++ b/crates/data-structures/src/slim_slice.rs @@ -32,11 +32,11 @@ //! //! The following convenience conversion functions are provided: //! -//! - [`slice`] converts `&[T] -> SlimSlice`, panicing on overflow -//! - [`slice_mut`] converts `&mut [T] -> SlimSliceMut`, panicing on overflow -//! - [`str`] converts `&str -> SlimStr`, panicing on overflow -//! - [`str_mut`] converts `&mut str -> SlimStrMut`, panicing on overflow -//! - [`string`] converts `&str -> SlimStrBox`, panicing on overflow +//! - [`from_slice`] converts `&[T] -> SlimSlice`, panicing on overflow +//! - [`from_slice_mut`] converts `&mut [T] -> SlimSliceMut`, panicing on overflow +//! - [`from_str`] converts `&str -> SlimStr`, panicing on overflow +//! - [`from_str_mut`] converts `&mut str -> SlimStrMut`, panicing on overflow +//! - [`from_string`] converts `&str -> SlimStrBox`, panicing on overflow //! //! These conversions should be reserved for cases where it is known //! that the length `<= u32::MAX` and should be used sparingly. diff --git a/crates/lib/src/connection_id.rs b/crates/lib/src/connection_id.rs index 42527d49f2e..a13b3a85e8d 100644 --- a/crates/lib/src/connection_id.rs +++ b/crates/lib/src/connection_id.rs @@ -12,7 +12,7 @@ use spacetimedb_sats::{impl_deserialize, impl_serialize, impl_st, AlgebraicType, /// A `ConnectionId` is a 128-bit unsigned integer. This can be serialized in various ways. /// - In JSON, an `ConnectionId` is represented as a BARE DECIMAL number. /// This requires some care when deserializing; see -/// [https://stackoverflow.com/questions/69644298/how-to-make-json-parse-to-treat-all-the-numbers-as-bigint] +/// /// - In BSATN, a `ConnectionId` is represented as a LITTLE-ENDIAN number 16 bytes long. /// - In memory, a `ConnectionId` is stored as a 128-bit number with the endianness of the host system. // diff --git a/crates/lib/src/db/raw_def/v8.rs b/crates/lib/src/db/raw_def/v8.rs index a5bab53c0b8..9557466813a 100644 --- a/crates/lib/src/db/raw_def/v8.rs +++ b/crates/lib/src/db/raw_def/v8.rs @@ -122,9 +122,7 @@ pub struct RawIndexDefV8 { } impl RawIndexDefV8 { - /// Creates a new [IndexDef] with the provided parameters. - /// - /// WARNING: Only [IndexType::Btree] is supported for now... + /// Creates a new [RawIndexDefV8] with the provided parameters. /// /// # Parameters /// @@ -140,7 +138,7 @@ impl RawIndexDefV8 { } } - /// Creates an [IndexDef] of type [IndexType::BTree] for a specific column of a table. + /// Creates an [RawIndexDefV8] for a specific column of a table. /// /// This method generates an index name based on the table name, index name, column positions, and uniqueness constraint. /// @@ -205,7 +203,7 @@ impl RawColumnDefV8 { } impl RawColumnDefV8 { - /// Creates a new [ColumnDef] for a system field with the specified data type. + /// Creates a new [RawColumnDefV8] for a system field with the specified data type. /// /// This method is typically used to define system columns with predefined names and data types. /// @@ -237,7 +235,7 @@ pub struct RawConstraintDefV8 { } impl RawConstraintDefV8 { - /// Creates a new [ConstraintDef] with the specified parameters. + /// Creates a new [RawConstraintDefV8] with the specified parameters. /// /// # Parameters /// @@ -384,7 +382,7 @@ impl RawTableDefV8 { generate_cols_name(columns, |p| self.get_column(p.idx()).map(|c| &*c.col_name)) } - /// Generate a [ConstraintDef] using the supplied `columns`. + /// Generate a [RawConstraintDefV8] using the supplied `columns`. pub fn with_column_constraint(mut self, kind: Constraints, columns: impl Into) -> Self { self.constraints.push(self.gen_constraint_def(kind, columns)); self @@ -402,7 +400,7 @@ impl RawTableDefV8 { x } - /// Generate a [IndexDef] using the supplied `columns`. + /// Generate a [RawIndexDefV8] using the supplied `columns`. pub fn with_column_index(self, columns: impl Into, is_unique: bool) -> Self { let mut x = self; let columns = columns.into(); @@ -457,7 +455,7 @@ impl RawTableDefV8 { ) } - /// Get an iterator deriving [IndexDef] from the constraints that require them like `UNIQUE`. + /// Get an iterator deriving [RawIndexDefV8]s from the constraints that require them like `UNIQUE`. /// /// It looks into [Self::constraints] for possible duplicates and remove them from the result pub fn generated_indexes(&self) -> impl Iterator + '_ { @@ -490,9 +488,9 @@ impl RawTableDefV8 { .filter(|seq| self.sequences.iter().all(|x| x.sequence_name != seq.sequence_name)) } - /// Get an iterator deriving [ConstraintDef] from the indexes that require them like `UNIQUE`. + /// Get an iterator deriving [RawConstraintDefV8] from the indexes that require them like `UNIQUE`. /// - /// It looks into [Self::constraints] for possible duplicates and remove them from the result + /// It looks into Self::constraints for possible duplicates and remove them from the result pub fn generated_constraints(&self) -> impl Iterator + '_ { // Collect the set of all col-lists with a constraint. let cols: HashSet<_> = self @@ -511,7 +509,7 @@ impl RawTableDefV8 { .map(|idx| self.gen_constraint_def(Constraints::from_is_unique(idx.is_unique), idx.columns.clone())) } - /// Check if the `name` of the [FieldName] exist on this [TableDef] + /// Check if the `name` of the [FieldName] exist on this [RawTableDefV8] /// /// Warning: It ignores the `table_id` pub fn get_column_by_field(&self, field: FieldName) -> Option<&RawColumnDefV8> { @@ -523,7 +521,7 @@ impl RawTableDefV8 { self.columns.get(pos) } - /// Check if the `col_name` exist on this [TableSchema] + /// Check if the `col_name` exist on this [RawTableDefV8] /// /// Warning: It ignores the `table_name` pub fn get_column_by_name(&self, col_name: &str) -> Option<&RawColumnDefV8> { diff --git a/crates/lib/src/db/raw_def/v9.rs b/crates/lib/src/db/raw_def/v9.rs index 4cae222c108..da2d49a4d4a 100644 --- a/crates/lib/src/db/raw_def/v9.rs +++ b/crates/lib/src/db/raw_def/v9.rs @@ -95,7 +95,7 @@ pub struct RawModuleDefV9 { /// constraints, sequences, type, and access rights. /// /// Validation rules: -/// - The table name must be a valid [`crate::db::identifier::Identifier`]. +/// - The table name must be a valid `spacetimedb_schema::identifier::Identifier`. /// - The table's indexes, constraints, and sequences need not be sorted; they will be sorted according to their respective ordering rules. /// - The table's column types may refer only to types in the containing `RawModuleDefV9`'s typespace. /// - The table's column names must be unique. @@ -105,7 +105,7 @@ pub struct RawModuleDefV9 { pub struct RawTableDefV9 { /// The name of the table. /// Unique within a module, acts as the table's identifier. - /// Must be a valid [crate::db::identifier::Identifier]. + /// Must be a valid `spacetimedb_schema::identifier::Identifier`. pub name: RawIdentifier, /// A reference to a `ProductType` containing the columns of this table. @@ -441,7 +441,7 @@ pub enum Lifecycle { OnDisconnect, } -/// A builder for a [`ModuleDef`]. +/// A builder for a [`RawModuleDefV9`]. #[derive(Default)] pub struct RawModuleDefV9Builder { /// The module definition. @@ -708,7 +708,7 @@ impl RawTableDefBuilder<'_> { self } - /// Generates a [UniqueConstraintDef] using the supplied `columns`. + /// Generates a [RawConstraintDefV9] using the supplied `columns`. pub fn with_unique_constraint(mut self, columns: impl Into) -> Self { let columns = columns.into(); self.table.constraints.push(RawConstraintDefV9 { @@ -734,7 +734,7 @@ impl RawTableDefBuilder<'_> { .with_column_sequence(column) } - /// Generates a [RawIndexDef] using the supplied `columns`. + /// Generates a [RawIndexDefV9] using the supplied `columns`. pub fn with_index(mut self, algorithm: RawIndexAlgorithm, accessor_name: impl Into) -> Self { let accessor_name = accessor_name.into(); @@ -746,7 +746,7 @@ impl RawTableDefBuilder<'_> { self } - /// Generates a [RawIndexDef] using the supplied `columns` but with no `accessor_name`. + /// Generates a [RawIndexDefV9] using the supplied `columns` but with no `accessor_name`. pub fn with_index_no_accessor_name(mut self, algorithm: RawIndexAlgorithm) -> Self { self.table.indexes.push(RawIndexDefV9 { name: None, @@ -756,7 +756,7 @@ impl RawTableDefBuilder<'_> { self } - /// Adds a [RawSequenceDef] on the supplied `column`. + /// Adds a [RawSequenceDefV9] on the supplied `column`. pub fn with_column_sequence(mut self, column: impl Into) -> Self { let column = column.into(); self.table.sequences.push(RawSequenceDefV9 { diff --git a/crates/lib/src/identity.rs b/crates/lib/src/identity.rs index 5790c097c61..d64f367bb63 100644 --- a/crates/lib/src/identity.rs +++ b/crates/lib/src/identity.rs @@ -33,6 +33,8 @@ impl AuthCtx { /// An `Identity` for something interacting with the database. /// +/// +/// /// An `Identity` is a 256-bit unsigned integer. These are encoded in various ways. /// - In JSON, an `Identity` is represented as a hexadecimal number wrapped in a string, `"0x[64 hex characters]"`. /// - In BSATN, an `Identity` is represented as a LITTLE-ENDIAN number 32 bytes long. diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 84d69312aca..44bb703b419 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -217,7 +217,8 @@ pub enum RawModuleDef { // but I'm not sure if that can be done via the Deserialize trait. } -/// A builder for a [`ModuleDef`]. +/// A builder for a [`RawModuleDefV8`]. +/// Deprecated. #[derive(Default)] pub struct ModuleDefBuilder { /// The module definition. diff --git a/crates/lib/src/operator.rs b/crates/lib/src/operator.rs index f10df16352d..4435a9c8f08 100644 --- a/crates/lib/src/operator.rs +++ b/crates/lib/src/operator.rs @@ -1,8 +1,5 @@ -//! Operators are implemented as "alias" of functions that are loaded -//! at the start of the [ProgramVm] creation, ie: -//! -//! `+` == std::math::add -//! +//! Operator support for the query macro. + use derive_more::From; use spacetimedb_lib::de::Deserialize; use spacetimedb_lib::ser::Serialize; diff --git a/crates/lib/src/relation.rs b/crates/lib/src/relation.rs index 09663c18280..7e6bfeed6a4 100644 --- a/crates/lib/src/relation.rs +++ b/crates/lib/src/relation.rs @@ -272,7 +272,6 @@ impl fmt::Display for Header { } } -/// A stored table from [RelationalDB] #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct DbTable { pub head: Arc
, diff --git a/crates/lib/src/scheduler.rs b/crates/lib/src/scheduler.rs index fc9c7cb9b36..fa20dcab243 100644 --- a/crates/lib/src/scheduler.rs +++ b/crates/lib/src/scheduler.rs @@ -29,7 +29,7 @@ impl_st!([] ScheduleAt, ScheduleAt::get_type()); impl ScheduleAt { /// Converts the `ScheduleAt` to a `std::time::Duration` from now. /// - /// Returns [`Duration::ZERO`] if `self` represents a time in the past. + /// Returns [`std::time::Duration::ZERO`] if `self` represents a time in the past. #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] pub fn to_duration_from_now(&self) -> std::time::Duration { use std::time::{Duration, SystemTime}; diff --git a/crates/sats/src/algebraic_type.rs b/crates/sats/src/algebraic_type.rs index 15c740ecfe0..cf5b178e201 100644 --- a/crates/sats/src/algebraic_type.rs +++ b/crates/sats/src/algebraic_type.rs @@ -39,16 +39,18 @@ pub enum AlgebraicType { /// Another name is [coproduct (category theory)](https://ncatlab.org/nlab/show/coproduct). /// /// These structures are known as sum types because the number of possible values a sum - /// ```ignore + /// ```text /// { N_0(T_0), N_1(T_1), ..., N_n(T_n) } /// ``` /// is: - /// ```ignore + /// ```text /// Σ (i ∈ 0..n). values(T_i) /// ``` /// so for example, `values({ A(U64), B(Bool) }) = values(U64) + values(Bool)`. /// - /// See also: https://ncatlab.org/nlab/show/sum+type. + /// See also: + /// - + /// - /// /// [structural]: https://en.wikipedia.org/wiki/Structural_type_system Sum(SumType), @@ -62,14 +64,16 @@ pub enum AlgebraicType { /// e.g., the names of its fields and their types in the case of a record. /// The name "product" comes from category theory. /// - /// See also: https://ncatlab.org/nlab/show/product+type. + /// See also: + /// - + /// - /// /// These structures are known as product types because the number of possible values in product - /// ```ignore + /// ```text /// { N_0: T_0, N_1: T_1, ..., N_n: T_n } /// ``` /// is: - /// ```ignore + /// ```text /// Π (i ∈ 0..n). values(T_i) /// ``` /// so for example, `values({ A: U64, B: Bool }) = values(U64) * values(Bool)`. @@ -393,7 +397,7 @@ impl AlgebraicType { /// in a `SpacetimeDB` client module. /// /// Such a type must be a non-special sum or product type. - /// All of the elements of the type must be [`valid_for_client_type_use`](AlgebraicType::valid_for_client_type_use). + /// All of the elements of the type must satisfy [`AlgebraicType::is_valid_for_client_type_use`]. /// /// This method does not actually follow `Ref`s to check the types they point to, /// it only checks the structure of this type. @@ -422,7 +426,7 @@ impl AlgebraicType { /// - a reference /// - a special, known type /// - a non-compound type like `U8`, `I32`, `F64`, etc. - /// - or a map, array, or option built from types that are [`valid_for_client_type_use`](AlgebraicType::valid_for_client_type_use). + /// - or a map, array, or option built from types that satisfy [`AlgebraicType::is_valid_for_client_type_use`] /// /// This method does not actually follow `Ref`s to check the types they point to, /// it only checks the structure of the type. diff --git a/crates/sats/src/algebraic_value/ser.rs b/crates/sats/src/algebraic_value/ser.rs index 6b1870646f2..3b660c463f8 100644 --- a/crates/sats/src/algebraic_value/ser.rs +++ b/crates/sats/src/algebraic_value/ser.rs @@ -206,8 +206,8 @@ unsafe fn write_byte_chunks<'a>(mut dst: *mut u8, chunks: impl Iterator]` into a `[T]` by asserting all elements are initialized. /// -/// Identitcal copy of the source of `MaybeUninit::slice_assume_init_ref`, but that's not stabilized. -/// https://doc.rust-lang.org/std/mem/union.MaybeUninit.html#method.slice_assume_init_ref +/// Identical copy of the source of `MaybeUninit::slice_assume_init_ref`, but that's not stabilized. +/// /// /// # Safety /// diff --git a/crates/sats/src/algebraic_value_hash.rs b/crates/sats/src/algebraic_value_hash.rs index b327a5e1b95..df6afd253f0 100644 --- a/crates/sats/src/algebraic_value_hash.rs +++ b/crates/sats/src/algebraic_value_hash.rs @@ -213,7 +213,7 @@ fn hash_bsatn_int_seq<'a, H: Hasher, R: BufReader<'a>>(state: &mut H, mut de: De // Extract and hash the bytes. // This is consistent with what `<$int_primitive>::hash_slice` will do // and for `U/I256` we provide special logic in `impl Hash for ArrayValue` above - // and handle it the same way for `RowRef`s. + // and handle it the same way for `spacetimedb_table::table::RowRef`s. let bytes = de.get_slice(len * width)?; hash_len_and_bytes(state, len, bytes); diff --git a/crates/sats/src/array_value.rs b/crates/sats/src/array_value.rs index b107a3619bb..b798873dc05 100644 --- a/crates/sats/src/array_value.rs +++ b/crates/sats/src/array_value.rs @@ -10,9 +10,9 @@ use core::fmt; /// as arrays are homogenous dynamically sized product types. #[derive(Clone, Eq, PartialEq, Ord, PartialOrd)] pub enum ArrayValue { - /// An array of [`SumValue`](crate::SumValue)s. + /// An array of [`SumValue`]s. Sum(Box<[SumValue]>), - /// An array of [`ProductValue`](crate::ProductValue)s. + /// An array of [`ProductValue`]s. Product(Box<[ProductValue]>), /// An array of [`bool`]s. Bool(Box<[bool]>), diff --git a/crates/sats/src/bsatn.rs b/crates/sats/src/bsatn.rs index f40241cb213..d1909db952f 100644 --- a/crates/sats/src/bsatn.rs +++ b/crates/sats/src/bsatn.rs @@ -108,8 +108,8 @@ codec_funcs!(val: crate::SumValue); /// Types that can be encoded to BSATN. /// -/// Implementations of this trait may be more efficient than directly calling [`bsatn::to_vec`]. -/// In particular, for [`RowRef`], this method will use a [`StaticLayout`] if one is available, +/// Implementations of this trait may be more efficient than directly calling [`to_vec`]. +/// In particular, for `spacetimedb_table::table::RowRef`, this method will use a `StaticLayout` if one is available, /// avoiding expensive runtime type dispatch. pub trait ToBsatn { /// BSATN-encode the row referred to by `self` into a freshly-allocated `Vec`. diff --git a/crates/sats/src/de.rs b/crates/sats/src/de.rs index 4a9c1248d9e..757faa0a243 100644 --- a/crates/sats/src/de.rs +++ b/crates/sats/src/de.rs @@ -15,10 +15,10 @@ use core::marker::PhantomData; use smallvec::SmallVec; use std::borrow::Borrow; -/// A **data format** that can deserialize any data structure supported by SATS. +/// A data format that can deserialize any data structure supported by SATS. /// -/// The `Deserializer` trait in SATS performs the same function as [`serde::Deserializer`] in [`serde`]. -/// See the documentation of [`serde::Deserializer`] for more information of the data model. +/// The `Deserializer` trait in SATS performs the same function as `serde::Deserializer` in [`serde`]. +/// See the documentation of `serde::Deserializer` for more information of the data model. /// /// Implementations of `Deserialize` map themselves into this data model /// by passing to the `Deserializer` a visitor that can receive the necessary types. @@ -28,7 +28,6 @@ use std::borrow::Borrow; /// /// The lifetime `'de` allows us to deserialize lifetime-generic types in a zero-copy fashion. /// -/// [`serde::Deserializer`]: ::serde::Deserializer /// [`serde`]: https://crates.io/crates/serde pub trait Deserializer<'de>: Sized { /// The error type that can be returned if some error occurs during deserialization. @@ -543,16 +542,19 @@ 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. /// -/// The `Deserialize` trait in SATS performs the same function as [`serde::Deserialize`] in [`serde`]. -/// See the documentation of [`serde::Deserialize`] for more information of the data model. +/// The `Deserialize` trait in SATS performs the same function as `serde::Deserialize` in [`serde`]. +/// See the documentation of `serde::Deserialize` for more information of the data model. /// /// The lifetime `'de` allows us to deserialize lifetime-generic types in a zero-copy fashion. /// -/// [`serde::Deserialize`]: ::serde::Deserialize +/// Do not manually implement this trait unless you know what you are doing. +/// Implementations must be consistent with `Serialize for T`, `SpacetimeType for T` and `Serialize, Deserialize for AlgebraicValue`. +/// Implementations that are inconsistent across these traits may result in data loss. +/// /// [`serde`]: https://crates.io/crates/serde pub trait Deserialize<'de>: Sized { /// Deserialize this value from the given `deserializer`. diff --git a/crates/sats/src/product_type.rs b/crates/sats/src/product_type.rs index 55e82f45578..38d41438627 100644 --- a/crates/sats/src/product_type.rs +++ b/crates/sats/src/product_type.rs @@ -26,14 +26,16 @@ pub const TIME_DURATION_TAG: &str = "__time_duration_micros__"; /// e.g., the names of its fields and their types in the case of a record. /// The name "product" comes from category theory. /// -/// See also: https://ncatlab.org/nlab/show/product+type. +/// See also: +/// - +/// - /// /// These structures are known as product types because the number of possible values in product -/// ```ignore +/// ```text /// { N_0: T_0, N_1: T_1, ..., N_n: T_n } /// ``` /// is: -/// ```ignore +/// ```text /// Π (i ∈ 0..n). values(T_i) /// ``` /// so for example, `values({ A: U64, B: Bool }) = values(U64) * values(Bool)`. diff --git a/crates/sats/src/satn.rs b/crates/sats/src/satn.rs index 5ce0d7c1877..f9a787190f5 100644 --- a/crates/sats/src/satn.rs +++ b/crates/sats/src/satn.rs @@ -8,7 +8,7 @@ use core::fmt; use core::fmt::Write as _; use derive_more::{From, Into}; -/// An extension trait for [`Serialize`](ser::Serialize) providing formatting methods. +/// An extension trait for [`Serialize`] providing formatting methods. pub trait Satn: ser::Serialize { /// Formats the value using the SATN data format into the formatter `f`. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { diff --git a/crates/sats/src/ser.rs b/crates/sats/src/ser.rs index f2bd05c5644..bfbe392a219 100644 --- a/crates/sats/src/ser.rs +++ b/crates/sats/src/ser.rs @@ -7,12 +7,11 @@ pub mod serde; use core::fmt; -/// A **data format** that can deserialize any data structure supported by SATs. +/// A data format that can deserialize any data structure supported by SATs. /// -/// The `Serializer` trait in SATS performs the same function as [`serde::Serializer`] in [`serde`]. -/// See the documentation of [`serde::Serializer`] for more information of the data model. +/// The `Serializer` trait in SATS performs the same function as `serde::Serializer` in [`serde`]. +/// See the documentation of `serde::Serializer` for more information on the data model. /// -/// [`serde::Serializer`]: ::serde::Serializer /// [`serde`]: https://crates.io/crates/serde pub trait Serializer: Sized { /// The output type produced by this `Serializer` during successful serialization. @@ -203,14 +202,18 @@ pub use spacetimedb_bindings_macro::Serialize; use crate::{bsatn, buffer::BufWriter, 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. +/// 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. +/// Implementations must be consistent with `Deerialize<'de> for T`, `SpacetimeType for T` and `Serialize, Deserialize for AlgebraicValue`. +/// Implementations that are inconsistent across these traits may result in data loss. /// -/// [`serde::Serialize`]: ::serde::Serialize /// [`serde`]: https://crates.io/crates/serde pub trait Serialize { /// Serialize `self` in the data format of `S` using the provided `serializer`. diff --git a/crates/sats/src/sum_type.rs b/crates/sats/src/sum_type.rs index 8d2fcc92177..e031ba1737e 100644 --- a/crates/sats/src/sum_type.rs +++ b/crates/sats/src/sum_type.rs @@ -25,16 +25,18 @@ pub const OPTION_NONE_TAG: &str = "none"; /// Another name is [coproduct (category theory)](https://ncatlab.org/nlab/show/coproduct). /// /// These structures are known as sum types because the number of possible values a sum -/// ```ignore +/// ```text /// { N_0(T_0), N_1(T_1), ..., N_n(T_n) } /// ``` /// is: -/// ```ignore +/// ```text /// Σ (i ∈ 0..n). values(T_i) /// ``` /// so for example, `values({ A(U64), B(Bool) }) = values(U64) + values(Bool)`. /// -/// See also: https://ncatlab.org/nlab/show/sum+type. +/// See also: +/// - +/// - /// /// [structural]: https://en.wikipedia.org/wiki/Structural_type_system #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, SpacetimeType)] diff --git a/crates/sats/src/typespace.rs b/crates/sats/src/typespace.rs index 1935f1366b0..7d54d12cbdc 100644 --- a/crates/sats/src/typespace.rs +++ b/crates/sats/src/typespace.rs @@ -202,9 +202,9 @@ impl Typespace { /// See also the `spacetimedb_schema` crate, which layers additional validation on top /// of these checks. /// - /// All types in the typespace must either be - /// [`valid_for_client_type_definition`](AlgebraicType::valid_for_client_type_definition) or - /// [`valid_for_client_type_use`](AlgebraicType::valid_for_client_type_use). + /// All types in the typespace must either satisfy + /// [`is_valid_for_client_type_definition`](AlgebraicType::is_valid_for_client_type_definition) or + /// [`is_valid_for_client_type_use`](AlgebraicType::is_valid_for_client_type_use). /// (Only the types that are `valid_for_client_type_definition` will have types generated in /// the client, but the other types are allowed for the convenience of module binding codegen.) pub fn is_valid_for_client_code_generation(&self) -> bool { @@ -232,14 +232,67 @@ 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.) +/// `Serialize` and `Deserialize` 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 as a table column or reducer argument. 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. +/// Implementations must be consistent with `Deerialize<'de> for T`, `Serialize for T` and `Serialize, Deserialize for AlgebraicValue`. +/// Implementations that are inconsistent across these traits may 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; }