Files
pansy/internal/store/migrations/0006_revisions.sql
T
steve f208da94d6
Build image / build-and-push (push) Successful in 5s
Revision history: change sets + revisions + revert — the undo substrate (#48) (#61)
Co-authored-by: Steve Dudenhoeffer <[email protected]>
2026-07-21 05:07:30 +00:00

54 lines
2.9 KiB
SQL

-- Revision history: the undo substrate (#48).
--
-- The unit of undo is the OPERATION, not the row. "Empty the garlic bed and
-- plant cucumbers" touches one object and many plantings; undoing half of that
-- is worse than useless. So a change_set groups the row-level revisions it
-- produced, and revert replays their inverses.
--
-- Two properties this schema is built around:
-- * Revert is itself a change set (reverts_id points at its target). History is
-- append-only and never rewritten, so an undo can be undone. git revert, not
-- git reset.
-- * Every revision carries full JSON row snapshots INCLUDING version, which is
-- what the revert guard compares against so a later edit is reported as a
-- conflict rather than silently clobbered.
--
-- JSON snapshots rather than a shadow table per entity: the rows are small,
-- household scale makes storage a non-issue, and one code path covers all three
-- entity types.
--
-- Deliberate gap: garden DELETION is not revertible. Dropping a garden cascades
-- its objects and plantings without the service ever seeing them, and the
-- ON DELETE CASCADE below would take the history with it. The history UI says so
-- rather than pretending otherwise.
CREATE TABLE change_sets (
id INTEGER PRIMARY KEY,
garden_id INTEGER NOT NULL REFERENCES gardens (id) ON DELETE CASCADE,
actor_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
source TEXT NOT NULL CHECK (source IN ('ui', 'agent', 'api')),
summary TEXT NOT NULL DEFAULT '',
agent_run_id TEXT, -- executus run id when source='agent'
reverts_id INTEGER REFERENCES change_sets (id), -- set when this change set reverts another
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
-- The history panel's only query: one garden, newest first.
CREATE INDEX idx_change_sets_garden ON change_sets (garden_id, created_at DESC);
-- "Has this change set been reverted?" — the list marks reverted entries, so the
-- lookup by target must not be a scan.
CREATE INDEX idx_change_sets_reverts ON change_sets (reverts_id) WHERE reverts_id IS NOT NULL;
CREATE TABLE revisions (
id INTEGER PRIMARY KEY,
change_set_id INTEGER NOT NULL REFERENCES change_sets (id) ON DELETE CASCADE,
seq INTEGER NOT NULL, -- order within the change set
entity_type TEXT NOT NULL CHECK (entity_type IN ('garden', 'object', 'planting')),
entity_id INTEGER NOT NULL,
op TEXT NOT NULL CHECK (op IN ('create', 'update', 'delete')),
before TEXT, -- JSON row snapshot; NULL for create
after TEXT -- JSON row snapshot; NULL for delete
);
CREATE INDEX idx_revisions_change_set ON revisions (change_set_id, seq);