@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.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/api-surface.md +283 -0
- package/docs/about.md +676 -0
- package/docs/admin/actions.md +183 -0
- package/docs/admin/auth.md +89 -0
- package/docs/admin/dashboard.md +155 -0
- package/docs/admin/extending-ui.md +229 -0
- package/docs/admin/extending.md +205 -0
- package/docs/admin/forms.md +155 -0
- package/docs/admin/index.md +144 -0
- package/docs/admin/operations.md +224 -0
- package/docs/admin/references.md +174 -0
- package/docs/admin/resources.md +132 -0
- package/docs/admin/structure.md +158 -0
- package/docs/admin/tables.md +185 -0
- package/docs/admin/testing.md +138 -0
- package/docs/ai.md +373 -0
- package/docs/application.md +435 -0
- package/docs/arch.md +263 -0
- package/docs/assets.md +755 -0
- package/docs/audit.md +367 -0
- package/docs/authentication.md +1000 -0
- package/docs/authorization.md +529 -0
- package/docs/broadcasting/channels.md +170 -0
- package/docs/broadcasting/client.md +108 -0
- package/docs/broadcasting/events.md +190 -0
- package/docs/broadcasting/index.md +163 -0
- package/docs/broadcasting/references.md +84 -0
- package/docs/broadcasting/testing.md +132 -0
- package/docs/cache.md +387 -0
- package/docs/carbon.md +830 -0
- package/docs/changelog.md +465 -0
- package/docs/client/auth.md +113 -0
- package/docs/client/errors.md +139 -0
- package/docs/client/files.md +118 -0
- package/docs/client/index.md +128 -0
- package/docs/client/references.md +58 -0
- package/docs/client/requests.md +131 -0
- package/docs/client/resilience.md +141 -0
- package/docs/client/testing.md +146 -0
- package/docs/commands.md +450 -0
- package/docs/components.md +2427 -0
- package/docs/config-system.md +258 -0
- package/docs/container.md +596 -0
- package/docs/context.md +579 -0
- package/docs/contributing.md +164 -0
- package/docs/controllers.md +354 -0
- package/docs/conventions.md +466 -0
- package/docs/cookies.md +101 -0
- package/docs/csrf.md +206 -0
- package/docs/database.md +476 -0
- package/docs/deployment.md +434 -0
- package/docs/devtools.md +1013 -0
- package/docs/email-verification.md +320 -0
- package/docs/encryption.md +380 -0
- package/docs/errors.md +317 -0
- package/docs/events.md +420 -0
- package/docs/flow/components.md +846 -0
- package/docs/flow/decorators.md +458 -0
- package/docs/flow/events.md +420 -0
- package/docs/flow/forms.md +551 -0
- package/docs/flow/index.md +568 -0
- package/docs/flow/layouts.md +793 -0
- package/docs/flow/lifecycle.md +336 -0
- package/docs/flow/pagination.md +402 -0
- package/docs/flow/performance.md +262 -0
- package/docs/flow/references.md +317 -0
- package/docs/flow/routing.md +550 -0
- package/docs/flow/testing.md +447 -0
- package/docs/getting-started.md +286 -0
- package/docs/health.md +269 -0
- package/docs/helpers.md +423 -0
- package/docs/i18n.md +414 -0
- package/docs/index.md +65 -0
- package/docs/inertia/build.md +95 -0
- package/docs/inertia/devtools.md +197 -0
- package/docs/inertia/index.md +267 -0
- package/docs/inertia/middleware.md +81 -0
- package/docs/inertia/props.md +507 -0
- package/docs/inertia/references.md +144 -0
- package/docs/inertia/rendering.md +189 -0
- package/docs/inertia/ssr.md +102 -0
- package/docs/inspirations.md +257 -0
- package/docs/lifecycle.md +331 -0
- package/docs/lock.md +454 -0
- package/docs/logger.md +517 -0
- package/docs/media.md +582 -0
- package/docs/middleware.md +509 -0
- package/docs/migrations.md +529 -0
- package/docs/monitor.md +377 -0
- package/docs/notifications.md +848 -0
- package/docs/orm/casts.md +365 -0
- package/docs/orm/factories.md +231 -0
- package/docs/orm/index.md +663 -0
- package/docs/orm/lifecycle.md +421 -0
- package/docs/orm/queries.md +692 -0
- package/docs/orm/relationships.md +440 -0
- package/docs/orm/serialization.md +251 -0
- package/docs/package-development.md +536 -0
- package/docs/pagination.md +340 -0
- package/docs/password-reset.md +335 -0
- package/docs/providers.md +447 -0
- package/docs/query-builder.md +465 -0
- package/docs/queue.md +549 -0
- package/docs/rate-limiting.md +253 -0
- package/docs/responses.md +336 -0
- package/docs/roles-and-2fa.md +418 -0
- package/docs/routing.md +1008 -0
- package/docs/scaffolding.md +335 -0
- package/docs/scheduler.md +533 -0
- package/docs/seeding.md +229 -0
- package/docs/session.md +451 -0
- package/docs/social.md +524 -0
- package/docs/storage.md +622 -0
- package/docs/structure.md +230 -0
- package/docs/support-policy.md +114 -0
- package/docs/telemetry.md +505 -0
- package/docs/tenancy.md +608 -0
- package/docs/testing/browser.md +153 -0
- package/docs/testing/console.md +130 -0
- package/docs/testing/database.md +238 -0
- package/docs/testing/flow-browser.md +216 -0
- package/docs/testing/http.md +441 -0
- package/docs/testing/index.md +240 -0
- package/docs/testing/mocking.md +410 -0
- package/docs/upgrade.md +192 -0
- package/docs/validator.md +549 -0
- package/docs/view.md +453 -0
- package/package.json +60 -0
- package/src/bin/mcp.ts +73 -0
- package/src/config.ts +63 -0
- package/src/errors.ts +27 -0
- package/src/index.ts +56 -0
- package/src/install/ArchInstallCommand.ts +189 -0
- package/src/install/detect.ts +98 -0
- package/src/install/guidelines.ts +290 -0
- package/src/install/markers.ts +77 -0
- package/src/install/mcpConfig.ts +81 -0
- package/src/mcp/index.ts +27 -0
- package/src/mcp/jsonrpc.ts +133 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/stdio.ts +111 -0
- package/src/mcp/types.ts +146 -0
- package/src/probe/ArchProbeCommand.ts +48 -0
- package/src/probe/sentinel.ts +13 -0
- package/src/probe/topics.ts +291 -0
- package/src/provider/ArchProvider.ts +101 -0
- package/src/tools/_probe.ts +164 -0
- package/src/tools/apiSurface.ts +241 -0
- package/src/tools/appInfo.ts +88 -0
- package/src/tools/baselines.ts +210 -0
- package/src/tools/context.ts +37 -0
- package/src/tools/doctor.ts +93 -0
- package/src/tools/index.ts +50 -0
- package/src/tools/logs.ts +265 -0
- package/src/tools/routes.ts +133 -0
- package/src/tools/schema.ts +119 -0
- 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.
|