@zerotal/arch 1.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.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,529 @@
1
+ ---
2
+ title: Migrations
3
+ description: Version-control your database schema with up/down migration files the ORM applies in order.
4
+ ---
5
+
6
+ # Migrations
7
+
8
+ Migrations are version-controlled database schema changes — the schema half of the
9
+ [ORM](/docs/orm) story, evolving the tables your models map to. Each migration is a
10
+ class with an `up()` method that applies the change and a `down()` method that reverses it.
11
+
12
+ Migrations ship inside `@zerotal/orm` and are wired up by its `DatabaseProvider`, so
13
+ there is no separate package to install — if you have the ORM, you have migrations.
14
+
15
+ > **Note** — For querying and transactions at runtime (the raw `DB` layer), see [Database](/docs/database).
16
+
17
+ ## Getting Started
18
+
19
+ Migrations ship with `@zerotal/orm`. If you have the [database](/docs/database)
20
+ set up there is nothing further to install:
21
+
22
+ ```typescript
23
+ import { Migration, Schema } from "@zerotal/orm";
24
+ ```
25
+
26
+ Run `bun zt migrate` to apply them — see [References](#references) for the full
27
+ command set.
28
+
29
+ ## Creating a migration
30
+
31
+ ```bash
32
+ # in your project root
33
+ bun zt make:migration create_posts_table
34
+ ```
35
+
36
+ Files are numbered in creation order, so this writes
37
+ `database/migrations/001_create_posts_table.ts` (the next file becomes `002_…`):
38
+
39
+ ```typescript
40
+ // database/migrations/001_create_posts_table.ts
41
+ import { Migration, Schema } from "@zerotal/orm";
42
+
43
+ export default class CreatePostsTable extends Migration {
44
+ async up(): Promise<void> {
45
+ await Schema.create("posts", (table) => {
46
+ table.increments("id");
47
+ table.integer("user_id").index();
48
+ table.string("title");
49
+ table.string("slug").unique();
50
+ table.text("body");
51
+ table.dateTime("published_at").nullable();
52
+ table.softDeletes();
53
+ table.timestamps();
54
+ });
55
+ }
56
+
57
+ async down(): Promise<void> {
58
+ await Schema.drop("posts");
59
+ }
60
+ }
61
+ ```
62
+
63
+ `Migration` is an abstract class with two abstract methods — `up()` and `down()`,
64
+ both returning `Promise<void>`. The default export of every migration file must
65
+ extend it.
66
+
67
+ ## Running migrations
68
+
69
+ ```bash
70
+ # in your project root
71
+ # Run all pending migrations
72
+ bun zt migrate
73
+
74
+ # Drop all tables and re-run every migration from scratch
75
+ bun zt migrate --fresh
76
+
77
+ # Roll back the most recent batch
78
+ bun zt migrate:rollback
79
+
80
+ # Show which migrations have run (name | ran | batch | ranAt)
81
+ bun zt migrate:status
82
+ ```
83
+
84
+ `migrate --fresh`, `migrate:fresh` and `migrate:refresh` all do the same thing:
85
+ roll everything back through each migration's `down()`, then re-run from scratch.
86
+ `migrate:refresh` exists because that is the name the command has elsewhere,
87
+ and a command you reach for and don't find is a `down()` you never exercise.
88
+
89
+ Add `--seed` to repopulate afterwards, which is usually why the database was wiped
90
+ in the first place:
91
+
92
+ ```bash
93
+ bun zt migrate:fresh --seed # rebuild the schema, then run the seeders
94
+ bun zt migrate --fresh --seed # the same thing
95
+ bun zt migrate --seed # apply pending migrations, then seed
96
+ ```
97
+
98
+ `--seed` runs the same seeders as `bun zt db:seed`. If seeding fails, the command
99
+ says so but does not fail: the migrations above already committed, and reporting
100
+ otherwise would suggest they need repeating when only the seeders do — fix the
101
+ seeder and run `bun zt db:seed`.
102
+
103
+ Migrations run in filename order. The `make:migration` numeric prefix keeps them
104
+ ordered automatically (`001_…`, `002_…`); see [Migration file naming](#migration-file-naming).
105
+
106
+ > **Warning** — `migrate --fresh` and `migrate:fresh` **drop every table** before
107
+ > re-running. Never run them against a database whose data you care about.
108
+
109
+ ### Which command do I use?
110
+
111
+ - **`migrate`** — day-to-day: apply the migrations that haven't run yet.
112
+ - **`migrate --fresh` / `migrate:fresh` / `migrate:refresh`** — local resets: throw the
113
+ schema away and rebuild it. Destroys all data. Because they run every `down()` on the
114
+ way, they are also the cheapest way to find out that a rollback is broken.
115
+ - **`migrate:rollback`** — undo the last batch you ran (calls each migration's `down()`).
116
+ - **`migrate:status`** — inspect what has and hasn't run before deciding.
117
+
118
+ ### What happens when a migration fails
119
+
120
+ On **PostgreSQL and SQLite**, each migration and its tracking-table row are written in one
121
+ transaction. A migration that throws half way leaves nothing behind — not the tables it
122
+ managed to create, and not a record claiming it ran. Fix the file and run `migrate` again;
123
+ the schema is exactly as it was. Migrations that committed before the failure stay
124
+ committed, so a retry only has the failure left to deal with.
125
+
126
+ This is what makes [`zt deploy:<env>`](/docs/deployment) safe to interrupt: the only two
127
+ states a deploy can be caught in are _not applied_ and _applied and recorded_.
128
+
129
+ > **Warning** — **MySQL and MariaDB have no transactional DDL.** Every DDL statement
130
+ > implicitly commits, so a migration that fails on its third `ALTER` leaves the first two
131
+ > applied and cannot be rolled back — the engine has nothing left to undo. `bun zt migrate`
132
+ > says so before it starts. Keep migrations small so a failure is easy to unpick by hand,
133
+ > and take a backup before running them against production.
134
+
135
+ Rollback carries the same guarantee in reverse: a `down()` that fails part-way undoes
136
+ nothing and keeps the migration recorded as applied, rather than leaving the schema and the
137
+ tracking table disagreeing.
138
+
139
+ ## Auto-generating from models
140
+
141
+ Zerotal can diff your `@column()` declarations against the live schema and generate
142
+ a migration for the difference. Pass a name for the file it writes:
143
+
144
+ ```bash
145
+ # in your project root
146
+ bun zt migrate:generate add_published_at
147
+ ```
148
+
149
+ The command loads your model files (default glob `app/Models/**/*.ts`), compares each
150
+ model's columns against the database, and writes one migration containing the new
151
+ tables and added columns it found. Point it at a different location with `--models`:
152
+
153
+ ```bash
154
+ # in your project root
155
+ bun zt migrate:generate add_published_at --models "src/models/**/*.ts"
156
+ ```
157
+
158
+ > **Note** — `migrate:generate` only emits **additive** changes (new tables, new
159
+ > columns). It does not generate drops or column-type changes — author those by hand.
160
+
161
+ ## Auto-migration
162
+
163
+ For local development and tests you can skip migration files entirely and have Zerotal
164
+ sync the schema additively at boot — create missing tables and add missing columns to
165
+ match your models (TypeORM-style). This is configured in `config/database.ts` via the
166
+ `synchronize` field:
167
+
168
+ ```typescript
169
+ // config/database.ts
170
+ import { DatabaseConfig } from "@zerotal/orm";
171
+ import { env } from "zerotal";
172
+
173
+ export default DatabaseConfig({
174
+ driver: "sqlite",
175
+ url: env("DATABASE_URL", "./database/db.sqlite"), // raw path — no sqlite:// prefix
176
+ synchronize: env("APP_ENV") !== "production", // local/test only
177
+ });
178
+ ```
179
+
180
+ > **Note** — For SQLite, pass a raw file path (or `:memory:`) as `url` — the
181
+ > `sqlite://` protocol prefix is not required and the docstring on `DatabaseConfig`
182
+ > advises against it. The provider normalises the URL internally.
183
+
184
+ It is **opt-in** (off unless enabled), **additive only by default** (creates missing
185
+ tables, adds missing columns — never drops), and **hard-off in production** regardless
186
+ of the value, where you run `migrate` with generated files.
187
+
188
+ ### Disruptive sync
189
+
190
+ Additive sync leaves columns in place even after you delete them from a model, so your
191
+ database accumulates orphaned columns over time. To let Zerotal also **drop** columns that
192
+ no model declares anymore, opt in explicitly with the object form:
193
+
194
+ ```typescript
195
+ // config/database.ts
196
+ export default DatabaseConfig({
197
+ url: env("DATABASE_URL", "./database/db.sqlite"),
198
+ synchronize: { enabled: true, disruptive: true },
199
+ });
200
+ ```
201
+
202
+ `synchronize` accepts a boolean or an object:
203
+
204
+ | Value | Effect |
205
+ | -------------------------------------- | -------------------------------------------------- |
206
+ | `false` _(default)_ | No sync. |
207
+ | `true` | Additive: create tables, add columns. Never drops. |
208
+ | `{ enabled: true, disruptive: false }` | Same as `true`, written explicitly. |
209
+ | `{ enabled: true, disruptive: true }` | Additive **plus** drops columns no model declares. |
210
+
211
+ > **Danger** — Disruptive sync **destroys the data** in any column it drops and logs a
212
+ > warning before each drop. The primary key is never dropped. Keep `disruptive: true` to
213
+ > local and test environments — production is hard-off regardless.
214
+
215
+ See [Conventions](/docs/conventions#auto-migration) for how the
216
+ boot-time sync is wired in.
217
+
218
+ ## Schema API
219
+
220
+ `Schema` is an object of `async` helpers that compile and run DDL against the current
221
+ connection. Call them from inside `up()` / `down()`.
222
+
223
+ ### Creating tables
224
+
225
+ ```typescript
226
+ // inside a migration's up()
227
+ await Schema.create("users", (table) => {
228
+ table.increments("id"); // INTEGER PRIMARY KEY AUTOINCREMENT
229
+ table.string("name");
230
+ table.string("email").unique();
231
+ table.string("password");
232
+ table.timestamps(); // created_at + updated_at (nullable TEXT)
233
+ });
234
+
235
+ // Idempotent — no error if table already exists
236
+ await Schema.createIfNotExists("settings", (table) => {
237
+ table.string("key").primary();
238
+ table.text("value").nullable();
239
+ });
240
+ ```
241
+
242
+ > **Warning** — `Schema.create()` throws if the table already exists. Use
243
+ > `createIfNotExists()` when a migration may run more than once.
244
+
245
+ ### Modifying tables
246
+
247
+ ```typescript
248
+ // inside a migration's up()
249
+ await Schema.table("users", (table) => {
250
+ table.string("role").default("user"); // ADD COLUMN
251
+ table.boolean("email_verified").default(false);
252
+ table.dropColumn("legacy_field"); // DROP COLUMN
253
+ table.renameColumn("bio", "biography"); // RENAME COLUMN
254
+ table.index(["role", "created_at"], "idx_role_created");
255
+ });
256
+ ```
257
+
258
+ ### Other Schema methods
259
+
260
+ ```typescript
261
+ // inside a migration
262
+ await Schema.drop("users");
263
+ await Schema.dropIfExists("temp_table");
264
+ await Schema.rename("old_name", "new_name");
265
+
266
+ // Introspection
267
+ const exists = await Schema.hasTable("users");
268
+ const hasCol = await Schema.hasColumn("users", "email");
269
+ ```
270
+
271
+ ## Blueprint column types
272
+
273
+ The `t` argument to `create()` / `table()` is a `Blueprint`. Each method below adds a
274
+ column; SQL types reflect the SQLite mappings (other drivers use their native types).
275
+
276
+ | Method | SQL type | Notes |
277
+ | -------------------------------------------- | ----------------------- | --------------------------------- |
278
+ | `id(name?)` | INTEGER PK AUTOINCR | Alias for `increments()` |
279
+ | `increments(name?)` | INTEGER PK AUTOINCR | |
280
+ | `bigIncrements(name?)` | INTEGER PK AUTOINCR | |
281
+ | `integer(name)` | INTEGER | |
282
+ | `bigInteger(name)` | INTEGER | |
283
+ | `tinyInteger / smallInteger / mediumInteger` | INTEGER | |
284
+ | `unsignedInteger(name)` | INTEGER | Marked unsigned (tracked only) |
285
+ | `unsignedBigInteger(name)` | INTEGER | Marked unsigned (tracked only) |
286
+ | `float(name)` | REAL | |
287
+ | `double(name, precision?, scale?)` | REAL | |
288
+ | `decimal(name, precision?, scale?)` | REAL | |
289
+ | `boolean(name)` | INTEGER | 0/1 |
290
+ | `string(name, length?)` | TEXT | `length` ignored on SQLite |
291
+ | `char(name, length?)` | TEXT | |
292
+ | `text(name)` | TEXT | |
293
+ | `tinyText / mediumText / longText` | TEXT | |
294
+ | `uuid(name)` | TEXT | 36-char UUID |
295
+ | `ulid(name)` | TEXT | 26-char ULID |
296
+ | `dateTime(name)` | TEXT | ISO 8601 |
297
+ | `timestamp(name)` | TEXT | Alias for `dateTime` |
298
+ | `date(name)` | TEXT | |
299
+ | `time(name)` | TEXT | |
300
+ | `year(name)` | INTEGER | |
301
+ | `binary(name)` | BLOB | |
302
+ | `json(name)` | TEXT | Serialised JSON |
303
+ | `enum(name, values[])` | TEXT + CHECK constraint | |
304
+ | `set(name, values[])` | TEXT | MySQL `SET`; plain TEXT on SQLite |
305
+ | `ipAddress(name)` / `macAddress(name)` | TEXT | |
306
+ | `foreignId(name)` | INTEGER | See [Foreign keys](#foreign-keys) |
307
+ | `foreignUuid(name)` | TEXT | |
308
+
309
+ ## Column modifiers
310
+
311
+ Chain modifiers on any column:
312
+
313
+ ```typescript
314
+ // inside a Blueprint callback
315
+ t.string("bio").nullable();
316
+ t.string("role").default("user");
317
+ t.string("slug").unique();
318
+ t.integer("views").default(0);
319
+ t.dateTime("published_at").nullable().useCurrent();
320
+ ```
321
+
322
+ | Modifier | Effect |
323
+ | --------------------------------------- | ------------------------------------------------- |
324
+ | `.nullable()` | Allow NULL |
325
+ | `.notNullable()` | Enforce NOT NULL |
326
+ | `.default(value)` / `.defaultTo(value)` | Set DEFAULT clause |
327
+ | `.useCurrent()` | DEFAULT CURRENT_TIMESTAMP |
328
+ | `.unique()` | Unique index on this column |
329
+ | `.index()` | Non-unique index |
330
+ | `.unsigned()` | Mark as unsigned (tracked; no SQLite type change) |
331
+ | `.primary()` | Set as primary key |
332
+ | `.check(expr)` | Add a `CHECK (expression)` constraint |
333
+
334
+ > **Note** — Each modifier locks at the type level: re-applying the same one (or its
335
+ > partner, like `.nullable()` after `.notNullable()`) is a compile-time error.
336
+
337
+ ## Indexes
338
+
339
+ ```typescript
340
+ // inside a migration's up()
341
+ await Schema.create("posts", (table) => {
342
+ table.increments("id");
343
+ table.integer("user_id");
344
+ table.string("slug");
345
+ table.dateTime("published_at").nullable();
346
+
347
+ // Single-column index
348
+ table.index("user_id");
349
+
350
+ // Multi-column index
351
+ table.index(["published_at", "slug"], "idx_posts_pub_slug");
352
+
353
+ // Unique constraint
354
+ table.unique("slug");
355
+
356
+ // Full-text index (plain index on SQLite)
357
+ table.fulltext(["title", "body"], "ft_posts");
358
+
359
+ // Composite primary key
360
+ table.primary(["post_id", "tag_id"]);
361
+ });
362
+ ```
363
+
364
+ ## Foreign keys
365
+
366
+ ```typescript
367
+ // inside a migration's up()
368
+ await Schema.create("comments", (table) => {
369
+ table.increments("id");
370
+ table.integer("user_id").index();
371
+ table.integer("post_id").index();
372
+
373
+ // Fluent foreign key definition
374
+ table.foreign("user_id").references("id").on("users");
375
+ table.foreign("post_id").references("id").on("posts").onDelete("CASCADE");
376
+ });
377
+ ```
378
+
379
+ `foreignId()` is shorthand for an unsigned integer column; chain `.constrained()` to
380
+ add the foreign-key constraint, inferring the referenced table from the column name:
381
+
382
+ ```typescript
383
+ // inside a Blueprint callback
384
+ t.foreignId("user_id").constrained(); // references users.id
385
+ t.foreignId("post_id").constrained("posts"); // explicit table
386
+ t.foreignId("author_id").references("id").on("users").onDelete("CASCADE");
387
+ ```
388
+
389
+ The `onDelete` / `onUpdate` actions are `"CASCADE"`, `"SET NULL"`, `"RESTRICT"`, or
390
+ `"NO ACTION"`. Shorthands `cascadeOnDelete()`, `nullOnDelete()`, and `restrictOnDelete()`
391
+ read more fluently.
392
+
393
+ ## Soft deletes
394
+
395
+ ```typescript
396
+ // inside a Blueprint callback
397
+ t.softDeletes(); // adds nullable deleted_at TEXT column
398
+ t.softDeletes("removed_at"); // custom column name
399
+ ```
400
+
401
+ Compose `SoftDeletes` into a model (`Model.using(SoftDeletes)`) and rows with a
402
+ non-null `deleted_at` are excluded from queries automatically.
403
+
404
+ ## Pivot / join tables
405
+
406
+ ```typescript
407
+ // inside a migration's up()
408
+ await Schema.create("post_tags", (table) => {
409
+ table.integer("post_id");
410
+ table.integer("tag_id");
411
+ table.primary(["post_id", "tag_id"]);
412
+ table.foreign("post_id").references("id").on("posts").onDelete("CASCADE");
413
+ table.foreign("tag_id").references("id").on("tags").onDelete("CASCADE");
414
+ });
415
+ ```
416
+
417
+ ## Conditional changes
418
+
419
+ Check schema state before making changes to keep migrations idempotent:
420
+
421
+ ```typescript
422
+ // inside a migration
423
+ async up(): Promise<void> {
424
+ if (await Schema.hasColumn("users", "role")) return;
425
+
426
+ await Schema.table("users", (table) => {
427
+ table.string("role").default("user");
428
+ });
429
+ }
430
+ ```
431
+
432
+ ## Migration file naming
433
+
434
+ Files in `database/migrations/` are loaded in alphabetical order. `make:migration`
435
+ writes a zero-padded numeric prefix so order is preserved as you add files:
436
+
437
+ ```
438
+ 001_create_users_table.ts
439
+ 002_create_posts_table.ts
440
+ 003_add_role_to_users_table.ts
441
+ ```
442
+
443
+ Timestamp-prefixed names (`2024_01_15_120000_create_users.ts`) also sort correctly if
444
+ you prefer them.
445
+
446
+ ## Testing
447
+
448
+ Set your suite up once as described in [Testing](/docs/testing). Migrations get
449
+ tested twice over, and only one of those is deliberate.
450
+
451
+ **Every suite already tests `up()`.** `refreshDatabase()` runs your migrations
452
+ before the suite, so a migration that throws fails everything loudly. That is
453
+ free coverage, and it is why a broken migration rarely reaches production.
454
+
455
+ **Almost nobody tests `down()`**, which is why rollbacks fail at the worst
456
+ moment. A migration is only reversible if you have run it backwards at least
457
+ once:
458
+
459
+ ```typescript
460
+ // tests/migrations/AddArchivedToPosts.test.ts
461
+ import { test, expect } from "bun:test";
462
+ import { Schema } from "@zerotal/orm";
463
+ import AddArchivedToPosts from "../../database/migrations/0004_add_archived_to_posts.ts";
464
+
465
+ test("the migration reverses cleanly", async () => {
466
+ const migration = new AddArchivedToPosts();
467
+
468
+ await migration.up();
469
+ expect(await Schema.hasColumn("posts", "archived_at")).toBe(true);
470
+
471
+ await migration.down();
472
+ expect(await Schema.hasColumn("posts", "archived_at")).toBe(false);
473
+ });
474
+ ```
475
+
476
+ Run it twice in the same test if the migration is meant to be idempotent — the
477
+ second `up()` should either succeed or fail for a reason you have chosen.
478
+
479
+ **A data migration deserves a real test**, because it is the only kind whose
480
+ mistakes are unrecoverable. Arrange rows in the old shape, run the migration,
481
+ assert the new shape:
482
+
483
+ ```typescript
484
+ // tests/migrations/BackfillSlugs.test.ts
485
+ test("backfills a slug for every existing post", async () => {
486
+ await DB.table("posts").insert({ title: "Hello World", slug: null });
487
+
488
+ await new BackfillSlugs().up();
489
+
490
+ await assertDatabaseHas("posts", { slug: "hello-world" });
491
+ });
492
+ ```
493
+
494
+ > **Warning** — Test the migration class directly, not through
495
+ > `bun zt migrate`. Shelling out to the CLI runs against your development
496
+ > database, not the suite's.
497
+
498
+ ## References
499
+
500
+ ### Migration commands
501
+
502
+ | Command | Description |
503
+ | ------------------------------------------- | ------------------------------------------------------------- |
504
+ | `make:migration <name>` | Scaffold a numbered migration file in `database/migrations/`. |
505
+ | `migrate [--fresh]` | Run pending migrations; `--fresh` drops all tables first. |
506
+ | `migrate:fresh` | Roll everything back, then re-run from scratch. |
507
+ | `migrate:rollback` | Roll back the most recent batch (runs each `down()`). |
508
+ | `migrate:status` | Show each migration's ran / batch / ranAt state. |
509
+ | `migrate:generate <name> [--models <glob>]` | Diff models against the DB and write an additive migration. |
510
+
511
+ ### Schema methods
512
+
513
+ | Method | Signature | Description |
514
+ | ------------------- | ------------------------------------------------------------------ | ------------------------------------- |
515
+ | `create` | `create(table: string, cb: (t: Blueprint) => void): Promise<void>` | Create a table (throws if it exists). |
516
+ | `createIfNotExists` | `createIfNotExists(table: string, cb): Promise<void>` | Create a table only if absent. |
517
+ | `table` | `table(name: string, cb: (t: Blueprint) => void): Promise<void>` | Alter an existing table. |
518
+ | `drop` | `drop(table: string): Promise<void>` | Drop a table (throws if absent). |
519
+ | `dropIfExists` | `dropIfExists(table: string): Promise<void>` | Drop a table only if present. |
520
+ | `rename` | `rename(from: string, to: string): Promise<void>` | Rename a table. |
521
+ | `hasTable` | `hasTable(table: string): Promise<boolean>` | Whether a table exists. |
522
+ | `hasColumn` | `hasColumn(table: string, column: string): Promise<boolean>` | Whether a column exists. |
523
+
524
+ ## Next steps
525
+
526
+ - [ORM](/docs/orm) — the models your migrations build tables for.
527
+ - [Seeding](/docs/seeding) — populate tables with factory data.
528
+ - [Database](/docs/database) — raw SQL, transactions, and connections.
529
+ - [Query builder](/docs/query-builder) — query the tables you create.