@lunora/cli 1.0.0-alpha.49 → 1.0.0-alpha.490

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.
Files changed (147) hide show
  1. package/LICENSE.md +234 -104
  2. package/README.md +6 -4
  3. package/dist/bin.mjs +3 -10
  4. package/dist/cli.d.mts +31 -0
  5. package/dist/cli.d.ts +31 -0
  6. package/dist/cli.mjs +1 -0
  7. package/dist/index.d.mts +1181 -427
  8. package/dist/index.d.ts +1181 -427
  9. package/dist/index.mjs +1 -19
  10. package/dist/packem_chunks/cli.mjs +2 -0
  11. package/dist/packem_chunks/dispatch.mjs +1 -0
  12. package/dist/packem_chunks/handler.mjs +61 -157
  13. package/dist/packem_chunks/handler10.mjs +1 -16
  14. package/dist/packem_chunks/handler11.mjs +1 -22
  15. package/dist/packem_chunks/handler12.mjs +1 -192
  16. package/dist/packem_chunks/handler13.mjs +3 -131
  17. package/dist/packem_chunks/handler14.mjs +1 -65
  18. package/dist/packem_chunks/handler15.mjs +1 -58
  19. package/dist/packem_chunks/handler16.mjs +1 -80
  20. package/dist/packem_chunks/handler17.mjs +1 -43
  21. package/dist/packem_chunks/handler18.mjs +1 -105
  22. package/dist/packem_chunks/handler19.mjs +1 -170
  23. package/dist/packem_chunks/handler2.mjs +1 -114
  24. package/dist/packem_chunks/handler20.mjs +1 -94
  25. package/dist/packem_chunks/handler21.mjs +7 -94
  26. package/dist/packem_chunks/handler22.mjs +1 -0
  27. package/dist/packem_chunks/handler23.mjs +3 -0
  28. package/dist/packem_chunks/handler24.mjs +3 -0
  29. package/dist/packem_chunks/handler25.mjs +2 -0
  30. package/dist/packem_chunks/handler26.mjs +1 -0
  31. package/dist/packem_chunks/handler27.mjs +2 -0
  32. package/dist/packem_chunks/handler28.mjs +99 -0
  33. package/dist/packem_chunks/handler29.mjs +3 -0
  34. package/dist/packem_chunks/handler3.mjs +1 -204
  35. package/dist/packem_chunks/handler30.mjs +15 -0
  36. package/dist/packem_chunks/handler31.mjs +1 -0
  37. package/dist/packem_chunks/handler32.mjs +1 -0
  38. package/dist/packem_chunks/handler4.mjs +1 -33
  39. package/dist/packem_chunks/handler5.mjs +3 -49
  40. package/dist/packem_chunks/handler6.mjs +1 -91
  41. package/dist/packem_chunks/handler7.mjs +1 -42
  42. package/dist/packem_chunks/handler8.mjs +1 -174
  43. package/dist/packem_chunks/handler9.mjs +2 -315
  44. package/dist/packem_chunks/runCodegenCommand.mjs +4 -52
  45. package/dist/packem_chunks/runDevCommand.mjs +8 -0
  46. package/dist/packem_chunks/runInitCommand.mjs +423 -1452
  47. package/dist/packem_chunks/runResetCommand.mjs +1 -41
  48. package/dist/packem_chunks/runRpcCommand.mjs +1 -68
  49. package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck9tABfr.mjs +1 -0
  50. package/dist/packem_shared/EXIT_CODE-DQVhSE-1.mjs +1 -0
  51. package/dist/packem_shared/admin-token-VdUnvnKW.mjs +1 -0
  52. package/dist/packem_shared/admin-url-DJB8P6Jh.mjs +1 -0
  53. package/dist/packem_shared/api-spec-lYyq4vL6.mjs +1 -0
  54. package/dist/packem_shared/binding-manifest-file-BZyQFSTF.mjs +2 -0
  55. package/dist/packem_shared/buildRegistryIndex-Dc8D8AR6.mjs +1 -0
  56. package/dist/packem_shared/cap-error-body-YBKO32BF.mjs +1 -0
  57. package/dist/packem_shared/catalog-CahzmDLV.mjs +1 -0
  58. package/dist/packem_shared/cli-manifest-DoQ_3bzs.mjs +1 -0
  59. package/dist/packem_shared/cloudflare-credentials-BVmZnDZk.mjs +1 -0
  60. package/dist/packem_shared/codegen-error-AmH54ofi.mjs +3 -0
  61. package/dist/packem_shared/command-BpMv2n0o.mjs +1 -0
  62. package/dist/packem_shared/commands-CkuCMUP_.mjs +13 -0
  63. package/dist/packem_shared/createLogger-4TujevyB.mjs +2 -0
  64. package/dist/packem_shared/createRecordingSpawner-DchLi0gR.mjs +1 -0
  65. package/dist/packem_shared/deploy-target-resolve-BTCW1Iv4.mjs +1 -0
  66. package/dist/packem_shared/diffSnapshots-DcE-jQUz.mjs +9 -0
  67. package/dist/packem_shared/docker-DpVxvYpL.mjs +1 -0
  68. package/dist/packem_shared/export-rows-EzdNDYpa.mjs +3 -0
  69. package/dist/packem_shared/free-port-DjbFhaaT.mjs +1 -0
  70. package/dist/packem_shared/health-probe-By9T5x2y.mjs +13 -0
  71. package/dist/packem_shared/import-3Y59Ry9F.mjs +13 -0
  72. package/dist/packem_shared/index-oHZBGBwB.mjs +1 -0
  73. package/dist/packem_shared/insertSchemaExtension-CbTH3lCQ.mjs +10 -0
  74. package/dist/packem_shared/js-literal-FWLi94Lh.mjs +1 -0
  75. package/dist/packem_shared/lint-ignore-report-DKZagpqk.mjs +2 -0
  76. package/dist/packem_shared/mcp-config-file-GR1Y_CVw.mjs +2 -0
  77. package/dist/packem_shared/open-url-EnKy--w-.mjs +1 -0
  78. package/dist/packem_shared/output-format-DWe_tkTR.mjs +2 -0
  79. package/dist/packem_shared/parseManifest-CwPTKdtS.mjs +1 -0
  80. package/dist/packem_shared/path-containment-CgxYZggb.mjs +1 -0
  81. package/dist/packem_shared/plan-BF2E-dq4.mjs +5 -0
  82. package/dist/packem_shared/planDevCommand-C8QHZzEJ.mjs +1 -0
  83. package/dist/packem_shared/platform-diagnostics-Cn2g6Jh-.mjs +4 -0
  84. package/dist/packem_shared/prompt-cancelled-BvsNxg_Q.mjs +1 -0
  85. package/dist/packem_shared/render-lunora-error-JsLelWwR.mjs +2 -0
  86. package/dist/packem_shared/resolve-Bdc9KEUv.mjs +5 -0
  87. package/dist/packem_shared/resolve-target-C_ZloTvd.mjs +1 -0
  88. package/dist/packem_shared/runAddCommand-gThKW-ZB.mjs +1 -0
  89. package/dist/packem_shared/runDeployCommand-DhgZz8Sj.mjs +1 -0
  90. package/dist/packem_shared/runExportCommand-_PtkJJDT.mjs +5 -0
  91. package/dist/packem_shared/runMigrateGenerateCommand-DHdSMKbu.mjs +11 -0
  92. package/dist/packem_shared/schemaIrToSnapshot-CbWOjany.mjs +1 -0
  93. package/dist/packem_shared/shared-CV7ESvuj.mjs +2 -0
  94. package/dist/packem_shared/sql-u1MSmopg.mjs +3 -0
  95. package/dist/packem_shared/storage-3GOIFVZm.mjs +1 -0
  96. package/dist/packem_shared/tui-prompts-B3YwUhGw.mjs +4 -0
  97. package/dist/packem_shared/variables-CPYyJtsL.mjs +1 -0
  98. package/dist/packem_shared/vectorize-metadata-DK61NmVx.mjs +2 -0
  99. package/dist/packem_shared/wrangler-name-Dsk5K1f-.mjs +1 -0
  100. package/dist/packem_shared/wrangler-secrets-C9siMBwN.mjs +1 -0
  101. package/package.json +46 -20
  102. package/skills/README.md +36 -17
  103. package/skills/lunora/SKILL.md +158 -65
  104. package/skills/lunora-cloudflare-audit/SKILL.md +164 -0
  105. package/skills/lunora-create-package/SKILL.md +98 -84
  106. package/skills/lunora-deploy/SKILL.md +212 -102
  107. package/skills/lunora-functions/SKILL.md +367 -60
  108. package/skills/lunora-migration-helper/SKILL.md +194 -149
  109. package/skills/lunora-performance-audit/SKILL.md +137 -95
  110. package/skills/lunora-quickstart/SKILL.md +136 -136
  111. package/skills/lunora-realtime/SKILL.md +219 -97
  112. package/skills/lunora-setup-auth/SKILL.md +203 -101
  113. package/skills/lunora-setup-hyperdrive/SKILL.md +153 -110
  114. package/skills/lunora-setup-hyperdrive-global/SKILL.md +152 -94
  115. package/skills/lunora-setup-mail/SKILL.md +97 -113
  116. package/skills/lunora-setup-scheduler/SKILL.md +133 -97
  117. package/skills/lunora-setup-storage/SKILL.md +189 -95
  118. package/dist/packem_chunks/planDevCommand.mjs +0 -541
  119. package/dist/packem_chunks/runDeployCommand.mjs +0 -594
  120. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +0 -397
  121. package/dist/packem_shared/COMMANDS-B0ftFD_3.mjs +0 -948
  122. package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs +0 -244
  123. package/dist/packem_shared/admin-url-4UzT-CI4.mjs +0 -19
  124. package/dist/packem_shared/api-spec-CtA6ilu4.mjs +0 -13
  125. package/dist/packem_shared/buildRegistryIndex-BcYe607_.mjs +0 -38
  126. package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +0 -31
  127. package/dist/packem_shared/command-D3lB_4Az.mjs +0 -19
  128. package/dist/packem_shared/commands-B-gR09Z_.mjs +0 -845
  129. package/dist/packem_shared/createLogger-B40gPzQo.mjs +0 -78
  130. package/dist/packem_shared/createRecordingSpawner-DxI3mebw.mjs +0 -43
  131. package/dist/packem_shared/detect-package-manager-DYp7n3mJ.mjs +0 -61
  132. package/dist/packem_shared/diffSnapshots-BeDvvNiF.mjs +0 -161
  133. package/dist/packem_shared/docker-hMQ97KSQ.mjs +0 -21
  134. package/dist/packem_shared/insertSchemaExtension-DAqbfr9Z.mjs +0 -64
  135. package/dist/packem_shared/open-url-Dfq6fAyT.mjs +0 -41
  136. package/dist/packem_shared/output-format-wUvAN6AL.mjs +0 -17
  137. package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +0 -56
  138. package/dist/packem_shared/parseManifest--vZf2FY1.mjs +0 -94
  139. package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +0 -9
  140. package/dist/packem_shared/resolve-target-qbsJ_5sF.mjs +0 -16
  141. package/dist/packem_shared/runAddCommand-bnY6-HKb.mjs +0 -4
  142. package/dist/packem_shared/schema-drift-gate-BtBt0as0.mjs +0 -79
  143. package/dist/packem_shared/schemaIrToSnapshot-DdsljJT-.mjs +0 -43
  144. package/dist/packem_shared/storage-BIsph-Vk.mjs +0 -84
  145. package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +0 -658
  146. package/dist/packem_shared/wrangler-name-cy4yhm9j.mjs +0 -12
  147. package/dist/packem_shared/wrangler-secrets-P2_ZUR-k.mjs +0 -47
