@cosmicdrift/kumiko-framework 0.163.0 → 0.163.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.163.0",
3
+ "version": "0.163.2",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -182,7 +182,7 @@
182
182
  "./package.json": "./package.json"
183
183
  },
184
184
  "dependencies": {
185
- "@cosmicdrift/kumiko-types": "0.163.0",
185
+ "@cosmicdrift/kumiko-types": "0.163.2",
186
186
  "bullmq": "^5.76.7",
187
187
  "bun-types": "^1.3.13",
188
188
  "hono": "^4.12.18",
@@ -198,7 +198,7 @@
198
198
  "zod": "^4.4.3"
199
199
  },
200
200
  "devDependencies": {
201
- "@cosmicdrift/kumiko-dispatcher-live": "0.163.0",
201
+ "@cosmicdrift/kumiko-dispatcher-live": "0.163.2",
202
202
  "bun-types": "^1.3.13",
203
203
  "pino-pretty": "^13.1.3"
204
204
  },
@@ -72,6 +72,54 @@ CREATE INDEX IF NOT EXISTS "read_accounts_tenant_id_idx" ON "read_accounts" ("te
72
72
  }
73
73
  });
74
74
 
75
+ // Regression: publicstatus#0007_fix-secrets-table-columns adds three
76
+ // columns in ONE statement (comma-separated ADD COLUMN clauses) — the
77
+ // replay used to only pick up the first, reporting "metadata" and
78
+ // "last_rotated_at" as missing even though the migration creates them.
79
+ test("multiple ADD COLUMN clauses in a single ALTER TABLE statement all extend the table", () => {
80
+ const dir = tmpMigrationsDir();
81
+ try {
82
+ write(dir, "0001_init.sql", `CREATE TABLE IF NOT EXISTS "read_a" ("id" uuid PRIMARY KEY);`);
83
+ write(
84
+ dir,
85
+ "0002_add-cols.sql",
86
+ `ALTER TABLE "read_a"
87
+ ADD COLUMN IF NOT EXISTS "envelope" jsonb NOT NULL,
88
+ ADD COLUMN IF NOT EXISTS "metadata" jsonb DEFAULT '{}'::jsonb NOT NULL,
89
+ ADD COLUMN IF NOT EXISTS "last_rotated_at" timestamp with time zone DEFAULT now() NOT NULL;`,
90
+ );
91
+ const replayed = replayMigrationsDir(dir);
92
+ expect([...(replayed.get("read_a")?.columns ?? [])].sort()).toEqual([
93
+ "envelope",
94
+ "id",
95
+ "last_rotated_at",
96
+ "metadata",
97
+ ]);
98
+ } finally {
99
+ rmSync(dir, { recursive: true, force: true });
100
+ }
101
+ });
102
+
103
+ test("mixed ADD + DROP COLUMN clauses in one statement apply in order", () => {
104
+ const dir = tmpMigrationsDir();
105
+ try {
106
+ write(
107
+ dir,
108
+ "0001_init.sql",
109
+ `CREATE TABLE IF NOT EXISTS "read_a" ("id" uuid PRIMARY KEY, "legacy" text);`,
110
+ );
111
+ write(
112
+ dir,
113
+ "0002_migrate-cols.sql",
114
+ `ALTER TABLE "read_a" DROP COLUMN IF EXISTS "legacy", ADD COLUMN IF NOT EXISTS "title" text;`,
115
+ );
116
+ const replayed = replayMigrationsDir(dir);
117
+ expect([...(replayed.get("read_a")?.columns ?? [])].sort()).toEqual(["id", "title"]);
118
+ } finally {
119
+ rmSync(dir, { recursive: true, force: true });
120
+ }
121
+ });
122
+
75
123
  test("DROP COLUMN IF EXISTS (hand-edited) still removes the column", () => {
76
124
  const dir = tmpMigrationsDir();
77
125
  try {
@@ -54,37 +54,40 @@ function applyStatement(schema: Map<string, { columns: Set<string> }>, statement
54
54
  const create = statement.match(
55
55
  /^CREATE TABLE\s+(?:IF NOT EXISTS\s+)?"([^"]+)"\s*\(([\s\S]*)\);?\s*$/i,
56
56
  );
57
- const createTableName = create?.[1];
58
- const createBody = create?.[2];
59
-
60
- const addColumn = statement.match(
61
- /^ALTER TABLE\s+"([^"]+)"\s+ADD COLUMN\s+(?:IF NOT EXISTS\s+)?"([^"]+)"/i,
62
- );
63
- const addColumnTable = addColumn?.[1];
64
- const addColumnName = addColumn?.[2];
57
+ if (create?.[1] !== undefined && create[2] !== undefined) {
58
+ schema.set(create[1], { columns: parseColumnNames(create[2]) });
59
+ // skip: CREATE TABLE fully handled above, no other clause can also match
60
+ return;
61
+ }
65
62
 
66
63
  const dropTable = statement.match(/^DROP TABLE\s+(?:IF EXISTS\s+)?"([^"]+)"/i);
67
- const dropTableName = dropTable?.[1];
68
-
69
- const dropColumn = statement.match(
70
- /^ALTER TABLE\s+"([^"]+)"\s+DROP COLUMN\s+(?:IF EXISTS\s+)?"([^"]+)"/i,
71
- );
72
- const dropColumnTable = dropColumn?.[1];
73
- const dropColumnName = dropColumn?.[2];
64
+ if (dropTable?.[1] !== undefined) {
65
+ schema.delete(dropTable[1]);
66
+ // skip: DROP TABLE fully handled above, no other clause can also match
67
+ return;
68
+ }
74
69
 
75
- if (createTableName !== undefined && createBody !== undefined) {
76
- schema.set(createTableName, { columns: parseColumnNames(createBody) });
77
- } else if (addColumnTable !== undefined && addColumnName !== undefined) {
78
- const table = schema.get(addColumnTable);
79
- if (table) table.columns.add(addColumnName);
80
- else schema.set(addColumnTable, { columns: new Set([addColumnName]) });
81
- } else if (dropTableName !== undefined) {
82
- schema.delete(dropTableName);
83
- } else if (dropColumnTable !== undefined && dropColumnName !== undefined) {
84
- schema.get(dropColumnTable)?.columns.delete(dropColumnName);
70
+ // A single ALTER TABLE statement can carry multiple comma-separated
71
+ // ADD/DROP COLUMN clauses (e.g. migration 0007_fix-secrets-table-columns'
72
+ // three-column fix in one statement) matchAll over the whole body
73
+ // instead of matching only the first clause, in statement order so an
74
+ // add-then-drop of the same column (unusual, but not impossible) resolves
75
+ // correctly.
76
+ const alterTable = statement.match(/^ALTER TABLE\s+"([^"]+)"\s+([\s\S]*?);?\s*$/i);
77
+ const alterTableName = alterTable?.[1];
78
+ const alterBody = alterTable?.[2];
79
+ if (alterTableName !== undefined && alterBody !== undefined) {
80
+ const table = schema.get(alterTableName) ?? { columns: new Set<string>() };
81
+ schema.set(alterTableName, table);
82
+ const clauseRe = /(ADD|DROP)\s+COLUMN\s+(?:IF (?:NOT )?EXISTS\s+)?"([^"]+)"/gi;
83
+ for (const [, verb, name] of alterBody.matchAll(clauseRe)) {
84
+ if (verb === undefined || name === undefined) continue;
85
+ if (verb.toUpperCase() === "ADD") table.columns.add(name);
86
+ else table.columns.delete(name);
87
+ }
85
88
  }
86
- // else: CREATE INDEX, ALTER COLUMN (TYPE/DEFAULT/NOT NULL) and everything
87
- // else don't change the table/column shape this replay tracks.
89
+ // else: CREATE INDEX and everything else don't change the table/column
90
+ // shape this replay tracks.
88
91
  }
89
92
 
90
93
  // Reads `<migrationsDir>/*.sql` in sequence order and replays every