6.2 KiB
Milestone B Schema Migration Plan (Prototype Scaffold)
Related tasks: FN-4491, FN-4490, FN-4487, FN-4471, governance gate FN-4359.
See also: DagCoordinator design · Implementation checklist · ADR v1
Goals and non-goals
ADR traceability: this plan derives from ADR-0001 Decision (first-class SQLite DAG model), Consequences #1 (additive storage), and Consequences #3 (project-local scope).
Goals
- Additive-only SQLite schema plan for per-project
.fusion/fusion.dbDAG persistence. - Preserve WAL mode and current migration runner contract in
packages/core/src/db.ts. - Keep startup safe: no destructive backfills, no blocking startup jobs.
- Ensure restart recovery can reconstruct DAG state from SQLite alone, aligned with
RestartRecoveryCoordinatorexpectations from FN-4490 deliverables.
Non-goals
- No schema file or runtime implementation in this task.
- No column renames/drops/destructive migrations.
- No changes to
~/.fusion/fusion-central.dbcentral registry schema.
Current state
- Project DB migration runner lives in
packages/core/src/db.ts:Database.init()callsmigrate().- Incremental migrations are version-gated
if (version < N)blocks. - Current top migration in-tree is
version < 77(SCHEMA_VERSION-driven sequence). - Version bump is written through
applyMigration(targetVersion, fn)updating__meta.schemaVersion.
- Existing DB settings preserve WAL + busy timeout in constructor (
PRAGMA journal_mode = WAL,PRAGMA busy_timeout = ...) inpackages/core/src/db.ts. - Central registry DB is distinct (
~/.fusion/fusion-central.db) perdocs/multi-project.md; DAG prototype schema changes are project DB only.
Proposed additive tables
Naming/type conventions follow existing project DB patterns (snake_case, TEXT IDs, INTEGER flags/counts, ISO timestamps as TEXT).
1) dag_run
Suggested columns:
id TEXT PRIMARY KEYproject_id TEXT NOT NULL(project scope identifier; per-project DB still records scope for auditability)status TEXT NOT NULL(pending|running|completed|aborted|cancelled|failed)started_at TEXTcompleted_at TEXTcreated_at TEXT NOT NULLupdated_at TEXT NOT NULLmetadata TEXT(JSON payload for run-level config/trace context)
Indexes:
CREATE INDEX ... ON dag_run(status)CREATE INDEX ... ON dag_run(project_id, status)CREATE INDEX ... ON dag_run(created_at)
Rationale:
started_at/completed_atnullable to support queued runs.project_idkept explicit for consistency with audit/event payloads and future cross-node read tooling, while still local to one project DB instance.
2) dag_node
Suggested columns:
id TEXT PRIMARY KEYdag_run_id TEXT NOT NULLtask_id TEXT(nullable until mapped/enqueued task exists)status TEXT NOT NULL(pending|ready|enqueued|running|completed|failed|blocked|skipped|cancelled)attempt_count INTEGER NOT NULL DEFAULT 0last_error TEXTcreated_at TEXT NOT NULLupdated_at TEXT NOT NULL
Constraints/indexes:
FOREIGN KEY (dag_run_id) REFERENCES dag_run(id) ON DELETE CASCADECREATE INDEX ... ON dag_node(dag_run_id, status)CREATE INDEX ... ON dag_node(task_id)
Rationale:
task_idnullable to represent not-yet-materialized nodes under enqueue-only adapter.attempt_countpersisted for retry accounting aligned to FN-4490/FN-4398 retry taxonomy.
3) dag_edge
Suggested columns:
id TEXT PRIMARY KEYdag_run_id TEXT NOT NULLfrom_node_id TEXT NOT NULLto_node_id TEXT NOT NULLedge_kind TEXT NOT NULL DEFAULT 'depends_on'created_at TEXT NOT NULL
Constraints/indexes:
FOREIGN KEY (dag_run_id) REFERENCES dag_run(id) ON DELETE CASCADEFOREIGN KEY (from_node_id) REFERENCES dag_node(id) ON DELETE CASCADEFOREIGN KEY (to_node_id) REFERENCES dag_node(id) ON DELETE CASCADEUNIQUE(dag_run_id, from_node_id, to_node_id, edge_kind)CREATE INDEX ... ON dag_edge(dag_run_id, to_node_id)CREATE INDEX ... ON dag_edge(dag_run_id, from_node_id)
Rationale:
- explicit run-scoped edge rows support deterministic readiness checks and restart rehydration.
Migration mechanics
- Add one new migration block at next version slot 78 in
packages/core/src/db.ts(if (version < 78) { applyMigration(78, ...) }). - Migration should create new tables/indexes via
CREATE TABLE IF NOT EXISTSandCREATE INDEX IF NOT EXISTSto stay idempotent. - Downgrade policy: forward-only migrations (consistent with current runner). Explicitly no down migration.
- Multi-project interaction:
~/.fusion/fusion-central.db: unchanged.- Every project’s local
.fusion/fusion.db: independently receives the additive tables when opened.
Restart recovery contract
ADR traceability: aligns with ADR-0001 Decision (SQLite-backed DAG state) and Consequences #2 (single-event-loop/non-blocking runtime constraints).
For engine boot recovery:
- Source of truth is SQLite rows in
dag_run+dag_node+dag_edge. - Recovery should identify non-terminal runs (
pending|running) and resume readiness evaluation from persisted statuses. - Required status invariants:
dag_run.statusanddag_node.statustransitions are monotonic toward terminal states.- crashes between transitions are safe because old state remains valid input for retry/re-evaluation.
task_idlinkage, once set, remains stable for node lifetime.
Rollback story
Because this milestone is scaffold-only and adapter is enqueue-only:
- if issues arise, runtime can ignore new DAG tables.
- scheduler/executor/merger behavior remains unchanged when DAG feature flag is off.
- no existing table semantics are modified.
Open questions
- Should
dag_run.project_idstore canonical project ID or normalized path-derived identity used by central registry APIs? - Do we need a dedicated
terminal_reasoncolumn ondag_nodevs. deriving fromlast_error+ status? - Should
dag_node.task_idbe unique within a run (UNIQUE(dag_run_id, task_id)with NULL-safe behavior) to prevent accidental dual binding? - How should cancellation provenance (operator/system/governance) be normalized for restart-safe replay?