-- 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);