@stacksjs/database 0.70.92 → 0.70.94

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.
@@ -30,9 +30,6 @@ export type { MigrationResult as MigrationResultType };
30
30
  * directory clean and prevents future runs from re-discovering it.
31
31
  */
32
32
  export declare function preprocessSqliteMigrations(): void;
33
- /**
34
- * Run database migrations
35
- */
36
33
  export declare function runDatabaseMigration(): Promise<Result<string, Error>>;
37
34
  /**
38
35
  * Reset the database (drop all tables)
@@ -46,6 +43,13 @@ export declare function resetDatabase(): Promise<Result<string, Error>>;
46
43
  */
47
44
  export declare function previewPendingMigrations(options?: GenerateMigrationsOptions): Promise<MigrationOperation[]>;
48
45
  export declare function generateMigrations(options?: GenerateMigrationsOptions): Promise<Result<string, Error>>;
46
+ /**
47
+ * Group generated SQL by the migration filename style the runner already
48
+ * uses for hand-written files: `create-<table>-table`,
49
+ * `alter-<table>-<col>`, `create-<index>-index-in-<table>`, or
50
+ * `drop-<table>-table`. Anything we can't match falls back to `auto-misc`.
51
+ */
52
+ export declare function groupGeneratedStatements(sqlStatements: string[]): GeneratedGroup[];
49
53
  /**
50
54
  * Generate fresh migrations (full regeneration, ignoring previous state)
51
55
  */
@@ -66,6 +70,10 @@ export declare interface GenerateMigrationsOptions {
66
70
  applyRenames?: boolean
67
71
  fromDb?: boolean
68
72
  }
73
+ declare interface GeneratedGroup {
74
+ label: string
75
+ statements: string[]
76
+ }
69
77
  /**
70
78
  * Migration result type for compatibility
71
79
  */