@@ -1,194 +1,239 @@
1
1
  ---
2
2
  name: lunora-migration-helper
3
- description: Plans Lunora schema and data migrations with widen-migrate-narrow. Use for
4
- breaking schema changes, backfills, table reshaping, online data migrations
5
- (`defineMigration` + `lunora migrate up`), the `.global()` D1 SQL flow, and the
6
- pre-deploy schema-drift gate.
3
+ description: Plans and runs Lunora schema and data migrations with the widen → migrate → narrow pattern. Covers online data migrations (`defineMigration` in `lunora/migrations.ts`, `lunora migrate create | up | down | status`), what the pre-deploy schema-drift gate treats as breaking and how a migration covers it, `.global()` table DDL on D1 (`lunora migrate generate`) and Hyperdrive, moving a dataset with `lunora migrate d1-to-hyperdrive` (and back with `hyperdrive-to-d1`), and re-keying a `.shardBy()` table. Use when the user wants to make a field required, rename or drop a field or table, change a field's type, add a unique index to a populated table, backfill data, switch a table between root / `.shardBy()` / `.global()`, or when `lunora deploy` reports "schema drift gate blocked".
7
4
  ---
8
5
 
9
6
  # Lunora Migration Helper
10
7
 
11
- Safely change a Lunora schema and migrate data when making breaking changes.
8
+ Change a Lunora schema that already has data in it without breaking the deploy
9
+ or losing rows.
10
+
11
+ Not needed for a greenfield schema with no data, a new optional field, a new
12
+ table, or a new non-unique index. Those are additive and deploy as-is.
13
+
14
+ ## Which storage layer are you migrating?
15
+
16
+ Check the table's modifier in `lunora/schema.ts` first; the tools differ.
17
+
18
+ | Table | Where rows live | Structural DDL | Data backfill |
19
+ | ------------------------------------ | ------------------------------------ | -------------------------------------------------------------- | -------------------------------------- |
20
+ | default (root) or `.shardBy(key)` | SQLite in the root / per-key ShardDO | Applied by the runtime from the schema | `defineMigration` + `lunora migrate` |
21
+ | `.global()` | D1 (`DB` binding) | Additive DDL auto-provisioned; destructive SQL applied by hand | SQL `UPDATE` via `wrangler d1 execute` |
22
+ | `.global({ backend: "hyperdrive" })` | Postgres/MySQL via Hyperdrive | Additive DDL auto-provisioned; no `migrate generate` support | SQL `UPDATE` against the database |
23
+
24
+ ## The pattern: widen, migrate, narrow
25
+
26
+ The schema-drift gate blocks a breaking change; one that needs a backfill is
27
+ let through once a new migration on that table covers it (the rest are listed
28
+ below). Stage every such change across deploys:
29
+
30
+ 1. **Widen.** Make the schema accept both shapes: add the new field as
31
+ `v.optional(...)`, keep the old one. Make reads handle both, and start
32
+ writing the new shape on every insert/patch so rows created during the
33
+ migration aren't missed. Deploy.
34
+ 2. **Migrate.** Backfill existing rows with a `defineMigration`, run it with
35
+ `lunora migrate up`, and confirm with `lunora migrate status`. On a
36
+ `.global()` table, backfill with SQL instead (see below).
37
+ 3. **Narrow.** Make the field required / drop the old one, remove the
38
+ both-shapes code. Deploy.
39
+
40
+ Prefer adding a new field to changing an existing field's type: it rolls back
41
+ cleanly. Keep a deprecated field as `v.optional` with a `// deprecated:` comment
42
+ until nothing reads it.
43
+
44
+ ## What the drift gate considers breaking
45
+
46
+ The gate (run by `lunora deploy`, `prepare`, `build` and, read-only, `verify`)
47
+ diffs the schema against the committed `lunora/.lunora-schema.json`.
48
+
49
+ - **Safe:** new table, new optional field, new non-unique index or relation, a
50
+ required field made optional.
51
+ - **Breaking, fixable by a migration:** dropped field or table, changed field
52
+ type, optional → required, new required field on an existing table, a new
53
+ unique index or `.unique()` column (existing duplicates would make the shard
54
+ fail to open).
55
+ - **Moves data, blocks until a migration accompanies it:** changing a table's
56
+ shard mode (root ↔ `.shardBy()` ↔ `.global()`, or the `.global()` backend), turning
57
+ on `.memory()`, or changing the jurisdiction. Move the rows with export → import,
58
+ then declare a migration whose literal `covers` names the change
59
+ (`covers: { table: "users", shardMode: "shardBy:tenantId" }`, `{ table, memory: true }`
60
+ or `{ jurisdiction: "eu" }`). An unrelated migration clears nothing. Where a table is
61
+ involved its `up` must rewrite rows (give each what its new home needs, e.g. its
62
+ shard key); one that returns the document unchanged covers nothing.
63
+ - **Also blocks until a migration names it:** a TTL added, moved to another column or
64
+ shortened (`covers: { table, ttl: { field, after } }`), because the next sweep deletes
65
+ every row already past the cutoff; the blocked message counts those rows when
66
+ `lunora deploy` can read the worker. Refresh the column on the rows that must survive
67
+ in `up`.
68
+ - **Never blocks, may warn:** a dropped table (its rows stay in place, unreachable),
69
+ a dropped relation, a TTL lengthened or removed (every row stays), `.commitOrdered()`
70
+ turned off, and a dropped index. A dropped index is refused only while a function
71
+ still names it through `.withIndex("name")`; point the query elsewhere first.
72
+
73
+ A migration covers a change only if it is new since the baseline, its `table`
74
+ is a string literal naming the affected table, and its `up` does more than return
75
+ the document (the `migrate create` placeholder covers nothing). The baseline advances
76
+ only after a successful deploy.
77
+
78
+ ## Online data migrations
79
+
80
+ A migration transforms one document at a time, runs inside each shard's Durable
81
+ Object in keyset batches, and is resumable: per-shard progress is kept in a
82
+ reserved `__lunora_migrations` table.
83
+
84
+ Scaffold it rather than writing the file from scratch:
12
85
 
