From 58ece32002d4c9349b7d9c3187bb316bf753ec44 Mon Sep 17 00:00:00 2001 From: Jason Larabie Date: Thu, 29 Jan 2026 11:35:38 -0800 Subject: [PATCH 1/3] Update the Functions documentation with C++ bindings code blocks --- .../00300-reducers/00300-reducers.md | 137 ++++++++ .../00300-reducers/00400-reducer-context.md | 99 ++++++ .../00300-reducers/00500-lifecycle.md | 92 ++++++ .../00300-reducers/00600-error-handling.md | 52 ++++ .../00200-functions/00400-procedures.md | 292 ++++++++++++++++++ .../00200-functions/00500-views.md | 175 +++++++++++ 6 files changed, 847 insertions(+) diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md index 3296c4bb96b..076bb404de5 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md +++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md @@ -108,6 +108,31 @@ use spacetimedb::Table; If you see errors like "no method named `try_insert` found", add this import. ::: + + + +Use the `SPACETIMEDB_REDUCER` macro on a function: + +```cpp +#include +using namespace SpacetimeDB; + +SPACETIMEDB_REDUCER(create_user, ReducerContext ctx, std::string name, std::string email) { + // Validate input + if (name.empty()) { + return Err("Name cannot be empty"); + } + + // Modify tables + User user{0, name, email}; // 0 for id - auto-increment will assign + ctx.db[user].insert(user); + + return Ok(); +} +``` + +Reducers must take `ReducerContext ctx` as their first parameter. Additional parameters can be any registered types. Reducers return `ReducerResult` (which is `Outcome`): use `Ok()` on success or `Err(message)` on error for convenience. + @@ -161,6 +186,13 @@ ctx.db.user().insert(User { }); ``` + + + +```cpp +ctx.db[user].insert(User{0, "Alice", "alice@example.com"}); // 0 for id - auto-increment will assign +``` + @@ -204,6 +236,17 @@ if let Some(user) = ctx.db.user().id().find(123) { let by_email = ctx.db.user().email().find("alice@example.com"); ``` + + + +```cpp +if (auto user = ctx.db[user_id].find(123)) { + LOG_INFO("Found: " + user->name); +} + +auto by_email = ctx.db[user_email].find("alice@example.com"); +``` + @@ -239,6 +282,15 @@ for user in ctx.db.user().name().filter("Alice") { } ``` + + + +```cpp +for (const auto& user : ctx.db[user_name].filter("Alice")) { + LOG_INFO("User " + std::to_string(user.id) + ": " + user.email); +} +``` + @@ -279,6 +331,16 @@ if let Some(mut user) = ctx.db.user().id().find(123) { } ``` + + + +```cpp +if (auto user = ctx.db[user_id].find(123)) { + user->name = "Bob"; + ctx.db[user_id].update(*user); +} +``` + @@ -322,6 +384,22 @@ let deleted = ctx.db.user().name().delete("Alice"); log::info!("Deleted {} row(s)", deleted); ``` + + + +```cpp +// Delete by primary key +ctx.db[user_id].delete_by_key(123); + +// Delete all matching an indexed column +uint32_t deleted = 0; +for (const auto& user : ctx.db[user_name].filter("Alice")) { + ctx.db[user_id].delete_by_key(user.id); + deleted++; +} +LOG_INFO("Deleted " + std::to_string(deleted) + " row(s)"); +``` + @@ -357,6 +435,15 @@ for user in ctx.db.user().iter() { } ``` + + + +```cpp +for (const auto& user : ctx.db[user]) { + LOG_INFO(std::to_string(user.id) + ": " + user.name); +} +``` + @@ -388,6 +475,14 @@ let total = ctx.db.user().count(); log::info!("Total users: {}", total); ``` + + + +```cpp +auto total = ctx.db[user].count(); +LOG_INFO("Total users: " + std::to_string(total)); +``` + @@ -553,6 +648,48 @@ fn queue_fetch(ctx: &ReducerContext, url: String) { } ``` + + + +```cpp +#define SPACETIMEDB_UNSTABLE_FEATURES +#include +using namespace SpacetimeDB; + +// Define a table to store scheduled tasks +struct FetchSchedule { + uint64_t scheduled_id; + ScheduleAt scheduled_at; + std::string url; +}; +SPACETIMEDB_STRUCT(FetchSchedule, scheduled_id, scheduled_at, url); +SPACETIMEDB_TABLE(FetchSchedule, fetch_schedule, Private); +FIELD_PrimaryKeyAutoInc(fetch_schedule, scheduled_id); + +// Register the table for scheduling (column 1 = scheduled_at field, 0-based index) +SPACETIMEDB_SCHEDULE(fetch_schedule, 1, fetch_external_data); + +// The procedure to be scheduled - called automatically when the time arrives +SPACETIMEDB_PROCEDURE(uint32_t, fetch_external_data, ProcedureContext ctx, FetchSchedule schedule) { + LOG_INFO("Fetching data from: " + schedule.url); + // Process response... + return 0; // Success +} + +// From a reducer, schedule the procedure by inserting into the schedule table +SPACETIMEDB_REDUCER(queue_fetch, ReducerContext ctx, std::string url) { + auto scheduled_at = ScheduleAt(TimeDuration::from_seconds(0)); // Run immediately + FetchSchedule fetch_task{ + 0, // scheduled_id - auto-increment will assign + scheduled_at, // When to execute + url + }; + ctx.db[fetch_schedule].insert(fetch_task); + LOG_INFO("Fetch scheduled for URL: " + url); + return Ok(); +} +``` + diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md index 32b96e5ed33..0c5963f6f69 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md +++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md @@ -81,6 +81,27 @@ fn create_user(ctx: &ReducerContext, name: String) { } ``` + + + +```cpp +#include +using namespace SpacetimeDB; + +struct User { + uint64_t id; + std::string name; +}; +SPACETIMEDB_STRUCT(User, id, name); +SPACETIMEDB_TABLE(User, user, Public); +FIELD_PrimaryKeyAutoInc(user, id); + +SPACETIMEDB_REDUCER(create_user, ReducerContext ctx, std::string name) { + ctx.db[user].insert(User{0, name}); + return Ok(); +} +``` + @@ -184,6 +205,35 @@ fn update_score(ctx: &ReducerContext, new_score: u32) { } ``` + + + +```cpp +#include +using namespace SpacetimeDB; + +struct Player { + Identity identity; + std::string name; + uint32_t score; +}; +SPACETIMEDB_STRUCT(Player, identity, name, score); +SPACETIMEDB_TABLE(Player, player, Public); +FIELD_PrimaryKey(player, identity); + +SPACETIMEDB_REDUCER(update_score, ReducerContext ctx, uint32_t new_score) { + // Get the caller's identity + auto caller = ctx.sender; + + // Find and update their player record + if (auto player = ctx.db[player_identity].find(caller)) { + player->score = new_score; + ctx.db[player_identity].update(*player); + } + return Ok(); +} +``` + @@ -298,6 +348,36 @@ fn send_reminder(ctx: &ReducerContext, task: ScheduledTask) { } ``` + + + +```cpp +#include +using namespace SpacetimeDB; + +struct ScheduledTask { + uint64_t task_id; + ScheduleAt scheduled_at; + std::string message; +}; +SPACETIMEDB_STRUCT(ScheduledTask, task_id, scheduled_at, message); +SPACETIMEDB_TABLE(ScheduledTask, scheduled_task, Private); +FIELD_PrimaryKeyAutoInc(scheduled_task, task_id); + +// Register the table for scheduling (column 1 = scheduled_at field, 0-based index) +SPACETIMEDB_SCHEDULE(scheduled_task, 1, send_reminder); + +SPACETIMEDB_REDUCER(send_reminder, ReducerContext ctx, ScheduledTask task) { + // Only allow the scheduler (module identity) to call this + if (ctx.sender != ctx.identity()) { + return Err("This reducer can only be called by the scheduler"); + } + + LOG_INFO("Reminder: " + task.message); + return Ok(); +} +``` + @@ -346,4 +426,23 @@ TypeScript uses `Math.random()` for random number generation, which is automatic - `random() -> T` - Generate a single random value - `sender_auth() -> &AuthCtx` - Get authorization context for the caller (includes JWT claims and internal call detection) + + +| Property | Type | Description | +| --------------- | ------------------------------ | ----------------------------------------------- | +| `db[table]` | `Table` | Access to a specific table's operations | +| `sender` | `Identity` | Identity of the caller | +| `timestamp` | `Timestamp` | Time when the reducer was invoked | +| `connection_id` | `std::optional` | Connection ID of the caller, if available | + +**Methods:** + +- `identity() -> Identity` - Get the module's identity +- `rng() -> StdbRng&` - Get the random number generator (deterministic and reproducible) +- `sender_auth() -> const AuthCtx&` - Get authorization context for the caller (includes JWT claims and internal call detection) + +:::note +C++ uses the `std::optional` type for the `connection_id` to represent values that may not be present. The `rng()` method returns a deterministic random number generator that is seeded consistently across all nodes. +::: + diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md index 975f589b437..2f267029d2d 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md +++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md @@ -71,6 +71,36 @@ pub fn init(ctx: &ReducerContext) -> Result<(), String> { } ``` + + + +```cpp +#include +using namespace SpacetimeDB; + +struct Settings { + std::string key; + std::string value; +}; +SPACETIMEDB_STRUCT(Settings, key, value); +SPACETIMEDB_TABLE(Settings, settings, Private); +FIELD_Unique(settings, key); + +SPACETIMEDB_INIT(init, ReducerContext ctx) { + LOG_INFO("Database initializing..."); + + // Set up default data + if (ctx.db[settings].count() == 0) { + ctx.db[settings].insert(Settings{ + "welcome_message", + "Hello, SpacetimeDB!" + }); + } + + return Ok(); +} +``` + @@ -147,6 +177,39 @@ pub fn on_connect(ctx: &ReducerContext) -> Result<(), String> { } ``` + + + +```cpp +#include +using namespace SpacetimeDB; + +struct Session { + ConnectionId connection_id; + Identity identity; + Timestamp connected_at; +}; +SPACETIMEDB_STRUCT(Session, connection_id, identity, connected_at); +SPACETIMEDB_TABLE(Session, sessions, Private); +FIELD_PrimaryKey(sessions, connection_id); + +SPACETIMEDB_CLIENT_CONNECTED(on_connect, ReducerContext ctx) { + LOG_INFO("Client connected: " + ctx.sender.to_string()); + + // ctx.connection_id is guaranteed to be present + auto conn_id = ctx.connection_id.value(); + + // Initialize client session + ctx.db[sessions].insert(Session{ + conn_id, + ctx.sender, + ctx.timestamp + }); + + return Ok(); +} +``` + @@ -210,6 +273,35 @@ pub fn on_disconnect(ctx: &ReducerContext) -> Result<(), String> { } ``` + + + +```cpp +#include +using namespace SpacetimeDB; + +struct Session { + ConnectionId connection_id; + Identity identity; + Timestamp connected_at; +}; +SPACETIMEDB_STRUCT(Session, connection_id, identity, connected_at); +SPACETIMEDB_TABLE(Session, sessions, Private); +FIELD_PrimaryKey(sessions, connection_id); + +SPACETIMEDB_CLIENT_DISCONNECTED(on_disconnect, ReducerContext ctx) { + LOG_INFO("Client disconnected: " + ctx.sender.to_string()); + + // ctx.connection_id is guaranteed to be present + auto conn_id = ctx.connection_id.value(); + + // Clean up client session + ctx.db[sessions_connection_id].delete_by_key(conn_id); + + return Ok(); +} +``` + diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00600-error-handling.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00600-error-handling.md index 714f68d1110..a50e26895ec 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00600-error-handling.md +++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00600-error-handling.md @@ -101,6 +101,36 @@ pub fn transfer_credits( ``` + + + ```cpp + #include + using namespace SpacetimeDB; + + struct User { + Identity identity; + uint32_t credits; + }; + SPACETIMEDB_STRUCT(User, identity, credits); + SPACETIMEDB_TABLE(User, users, Private); + FIELD_PrimaryKey(users, identity); + + SPACETIMEDB_REDUCER(transfer_credits, ReducerContext ctx, Identity to_user, uint32_t amount) { + auto from_user = ctx.db[users_identity].find(ctx.sender); + if (!from_user) { + return Err("User not found"); + } + + if (from_user->credits < amount) { + return Err("Insufficient credits"); + } + + // ... perform transfer + return Ok(); + } + ``` + + ### Programmer Errors @@ -165,6 +195,28 @@ pub fn process_data(ctx: &ReducerContext, data: Vec) -> Result<(), String> { ``` + + + ```cpp + #include + #include + using namespace SpacetimeDB; + + SPACETIMEDB_REDUCER(process_data, ReducerContext ctx, Vec data) { + // This indicates a bug + assert(!data.empty() && "Unexpected empty data"); + + auto parsed = parse_data(data); + if (!parsed) { + LOG_PANIC("Failed to parse data"); + } + + // ... + return Ok(); + } + ``` + + Programmer errors are logged and visible in your project dashboard. Consider setting up alerting to be notified when these occur. diff --git a/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md b/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md index 51af1e5a736..c31276de352 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md +++ b/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md @@ -96,6 +96,27 @@ fn add_two_numbers(ctx: &mut spacetimedb::ProcedureContext, lhs: u32, rhs: u32) } ``` + + + +:::warning Unstable Feature +Procedures in C++ are currently unstable. To use them, add `#define SPACETIMEDB_UNSTABLE_FEATURES` before including the SpacetimeDB header. +::: + +Define a procedure using the `SPACETIMEDB_PROCEDURE` macro. + +The macro's first parameter is the return type, followed by the procedure name. The function's first argument must be of type `ProcedureContext`. By convention, this argument is named `ctx`. A procedure may accept any number of additional arguments and must return a value. + +```cpp +#define SPACETIMEDB_UNSTABLE_FEATURES +#include +using namespace SpacetimeDB; + +SPACETIMEDB_PROCEDURE(uint64_t, add_two_numbers, ProcedureContext ctx, uint32_t lhs, uint32_t rhs) { + return static_cast(lhs) + static_cast(rhs); +} +``` + @@ -244,6 +265,55 @@ values observed during prior runs must not influence the behavior of the functio Avoid capturing mutable state within functions passed to `with_tx`. ::: + + + +Unlike reducers, procedures don't automatically run in database transactions. +This means there's no `ctx.db` field to access the database. +Instead, procedure code must manage transactions explicitly with `ctx.with_tx`. + +```cpp +#define SPACETIMEDB_UNSTABLE_FEATURES +#include +using namespace SpacetimeDB; + +struct MyTable { + uint32_t a; + std::string b; +}; +SPACETIMEDB_STRUCT(MyTable, a, b) +SPACETIMEDB_TABLE(MyTable, my_table, Public) + +SPACETIMEDB_PROCEDURE(Unit, insert_a_value, ProcedureContext ctx, uint32_t a, std::string b) { + ctx.with_tx([&](TxContext& tx) { + tx.db[my_table].insert(MyTable{a, b}); + }); + return Unit{}; +} +``` + +`ctx.with_tx` takes a lambda function with signature `[](TxContext& tx) -> T`. +Within that function, the `TxContext` can be used to access the database +[in all the same ways as a `ReducerContext`](/functions/reducers/reducer-context). +When the function returns, the transaction will be committed, +and its changes to the database state will become permanent and be broadcast to clients. +If the function throws an exception, the transaction will be rolled back, and its changes will be discarded. +However, for transactions that may fail, +[prefer calling `try_with_tx` and returning `bool`](#fallible-database-operations) rather than throwing. + +:::warning +The function passed to `ctx.with_tx` may be invoked multiple times, +possibly seeing a different version of the database state each time. + +If invoked more than once with reference to the same database state, +it must perform the same operations and return the same result each time. + +If invoked more than once with reference to different database states, +values observed during prior runs must not influence the behavior of the function or the calling procedure. + +Avoid capturing mutable state within functions passed to `with_tx`. +::: + @@ -315,6 +385,36 @@ If the function returns `Ok`, the transaction will be committed, and its changes to the database state will become permanent and be broadcast to clients. If that function returns `Err`, the transaction will be rolled back, and its changes will be discarded. + + + +For fallible database operations, use `ctx.try_with_tx` with a lambda that returns `bool`: + +```cpp +#define SPACETIMEDB_UNSTABLE_FEATURES +#include +using namespace SpacetimeDB; + +SPACETIMEDB_PROCEDURE(bool, maybe_insert_a_value, ProcedureContext ctx, uint32_t a, std::string b) { + return ctx.try_with_tx([&](TxContext& tx) -> bool { + if (a < 10) { + return false; // Rollback transaction + } + tx.db[my_table].insert(MyTable{a, b}); + return true; // Commit transaction + }); +} +``` + +`ctx.try_with_tx` takes a lambda function with signature `[](TxContext& tx) -> bool`. +If the function returns `true`, the transaction will be committed, +and its changes to the database state will become permanent and be broadcast to clients. +If the function returns `false`, the transaction will be rolled back, and its changes will be discarded. + +:::note +For non-bool return types, `try_with_tx` always commits the transaction. To abort in those cases, use `LOG_PANIC`. +::: + @@ -434,6 +534,47 @@ fn find_highest_level_player(ctx: &mut ProcedureContext) { } ``` + + + +Functions passed to +[`ctx.with_tx`](#accessing-the-database) and [`ctx.try_with_tx`](#fallible-database-operations) +may return a value, and that value will be returned to the calling procedure. + +Transaction return values are never saved or broadcast to clients, and are used only by the calling procedure. + +```cpp +#define SPACETIMEDB_UNSTABLE_FEATURES +#include +using namespace SpacetimeDB; + +struct Player { + Identity id; + uint32_t level; +}; +SPACETIMEDB_STRUCT(Player, id, level) +SPACETIMEDB_TABLE(Player, player, Public) + +SPACETIMEDB_PROCEDURE(Unit, find_highest_level_player, ProcedureContext ctx) { + auto highest_level_player = ctx.with_tx([](TxContext& tx) -> std::optional { + std::optional highest; + for (const auto& player : tx.db[player]) { + if (!highest || player.level > highest->level) { + highest = player; + } + } + return highest; + }); + + if (highest_level_player) { + LOG_INFO("Congratulations to " + highest_level_player->id.to_hex_string()); + } else { + LOG_WARN("No players..."); + } + return Unit{}; +} +``` + @@ -639,6 +780,103 @@ fn get_request_with_short_timeout(ctx: &mut spacetimedb::ProcedureContext) { Procedures can't send requests at the same time as holding open a [transaction](#accessing-the-database). + + + +:::warning Unstable Feature +HTTP requests in C++ procedures are currently unstable. To use them, add `#define SPACETIMEDB_UNSTABLE_FEATURES` before including the SpacetimeDB header. +::: + +Procedures can make HTTP requests to external services using methods on `ctx.http`. + +`ctx.http.get` performs simple `GET` requests: + +```cpp +#define SPACETIMEDB_UNSTABLE_FEATURES +#include +using namespace SpacetimeDB; + +SPACETIMEDB_PROCEDURE(Unit, get_request, ProcedureContext ctx) { + auto result = ctx.http.get("https://example.invalid"); + + if (result.is_ok()) { + auto& response = result.value(); + auto body = response.body.to_string_utf8_lossy(); + LOG_INFO("Got response with status " + std::to_string(response.status_code) + + " and body " + body); + } else { + LOG_ERROR("Request failed: " + result.error()); + } + + return Unit{}; +} +``` + +`ctx.http.send` sends an `HttpRequest` with custom method, headers, and body: + +```cpp +#define SPACETIMEDB_UNSTABLE_FEATURES +#include +using namespace SpacetimeDB; + +SPACETIMEDB_PROCEDURE(Unit, post_request, ProcedureContext ctx) { + HttpRequest request{ + .uri = "https://example.invalid/upload", + .method = HttpMethod::post(), + .headers = {HttpHeader{"Content-Type", "text/plain"}}, + .body = HttpBody::from_string("This is the body of the HTTP request") + }; + + auto result = ctx.http.send(request); + + if (result.is_ok()) { + auto& response = result.value(); + auto body = response.body.to_string_utf8_lossy(); + LOG_INFO("Got response with status " + std::to_string(response.status_code) + + " and body " + body); + } else { + LOG_ERROR("Request failed: " + result.error()); + } + + return Unit{}; +} +``` + +Set a timeout for a request using `TimeDuration::from_millis()`: + +```cpp +#define SPACETIMEDB_UNSTABLE_FEATURES +#include +using namespace SpacetimeDB; + +SPACETIMEDB_PROCEDURE(Unit, get_request_with_short_timeout, ProcedureContext ctx) { + HttpRequest request{ + .uri = "https://example.invalid", + .method = HttpMethod::get(), + .timeout = TimeDuration::from_millis(10) + }; + + auto result = ctx.http.send(request); + + if (result.is_ok()) { + auto& response = result.value(); + auto body = response.body.to_string_utf8_lossy(); + LOG_INFO("Got response with status " + std::to_string(response.status_code) + + " and body " + body); + } else { + LOG_ERROR("Request failed: " + result.error()); + } + + return Unit{}; +} +``` + +:::note +All timeouts are clamped to a maximum of 500ms by the host. +::: + +Procedures can't send requests at the same time as holding open a [transaction](#accessing-the-database). + @@ -736,6 +974,60 @@ fn fetch_and_process(ctx: &mut ProcedureContext, url: String) -> Result<(), Stri } ``` + + + +:::warning Unstable Feature +Procedures in C++ are currently unstable. To use them, add `#define SPACETIMEDB_UNSTABLE_FEATURES` before including the SpacetimeDB header. +::: + +In C++, `TxContext` and `ReducerContext` share the same database API, so it’s common to move shared logic into a helper that takes a `DatabaseContext&` and call it from both the reducer and the procedure. + +```cpp +#define SPACETIMEDB_UNSTABLE_FEATURES +#include +using namespace SpacetimeDB; + +struct ProcessedItem { + uint64_t id; +}; +SPACETIMEDB_STRUCT(ProcessedItem, id) +SPACETIMEDB_TABLE(ProcessedItem, processed_item_proc, Public) +FIELD_PrimaryKey(processed_item_proc, id) + +static void process_item_logic(DatabaseContext& db, uint64_t item_id) { + db[processed_item_proc].insert(ProcessedItem{item_id}); +} + +SPACETIMEDB_REDUCER(process_item, ReducerContext& ctx, uint64_t item_id) { + process_item_logic(ctx.db, item_id); + return Ok(); +} + +SPACETIMEDB_PROCEDURE(Unit, fetch_and_process, ProcedureContext ctx, std::string url) { + auto result = ctx.http.get(url); + if (!result.is_ok()) { + LOG_ERROR("Request failed: " + result.error()); + return Unit{}; + } + + auto& response = result.value(); + if (response.status_code != 200) { + LOG_ERROR("HTTP status: " + std::to_string(response.status_code)); + return Unit{}; + } + + auto body = response.body.to_string_utf8_lossy(); + uint64_t item_id = std::stoull(body); + + ctx.with_tx([&](TxContext& tx) { + process_item_logic(tx.db, item_id); + }); + + return Unit{}; +} +``` + diff --git a/docs/docs/00200-core-concepts/00200-functions/00500-views.md b/docs/docs/00200-core-concepts/00200-functions/00500-views.md index 97ec7e2077f..0b5cb7e9343 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00500-views.md +++ b/docs/docs/00200-core-concepts/00200-functions/00500-views.md @@ -225,6 +225,69 @@ fn players_for_level(ctx: &AnonymousViewContext) -> Vec { Views can return either `Option` for at-most-one row or `Vec` for multiple rows, where `T` can be a table type or any product type. + + + +Use the `SPACETIMEDB_VIEW` macro: + +```cpp +#include +using namespace SpacetimeDB; + +struct Player { + uint64_t id; + Identity identity; + std::string name; +}; +SPACETIMEDB_STRUCT(Player, id, identity, name) +SPACETIMEDB_TABLE(Player, player, Public) +FIELD_PrimaryKeyAuto(player, id) +FIELD_Unique(player, identity) + +struct PlayerLevel { + uint64_t player_id; + uint64_t level; +}; +SPACETIMEDB_STRUCT(PlayerLevel, player_id, level) +SPACETIMEDB_TABLE(PlayerLevel, player_level, Public) +FIELD_Unique(player_level, player_id) +FIELD_Index(player_level, level) + +struct PlayerAndLevel { + uint64_t id; + std::string name; + uint64_t level; +}; +SPACETIMEDB_STRUCT(PlayerAndLevel, id, name, level) + +// At-most-one row: return std::optional +SPACETIMEDB_VIEW(std::optional, my_player, Public, ViewContext ctx) { + return ctx.db[player_identity].find(ctx.sender); +} + +// Multiple rows: return std::vector +SPACETIMEDB_VIEW(std::vector, players_for_level, Public, AnonymousViewContext ctx) { + std::vector results; + + // Use explicit iterator loop (range-based for not yet supported in views) + for (auto iter = ctx.db[player_level_level].filter(2u); iter != IndexIterator(); ++iter) { + const auto& player_lv = *iter; + auto p = ctx.db[player_id].find(player_lv.player_id); + if (p) { + results.push_back(PlayerAndLevel{p->id, p->name, player_lv.level}); + } + } + + return results; +} +``` + +Views can return either `std::optional` for at-most-one row or `std::vector` for multiple rows, where `T` can be a table type or any product type. + +:::note +Views use `ReadOnlyDatabaseContext`, which currently requires explicit iterator loops instead of range-based for loops when filtering indexed fields. Use the pattern shown above with `iter != IndexIterator()` as the loop condition. +::: + @@ -298,6 +361,16 @@ fn my_player(ctx: &ViewContext) -> Option { } ``` + + + +```cpp +// Per-user: each client sees their own player +SPACETIMEDB_VIEW(std::optional, my_player, Public, ViewContext ctx) { + return ctx.db[player_identity].find(ctx.sender); +} +``` + @@ -377,6 +450,39 @@ fn high_scorers(ctx: &AnonymousViewContext) -> Vec { } ``` + + + +```cpp +struct Player { + uint64_t id; + std::string name; + uint64_t score; +}; +SPACETIMEDB_STRUCT(Player, id, name, score) +SPACETIMEDB_TABLE(Player, player, Public) +FIELD_PrimaryKeyAutoInc(player, id) +FIELD_Index(player, score) + +// Shared: same high scorers for all clients +SPACETIMEDB_VIEW(std::vector, high_scorers, Public, AnonymousViewContext ctx) { + std::vector results; + + // Get all players with score == 1000 using the btree index + // Note: Currently using exact match instead of range (score >= 1000) + // because range queries (range_from, etc.) don't yet work in C++ views + for (auto iter = ctx.db[player_score].filter(1000u); iter != IndexIterator(); ++iter) { + results.push_back(*iter); + } + + return results; +} +``` + +:::note +Range query functions like `range_from()`, `range_to()`, and `range_inclusive()` are not yet fully supported in C++ views. Use exact value filtering with `filter(value)` for now. Range queries work correctly in reducers. +::: + @@ -554,6 +660,75 @@ fn entities_in_my_chunk(ctx: &ViewContext) -> Vec { } ``` + + + +```cpp +struct Entity { + uint64_t id; + int32_t chunk_x; + int32_t chunk_y; + float local_x; + float local_y; + std::string entity_type; +}; +SPACETIMEDB_STRUCT(Entity, id, chunk_x, chunk_y, local_x, local_y, entity_type) +SPACETIMEDB_TABLE(Entity, entity, Public) +FIELD_PrimaryKeyAutoInc(entity, id) +FIELD_Index(entity, chunk_x) +FIELD_Index(entity, chunk_y) + +// Track which chunks each player is subscribed to +struct PlayerChunk { + uint64_t player_id; + int32_t chunk_x; + int32_t chunk_y; +}; +SPACETIMEDB_STRUCT(PlayerChunk, player_id, chunk_x, chunk_y) +SPACETIMEDB_TABLE(PlayerChunk, player_chunk, Public) +FIELD_PrimaryKey(player_chunk, player_id) + +// Shared: all players in chunk (0,0) share this view +SPACETIMEDB_VIEW(std::vector, entities_in_origin_chunk, Public, AnonymousViewContext ctx) { + std::vector results; + + // All entities in chunk (0, 0) - shared by everyone viewing this chunk + for (auto iter = ctx.db[entity_chunk_x].filter(0); iter != IndexIterator(); ++iter) { + if (iter->chunk_y == 0) { + results.push_back(*iter); + } + } + + return results; +} + +// Per-user: returns entities in the chunk the player is currently in +SPACETIMEDB_VIEW(std::vector, entities_in_my_chunk, Public, ViewContext ctx) { + std::vector results; + + // Find the player's current location + auto player_opt = ctx.db[player_identity].find(ctx.sender); + if (!player_opt) { + return results; // Player not found + } + + // Find which chunk the player is in + auto chunk_opt = ctx.db[player_chunk_player_id].find(player_opt->id); + if (!chunk_opt) { + return results; // Player has no chunk assigned + } + + // Return all entities in the player's chunk + for (auto iter = ctx.db[entity_chunk_x].filter(chunk_opt->chunk_x); iter != IndexIterator(); ++iter) { + if (iter->chunk_y == chunk_opt->chunk_y) { + results.push_back(*iter); + } + } + + return results; +} +``` + From 3747a3966b21d68547347461982e6dad4598d1a7 Mon Sep 17 00:00:00 2001 From: Jason Larabie Date: Thu, 29 Jan 2026 21:58:57 -0800 Subject: [PATCH 2/3] Cleanup for Views to use the correct index iterator --- .../00200-functions/00500-views.md | 36 +++++-------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/docs/docs/00200-core-concepts/00200-functions/00500-views.md b/docs/docs/00200-core-concepts/00200-functions/00500-views.md index 0b5cb7e9343..c18217ed776 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00500-views.md +++ b/docs/docs/00200-core-concepts/00200-functions/00500-views.md @@ -269,9 +269,8 @@ SPACETIMEDB_VIEW(std::optional, my_player, Public, ViewContext ctx) { SPACETIMEDB_VIEW(std::vector, players_for_level, Public, AnonymousViewContext ctx) { std::vector results; - // Use explicit iterator loop (range-based for not yet supported in views) - for (auto iter = ctx.db[player_level_level].filter(2u); iter != IndexIterator(); ++iter) { - const auto& player_lv = *iter; + // Find all player levels with level == 2 + for (auto player_lv : ctx.db[player_level_level].filter(uint64_t(2))) { auto p = ctx.db[player_id].find(player_lv.player_id); if (p) { results.push_back(PlayerAndLevel{p->id, p->name, player_lv.level}); @@ -284,10 +283,6 @@ SPACETIMEDB_VIEW(std::vector, players_for_level, Public, Anonymo Views can return either `std::optional` for at-most-one row or `std::vector` for multiple rows, where `T` can be a table type or any product type. -:::note -Views use `ReadOnlyDatabaseContext`, which currently requires explicit iterator loops instead of range-based for loops when filtering indexed fields. Use the pattern shown above with `iter != IndexIterator()` as the loop condition. -::: - @@ -466,23 +461,10 @@ FIELD_Index(player, score) // Shared: same high scorers for all clients SPACETIMEDB_VIEW(std::vector, high_scorers, Public, AnonymousViewContext ctx) { - std::vector results; - - // Get all players with score == 1000 using the btree index - // Note: Currently using exact match instead of range (score >= 1000) - // because range queries (range_from, etc.) don't yet work in C++ views - for (auto iter = ctx.db[player_score].filter(1000u); iter != IndexIterator(); ++iter) { - results.push_back(*iter); - } - - return results; + return ctx.db[player_high_scorers_score].filter(range_from(uint64_t(1000))).collect(); } ``` -:::note -Range query functions like `range_from()`, `range_to()`, and `range_inclusive()` are not yet fully supported in C++ views. Use exact value filtering with `filter(value)` for now. Range queries work correctly in reducers. -::: - @@ -693,9 +675,9 @@ SPACETIMEDB_VIEW(std::vector, entities_in_origin_chunk, Public, Anonymou std::vector results; // All entities in chunk (0, 0) - shared by everyone viewing this chunk - for (auto iter = ctx.db[entity_chunk_x].filter(0); iter != IndexIterator(); ++iter) { - if (iter->chunk_y == 0) { - results.push_back(*iter); + for (auto entity : ctx.db[entity_chunk_x].filter(int32_t(0))) { + if (entity.chunk_y == 0) { + results.push_back(entity); } } @@ -719,9 +701,9 @@ SPACETIMEDB_VIEW(std::vector, entities_in_my_chunk, Public, ViewContext } // Return all entities in the player's chunk - for (auto iter = ctx.db[entity_chunk_x].filter(chunk_opt->chunk_x); iter != IndexIterator(); ++iter) { - if (iter->chunk_y == chunk_opt->chunk_y) { - results.push_back(*iter); + for (auto entity : ctx.db[entity_chunk_x].filter(chunk_opt->chunk_x)) { + if (entity.chunk_y == chunk_opt->chunk_y) { + results.push_back(entity); } } From 811c5289c3ad5ee6e1ed81b9c8006bd506850cc1 Mon Sep 17 00:00:00 2001 From: Jason Larabie Date: Mon, 2 Feb 2026 08:21:23 -0800 Subject: [PATCH 3/3] Update docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md Co-authored-by: Ryan Signed-off-by: Jason Larabie --- .../00200-functions/00300-reducers/00300-reducers.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md index 076bb404de5..1733f8997b4 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md +++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md @@ -190,7 +190,11 @@ ctx.db.user().insert(User { ```cpp -ctx.db[user].insert(User{0, "Alice", "alice@example.com"}); // 0 for id - auto-increment will assign +ctx.db[user].insert(User{ + 0, // auto-increment will assign + "Alice", + "alice@example.com" +}); ```