Refactor Query contract: uniform ergonomics for SQLite + PostgreSQL

- Add `publish_key()` default method to `Query` trait (returns `None`)
- Add `mutation_exec!` macro for SQLite INSERT/UPDATE/DELETE ops; overrides
  `publish_key()` so the mutation carries its own invalidation key
- Add `Store::exec()` which runs a mutation and auto-publishes its key —
  action handlers no longer need a separate `store.publish()` call
- Add `PgQuery` trait + `pg_query_one!` / `pg_query_all!` macros
  (feature-gated on `pg_replication`) mirroring the SQLite Query API
- Add `cx.run_pg(pool, query)` to `RenderContext` which executes a PgQuery
  and auto-subscribes its cache key — no more manual `cx.subscribe()` calls
- Update todo example: replace plain mutation fns with `mutation_exec!` structs
  and `store.exec()` calls; remove `action_then_publish` helper
- Update pg_replication example: add `ListMessages` via `pg_query_all!` and
  replace manual subscribe + raw query with `cx.run_pg()`

https://claude.ai/code/session_01HXe8rAZPweU9j2piBo9MdG
This commit is contained in:
Claude
2026-03-11 22:37:33 +00:00
parent 23fab7a5ff
commit 9dcb0d6dc8
8 changed files with 331 additions and 84 deletions
+29 -23
View File
@@ -40,36 +40,42 @@ livevue_rs::query_one!(GetTodo {
});
// ---------------------------------------------------------------------------
// Write operations (plain sqlx, not through Query trait)
// Write operations (mutation structs via mutation_exec!)
// ---------------------------------------------------------------------------
//
// Mutations don't go through cx.run() because they're not reads and don't
// produce subscription keys. They're called directly by action handlers,
// which then trigger a full rerender.
// Each mutation struct implements Query with publish_key() set to the cache
// key it invalidates. Use Store::exec() in action handlers — it executes the
// mutation and publishes the key automatically.
/// Insert a new todo. Returns the new row's id.
pub async fn create_todo(db: &sqlx::SqlitePool, title: &str) -> anyhow::Result<i64> {
let result = sqlx::query("INSERT INTO todos (title, completed) VALUES (?, false)")
.bind(title)
.execute(db)
.await?;
Ok(result.last_insert_rowid())
/// Insert a new todo.
pub struct CreateTodo {
pub title: String,
}
livevue_rs::mutation_exec!(CreateTodo {
sql: "INSERT INTO todos (title, completed) VALUES (?, false)",
params: [|s: &CreateTodo| s.title.clone()],
publish: |_| CacheKey::Table { table: "todos" },
});
/// Toggle a todo's completed status.
pub async fn toggle_todo(db: &sqlx::SqlitePool, id: i64) -> anyhow::Result<()> {
sqlx::query("UPDATE todos SET completed = NOT completed WHERE id = ?")
.bind(id)
.execute(db)
.await?;
Ok(())
pub struct ToggleTodo {
pub id: i64,
}
livevue_rs::mutation_exec!(ToggleTodo {
sql: "UPDATE todos SET completed = NOT completed WHERE id = ?",
params: [|s: &ToggleTodo| s.id],
publish: |_| CacheKey::Table { table: "todos" },
});
/// Delete a todo by id.
pub async fn delete_todo(db: &sqlx::SqlitePool, id: i64) -> anyhow::Result<()> {
sqlx::query("DELETE FROM todos WHERE id = ?")
.bind(id)
.execute(db)
.await?;
Ok(())
pub struct DeleteTodo {
pub id: i64,
}
livevue_rs::mutation_exec!(DeleteTodo {
sql: "DELETE FROM todos WHERE id = ?",
params: [|s: &DeleteTodo| s.id],
publish: |_| CacheKey::Table { table: "todos" },
});