From 718f58c5fcb32108cb8772ec0317fb46ba87b571 Mon Sep 17 00:00:00 2001 From: Jason Larabie Date: Mon, 26 Jan 2026 16:02:12 -0800 Subject: [PATCH 1/3] Added C++ examples to the tables docs --- docs/docs/00200-core-concepts/00300-tables.md | 88 +++++++ .../00300-tables/00200-column-types.md | 75 ++++++ .../00300-tables/00210-file-storage.md | 87 ++++++- .../00300-tables/00230-auto-increment.md | 23 ++ .../00300-tables/00240-constraints.md | 66 +++++ .../00300-tables/00250-default-values.md | 21 ++ .../00300-tables/00300-indexes.md | 68 ++++++ .../00300-tables/00400-access-permissions.md | 225 ++++++++++++++++++ .../00300-tables/00500-schedule-tables.md | 79 ++++++ .../00300-tables/00600-performance.md | 136 +++++++++++ 10 files changed, 867 insertions(+), 1 deletion(-) diff --git a/docs/docs/00200-core-concepts/00300-tables.md b/docs/docs/00200-core-concepts/00300-tables.md index c9a3405a2bc..f820e8ba104 100644 --- a/docs/docs/00200-core-concepts/00300-tables.md +++ b/docs/docs/00200-core-concepts/00300-tables.md @@ -160,6 +160,24 @@ pub struct Person { } ``` + + + +Register the struct with `SPACETIMEDB_STRUCT`, the table with `SPACETIMEDB_TABLE`, then add field constraints: + +```cpp +struct Person { + uint32_t id; + std::string name; + std::string email; +}; +SPACETIMEDB_STRUCT(Person, id, name, email) +SPACETIMEDB_TABLE(Person, person, Public) +FIELD_PrimaryKeyAutoInc(person, id) +FIELD_Index(person, name) +FIELD_Unique(person, email) +``` + @@ -235,6 +253,29 @@ ctx.db.player().insert(Player { /* ... */ }); | `name = player` | `ctx.db.player()` | | `name = game_session` | `ctx.db.game_session()` | + + + +The accessor name matches the table identifier you pass to `SPACETIMEDB_TABLE`: + +```cpp +struct PlayerScores { + uint64_t id; +}; +SPACETIMEDB_STRUCT(PlayerScores, id) +SPACETIMEDB_TABLE(PlayerScores, player_scores, Public) +FIELD_PrimaryKeyAutoInc(player_scores, id) + +// Accessor matches the table identifier +ctx.db[player_scores].insert(PlayerScores{ /* ... */ }); +``` + +| Table Identifier | Accessor | +|------------------|----------| +| `user` | `ctx.db[user]` | +| `player_scores` | `ctx.db[player_scores]` | +| `game_session` | `ctx.db[game_session]` | + @@ -247,6 +288,7 @@ Use idiomatic naming conventions for each language: | **TypeScript** | snake_case | `'player_score'` | `ctx.db.playerScore` | | **C#** | PascalCase | `Name = "PlayerScore"` | `ctx.Db.PlayerScore` | | **Rust** | lower_snake_case | `name = player_score` | `ctx.db.player_score()` | +| **C++** | lower_snake_case | `player_score` | `ctx.db[player_score]` | These conventions align with each language's standard style guides and make your code feel natural within its ecosystem. @@ -287,6 +329,25 @@ pub struct User { /* ... */ } pub struct Secret { /* ... */ } ``` + + + +```cpp +struct User { + uint64_t id; +}; +SPACETIMEDB_STRUCT(User, id) +SPACETIMEDB_TABLE(User, user, Public) +FIELD_PrimaryKeyAutoInc(user, id) + +struct Secret { + uint64_t id; +}; +SPACETIMEDB_STRUCT(Secret, id) +SPACETIMEDB_TABLE(Secret, secret, Private) +FIELD_PrimaryKeyAutoInc(secret, id) +``` + @@ -384,6 +445,33 @@ if let Some(player) = ctx.db.logged_out_player().identity().find(&ctx.sender) { } ``` + + + +Apply multiple `SPACETIMEDB_TABLE` macros to the same struct: + +```cpp +struct Player { + Identity identity; + int32_t player_id; + std::string name; +}; +SPACETIMEDB_STRUCT(Player, identity, player_id, name) +SPACETIMEDB_TABLE(Player, player, Public) +SPACETIMEDB_TABLE(Player, logged_out_player, Private) +FIELD_PrimaryKey(player, identity) +FIELD_PrimaryKey(logged_out_player, identity) +FIELD_UniqueAutoInc(player, player_id) +FIELD_UniqueAutoInc(logged_out_player, player_id) + +// Move between tables +auto maybe_logged_out = ctx.db[logged_out_player_identity].find(ctx.sender); +if (maybe_logged_out) { + ctx.db[player].insert(*maybe_logged_out); + ctx.db[logged_out_player_identity].delete_by_key(ctx.sender); +} +``` + diff --git a/docs/docs/00200-core-concepts/00300-tables/00200-column-types.md b/docs/docs/00200-core-concepts/00300-tables/00200-column-types.md index ba7c7cac159..4062eed306f 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00200-column-types.md +++ b/docs/docs/00200-core-concepts/00300-tables/00200-column-types.md @@ -132,6 +132,29 @@ These optimizations apply across all supported languages. | Special | `TimeDuration` | Relative duration in microseconds | | Special | `ScheduleAt` | When a scheduled reducer should execute | + + + +| Category | Type | Description | +|----------|------|-------------| +| Primitive | `bool` | Boolean value | +| Primitive | `std::string` | UTF-8 string | +| Primitive | `float` | 32-bit floating point | +| Primitive | `double` | 64-bit floating point | +| Primitive | `int8_t`, `int16_t`, `int32_t`, `int64_t` | Signed integers (8-bit to 64-bit) | +| Primitive | `uint8_t`, `uint16_t`, `uint32_t`, `uint64_t` | Unsigned integers (8-bit to 64-bit) | +| Primitive | `SpacetimeDb::i128`, `SpacetimeDb::i256` | Signed 128-bit and 256-bit integers | +| Primitive | `SpacetimeDb::u128`, `SpacetimeDb::u256` | Unsigned 128-bit and 256-bit integers | +| Composite | `struct` with `SPACETIMEDB_STRUCT` | Product type for nested data | +| Composite | `SPACETIMEDB_ENUM` | Sum type (tagged union) | +| Composite | `std::vector` | Vector of elements | +| Composite | `std::optional` | Optional value | +| Special | `Identity` | Unique identity for authentication | +| Special | `ConnectionId` | Client connection identifier | +| Special | `Timestamp` | Absolute point in time (microseconds since Unix epoch) | +| Special | `TimeDuration` | Relative duration in microseconds | +| Special | `ScheduleAt` | When a scheduled reducer should execute | + @@ -289,5 +312,57 @@ pub struct Player { } ``` + + + +```cpp +// Define a nested struct type for coordinates +struct Coordinates { + double x; + double y; + double z; +}; +SPACETIMEDB_STRUCT(Coordinates, x, y, z) + +// Define unit types for enum variants +SPACETIMEDB_UNIT_TYPE(Active) +SPACETIMEDB_UNIT_TYPE(Inactive) + +// Define an enum for status +SPACETIMEDB_ENUM(PlayerStatus, + (Active, Active), + (Inactive, Inactive), + (Suspended, std::string) +) + +struct Player { + // Primitive types + uint64_t id; + std::string name; + uint8_t level; + uint32_t experience; + float health; + int64_t score; + bool is_online; + + // Composite types + Coordinates position; + PlayerStatus status; + std::vector inventory; + std::optional guild_id; + + // Special types + Identity owner; + std::optional connection; + Timestamp created_at; + TimeDuration play_time; +}; +SPACETIMEDB_STRUCT(Player, id, name, level, experience, health, score, is_online, + position, status, inventory, guild_id, + owner, connection, created_at, play_time) +SPACETIMEDB_TABLE(Player, player, Public) +FIELD_PrimaryKeyAutoInc(player, id) +``` + diff --git a/docs/docs/00200-core-concepts/00300-tables/00210-file-storage.md b/docs/docs/00200-core-concepts/00300-tables/00210-file-storage.md index 0925140db83..4c8d0c4c588 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00210-file-storage.md +++ b/docs/docs/00200-core-concepts/00300-tables/00210-file-storage.md @@ -11,7 +11,7 @@ SpacetimeDB can store binary data directly in table columns, making it suitable ## Storing Binary Data Inline -Store binary data using `Vec` (Rust), `List` (C#), or `t.array(t.u8())` (TypeScript). This approach keeps data within the database, ensuring it participates in transactions and real-time updates. +Store binary data using `Vec` (Rust), `List` (C#), `std::vector` (C++), or `t.array(t.u8())` (TypeScript). This approach keeps data within the database, ensuring it participates in transactions and real-time updates. @@ -120,6 +120,37 @@ pub fn upload_avatar( } ``` + + + +```cpp +struct UserAvatar { + uint64_t user_id; + std::string mime_type; + std::vector data; // Binary data stored inline + Timestamp uploaded_at; +}; +SPACETIMEDB_STRUCT(UserAvatar, user_id, mime_type, data, uploaded_at) +SPACETIMEDB_TABLE(UserAvatar, user_avatar, Public) +FIELD_PrimaryKey(user_avatar, user_id) + +SPACETIMEDB_REDUCER(upload_avatar, ReducerContext ctx, + uint64_t user_id, std::string mime_type, std::vector data) { + // Delete existing avatar if present + ctx.db[user_avatar_user_id].delete_by_key(user_id); + + // Insert new avatar + ctx.db[user_avatar].insert(UserAvatar{ + .user_id = user_id, + .mime_type = mime_type, + .data = data, + .uploaded_at = ctx.timestamp, + }); + + return Ok(); +} +``` + @@ -273,6 +304,41 @@ pub fn register_document( } ``` + + + +```cpp +struct Document { + uint64_t id; + Identity owner_id; + std::string filename; + std::string mime_type; + uint64_t size_bytes; + std::string storage_url; // Reference to external storage + Timestamp uploaded_at; +}; +SPACETIMEDB_STRUCT(Document, id, owner_id, filename, mime_type, size_bytes, storage_url, uploaded_at) +SPACETIMEDB_TABLE(Document, document, Public) +FIELD_PrimaryKeyAutoInc(document, id) +FIELD_Index(document, owner_id) + +// Called after uploading file to external storage +SPACETIMEDB_REDUCER(register_document, ReducerContext ctx, + std::string filename, std::string mime_type, uint64_t size_bytes, std::string storage_url) { + ctx.db[document].insert(Document{ + .id = 0, // auto-increment + .owner_id = ctx.sender, + .filename = filename, + .mime_type = mime_type, + .size_bytes = size_bytes, + .storage_url = storage_url, + .uploaded_at = ctx.timestamp, + }); + + return Ok(); +} +``` + @@ -368,6 +434,25 @@ pub struct Image { } ``` + + + +```cpp +struct Image { + uint64_t id; + Identity owner_id; + std::vector thumbnail; // Small preview stored inline + std::string original_url; // Large original in external storage + uint32_t width; + uint32_t height; + Timestamp uploaded_at; +}; +SPACETIMEDB_STRUCT(Image, id, owner_id, thumbnail, original_url, width, height, uploaded_at) +SPACETIMEDB_TABLE(Image, image, Public) +FIELD_PrimaryKeyAutoInc(image, id) +FIELD_Index(image, owner_id) +``` + diff --git a/docs/docs/00200-core-concepts/00300-tables/00230-auto-increment.md b/docs/docs/00200-core-concepts/00300-tables/00230-auto-increment.md index 7ba9ab238d0..f3ac44e1d7e 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00230-auto-increment.md +++ b/docs/docs/00200-core-concepts/00300-tables/00230-auto-increment.md @@ -86,6 +86,29 @@ fn add_post(ctx: &ReducerContext, title: String) -> Result<(), String> { Use the `#[auto_inc]` attribute. + + + +```cpp +struct Post { + uint64_t id; + std::string title; +}; +SPACETIMEDB_STRUCT(Post, id, title) +SPACETIMEDB_TABLE(Post, post, Public) +FIELD_PrimaryKeyAutoInc(post, id) + +SPACETIMEDB_REDUCER(add_post, ReducerContext ctx, std::string title) { + // Pass 0 for the auto-increment field + auto inserted = ctx.db[post].insert(Post{0, title}); + // inserted.id now contains the assigned value + LOG_INFO("Created post with id: " + std::to_string(inserted.id)); + return Ok(); +} +``` + +Use the `FIELD_PrimaryKeyAutoInc(table, field)` macro after table registration. + diff --git a/docs/docs/00200-core-concepts/00300-tables/00240-constraints.md b/docs/docs/00200-core-concepts/00300-tables/00240-constraints.md index 0a14605d1fd..b262f94a92a 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00240-constraints.md +++ b/docs/docs/00200-core-concepts/00300-tables/00240-constraints.md @@ -62,6 +62,22 @@ pub struct User { Use the `#[primary_key]` attribute to mark a field as the primary key. + + + +```cpp +struct User { + uint64_t id; + std::string name; + std::string email; +}; +SPACETIMEDB_STRUCT(User, id, name, email) +SPACETIMEDB_TABLE(User, user, Public) +FIELD_PrimaryKey(user, id) +``` + +Use `FIELD_PrimaryKey(table, field)` after table registration to mark the primary key. + @@ -131,6 +147,23 @@ pub struct Inventory { } ``` + + + +```cpp +struct Inventory { + uint64_t id; + uint64_t user_id; + uint64_t item_id; + uint32_t quantity; +}; +SPACETIMEDB_STRUCT(Inventory, id, user_id, item_id, quantity) +SPACETIMEDB_TABLE(Inventory, inventory, Public) +FIELD_PrimaryKeyAutoInc(inventory, id) +// Named multi-column btree index on (user_id, item_id) +FIELD_NamedMultiColumnIndex(inventory, by_user_item, user_id, item_id) +``` + @@ -188,6 +221,21 @@ fn update_user_name(ctx: &ReducerContext, id: u64, new_name: String) -> Result<( } ``` + + + +```cpp +SPACETIMEDB_REDUCER(update_user_name, ReducerContext ctx, uint64_t id, std::string new_name) { + auto user_opt = ctx.db[user_id].find(id); + if (user_opt.has_value()) { + User user_update = user_opt.value(); + user_update.name = new_name; + ctx.db[user_id].update(user_update); + } + return Ok(); +} +``` + @@ -289,6 +337,24 @@ pub struct User { Use the `#[unique]` attribute. + + + +```cpp +struct User { + uint32_t id; + std::string email; + std::string username; +}; +SPACETIMEDB_STRUCT(User, id, email, username) +SPACETIMEDB_TABLE(User, user, Public) +FIELD_PrimaryKey(user, id) +FIELD_Unique(user, email) +FIELD_Unique(user, username) +``` + +Use `FIELD_Unique(table, field)` after table registration to mark columns as unique. + diff --git a/docs/docs/00200-core-concepts/00300-tables/00250-default-values.md b/docs/docs/00200-core-concepts/00300-tables/00250-default-values.md index db7024cbe24..845b7837af6 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00250-default-values.md +++ b/docs/docs/00200-core-concepts/00300-tables/00250-default-values.md @@ -85,6 +85,27 @@ The `#[default(value)]` attribute specifies the default value. The expression mu Default values in Rust must be const-evaluable. This means you **cannot** use `String` defaults like `#[default("".to_string())]` because `.to_string()` is not a const fn. Only primitive types, enums, and other const-constructible types can have defaults. ::: + + + +```cpp +struct Player { + uint64_t id; + std::string name; + uint32_t score; + bool is_active; + std::string bio; +}; +SPACETIMEDB_STRUCT(Player, id, name, score, is_active, bio) +SPACETIMEDB_TABLE(Player, player, Public) +FIELD_PrimaryKeyAutoInc(player, id) +FIELD_Default(player, score, 0u) +FIELD_Default(player, is_active, true) +FIELD_Default(player, bio, std::string("")) +``` + +Use `FIELD_Default(table, field, value)` after table registration to specify default values. These defaults are applied during schema migration when new columns are added. + diff --git a/docs/docs/00200-core-concepts/00300-tables/00300-indexes.md b/docs/docs/00200-core-concepts/00300-tables/00300-indexes.md index 66d3c0fb692..f1dc2270e5a 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00300-indexes.md +++ b/docs/docs/00200-core-concepts/00300-tables/00300-indexes.md @@ -146,6 +146,24 @@ pub struct User { } ``` + + + +```cpp +struct User { + uint32_t id; + std::string name; + uint8_t age; +}; +SPACETIMEDB_STRUCT(User, id, name, age) +SPACETIMEDB_TABLE(User, user, Public) +FIELD_PrimaryKey(user, id) +FIELD_Index(user, name) +FIELD_Index(user, age) +``` + +Use `FIELD_Index(table, field)` to create a B-tree index on individual columns. + @@ -268,6 +286,22 @@ pub struct Score { } ``` + + + +```cpp +struct Score { + uint32_t player_id; + uint32_t level; + int64_t points; +}; +SPACETIMEDB_STRUCT(Score, player_id, level, points) +SPACETIMEDB_TABLE(Score, score, Public) +FIELD_NamedMultiColumnIndex(score, by_player_and_level, player_id, level) +``` + +Use `FIELD_NamedMultiColumnIndex(table, index_name, field1, field2, ...)` to create a named multi-column B-tree index. + @@ -310,6 +344,18 @@ for user in ctx.db.user().name().filter("Alice") { } ``` + + + +```cpp +// Find users with a specific name +for (auto user : ctx.db[user_name].filter("Alice")) { + LOG_INFO("Found user: " + user.name); +} +``` + +Use the index accessor `ctx.db[index_name]` created by `FIELD_Index` to perform filtered queries. + @@ -376,6 +422,28 @@ for user in ctx.db.user().age().filter(..18) { } ``` + + + +```cpp +// Find users aged 18 to 65 (inclusive) +for (auto user : ctx.db[user_age].filter(range_inclusive(uint8_t(18), uint8_t(65)))) { + // Process user +} + +// Find users aged 18 or older +for (auto user : ctx.db[user_age].filter(range_from(uint8_t(18)))) { + // Process user +} + +// Find users younger than 18 +for (auto user : ctx.db[user_age].filter(range_to(uint8_t(18)))) { + // Process user +} +``` + +Use range query functions: `range_inclusive()`, `range_from()`, `range_to()`, and `range_to_inclusive()`. Include `` for full range query support. + diff --git a/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md b/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md index 659ddca3cbc..c5bc3ea7b2f 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md +++ b/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md @@ -87,6 +87,32 @@ pub struct Player { } ``` + + + +```cpp +// Private table (default) - only accessible from server-side code +struct InternalConfig { + std::string key; + std::string value; +}; +SPACETIMEDB_STRUCT(InternalConfig, key, value) +SPACETIMEDB_TABLE(InternalConfig, internal_config, Private) +FIELD_PrimaryKey(internal_config, key) + +// Public table - clients can subscribe and query +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) +``` + +Use `Private` or `Public` as the third parameter to `SPACETIMEDB_TABLE` to control table visibility. + @@ -190,6 +216,35 @@ fn example(ctx: &ReducerContext) -> Result<(), String> { } ``` + + + +```cpp +SPACETIMEDB_REDUCER(example, ReducerContext ctx) { + // Insert + ctx.db[user].insert(User{.id = 0, .name = "Alice", .email = "alice@example.com"}); + + // Read: iterate all rows + for (const auto& user_row : ctx.db[user]) { + LOG_INFO("User: " + user_row.name); + } + + // Read: find by unique column + auto found_user = ctx.db[user_id].find(123); + if (found_user.has_value()) { + // Update + auto updated = found_user.value(); + updated.name = "Bob"; + ctx.db[user_id].update(updated); + } + + // Delete + ctx.db[user_id].delete_by_key(456); + + return Ok(); +} +``` + @@ -240,6 +295,26 @@ fn update_user_procedure(ctx: &mut ProcedureContext, user_id: u64, new_name: Str } ``` + + + +```cpp +SPACETIMEDB_PROCEDURE(Unit, update_user_procedure, ProcedureContext ctx, uint64_t userId, std::string newName) { + // Must explicitly open a transaction + ctx.with_tx([userId, newName](TxContext& tx) { + // Full read-write access within the transaction + auto user_opt = tx.db[user_id].find(userId); + if (user_opt.has_value()) { + User updated = user_opt.value(); + updated.name = newName; + tx.db[user_id].update(updated); + } + }); + // Transaction is committed when the lambda returns + return Unit{}; +} +``` + @@ -294,6 +369,23 @@ fn find_users_by_name(ctx: &ViewContext) -> Vec { } ``` + + + +```cpp +SPACETIMEDB_VIEW(std::vector, find_users_by_name, Public, ViewContext ctx) { + // Can read and filter using indexed lookups + std::vector results; + for (auto iter = ctx.db[user_name].filter("Alice"); iter != IndexIterator(); ++iter) { + results.push_back(*iter); + } + return results; + + // Cannot insert, update, or delete + // ctx.db[user].insert(...) // ❌ Methods not available in ViewContext +} +``` + @@ -411,6 +503,41 @@ fn my_messages(ctx: &ViewContext) -> Vec { } ``` + + + +```cpp +struct Message { + uint64_t id; + Identity sender; + Identity recipient; + std::string content; +}; +SPACETIMEDB_STRUCT(Message, id, sender, recipient, content) +SPACETIMEDB_TABLE(Message, message, Private) // Private by default +FIELD_PrimaryKeyAutoInc(message, id) +FIELD_Index(message, sender) +FIELD_Index(message, recipient) + +// Public view that only returns messages the caller can see +SPACETIMEDB_VIEW(std::vector, my_messages, Public, ViewContext ctx) { + // Look up messages by index where caller is sender or recipient + std::vector results; + + // Find messages where caller is sender + for (auto iter = ctx.db[message_sender].filter(ctx.sender); iter != IndexIterator(); ++iter) { + results.push_back(*iter); + } + + // Find messages where caller is recipient + for (auto iter = ctx.db[message_recipient].filter(ctx.sender); iter != IndexIterator(); ++iter) { + results.push_back(*iter); + } + + return results; +} +``` + @@ -563,6 +690,50 @@ fn my_profile(ctx: &ViewContext) -> Option { } ``` + + + +```cpp +struct UserAccount { + uint64_t id; + Identity identity; + std::string username; + std::string email; + std::string password_hash; // Sensitive - not exposed in view + std::string api_key; // Sensitive - not exposed in view + Timestamp created_at; +}; +SPACETIMEDB_STRUCT(UserAccount, id, identity, username, email, password_hash, api_key, created_at) +SPACETIMEDB_TABLE(UserAccount, user_account, Private) // Private by default +FIELD_PrimaryKeyAutoInc(user_account, id) +FIELD_Unique(user_account, identity) + +// Public type without sensitive columns +struct PublicUserProfile { + uint64_t id; + std::string username; + Timestamp created_at; +}; +SPACETIMEDB_STRUCT(PublicUserProfile, id, username, created_at) + +// Public view that returns the caller's profile without sensitive data +SPACETIMEDB_VIEW(std::optional, my_profile, Public, ViewContext ctx) { + // Look up the caller's account by their identity (unique index) + auto user_opt = ctx.db[user_account_identity].find(ctx.sender); + if (!user_opt.has_value()) { + return std::nullopt; + } + + UserAccount user = user_opt.value(); + return PublicUserProfile{ + user.id, + user.username, + user.created_at + // email, password_hash, and api_key are not included + }; +} +``` + @@ -729,6 +900,60 @@ fn my_team(ctx: &ViewContext) -> Vec { } ``` + + + +```cpp +// Private table with all employee data +struct Employee { + uint64_t id; + Identity identity; + std::string name; + std::string department; + uint64_t salary; // Sensitive - not exposed in view + uint64_t manager_id; // 0 means no manager +}; +SPACETIMEDB_STRUCT(Employee, id, identity, name, department, salary, manager_id) +SPACETIMEDB_TABLE(Employee, employee, Private) +FIELD_PrimaryKey(employee, id) +FIELD_Unique(employee, identity) +FIELD_Index(employee, manager_id) + +// Public type for team members (no salary) +struct TeamMember { + uint64_t id; + std::string name; + std::string department; +}; +SPACETIMEDB_STRUCT(TeamMember, id, name, department) + +// View that returns only the caller's team members, without salary info +SPACETIMEDB_VIEW(std::vector, my_team, Public, ViewContext ctx) { + // Find the caller's employee record by identity (unique index) + auto me_opt = ctx.db[employee_identity].find(ctx.sender); + if (!me_opt.has_value()) { + return std::vector(); + } + + Employee me = me_opt.value(); + std::vector results; + + // Look up employees who report to the caller by manager_id index + for (auto iter = ctx.db[employee_manager_id].filter(me.id); + iter != IndexIterator(); ++iter) { + const Employee& emp = *iter; + results.push_back(TeamMember{ + emp.id, + emp.name, + emp.department + // salary is not included + }); + } + + return results; +} +``` + diff --git a/docs/docs/00200-core-concepts/00300-tables/00500-schedule-tables.md b/docs/docs/00200-core-concepts/00300-tables/00500-schedule-tables.md index d6307880e77..1266f3e2401 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00500-schedule-tables.md +++ b/docs/docs/00200-core-concepts/00300-tables/00500-schedule-tables.md @@ -77,6 +77,30 @@ fn send_reminder(ctx: &ReducerContext, reminder: Reminder) -> Result<(), String> } ``` + + + +```cpp +struct Reminder { + uint64_t scheduled_id; + ScheduleAt scheduled_at; + std::string message; +}; +SPACETIMEDB_STRUCT(Reminder, scheduled_id, scheduled_at, message) +SPACETIMEDB_TABLE(Reminder, reminder, Public) +FIELD_PrimaryKeyAutoInc(reminder, scheduled_id) +SPACETIMEDB_SCHEDULE(reminder, 1, send_reminder) // Column 1 is scheduled_at + +// Reducer invoked automatically by the scheduler +SPACETIMEDB_REDUCER(send_reminder, ReducerContext ctx, Reminder arg) +{ + // Invoked automatically by the scheduler + // arg.message, arg.scheduled_at, arg.scheduled_id + LOG_INFO("Scheduled reminder: " + arg.message); + return Ok(); +} +``` + @@ -152,6 +176,25 @@ ctx.db.reminder().insert(Reminder { }); ``` + + + +```cpp +// Schedule to run every 5 seconds +ctx.db[reminder].insert(Reminder{ + 0, + ScheduleAt::interval(TimeDuration::from_seconds(5)), + "Check for updates" +}); + +// Schedule to run every 100 milliseconds +ctx.db[reminder].insert(Reminder{ + 0, + ScheduleAt::interval(TimeDuration::from_millis(100)), + "Game tick" +}); +``` + @@ -224,6 +267,26 @@ ctx.db.reminder().insert(Reminder { }); ``` + + + +```cpp +// Schedule for 10 seconds from now +Timestamp tenSecondsFromNow = ctx.timestamp + TimeDuration::from_seconds(10); +ctx.db[reminder].insert(Reminder{ + 0, + ScheduleAt::time(tenSecondsFromNow), + "Your auction has ended" +}); + +// Schedule for immediate execution (current timestamp) +ctx.db[reminder].insert(Reminder{ + 0, + ScheduleAt::time(ctx.timestamp), + "Process now" +}); +``` + @@ -281,6 +344,22 @@ fn send_reminder(ctx: &ReducerContext, reminder: Reminder) -> Result<(), String> } ``` + + + +```cpp +SPACETIMEDB_REDUCER(send_reminder, ReducerContext ctx, Reminder arg) +{ + // Check if this reducer is being called by the scheduler (internal) + if (!ctx.sender_auth().is_internal()) { + return Err("This reducer can only be called by the scheduler"); + } + + // Process the scheduled reminder + return Ok(); +} +``` + diff --git a/docs/docs/00200-core-concepts/00300-tables/00600-performance.md b/docs/docs/00200-core-concepts/00300-tables/00600-performance.md index f132903069e..a54d9206201 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00600-performance.md +++ b/docs/docs/00200-core-concepts/00300-tables/00600-performance.md @@ -39,6 +39,14 @@ ctx.Db.Player.Name.Filter("Alice") ctx.db.player().name().filter("Alice") ``` + + + +```cpp +// Fast: Uses index on name +auto results = ctx.db[player_name].filter("Alice"); +``` + @@ -72,6 +80,19 @@ ctx.db.player() .find(|p| p.name == "Alice") ``` + + + +```cpp +// Slow: Iterates through all rows +for (const auto& p : ctx.db[player]) { + if (p.name == "Alice") { + // Found it + break; + } +} +``` + @@ -152,6 +173,28 @@ pub struct Player { } ``` + + + +```cpp +// Instead of one large table with all fields: +struct Player { + uint32_t id; + std::string name; + // Game state + float position_x; + float position_y; + uint32_t health; + // Statistics (rarely accessed) + uint32_t total_kills; + uint32_t total_deaths; + uint64_t play_time_seconds; + // Settings (rarely changed) + float audio_volume; + uint8_t graphics_quality; +}; +``` + @@ -281,6 +324,48 @@ pub struct PlayerSettings { } ``` + + + +```cpp +struct Player { + uint32_t id; + std::string name; +}; +SPACETIMEDB_STRUCT(Player, id, name) +SPACETIMEDB_TABLE(Player, player, Public) +FIELD_PrimaryKey(player, id) + +struct PlayerState { + uint32_t player_id; + float position_x; + float position_y; + uint32_t health; +}; +SPACETIMEDB_STRUCT(PlayerState, player_id, position_x, position_y, health) +SPACETIMEDB_TABLE(PlayerState, player_state, Public) +FIELD_Unique(player_state, player_id) + +struct PlayerStats { + uint32_t player_id; + uint32_t total_kills; + uint32_t total_deaths; + uint64_t play_time_seconds; +}; +SPACETIMEDB_STRUCT(PlayerStats, player_id, total_kills, total_deaths, play_time_seconds) +SPACETIMEDB_TABLE(PlayerStats, player_stats, Public) +FIELD_Unique(player_stats, player_id) + +struct PlayerSettings { + uint32_t player_id; + float audio_volume; + uint8_t graphics_quality; +}; +SPACETIMEDB_STRUCT(PlayerSettings, player_id, audio_volume, graphics_quality) +SPACETIMEDB_TABLE(PlayerSettings, player_settings, Public) +FIELD_Unique(player_settings, player_id) +``` + @@ -324,6 +409,17 @@ player_count: u16, // Not u64 entity_id: u32, // Not u64 ``` + + + +```cpp +// If you only need 0-255, use byte instead of uint64_t +uint8_t level; // Not uint64_t +uint16_t player_count; // Not uint64_t +uint32_t entity_id; // Not uint64_t + +``` + @@ -382,6 +478,22 @@ pub struct Player { /* ... */ } pub struct InternalState { /* ... */ } ``` + + + +```cpp +// Public table - clients can subscribe and receive updates +struct Player { /* ... */ }; +SPACETIMEDB_STRUCT(Player, /* ... fields ... */) +SPACETIMEDB_TABLE(Player, player, Public) + +// Private table - only visible to module and owner +// Better for internal state, caches, or sensitive data +struct InternalState {/* ... */ }; +SPACETIMEDB_STRUCT(InternalState, /* ... fields ... */ ) +SPACETIMEDB_TABLE(InternalState, internal_state, Private) +``` + @@ -443,6 +555,20 @@ fn spawn_enemies(ctx: &ReducerContext, count: u32) { } ``` + + + +```cpp +// Good - Batch operation in a single reducer call +void spawn_enemies(ReducerContext& ctx, uint32_t count) { + for (uint32_t i = 0; i < count; ++i) { + Enemy new_enemy; + new_enemy.health = 100; + ctx.db[enemy].insert(new_enemy); + } +} +``` + @@ -479,6 +605,16 @@ for i in 0..10 { } ``` + + + +```cpp +// Client making many separate reducer calls +for (int i = 0; i < 10; ++i) { + connection.reducers.spawn_enemy(); +} +``` + From e8519aa166b3837ed20a6c7f6a50bfeb2b406f17 Mon Sep 17 00:00:00 2001 From: Jason Larabie Date: Wed, 28 Jan 2026 08:20:15 -0800 Subject: [PATCH 2/3] Update for SpacetimeDb to SpacetimeDB namespace for C++ --- .../00200-core-concepts/00300-tables/00200-column-types.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/00200-core-concepts/00300-tables/00200-column-types.md b/docs/docs/00200-core-concepts/00300-tables/00200-column-types.md index 4062eed306f..07204fbfeab 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00200-column-types.md +++ b/docs/docs/00200-core-concepts/00300-tables/00200-column-types.md @@ -143,8 +143,8 @@ These optimizations apply across all supported languages. | Primitive | `double` | 64-bit floating point | | Primitive | `int8_t`, `int16_t`, `int32_t`, `int64_t` | Signed integers (8-bit to 64-bit) | | Primitive | `uint8_t`, `uint16_t`, `uint32_t`, `uint64_t` | Unsigned integers (8-bit to 64-bit) | -| Primitive | `SpacetimeDb::i128`, `SpacetimeDb::i256` | Signed 128-bit and 256-bit integers | -| Primitive | `SpacetimeDb::u128`, `SpacetimeDb::u256` | Unsigned 128-bit and 256-bit integers | +| Primitive | `SpacetimeDB::i128`, `SpacetimeDB::i256` | Signed 128-bit and 256-bit integers | +| Primitive | `SpacetimeDB::u128`, `SpacetimeDB::u256` | Unsigned 128-bit and 256-bit integers | | Composite | `struct` with `SPACETIMEDB_STRUCT` | Product type for nested data | | Composite | `SPACETIMEDB_ENUM` | Sum type (tagged union) | | Composite | `std::vector` | Vector of elements | From f3681e171761b54a6a92523cfd00660a3c403389 Mon Sep 17 00:00:00 2001 From: Jason Larabie Date: Thu, 29 Jan 2026 21:30:40 -0800 Subject: [PATCH 3/3] Cleanup views to match corrected index iterators --- .../00300-tables/00400-access-permissions.md | 57 ++++++------------- 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md b/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md index 81e278c789b..43e92aa2f2e 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md +++ b/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md @@ -395,13 +395,7 @@ fn find_users_by_name(ctx: &ViewContext) -> Vec { ```cpp SPACETIMEDB_VIEW(std::vector, find_users_by_name, Public, ViewContext ctx) { - // Can read and filter using indexed lookups - std::vector results; - for (auto iter = ctx.db[user_name].filter("Alice"); iter != IndexIterator(); ++iter) { - results.push_back(*iter); - } - return results; - + return ctx.db[user_name].filter("Alice").collect(); // Cannot insert, update, or delete // ctx.db[user].insert(...) // ❌ Methods not available in ViewContext } @@ -543,19 +537,12 @@ FIELD_Index(message, recipient) // Public view that only returns messages the caller can see SPACETIMEDB_VIEW(std::vector, my_messages, Public, ViewContext ctx) { // Look up messages by index where caller is sender or recipient - std::vector results; - - // Find messages where caller is sender - for (auto iter = ctx.db[message_sender].filter(ctx.sender); iter != IndexIterator(); ++iter) { - results.push_back(*iter); - } - - // Find messages where caller is recipient - for (auto iter = ctx.db[message_recipient].filter(ctx.sender); iter != IndexIterator(); ++iter) { - results.push_back(*iter); - } + auto sent = ctx.db[message_sender].filter(ctx.sender).collect(); + auto received = ctx.db[message_recipient].filter(ctx.sender).collect(); - return results; + // Combine both vectors + sent.insert(sent.end(), received.begin(), received.end()); + return sent; } ``` @@ -924,43 +911,35 @@ struct Employee { std::string name; std::string department; uint64_t salary; // Sensitive - not exposed in view - uint64_t manager_id; // 0 means no manager }; -SPACETIMEDB_STRUCT(Employee, id, identity, name, department, salary, manager_id) +SPACETIMEDB_STRUCT(Employee, id, identity, name, department, salary) SPACETIMEDB_TABLE(Employee, employee, Private) FIELD_PrimaryKey(employee, id) FIELD_Unique(employee, identity) -FIELD_Index(employee, manager_id) +FIELD_Index(employee, department) -// Public type for team members (no salary) -struct TeamMember { +// Public type for colleagues (no salary) +struct Colleague { uint64_t id; std::string name; std::string department; }; -SPACETIMEDB_STRUCT(TeamMember, id, name, department) +SPACETIMEDB_STRUCT(Colleague, id, name, department) -// View that returns only the caller's team members, without salary info -SPACETIMEDB_VIEW(std::vector, my_team, Public, ViewContext ctx) { +// View that returns colleagues in the caller's department, without salary info +SPACETIMEDB_VIEW(std::vector, my_colleagues, Public, ViewContext ctx) { // Find the caller's employee record by identity (unique index) auto me_opt = ctx.db[employee_identity].find(ctx.sender); if (!me_opt.has_value()) { - return std::vector(); + return std::vector(); } Employee me = me_opt.value(); - std::vector results; + std::vector results; - // Look up employees who report to the caller by manager_id index - for (auto iter = ctx.db[employee_manager_id].filter(me.id); - iter != IndexIterator(); ++iter) { - const Employee& emp = *iter; - results.push_back(TeamMember{ - emp.id, - emp.name, - emp.department - // salary is not included - }); + // Look up employees in the same department + for (auto row : ctx.db[employee_department].filter(me.department)) { + results.push_back(Colleague{row.id, row.name, row.department}); } return results;