diff --git a/docs/docs/00200-core-concepts/00300-tables.md b/docs/docs/00200-core-concepts/00300-tables.md index 763bbcd8df7..8e103267e2f 100644 --- a/docs/docs/00200-core-concepts/00300-tables.md +++ b/docs/docs/00200-core-concepts/00300-tables.md @@ -164,6 +164,24 @@ pub struct Person { The `pub` modifier on the struct follows normal Rust visibility rules and has no meaning to SpacetimeDB. It controls whether the struct is accessible from other Rust modules in your crate, not whether the table is public to clients. Use the `public` attribute in `#[spacetimedb::table]` to control client visibility. ::: + + + +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) +``` + @@ -239,6 +257,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]` | + @@ -251,6 +292,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. @@ -291,6 +333,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) +``` + @@ -388,6 +449,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..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 @@ -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 5e0f51551b4..10b3acba33c 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(); +} +``` + @@ -735,6 +801,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 04d5af717de..569a242b911 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 08cb9ed04f1..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 @@ -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(); +} +``` + @@ -261,6 +316,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{}; +} +``` + @@ -315,6 +390,17 @@ fn find_users_by_name(ctx: &ViewContext) -> Vec { } ``` + + + +```cpp +SPACETIMEDB_VIEW(std::vector, find_users_by_name, Public, ViewContext ctx) { + return ctx.db[user_name].filter("Alice").collect(); + // Cannot insert, update, or delete + // ctx.db[user].insert(...) // ❌ Methods not available in ViewContext +} +``` + @@ -432,6 +518,34 @@ 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 + auto sent = ctx.db[message_sender].filter(ctx.sender).collect(); + auto received = ctx.db[message_recipient].filter(ctx.sender).collect(); + + // Combine both vectors + sent.insert(sent.end(), received.begin(), received.end()); + return sent; +} +``` + @@ -584,6 +698,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 + }; +} +``` + @@ -742,6 +900,52 @@ fn my_colleagues(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 +}; +SPACETIMEDB_STRUCT(Employee, id, identity, name, department, salary) +SPACETIMEDB_TABLE(Employee, employee, Private) +FIELD_PrimaryKey(employee, id) +FIELD_Unique(employee, identity) +FIELD_Index(employee, department) + +// Public type for colleagues (no salary) +struct Colleague { + uint64_t id; + std::string name; + std::string department; +}; +SPACETIMEDB_STRUCT(Colleague, id, name, department) + +// 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(); + } + + Employee me = me_opt.value(); + std::vector results; + + // 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; +} +``` + 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 0907ab79f07..b91b2a0daac 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 @@ -86,6 +86,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(); +} +``` + @@ -176,6 +200,25 @@ fn schedule_periodic_tasks(ctx: &ReducerContext) { } ``` + + + +```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" +}); +``` + @@ -265,6 +308,26 @@ fn schedule_timed_tasks(ctx: &ReducerContext) { } ``` + + + +```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" +}); +``` + @@ -327,6 +390,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(); +} +``` +