@@ -247,7 +247,7 @@ async function ensureDatabaseExists() {
247
247
  async function hideDisabledFeatureMigrations() {
248
248
  const hidden = [];
249
249
  try {
250
- const { FEATURE_NAMES, migrationFeature } = await import("@stacksjs/buddy"), { feature: isFeatureEnabled } = await import("@stacksjs/config"), fs = await import("node:fs/promises"), migrationsDir = path.projectPath("database/migrations");
250
+ const { appModelClaimsTable, FEATURE_NAMES, migrationFeature, migrationTable } = await import("@stacksjs/buddy"), { feature: isFeatureEnabled } = await import("@stacksjs/config"), fs = await import("node:fs/promises"), migrationsDir = path.projectPath("database/migrations");
251
251
  if (!existsSync(migrationsDir))
252
252
  return hidden;
253
253
  const disabledFeatures = new Set(FEATURE_NAMES.filter((f) => !isFeatureEnabled(f)));
@@ -258,6 +258,9 @@ async function hideDisabledFeatureMigrations() {
258
258
  const owner = migrationFeature(file);
259
259
  if (!owner || !disabledFeatures.has(owner))
260
260
  continue;
261
+ const table = migrationTable(file);
262
+ if (table && appModelClaimsTable(table))
263
+ continue;
261
264
  const original = join(migrationsDir, file), hiddenPath = `${original}.disabled`;
262
265
  await fs.rename(original, hiddenPath);
263
266
  hidden.push({ original, hidden: hiddenPath, feature: owner });
@@ -301,6 +304,50 @@ async function writeMigrateMarker(appliedCount) {
301
304
  await fs.writeFile(file, body, "utf8");
302
305
  } catch {}
303
306
  }
307
+ function idempotentSql(sql) {
308
+ const stmts = sql.split(";").map((s) => s.trim()).filter(Boolean);
309
+ if (stmts.length === 0)
310
+ return sql;
311
+ const out = [];
312
+ for (const raw of stmts) {
313
+ const stmt = raw.replace(/\bADD\s+COLUMN\s+(?!IF\s+NOT\s+EXISTS\b)/gi, "ADD COLUMN IF NOT EXISTS "), m = /^ALTER\s+TABLE\s+("?\w+"?)\s+ADD\s+CONSTRAINT\s+("?\w+"?)/i.exec(stmt);
314
+ if (m) {
315
+ const drop = `ALTER TABLE ${m[1]} DROP CONSTRAINT IF EXISTS ${m[2]}`;
316
+ if ((out[out.length - 1] ?? "").toUpperCase() !== drop.toUpperCase())
317
+ out.push(drop);
318
+ }
319
+ out.push(stmt);
320
+ }
321
+ return `${out.join(`;
322
+ `)};
323
+ `;
324
+ }
325
+ function makeMigrationsIdempotent() {
326
+ const migrationsDir = join(process.cwd(), "database", "migrations");
327
+ let files;
328
+ try {
329
+ files = readdirSync(migrationsDir).filter((f) => f.endsWith(".sql"));
330
+ } catch {
331
+ return;
332
+ }
333
+ for (const f of files) {
334
+ const p = join(migrationsDir, f);
335
+ let sql;
336
+ try {
337
+ sql = readFileSync(p, "utf8");
338
+ } catch {
339
+ continue;
340
+ }
341
+ if (!/\bADD\s+(?:COLUMN|CONSTRAINT)\b/i.test(sql))
342
+ continue;
343
+ const next = idempotentSql(sql);
344
+ if (next !== sql)
345
+ try {
346
+ writeFileSync(p, next);
347
+ log.debug(`[migration] made idempotent: ${f}`);
348
+ } catch {}
349
+ }
350
+ }
304
351
  export async function runDatabaseMigration() {
305
352
  const startedAt = Date.now(), hidden = await hideDisabledFeatureMigrations();
306
353
  let lockHandle = null;
@@ -312,6 +359,8 @@ export async function runDatabaseMigration() {
312
359
  lockHandle = await acquireMigrationLock(dialect, lockDb);
313
360
  if (dialect === "sqlite")
314
361
  preprocessSqliteMigrations();
362
+ else if (dialect === "postgres")
363
+ makeMigrationsIdempotent();
315
364
  const modelsDir = path.userModelsPath(), appliedBefore = await countAppliedMigrations();
316
365
  log.debug(`[migration] Running migrations from: ${modelsDir}`);
317
366
  await qbExecuteMigration(modelsDir);
@@ -486,12 +535,15 @@ ${readFileSync(join(migrationsDir, f), "utf8")}`;
486
535
  }
487
536
  return written;
488
537
  }
489
- function groupGeneratedStatements(sqlStatements) {
538
+ export function groupGeneratedStatements(sqlStatements) {
490
539
  const groups = new Map, push = (label, stmt) => {
491
540
  const list = groups.get(label) ?? [];
492
541
  list.push(stmt);
493
542
  groups.set(label, list);
494
- };
543
+ }, createdTables = new Set(sqlStatements.flatMap((raw) => {
544
+ const match = raw.trim().match(/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(\w+)["`]?/i);
545
+ return match?.[1] ? [match[1]] : [];
546
+ }));
495
547
  for (const raw of sqlStatements) {
496
548
  const stmt = raw.trim();
497
549
  if (!stmt)
@@ -508,7 +560,7 @@ function groupGeneratedStatements(sqlStatements) {
508
560
  }
509
561
  const idx = stmt.match(/^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(\w+)["`]?\s+ON\s+["`]?(\w+)["`]?/i);
510
562
  if (idx) {
511
- push(`create-${idx[1]}-index-in-${idx[2]}`, stmt);
563
+ push(createdTables.has(idx[2]) ? `create-${idx[2]}-table` : `create-${idx[1]}-index-in-${idx[2]}`, stmt);
512
564
  continue;
513
565
  }
514
566
  const drop = stmt.match(/^\s*DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?["`]?(\w+)["`]?/i);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/database",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.92",
5
+ "version": "0.70.94",
6
6
  "description": "The Stacks database integration.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -55,18 +55,18 @@
55
55
  "prepublishOnly": "bun run build"
56
56
  },
57
57
  "dependencies": {
58
- "bun-query-builder": "^0.1.50"
58
+ "bun-query-builder": "^0.1.51"
59
59
  },
60
60
  "devDependencies": {
61
- "@stacksjs/cli": "0.70.92",
62
- "@stacksjs/config": "0.70.92",
63
- "@stacksjs/logging": "0.70.92",
64
- "@stacksjs/router": "0.70.92",
61
+ "@stacksjs/cli": "0.70.94",
62
+ "@stacksjs/config": "0.70.94",
63
+ "@stacksjs/logging": "0.70.94",
64
+ "@stacksjs/router": "0.70.94",
65
65
  "better-dx": "^0.2.16",
66
- "@stacksjs/path": "0.70.92",
67
- "@stacksjs/query-builder": "0.70.92",
68
- "@stacksjs/storage": "0.70.92",
69
- "@stacksjs/strings": "0.70.92",
70
- "@stacksjs/utils": "0.70.92"
66
+ "@stacksjs/path": "0.70.94",
67
+ "@stacksjs/query-builder": "0.70.94",
68
+ "@stacksjs/storage": "0.70.94",
69
+ "@stacksjs/strings": "0.70.94",
70
+ "@stacksjs/utils": "0.70.94"
71
71
  }
72
72
  }