Files
PiCloud/crates/manager-core/migrations/0007_kv.sql
MechaCat02 434fb63cd2 feat(v1.1.1-kv): migrations + KvService trait + Postgres impl
First v1.1.1 commit. Adds the KV store the design notes commit to:
`(app_id, collection, key)` identity with JSONB value and a per-app
index. Trait lives in `picloud-shared` so the executor-core Rhai
bridge (next commit), the Postgres impl, and tests all depend on the
same surface without coupling crates.

The `Services` bundle grows from empty to three fields: `kv`,
`dead_letters` (NoopDeadLetterService stub — replaced by the
Postgres impl in commit 8), and `events` (NoopEventEmitter until the
outbox emitter lands with the dispatcher). Tests use
`Services::default()` for an all-noop bundle.

New capabilities `AppKvRead` / `AppKvWrite` join the Capability
enum. They map onto the existing seven-value `Scope` (script:read /
script:write) — the scope vocabulary stays locked per the
`docs/versioning.md` commitment.

Script-as-gate semantics in `KvServiceImpl`: capability check runs
when `cx.principal.is_some()`, skipped when None (public HTTP).
Cross-app isolation is enforced independently by deriving every
row's `app_id` from `cx.app_id` rather than a script-passed argument.

In-memory `KvRepo` impl + unit tests cover the round-trips, the
cross-app isolation property, empty-collection rejection,
script-as-gate behaviour for both anonymous and authed contexts,
and cursor-style pagination. Postgres impl exists; integration
testing waits for a real DB harness (see HANDBACK).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 21:29:59 +02:00

29 lines
1.3 KiB
SQL

-- v1.1.1: Key-value store — see blueprint §8.1 + docs/sdk-shape.md.
--
-- Identity tuple `(app_id, collection, key)`. `app_id` is first in the
-- primary key so the implicit index is always per-app; cross-app reads
-- cannot happen even with a buggy query. Collections are a required
-- namespace inside an app — the same key can live in different
-- collections without collision.
--
-- `value` is JSONB so scripts can store nested structures without
-- a separate serialization step. No TTL column in v1.1.1; deferred
-- until a concrete need surfaces (the blueprint reserved one but the
-- v1.1.1 SDK surface — get/set/has/delete/list — doesn't expose TTL).
CREATE TABLE kv_entries (
app_id UUID NOT NULL REFERENCES apps(id) ON DELETE CASCADE,
collection TEXT NOT NULL,
key TEXT NOT NULL,
value JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (app_id, collection, key)
);
-- Supports list-by-collection (keyset pagination) and per-collection
-- triggers' fan-out scans. The PK already covers (app_id, collection)
-- as a prefix but spelling out the explicit index makes intent clear
-- for the planner.
CREATE INDEX idx_kv_entries_app_collection ON kv_entries (app_id, collection);