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
+17 -9
View File
@@ -46,12 +46,26 @@ use livevue_rs::{
spawn_fanout, RenderContext, Store,
};
// ---------------------------------------------------------------------------
// Queries
// ---------------------------------------------------------------------------
/// Fetch all messages, newest first.
pub struct ListMessages;
livevue_rs::pg_query_all!(ListMessages {
sql: "SELECT id, body FROM messages ORDER BY id DESC",
params: [],
output: Message,
cache: |_| pg_table_key("public", "messages"),
});
// ---------------------------------------------------------------------------
// Domain types
// ---------------------------------------------------------------------------
#[derive(Debug, Clone, FromRow)]
struct Message {
pub struct Message {
id: i64,
body: String,
}
@@ -101,14 +115,8 @@ struct RenderState {
}
async fn render_page(cx: &mut RenderContext, rs: &RenderState) -> anyhow::Result<Markup> {
// Subscribe to PG-sourced invalidation events for the messages table.
// The fanout task will re-render this connection whenever the WAL emitter
// fires a `CacheKey::Channel { key: "pg:public.messages" }` event.
cx.subscribe(pg_table_key("public", "messages"));
let messages: Vec<Message> = sqlx::query_as("SELECT id, body FROM messages ORDER BY id DESC")
.fetch_all(&rs.pg)
.await?;
// Fetch messages and auto-subscribe to the WAL invalidation key.
let messages = cx.run_pg(&rs.pg, ListMessages).await?;
let new_msg = cx.signal("newMsg", livevue_rs::global());
let _conn_id_signal = cx.signal("connId", livevue_rs::global());