13
- ## When to Use
86
+ ```bash
87
+ lunora migrate create backfill-display-name --table users
88
+ ```
14
89
 
15
- - Adding required fields to existing tables.
16
- - Changing field types or structure.
17
- - Splitting/merging tables, renaming/removing fields.
18
- - Reshaping `.global()` (D1-backed) tables.
90
+ This appends to `lunora/migrations.ts`. Codegen only discovers
91
+ `export const x = defineMigration({...})` with a literal `id`. An
92
+ `export default` is silently skipped, and `lunora migrate up` then can't find it.
19
93
 
20
- ## When Not to Use
94
+ ```ts
95
+ // lunora/migrations.ts
96
+ import { defineMigration } from "@lunora/server"; // or "lunorash/server"
21
97
 
22
- - Greenfield schema with no data at rest.
23
- - Adding **optional** fields that need no backfill.
24
- - Adding new tables or indexes with no correctness concern.
98
+ export const backfillDisplayName = defineMigration({
99
+ id: "backfill-display-name",
100
+ table: "users",
101
+ batchSize: 200, // optional
102
+ up: (doc) => {
103
+ if (typeof doc.displayName === "string") {
104
+ return undefined; // skip: not rewritten, not counted as changed
105
+ }
25
106
 
26
- ## Two Storage Layers — Know Which You Are Migrating
107
+ return { ...doc, displayName: doc.name ?? "Anonymous" };
108
+ },
109
+ // optional; applied by `lunora migrate down`
110
+ down: (doc) => {
111
+ const { displayName, ...rest } = doc;
27
112
 
28
- Lunora tables live in one of two backends, and they migrate differently:
113
+ return rest;
114
+ },
115
+ });
116
+ ```
29
117
 
