Skip to main content

max / makenotwork

Add the app dimension to Sando's schema Every table here was implicitly about the MNW server. versions keyed a version string with no room for two products releasing 0.4.1 in the same week, tiers keyed a tier name so pom's host and MNW's host would be one row, and tier_state recorded what is deployed with no way to say deployed of what. None of that was wrong with one product; all of it is wrong with two, and every collision would read as ordinary state rather than as an error. So app joins the key of what was keyed and rides along on what was merely recorded. Existing rows backfill to mnw, which is not a guess: this daemon has only ever deployed the MNW server, and a config with no [app.*] tables loads under that same id, so rows and config agree with nobody editing either. The four tables whose primary key widens are rebuilt, since SQLite cannot alter one in place. defer_foreign_keys holds enforcement to COMMIT, by which point every reference resolves again.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-06 23:33 UTC
Signed with PGP, not checked
Commit: bd074e4870b6d1e4e0547d394a275f9225ecbb4f
Parent: 7a76651
1 file changed, +119 insertions, -0 deletions
@@ -1,0 +1,119 @@
1 + -- Sando ships more than one product. Design: wiki [[sando-bento-boundary]].
2 + --
3 + -- Every table here was implicitly about the MNW server. `versions` keyed a
4 + -- version string with no room for two products releasing 0.4.1 in the same
5 + -- week; `tiers` keyed a tier name, so pom's `host` and MNW's `host` would be
6 + -- one row; `tier_state` recorded what is deployed with no way to say deployed
7 + -- *of what*. None of that was wrong while there was one product. All of it is
8 + -- wrong the moment there are two, and each collision would read as ordinary
9 + -- state rather than as an error.
10 + --
11 + -- So `app` joins the key of everything that was keyed, and rides along on
12 + -- everything that was merely recorded.
13 + --
14 + -- Existing rows backfill to 'mnw'. That is not a guess: this daemon has only
15 + -- ever deployed the MNW server, and a config with no [app.*] tables loads as
16 + -- that same id (see config.rs), so the rows and the config agree without
17 + -- anybody editing either.
18 + --
19 + -- Four tables need their PRIMARY KEY widened, which SQLite cannot do in place,
20 + -- so they are rebuilt. `defer_foreign_keys` holds enforcement until COMMIT:
21 + -- mid-migration the referenced tables briefly do not exist, and every reference
22 + -- is satisfied again by the time the transaction closes. (`PRAGMA foreign_keys`
23 + -- itself is a no-op inside a transaction, which is why it is not used here.)
24 +
25 + PRAGMA defer_foreign_keys = ON;
26 +
27 + -- Tiers are per product. MNW's `a`/`b` and pom's tiers are different pipelines
28 + -- that happen to be spelled alike.
29 + CREATE TABLE tiers_new (
30 + app TEXT NOT NULL,
31 + name TEXT NOT NULL,
32 + ord INTEGER NOT NULL,
33 + provisioned INTEGER NOT NULL DEFAULT 0,
34 + canary TEXT NOT NULL DEFAULT 'sequential',
35 + PRIMARY KEY (app, name)
36 + );
37 + INSERT INTO tiers_new (app, name, ord, provisioned, canary)
38 + SELECT 'mnw', name, ord, provisioned, canary FROM tiers;
39 +
40 + -- A node name is only unique within its product: two products may each deploy
41 + -- to a node they both call `prod-1`, and they are not the same machine role.
42 + CREATE TABLE nodes_new (
43 + app TEXT NOT NULL,
44 + name TEXT NOT NULL,
45 + tier TEXT NOT NULL,
46 + ssh_target TEXT NOT NULL,
47 + release_root TEXT NOT NULL,
48 + PRIMARY KEY (app, name),
49 + FOREIGN KEY (app, tier) REFERENCES tiers(app, name)
50 + );
51 + INSERT INTO nodes_new (app, name, tier, ssh_target, release_root)
52 + SELECT 'mnw', name, tier, ssh_target, release_root FROM nodes;
53 +
54 + -- Two products releasing the same version string are two artifacts.
55 + CREATE TABLE versions_new (
56 + app TEXT NOT NULL,
57 + version TEXT NOT NULL,
58 + git_sha TEXT NOT NULL,
59 + built_at TEXT NOT NULL,
60 + artifact_path TEXT NOT NULL,
61 + PRIMARY KEY (app, version)
62 + );
63 + INSERT INTO versions_new (app, version, git_sha, built_at, artifact_path)
64 + SELECT 'mnw', version, git_sha, built_at, artifact_path FROM versions;
65 +
66 + -- What is deployed, per product per tier. Columns added since 001 are carried
67 + -- across explicitly rather than by SELECT *, so a future column cannot be
68 + -- silently dropped by this migration.
69 + CREATE TABLE tier_state_new (
70 + app TEXT NOT NULL,
71 + tier TEXT NOT NULL,
72 + current_version TEXT,
73 + previous_version TEXT,
74 + burn_in_started_at TEXT,
75 + partial_reason TEXT,
76 + current_build_id INTEGER REFERENCES build_runs(id),
77 + previous_build_id INTEGER REFERENCES build_runs(id),
78 + advanced_at TEXT,
79 + PRIMARY KEY (app, tier),
80 + FOREIGN KEY (app, tier) REFERENCES tiers(app, name)
81 + );
82 + INSERT INTO tier_state_new (
83 + app, tier, current_version, previous_version, burn_in_started_at,
84 + partial_reason, current_build_id, previous_build_id, advanced_at
85 + )
86 + SELECT 'mnw', tier, current_version, previous_version, burn_in_started_at,
87 + partial_reason, current_build_id, previous_build_id, advanced_at
88 + FROM tier_state;
89 +
90 + DROP TABLE tier_state;
91 + DROP TABLE nodes;
92 + DROP TABLE versions;
93 + DROP TABLE tiers;
94 +
95 + ALTER TABLE tiers_new RENAME TO tiers;
96 + ALTER TABLE nodes_new RENAME TO nodes;
97 + ALTER TABLE versions_new RENAME TO versions;
98 + ALTER TABLE tier_state_new RENAME TO tier_state;
99 +
100 + CREATE INDEX nodes_by_tier ON nodes(app, tier);
101 +
102 + -- The append-only tables keep their integer primary keys; `app` is a column,
103 + -- not part of an identity they already had.
104 + ALTER TABLE deploys ADD COLUMN app TEXT NOT NULL DEFAULT 'mnw';
105 + ALTER TABLE gate_runs ADD COLUMN app TEXT NOT NULL DEFAULT 'mnw';
106 + ALTER TABLE backups ADD COLUMN app TEXT NOT NULL DEFAULT 'mnw';
107 + ALTER TABLE build_runs ADD COLUMN app TEXT NOT NULL DEFAULT 'mnw';
108 +
109 + -- Every lookup that used to be by (tier, version) or by name is now by product
110 + -- first. Left as separate indexes from the 001 ones, which are dropped: an
111 + -- index that omits the leading column of the predicate cannot serve it.
112 + DROP INDEX IF EXISTS deploys_by_tier_version;
113 + DROP INDEX IF EXISTS gate_runs_lookup;
114 + DROP INDEX IF EXISTS backups_name_fetched;
115 + DROP INDEX IF EXISTS build_runs_by_sha;
116 + CREATE INDEX deploys_by_tier_version ON deploys(app, tier, version);
117 + CREATE INDEX gate_runs_lookup ON gate_runs(app, tier, version, gate_kind);
118 + CREATE INDEX backups_name_fetched ON backups(app, name, fetched_at);
119 + CREATE INDEX build_runs_by_sha ON build_runs(app, sha);