@voltro/database 0.5.0 → 0.7.0

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/dist/sql.d.ts CHANGED
@@ -2003,6 +2003,24 @@ declare interface TableUnique {
2003
2003
  };
2004
2004
  }
2005
2005
 
2006
+ /**
2007
+ * Clear the `blocked` flag on every LOSSY operation in a plan.
2008
+ *
2009
+ * Called only after the destructive opt-in has been established, and it is not
2010
+ * cosmetic. `applyPlan` carries its OWN unconditional refusal on any blocked
2011
+ * operation, with no override parameter. So a plan that the boot gate has
2012
+ * decided may proceed, but which still arrives marked blocked, is refused a
2013
+ * second time by the applier — which meant `VOLTRO_DESTRUCTIVE_OK=1` could
2014
+ * never actually drop a table. The planner's fix hint names that exact
2015
+ * variable, so the documented escape hatch pointed at a wall.
2016
+ *
2017
+ * Only `lossy` ops are unblocked, and callers must have already established
2018
+ * that no other class is blocked. A rename-without-marker or a NOT NULL
2019
+ * without backfill stays blocked even in destructive-OK mode: those lose data
2020
+ * regardless of intent, so intent is not the question being asked.
2021
+ */
2022
+ export declare const unblockLossy: (plan: MigrationPlan) => MigrationPlan;
2023
+
2006
2024
  /**
2007
2025
  * Unique-constraint metadata. Set by `.unique()` (or `.unique({ dedup })`).
2008
2026
  *
package/dist/sql.js CHANGED
@@ -1261,6 +1261,7 @@ BEGIN
1261
1261
  JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = ANY (con.conkey)
1262
1262
  WHERE con.contype = 'p' AND nsp.nspname = current_schema()
1263
1263
  AND rel.relname IN ${t}
1264
+ ORDER BY rel.relname, array_position(con.conkey, att.attnum)
1264
1265
  `), p = yield* a(l, (t) => e`
1265
1266
  SELECT rel.relname AS table_name, con.conname AS constraint_name,
1266
1267
  att.attname AS column_name, array_position(con.conkey, att.attnum) AS position
@@ -2337,7 +2338,14 @@ BEGIN
2337
2338
  }), rr = () => {
2338
2339
  let e = (process.env.VOLTRO_DB_IGNORE_TABLES ?? "").split(",").map((e) => e.trim()).filter((e) => e.length > 0);
2339
2340
  return e.length > 0 ? { ignoreTables: e } : {};
2340
- }, ir = (e, t, n) => d.gen(function* () {
2341
+ }, ir = (e) => ({
2342
+ ...e,
2343
+ operations: e.operations.map((e) => {
2344
+ if (!e.blocked || e.classification !== "lossy") return e;
2345
+ let { blocked: t, ...n } = e;
2346
+ return n;
2347
+ })
2348
+ }), ar = (e, t, n) => d.gen(function* () {
2341
2349
  let r = Qn(e), i = t.filter((e) => !at(e.tableName));
2342
2350
  if (process.env.VOLTRO_MIGRATE_FORCE !== "1") {
2343
2351
  let t = M(P(i, r));
@@ -2363,6 +2371,7 @@ BEGIN
2363
2371
  kind: "refused-blocked",
2364
2372
  plan: s
2365
2373
  };
2374
+ s = ir(s);
2366
2375
  }
2367
2376
  return s.operations.length === 0 ? {
2368
2377
  kind: "up-to-date",
@@ -2377,10 +2386,10 @@ BEGIN
2377
2386
  }),
2378
2387
  plan: s
2379
2388
  };
2380
- }), ar = (e, t, n) => process.env.VOLTRO_AUTO_MIGRATE === "0" ? d.succeed({
2389
+ }), or = (e, t, n) => process.env.VOLTRO_AUTO_MIGRATE === "0" ? d.succeed({
2381
2390
  kind: "skipped",
2382
2391
  reason: "VOLTRO_AUTO_MIGRATE=0"
2383
- }) : n.environment === "prod" ? $n(e, t) : ir(e, t, n), or = (e, t) => {
2392
+ }) : n.environment === "prod" ? $n(e, t) : ar(e, t, n), sr = (e, t) => {
2384
2393
  switch (e.kind) {
2385
2394
  case "skipped": return `auto-migrate: skipped (${e.reason})`;
2386
2395
  case "up-to-date": return `auto-migrate: schema up to date (${t}, fingerprint=${N(e.fingerprint)})`;
@@ -2394,7 +2403,7 @@ BEGIN
2394
2403
  return n > 0 && s.push(`${n} table(s) exist in the live DB but aren't in your declared schema.`, "This is usually one of three things:", " (a) You're iterating on the schema — the table file got renamed, moved, or temporarily commented out.", " → Restore the declaration. Your data stays intact.", " (b) You want to KEEP the table but NOT manage it with Voltro (a leftover from another tool —", " a Strapi/Rails/legacy migration artifact, or an externally-owned table).", ` → Set \`VOLTRO_DB_IGNORE_TABLES=${t.filter((e) => e.op.kind === "drop-table").map((e) => "table" in e.op ? e.op.table : "").filter(Boolean).join(",")}\` (comma-separated).`, " The planner excludes those tables from the diff entirely — it never drops them, and they", " stop blocking your OTHER (additive) changes. This is the fix when a leftover freezes your schema.", " (c) You actually want to drop the table.", " → Add it to your declared set + chain `.dropped()` on it, OR", " write a file-based migration in `migrations/` that DROPs it, OR", " set `VOLTRO_DESTRUCTIVE_OK=1` for ONE `voltro dev` run (acknowledges data loss).", " Soft-drop: set `VOLTRO_SOFT_DROP=1` alongside DESTRUCTIVE_OK to RENAME instead of DROP", " (data survives as `<name>__dropped_<ts>` for ~7d; restorable via `voltro db restore-snapshot`)."), r > 0 && s.push(`${r} required column(s) need a backfill strategy before adding to a populated table.`, " → Annotate the column: `.backfill(sql`<expr>`)` or `.backfill(row => <fn>)` or `.default(<value>)`.", " Existing rows get the backfill value; the column then lands as NOT NULL."), i > 0 && s.push(`${i} column(s) are missing from your declared schema.`, " → If intentional: chain `.dropped()` on the column slot in the table declaration.", " → If a typo: restore the field. Data survives until you drop it."), a > 0 && s.push(`${a} column(s) look like renames (one column gone + a new one of same shape appeared).`, " → On the NEW column add `.renamedFrom('<oldName>')` so the planner emits RENAME instead of DROP+ADD."), s.push("", "See `voltro db plan` for the full machine-readable diff."), o + "\n" + s.join("\n");
2395
2404
  }
2396
2405
  }
2397
- }, sr = /* @__PURE__ */ new Set([
2406
+ }, cr = /* @__PURE__ */ new Set([
2398
2407
  "node_modules",
2399
2408
  "dist",
2400
2409
  "build",
@@ -2403,11 +2412,11 @@ BEGIN
2403
2412
  ".next",
2404
2413
  ".git",
2405
2414
  ".framework"
2406
- ]), cr = async (e) => {
2415
+ ]), lr = async (e) => {
2407
2416
  let t = [], n = async (e) => {
2408
2417
  let r = await h.readdir(e, { withFileTypes: !0 }).catch(() => []);
2409
2418
  for (let i of r) {
2410
- if (sr.has(i.name)) continue;
2419
+ if (cr.has(i.name)) continue;
2411
2420
  let r = g(e, i.name);
2412
2421
  i.isDirectory() ? await n(r) : i.isFile() && c.test(i.name) && t.push(r);
2413
2422
  }
@@ -2416,7 +2425,7 @@ BEGIN
2416
2425
  let n = e.split("/").pop() ?? e, r = t.split("/").pop() ?? t;
2417
2426
  return n.localeCompare(r);
2418
2427
  });