30
- - **ShardDO SQLite (default `root`, and `.shardBy(key)` tables).** State lives in
31
- the per-app / per-shard Durable Object. Data is reshaped with **online data
32
- migrations** — `defineMigration` declarations run by `lunora migrate up`,
33
- resumable per shard.
34
- - **`.global()` tables (D1).** Replicated to D1 for cross-region reads. Their
35
- structural DDL gets versioned **SQL migrations** via `lunora migrate generate`,
36
- applied by `@lunora/d1`'s runner at deploy time.
118
+ The runner keeps each row's original `_id` and `_creationTime`.
37
119
 
38
- A breaking change to a `.global()` table needs a generated SQL migration; a data
39
- backfill (either layer) is an online `defineMigration`. Both follow the same
40
- **widen → migrate → narrow** discipline.
120
+ ### Reading another table
41
121
 
42
- ## Key Principle: Widen, Migrate, Narrow
122
+ The transform's second argument is a shard-scoped, read-only `ctx.db`
123
+ (`get`, `findFirst`, `findMany`, `count`), and the transform may be `async`.
124
+ That covers the common case of copying a field down from a parent:
43
125
 
44
- The schema-drift gate (and D1 itself) will not let a breaking change deploy
45
- without an accompanying migration. So every breaking change is staged:
126
+ ```ts
127
+ up: async (doc, ctx) => {
128
+ const thread = await ctx.db.get("threads", String(doc.threadId));
46
129
 
47
- 1. **Widen** — make the schema accept both old and new shapes (add the new field
48
- as `v.optional`, keep the old one). Update reads to handle both; start writing
49
- the new shape for new rows. Deploy.
50
- 2. **Migrate** — backfill existing rows to the new shape (an online
51
- `defineMigration` run with `lunora migrate up`; plus `lunora migrate generate`
52
- for `.global()` structural DDL). Verify completeness with `lunora migrate
53
- status`.
54
- 3. **Narrow** — make the field required / drop the old field, remove the
55
- both-shapes read code. Deploy.
130
+ return thread ? { ...doc, userId: thread.userId } : undefined;
131
+ },
132
+ ```
56
133
 
