@lunora/cli 1.0.0-alpha.48 → 1.0.0-alpha.480
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/LICENSE.md +33 -0
- package/README.md +3 -1
- package/dist/bin.mjs +3 -10
- package/dist/cli.d.mts +31 -0
- package/dist/cli.d.ts +31 -0
- package/dist/cli.mjs +1 -0
- package/dist/index.d.mts +1168 -416
- package/dist/index.d.ts +1168 -416
- package/dist/index.mjs +1 -19
- package/dist/packem_chunks/cli.mjs +2 -0
- package/dist/packem_chunks/dispatch.mjs +1 -0
- package/dist/packem_chunks/handler.mjs +61 -157
- package/dist/packem_chunks/handler10.mjs +2 -16
- package/dist/packem_chunks/handler11.mjs +1 -22
- package/dist/packem_chunks/handler12.mjs +1 -192
- package/dist/packem_chunks/handler13.mjs +1 -131
- package/dist/packem_chunks/handler14.mjs +3 -65
- package/dist/packem_chunks/handler15.mjs +1 -58
- package/dist/packem_chunks/handler16.mjs +1 -80
- package/dist/packem_chunks/handler17.mjs +1 -43
- package/dist/packem_chunks/handler18.mjs +1 -105
- package/dist/packem_chunks/handler19.mjs +1 -170
- package/dist/packem_chunks/handler2.mjs +1 -114
- package/dist/packem_chunks/handler20.mjs +1 -94
- package/dist/packem_chunks/handler21.mjs +7 -94
- package/dist/packem_chunks/handler22.mjs +1 -0
- package/dist/packem_chunks/handler23.mjs +3 -0
- package/dist/packem_chunks/handler24.mjs +3 -0
- package/dist/packem_chunks/handler25.mjs +2 -0
- package/dist/packem_chunks/handler26.mjs +1 -0
- package/dist/packem_chunks/handler27.mjs +2 -0
- package/dist/packem_chunks/handler28.mjs +99 -0
- package/dist/packem_chunks/handler29.mjs +3 -0
- package/dist/packem_chunks/handler3.mjs +1 -204
- package/dist/packem_chunks/handler30.mjs +15 -0
- package/dist/packem_chunks/handler31.mjs +1 -0
- package/dist/packem_chunks/handler32.mjs +1 -0
- package/dist/packem_chunks/handler4.mjs +1 -33
- package/dist/packem_chunks/handler5.mjs +1 -49
- package/dist/packem_chunks/handler6.mjs +3 -91
- package/dist/packem_chunks/handler7.mjs +1 -42
- package/dist/packem_chunks/handler8.mjs +1 -174
- package/dist/packem_chunks/handler9.mjs +1 -315
- package/dist/packem_chunks/runCodegenCommand.mjs +4 -52
- package/dist/packem_chunks/runDevCommand.mjs +8 -0
- package/dist/packem_chunks/runInitCommand.mjs +423 -1447
- package/dist/packem_chunks/runResetCommand.mjs +1 -41
- package/dist/packem_chunks/runRpcCommand.mjs +1 -68
- package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck9tABfr.mjs +1 -0
- package/dist/packem_shared/EXIT_CODE-DQVhSE-1.mjs +1 -0
- package/dist/packem_shared/admin-token-VdUnvnKW.mjs +1 -0
- package/dist/packem_shared/admin-url-DJB8P6Jh.mjs +1 -0
- package/dist/packem_shared/api-spec-lYyq4vL6.mjs +1 -0
- package/dist/packem_shared/binding-manifest-file-CQ1eQYy1.mjs +2 -0
- package/dist/packem_shared/buildRegistryIndex-Dc8D8AR6.mjs +1 -0
- package/dist/packem_shared/cap-error-body-YBKO32BF.mjs +1 -0
- package/dist/packem_shared/catalog-CahzmDLV.mjs +1 -0
- package/dist/packem_shared/cli-manifest-DoQ_3bzs.mjs +1 -0
- package/dist/packem_shared/cloudflare-credentials-BVmZnDZk.mjs +1 -0
- package/dist/packem_shared/codegen-error-AmH54ofi.mjs +3 -0
- package/dist/packem_shared/command-BpMv2n0o.mjs +1 -0
- package/dist/packem_shared/commands-CkuCMUP_.mjs +13 -0
- package/dist/packem_shared/createLogger-4TujevyB.mjs +2 -0
- package/dist/packem_shared/createRecordingSpawner-DchLi0gR.mjs +1 -0
- package/dist/packem_shared/deploy-target-resolve-BTCW1Iv4.mjs +1 -0
- package/dist/packem_shared/diffSnapshots-Y1G8am6C.mjs +7 -0
- package/dist/packem_shared/docker-DpVxvYpL.mjs +1 -0
- package/dist/packem_shared/export-rows-EzdNDYpa.mjs +3 -0
- package/dist/packem_shared/free-port-DjbFhaaT.mjs +1 -0
- package/dist/packem_shared/health-probe-BLnoq_oC.mjs +13 -0
- package/dist/packem_shared/import-3Y59Ry9F.mjs +13 -0
- package/dist/packem_shared/index-oHZBGBwB.mjs +1 -0
- package/dist/packem_shared/insertSchemaExtension-CbTH3lCQ.mjs +10 -0
- package/dist/packem_shared/js-literal-FWLi94Lh.mjs +1 -0
- package/dist/packem_shared/lint-ignore-report-DKZagpqk.mjs +2 -0
- package/dist/packem_shared/mcp-config-file-GR1Y_CVw.mjs +2 -0
- package/dist/packem_shared/open-url-EnKy--w-.mjs +1 -0
- package/dist/packem_shared/output-format-DWe_tkTR.mjs +2 -0
- package/dist/packem_shared/parseManifest-CwPTKdtS.mjs +1 -0
- package/dist/packem_shared/path-containment-CgxYZggb.mjs +1 -0
- package/dist/packem_shared/plan-pog5AvMk.mjs +5 -0
- package/dist/packem_shared/planDevCommand-Bl6OrwXm.mjs +1 -0
- package/dist/packem_shared/platform-diagnostics-Cn2g6Jh-.mjs +4 -0
- package/dist/packem_shared/prompt-cancelled-BvsNxg_Q.mjs +1 -0
- package/dist/packem_shared/render-lunora-error-JsLelWwR.mjs +2 -0
- package/dist/packem_shared/resolve-Bdc9KEUv.mjs +5 -0
- package/dist/packem_shared/resolve-target-BRvw8Pf7.mjs +1 -0
- package/dist/packem_shared/runAddCommand-gThKW-ZB.mjs +1 -0
- package/dist/packem_shared/runDeployCommand-ByBuze9W.mjs +1 -0
- package/dist/packem_shared/runExportCommand-_PtkJJDT.mjs +5 -0
- package/dist/packem_shared/runMigrateGenerateCommand-CguLEIZp.mjs +11 -0
- package/dist/packem_shared/schemaIrToSnapshot-Ck7DSPdf.mjs +1 -0
- package/dist/packem_shared/shared-CV7ESvuj.mjs +2 -0
- package/dist/packem_shared/storage-3GOIFVZm.mjs +1 -0
- package/dist/packem_shared/tui-prompts-B3YwUhGw.mjs +4 -0
- package/dist/packem_shared/variables-CPYyJtsL.mjs +1 -0
- package/dist/packem_shared/vectorize-metadata-DK61NmVx.mjs +2 -0
- package/dist/packem_shared/wrangler-name-Dsk5K1f-.mjs +1 -0
- package/dist/packem_shared/wrangler-secrets-C9siMBwN.mjs +1 -0
- package/package.json +45 -19
- package/skills/README.md +36 -17
- package/skills/lunora/SKILL.md +158 -65
- package/skills/lunora-cloudflare-audit/SKILL.md +164 -0
- package/skills/lunora-create-package/SKILL.md +98 -84
- package/skills/lunora-deploy/SKILL.md +212 -102
- package/skills/lunora-functions/SKILL.md +367 -60
- package/skills/lunora-migration-helper/SKILL.md +194 -149
- package/skills/lunora-performance-audit/SKILL.md +137 -95
- package/skills/lunora-quickstart/SKILL.md +136 -136
- package/skills/lunora-realtime/SKILL.md +219 -97
- package/skills/lunora-setup-auth/SKILL.md +203 -101
- package/skills/lunora-setup-hyperdrive/SKILL.md +153 -110
- package/skills/lunora-setup-hyperdrive-global/SKILL.md +152 -94
- package/skills/lunora-setup-mail/SKILL.md +87 -113
- package/skills/lunora-setup-scheduler/SKILL.md +133 -97
- package/skills/lunora-setup-storage/SKILL.md +189 -95
- package/dist/packem_chunks/planDevCommand.mjs +0 -541
- package/dist/packem_chunks/runDeployCommand.mjs +0 -594
- package/dist/packem_chunks/runMigrateGenerateCommand.mjs +0 -397
- package/dist/packem_shared/COMMANDS-B0ftFD_3.mjs +0 -948
- package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs +0 -244
- package/dist/packem_shared/admin-url-4UzT-CI4.mjs +0 -19
- package/dist/packem_shared/api-spec-CtA6ilu4.mjs +0 -13
- package/dist/packem_shared/buildRegistryIndex-BcYe607_.mjs +0 -38
- package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +0 -31
- package/dist/packem_shared/command-D3lB_4Az.mjs +0 -19
- package/dist/packem_shared/commands-B-gR09Z_.mjs +0 -845
- package/dist/packem_shared/createLogger-B40gPzQo.mjs +0 -78
- package/dist/packem_shared/createRecordingSpawner-DxI3mebw.mjs +0 -43
- package/dist/packem_shared/detect-package-manager-DYp7n3mJ.mjs +0 -61
- package/dist/packem_shared/diffSnapshots-BeDvvNiF.mjs +0 -161
- package/dist/packem_shared/docker-hMQ97KSQ.mjs +0 -21
- package/dist/packem_shared/insertSchemaExtension-DAqbfr9Z.mjs +0 -64
- package/dist/packem_shared/open-url-Dfq6fAyT.mjs +0 -41
- package/dist/packem_shared/output-format-wUvAN6AL.mjs +0 -17
- package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +0 -56
- package/dist/packem_shared/parseManifest--vZf2FY1.mjs +0 -94
- package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +0 -9
- package/dist/packem_shared/resolve-target-qbsJ_5sF.mjs +0 -16
- package/dist/packem_shared/runAddCommand-bnY6-HKb.mjs +0 -4
- package/dist/packem_shared/schema-drift-gate-BtBt0as0.mjs +0 -79
- package/dist/packem_shared/schemaIrToSnapshot-DdsljJT-.mjs +0 -43
- package/dist/packem_shared/storage-BIsph-Vk.mjs +0 -84
- package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +0 -658
- package/dist/packem_shared/wrangler-name-cy4yhm9j.mjs +0 -12
- 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-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
+
```bash
|
|
87
|
+
lunora migrate create backfill-display-name --table users
|
|
88
|
+
```
|
|
14
89
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
94
|
+
```ts
|
|
95
|
+
// lunora/migrations.ts
|
|
96
|
+
import { defineMigration } from "@lunora/server"; // or "lunorash/server"
|
|
21
97
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
+
return rest;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
```
|
|
29
117
|
|
|
30
|
-
|
|
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
|
-
|
|
39
|
-
backfill (either layer) is an online `defineMigration`. Both follow the same
|
|
40
|
-
**widen → migrate → narrow** discipline.
|
|
120
|
+
### Reading another table
|
|
41
121
|
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
126
|
+
```ts
|
|
127
|
+
up: async (doc, ctx) => {
|
|
128
|
+
const thread = await ctx.db.get("threads", String(doc.threadId));
|
|
46
129
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
134
|
+
To change rows in a second table, write a second migration over that table.
|
|
58
135
|
|
|
59
|
-
|
|
60
|
-
one — safer transition, easier rollback.
|
|
136
|
+
### A shard key can't be backfilled
|
|
61
137
|
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
comment explaining why it existed. Delete only once you are sure nothing reads
|
|
66
|
-
it.
|
|
144
|
+
### Run it
|
|
67
145
|
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
157
|
+
```bash
|
|
158
|
+
LUNORA_ADMIN_TOKEN=… lunora migrate up backfill-display-name --prod --url https://app.example.com --yes
|
|
82
159
|
```
|
|
83
160
|
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
116
|
-
|
|
175
|
+
```bash
|
|
176
|
+
lunora migrate generate --name add_user_status
|
|
177
|
+
```
|
|
117
178
|
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
lunora migrate generate --name=add_user_status
|
|
202
|
+
### Moving a dataset from D1 to Hyperdrive
|
|
140
203
|
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
`
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
`
|
|
170
|
-
|
|
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
|
|
186
|
-
- [ ] Widened the schema
|
|
187
|
-
- [ ]
|
|
188
|
-
- [ ]
|
|
189
|
-
- [ ]
|
|
190
|
-
- [ ]
|
|
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.
|