2419
- }, lr = async (e) => {
2428
+ }, ur = async (e) => {
2420
2429
  let t = [];
2421
2430
  for (let n of e) {
2422
2431
  let e = (await import(ee(n).href)).default;
@@ -2430,11 +2439,11 @@ BEGIN
2430
2439
  });
2431
2440
  }
2432
2441
  return t;
2433
- }, ur = (e) => e`
2442
+ }, dr = (e) => e`
2434
2443
  SELECT ${e("id")}
2435
2444
  FROM ${e("_voltro_migration_plans")}
2436
2445
  WHERE ${e("source")} = 'file'
2437
- `.pipe(d.map((e) => e.map((e) => e.id)), d.catchAll(() => d.succeed([]))), dr = (e, t, n, r) => d.gen(function* () {
2446
+ `.pipe(d.map((e) => e.map((e) => e.id)), d.catchAll(() => d.succeed([]))), fr = (e, t, n, r) => d.gen(function* () {
2438
2447
  let i = Date.now(), a = (/* @__PURE__ */ new Date()).toISOString(), o = {
2439
2448
  sql: e,
2440
2449
  log: {
@@ -2467,9 +2476,9 @@ BEGIN
2467
2476
  ${r}, ${n}, ${"file"},
2468
2477
  ${c}, ${a}, ${t.migration.description})
2469
2478
  `, { durationMs: c };
2470
- }), fr = (e, t) => d.gen(function* () {
2479
+ }), pr = (e, t) => d.gen(function* () {
2471
2480
  let n = yield* d.tryPromise({
2472
- try: () => cr(g(t.projectRoot, "migrations")),
2481
+ try: () => lr(g(t.projectRoot, "migrations")),
2473
2482
  catch: (e) => e
2474
2483
  });
2475
2484
  if (n.length === 0) return {
@@ -2477,14 +2486,14 @@ BEGIN
2477
2486
  skipped: []
2478
2487
  };
2479
2488
  let r = yield* d.tryPromise({
2480
- try: () => lr(n),
2489
+ try: () => ur(n),
2481
2490
  catch: (e) => e
2482
2491
  });
2483
2492
  if (r.length === 0) return {
2484
2493
  applied: [],
2485
2494
  skipped: []
2486
2495
  };
2487
- let i = new Set(yield* ur(e)), a = r.filter((e) => !i.has(e.migration.id)), o = r.filter((e) => i.has(e.migration.id)).map((e) => e.migration.id);
2496
+ let i = new Set(yield* dr(e)), a = r.filter((e) => !i.has(e.migration.id)), o = r.filter((e) => i.has(e.migration.id)).map((e) => e.migration.id);
2488
2497
  if (a.length === 0) return {
2489
2498
  applied: [],
2490
2499
  skipped: o
@@ -2498,7 +2507,7 @@ BEGIN
2498
2507
  id: n.migration.id,
2499
2508
  file: n.file
2500
2509
  }));
2501
- let { durationMs: r } = yield* dr(e, n, t.env, t.appliedBy);
2510
+ let { durationMs: r } = yield* fr(e, n, t.env, t.appliedBy);
2502
2511
  s.push({
2503
2512
  id: n.migration.id,
2504
2513
  durationMs: r
@@ -2515,18 +2524,18 @@ BEGIN
2515
2524
  applied: s,
2516
2525
  skipped: o
2517
2526
  };
2518
- }), pr = (e, t) => e`
2527
+ }), mr = (e, t) => e`
2519
2528
  SELECT ${e("id")}
2520
2529
  FROM ${e("_voltro_migration_plans")}
2521
2530
  WHERE ${e("id")} = ${t} AND ${e("source")} = 'file'
2522
2531
  LIMIT 1
2523
- `, mr = (e, t) => d.gen(function* () {
2524
- if (!(yield* pr(e, t.id))[0]) return yield* d.fail(/* @__PURE__ */ Error(`rollback: ${t.id} not found in _voltro_migration_plans (file source)`));
2532
+ `, hr = (e, t) => d.gen(function* () {
2533
+ if (!(yield* mr(e, t.id))[0]) return yield* d.fail(/* @__PURE__ */ Error(`rollback: ${t.id} not found in _voltro_migration_plans (file source)`));
2525
2534
  let n = yield* d.tryPromise({
2526
- try: () => cr(g(t.projectRoot, "migrations")),
2535
+ try: () => lr(g(t.projectRoot, "migrations")),
2527
2536
  catch: (e) => e
2528
2537
  }), r = (yield* d.tryPromise({
2529
- try: () => lr(n),
2538
+ try: () => ur(n),
2530
2539
  catch: (e) => e
2531
2540
  })).find((e) => e.migration.id === t.id);
2532
2541
  if (!r) return yield* d.fail(/* @__PURE__ */ Error(`rollback: file for migration ${t.id} not found on disk under ${t.projectRoot}/migrations/`));
@@ -2559,7 +2568,7 @@ BEGIN
2559
2568
  } finally {
2560
2569
  yield* T(e).pipe(d.orDie);
2561
2570
  }
2562
- }), hr = (e) => d.gen(function* () {
2571
+ }), gr = (e) => d.gen(function* () {
2563
2572
  let t = yield* p.SqlClient, n = yield* t`
2564
2573
  SELECT id, fingerprint, appliedAt FROM _voltro_migration_plans
2565
2574
  WHERE source = 'auto-diff'
@@ -2604,10 +2613,10 @@ BEGIN
2604
2613
  snapshotFingerprint: e.fingerprint,
2605
2614
  snapshotId: i
2606
2615
  };
2607
- }), gr = (e, ...t) => ({
2616
+ }), _r = (e, ...t) => ({
2608
2617
  _tag: "RawSqlFragment",
2609
2618
  strings: [...e],
2610
2619
  values: [...t]
2611
- }), _r = (e) => typeof e == "object" && !!e && e._tag === "RawSqlFragment";
2620
+ }), vr = (e) => typeof e == "object" && !!e && e._tag === "RawSqlFragment";
2612
2621
  //#endregion