57
- ### Prefer new fields over changing types
134
+ To change rows in a second table, write a second migration over that table.
58
135
 
59
- When changing a field's shape, add a new field rather than mutating the existing
60
- one — safer transition, easier rollback.
136
+ ### A shard key can't be backfilled
61
137
 
62
- ### Don't delete data prematurely
138
+ If the field is the table's `.shardBy()` key, a migration can't help: a row with
139
+ no shard key belongs to no shard (so the runner never sees it), and setting the
140
+ key would mean moving the row to another Durable Object. Re-key with
141
+ `lunora export` → rewrite the NDJSON → `lunora import`. A rewrite that adds, drops or splits rows must also update the last line, the end marker `{"end":true,"rows":N}`, to the new row count, or the import refuses the file. Ids are preserved, so
142
+ foreign keys survive. The same route applies to any shard-mode change.
63
143
 
64
- Prefer deprecating: mark the old field `v.optional` with a `// deprecated:` code
65
- comment explaining why it existed. Delete only once you are sure nothing reads
66
- it.
144
+ ### Run it
67
145
 
68
- ## Safe Changes (No Migration Needed)
146
+ ```bash
147
+ lunora migrate up backfill-display-name --dry-run # preview, rewrites nothing
148
+ lunora migrate up backfill-display-name # against the dev worker (localhost:8787)
149
+ lunora migrate status backfill-display-name # per-shard progress
150
+ lunora migrate down backfill-display-name # only if `down` is defined
151
+ ```
69
152
 