2613
- export { c as FILE_MIGRATION_PATTERN, C as VOLTRO_MIGRATION_LOCK_KEY, ut as _internalCmp, w as acquireMigrationLock, Ke as applyNamespacedSchema, Yn as applyPlan, Be as applySchema, Lt as chunkTables, P as declaredSnapshot, ve as defaultClause, O as defaultJsonClause, or as describeOutcome, Yt as emitDropColumnDdl, Xt as emitDropColumnDdlMysql, ze as emitFrameworkBootstrapSql, Le as emitNamespaceProvisionDdl, Re as emitNamespacedSchemaSql, j as emitSchemaSql, M as fingerprintSchema, rr as ignoreTablesFromEnv, qt as introspectSchema, a as isFileMigration, _r as isRawSqlFragment, Mt as mapPgType, o as migration, k as numericIdSql, jt as parseEnumCheck, At as planMigrations, qe as provisionTenantNamespace, T as releaseMigrationLock, sn as renderColumnMysql, rn as renderColumnPg, mr as rollbackFileBasedMigration, fr as runFileBasedMigrations, Je as runFrameworkBootstrap, Ge as runMigrate, ar as runPlannedMigrations, N as shortFingerprint, gr as sql, D as sqlType, hr as squashMigrationPlans, ge as withMigrationLock };
2622
+ export { c as FILE_MIGRATION_PATTERN, C as VOLTRO_MIGRATION_LOCK_KEY, ut as _internalCmp, w as acquireMigrationLock, Ke as applyNamespacedSchema, Yn as applyPlan, Be as applySchema, Lt as chunkTables, P as declaredSnapshot, ve as defaultClause, O as defaultJsonClause, sr as describeOutcome, Yt as emitDropColumnDdl, Xt as emitDropColumnDdlMysql, ze as emitFrameworkBootstrapSql, Le as emitNamespaceProvisionDdl, Re as emitNamespacedSchemaSql, j as emitSchemaSql, M as fingerprintSchema, rr as ignoreTablesFromEnv, qt as introspectSchema, a as isFileMigration, vr as isRawSqlFragment, Mt as mapPgType, o as migration, k as numericIdSql, jt as parseEnumCheck, At as planMigrations, qe as provisionTenantNamespace, T as releaseMigrationLock, sn as renderColumnMysql, rn as renderColumnPg, hr as rollbackFileBasedMigration, pr as runFileBasedMigrations, Je as runFrameworkBootstrap, Ge as runMigrate, or as runPlannedMigrations, N as shortFingerprint, _r as sql, D as sqlType, gr as squashMigrationPlans, ir as unblockLossy, ge as withMigrationLock };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/database",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Browser-safe schema DSL, query builder, and cross-dialect migration planner for Voltro — one schema, every SQL backend.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@effect/sql": "^0.51.1",
41
- "@voltro/logger": "0.5.0",
41
+ "@voltro/logger": "0.7.0",
42
42
  "typeid-js": "^1.2.0",
43
43
  "ulidx": "^2.4.1"
44
44
  },