70
- ```ts
71
- // Adding an optional field — safe.
72
- users: defineTable({
73
- name: v.string(),
74
- bio: v.optional(v.string()),
75
- });
153
+ Options: `--batch-size <n>`, `--steps <n>` (cap batches per run), `--format json`.
76
154
 
77
- // Adding a new table — safe.
78
- posts: defineTable({ userId: v.id("users"), title: v.string() }).index("by_user", ["userId"]);
155
+ Production runs rewrite live data, so they require an explicit confirmation:
79
156
 
80
- // Adding an index — safe.
81
- users: defineTable({ name: v.string(), email: v.string() }).index("by_email", ["email"]);
157
+ ```bash
158
+ LUNORA_ADMIN_TOKEN=… lunora migrate up backfill-display-name --prod --url https://app.example.com --yes
82
159
  ```
83
160
 
84
- ## Online Data Migrations (the backfill workhorse)
161
+ `--url` can be omitted when the checkout is linked to production. Alternatively,
162
+ `lunora deploy --migrate --migrate-yes --migrate-url <url>` runs every declared
163
+ migration right after the deploy (`up` is idempotent, so finished ones no-op).
85
164
 
86
- For backfilling/reshaping rows, declare a migration with `defineMigration` from
87
- `@lunora/server`. It transforms one document at a time, runs **inside each
88
- shard's** Durable Object in keyset batches, and is **resumable** — per-shard
89
- progress is tracked in a reserved `__lunora_migrations` table, so an interrupted
90
- run resumes where it stopped. Codegen discovers declarations and emits them into
91
- the registry the DO and CLI look up by `id`.
165
+ ## `.global()` tables
92
166
 
93
- ```ts
94
- // lunora/migrations/backfill-display-name.ts
95
- import { defineMigration } from "@lunora/server";
167
+ The runtime provisions `.global()` tables itself on first use, on both D1 and
168
+ Hyperdrive: `CREATE TABLE IF NOT EXISTS`, additive `ADD COLUMN`, and declared
169
+ indexes. Additive changes therefore need no DDL step.
96
170
 
97
- export default defineMigration({
98
- id: "backfill-display-name",
99
- table: "users",
100
- batchSize: 200, // optional; defaults to the runner's batch size
101
- up: (doc) => {
102
- if (typeof doc.displayName === "string") {
103
- return; // already migrated — return undefined to skip (not counted as changed)
104
- }
105
- return { ...doc, displayName: doc.name ?? "Anonymous" };
106
- },
107
- // optional reverse transform applied by `lunora migrate down`
108
- down: (doc) => {
109
- const { displayName, ...rest } = doc as Record<string, unknown>;
110
- return rest;
111
- },
112
- });
113
- ```
171
+ For **D1-backed** global tables, `lunora migrate generate` diffs the schema
172
+ against `lunora/migrations/.snapshot.json` and writes a reviewable
173
+ `lunora/migrations/<timestamp>_<name>.sql`:
114
174
 
115
- The transform must preserve row identity — the runner always keeps the original
116
- `_id` / `_creationTime`, so do not change them.
175
+ ```bash
176
+ lunora migrate generate --name add_user_status
177
+ ```
117
178
 
118
- ### Run it
179
+ No command applies that file, `lunora deploy` included. Its value is the
180
+ statements the runtime never issues (`DROP TABLE`, `DROP INDEX`) plus a comment
181
+ block of changes it couldn't generate (renames, backfilling `UPDATE`s), which you
182
+ write yourself. Apply it deliberately, after review:
119
183
 
120
184
  ```bash
121
- lunora migrate create backfill-display-name # scaffold the migration file
122
- lunora codegen # discover + register it
123
- lunora migrate up backfill-display-name --dry-run # preview, no rows rewritten
124
- lunora migrate up backfill-display-name # run across shards (keyset batches)
125
- lunora migrate status backfill-display-name # per-shard progress
126
- lunora migrate down backfill-display-name # revert (if `down` defined)
185
+ wrangler d1 execute DB --remote --file=lunora/migrations/<file>.sql
127
186
  ```
128
187
 
129
- Useful flags: `--batch-size <n>`, `--steps <n>` (cap batches this run), and
130
- `--prod --url <worker> --yes` to target production (with `LUNORA_ADMIN_TOKEN`).
188
+ The file is multi-statement, so it is not a `@lunora/d1` `Migration`. Commit
189
+ `.snapshot.json` with it, or the next diff won't see a dropped table.
131
190
 
132
- ## `.global()` (D1) Structural Migration Flow
191
+ Hyperdrive-backed tables are left out of `migrate generate` (it emits SQLite
192
+ DDL). Write destructive DDL for them by hand against your database. See
193
+ `lunora-setup-hyperdrive-global`.
133
194
 
134
- ```bash
135
- # 1. Edit lunora/schema.ts (widen: add the optional new field to the .global() table).
136
- lunora codegen
195
+ `defineMigration` does not reach `.global()` rows on either backend: it runs
196
+ inside each shard's Durable Object against that shard's SQLite, never D1 or
197
+ Hyperdrive. Backfill a global table with a SQL `UPDATE` (D1: `wrangler d1
198
+ execute`; Hyperdrive: your Postgres/MySQL client). The drift gate still keys on
199
+ a migration naming the table, so once the SQL backfill is done, deploy the
200
+ narrowing change with `--allow-schema-drift` rather than a no-op migration.
137
201
 
138
- # 2. Generate the SQL migration by diffing schema against the snapshot baseline.
139
- lunora migrate generate --name=add_user_status
202
+ ### Moving a dataset from D1 to Hyperdrive
140
203
 
141
- # Writes lunora/migrations/<timestamp>_add_user_status.sql and updates
142
- # lunora/migrations/.snapshot.json. Review the SQL before committing.
143
-
144
- # 3. Deploy — @lunora/d1's runner applies pending migrations.
145
- lunora deploy
204
+ ```bash
205
+ lunora migrate d1-to-hyperdrive --from-url https://old-worker --to-url https://new-worker
146
206
  ```
147
207
 
148
- `lunora migrate generate` only considers `.global()` tables (root/sharded tables
149
- are not D1-backed). Run it after each schema edit in the widen and narrow steps;
150
- backfill data with an online migration between them.
151
-
152
- ## The Schema-Drift Gate
153
-
154
- `lunora deploy` (and `verify` / `prepare`) run a **pre-deploy schema-drift gate**:
155
- it compares the committed structural baseline (`lunora/.lunora-schema.json`)
156
- against the snapshot codegen produced this run. Breaking drift **without an
157
- accompanying data migration blocks the deploy**. The baseline is only re-blessed
158
- _after_ the deploy succeeds, so a failed deploy never advances it past a change
159
- that never shipped.
160
-
161
- If the gate blocks you: that is the signal to stage the change (widen first) or
162
- add the migration — not to bypass it.
163
-
164
- ## Common Pitfalls
165
-
166
- 1. **Making a field required before backfilling.** The drift gate / D1 rejects
167
- the deploy because existing rows lack it. Widen first.
168
- 2. **Reshaping rows by hand instead of `defineMigration`.** A hand-rolled
169
- `internalMutation` that `.collect()`s a large table hits transaction limits
170
- and is not resumable. Use `defineMigration` — it batches and tracks per-shard
171
- progress.
172
- 3. **Not writing the new shape during the migration window.** Rows created mid-
173
- migration get missed, leaving unmigrated data after it "completes." Start
174
- dual-writing in the widen step.
175
- 4. **Skipping the dry run.** `lunora migrate up <id> --dry-run` validates the
176
- transform before it touches real rows.
177
- 5. **Deleting a field prematurely.** Deprecate with `v.optional` + a comment;
178
- delete only once nothing references it.
179
- 6. **Migrating the wrong layer.** A `.global()` structural change needs `lunora
180
- migrate generate` (SQL); a data backfill needs a `defineMigration`. Check the
181
- table's `.global()` / `.shardBy()` modifier first.
208
+ `--tables a,b` limits it to some tables, and `--out <path>` keeps the
209
+ intermediate NDJSON dump. `lunora migrate hyperdrive-to-d1` takes the same
210
+ flags and moves the data back, for a rollback. Both are insert-only by default:
211
+ a row whose `_id` already exists in the target is reported as a conflict and
212
+ left unchanged, so a rollback restores missing rows, not later edits. Add
213
+ `--replace` (with `--tables` naming the `.global()` tables; it is refused
214
+ without one) to overwrite those rows with the source's values, or keep the dump
215
+ with `--out` and reconcile by hand. This moves the data to another backend; any
216
+ reshaping still follows widen → migrate → narrow.
217
+
218
+ ## Common pitfalls
219
+
220
+ 1. **Making a field required before backfilling.** The gate blocks it unless a
221
+ new migration on that table is part of the same deploy. Widen first.
222
+ 2. **Backfilling with a hand-written mutation.** A mutation that `.collect()`s
223
+ a large table runs into transaction limits and can't resume.
224
+ `defineMigration` batches per shard and resumes.
225
+ 3. **Not dual-writing during the window.** Rows inserted mid-migration keep the
226
+ old shape. Write the new shape from the widen step on.
227
+ 4. **A non-literal `table`.** `table: TABLE_NAME` covers nothing in the gate.
228
+ 5. **Bypassing the gate.** `--allow-schema-drift` ships the shape for one run
229
+ without checking anything. `lunora deploy` reads the stored rows of the affected tables and
230
+ lets a change through only when every row already fits.
182
231
 
183
232
  ## Checklist
184
233
 
185
- - [ ] Identified the change and which layer it touches (ShardDO vs `.global()`).
186
- - [ ] Widened the schema to accept both shapes; `lunora codegen` clean.
187
- - [ ] Updated reads to handle both shapes; started writing the new shape.
188
- - [ ] Deployed the widened schema.
189
- - [ ] Authored a `defineMigration`; previewed with `lunora migrate up --dry-run`.
190
- - [ ] Ran `lunora migrate up`; `lunora migrate generate` + deploy for `.global()`
191
- structural changes.
192
- - [ ] Verified completion with `lunora migrate status`.
193
- - [ ] Narrowed the schema (required / drop old field); removed both-shapes code.
194
- - [ ] Deployed the final schema; schema-drift gate passed.
234
+ - [ ] Identified the layer (root / `.shardBy()` / `.global()` D1 / Hyperdrive).
235
+ - [ ] Widened the schema; reads handle both shapes; writes use the new shape. Deployed.
236
+ - [ ] `lunora migrate create <id> --table <t>`; transform filled in; `--dry-run` looks right.
237
+ - [ ] `lunora migrate up <id>` (prod: `--prod --yes`); `lunora migrate status <id>` complete on every shard.
238
+ - [ ] D1 destructive DDL: `migrate generate` output reviewed and applied with `wrangler d1 execute`.
239
+ - [ ] Narrowed the schema and removed both-shapes code; drift gate green on deploy.