@sedrino/db-schema 0.1.1 → 0.1.2
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/README.md +62 -6
- package/dist/cli.js +1904 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +365 -85
- package/dist/index.js +1108 -187
- package/dist/index.js.map +1 -1
- package/docs/cli.md +93 -0
- package/docs/expressions-and-transforms.md +165 -0
- package/docs/index.md +5 -2
- package/docs/migrations.md +183 -3
- package/docs/planning-and-apply.md +200 -0
- package/docs/relations.md +130 -0
- package/docs/schema-document.md +62 -0
- package/package.json +3 -2
- package/src/apply.ts +67 -0
- package/src/cli.ts +105 -7
- package/src/drizzle.ts +348 -1
- package/src/index.ts +38 -1
- package/src/migration.ts +315 -3
- package/src/operations.ts +278 -0
- package/src/planner.ts +7 -190
- package/src/project.ts +157 -1
- package/src/sql-expression.ts +123 -0
- package/src/sqlite.ts +150 -9
- package/src/transforms.ts +94 -0
- package/src/utils.ts +54 -0
package/docs/cli.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
The package ships a Bun-first CLI named `sedrino-db`.
|
|
4
4
|
|
|
5
|
+
The published CLI expects `bun` to be installed because the generated executable uses a Bun shebang.
|
|
6
|
+
|
|
5
7
|
## Default project layout
|
|
6
8
|
|
|
7
9
|
```text
|
|
@@ -14,6 +16,40 @@ db/
|
|
|
14
16
|
|
|
15
17
|
## Commands
|
|
16
18
|
|
|
19
|
+
### Create
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
sedrino-db migrate create create-account --dir db
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This:
|
|
26
|
+
|
|
27
|
+
- creates `db/migrations` if it does not exist yet
|
|
28
|
+
- assigns the next migration id using the local date plus a zero-padded sequence
|
|
29
|
+
- writes a new TypeScript migration scaffold
|
|
30
|
+
|
|
31
|
+
Example output file:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
db/migrations/2026-04-08-001-create-account.ts
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Generated scaffold:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { createMigration } from "@sedrino/db-schema";
|
|
41
|
+
|
|
42
|
+
export default createMigration(
|
|
43
|
+
{
|
|
44
|
+
id: "2026-04-08-001-create-account",
|
|
45
|
+
name: "Create account",
|
|
46
|
+
},
|
|
47
|
+
(m) => {
|
|
48
|
+
// TODO: define migration operations.
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
```
|
|
52
|
+
|
|
17
53
|
### Plan
|
|
18
54
|
|
|
19
55
|
```bash
|
|
@@ -29,6 +65,15 @@ This:
|
|
|
29
65
|
|
|
30
66
|
Add `--sql` to print the SQLite statements per migration.
|
|
31
67
|
|
|
68
|
+
You can override the generated artifact paths explicitly:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
sedrino-db migrate plan \
|
|
72
|
+
--dir db \
|
|
73
|
+
--snapshot db/schema/custom.snapshot.json \
|
|
74
|
+
--drizzle-out db/schema/custom.generated.ts
|
|
75
|
+
```
|
|
76
|
+
|
|
32
77
|
### Apply
|
|
33
78
|
|
|
34
79
|
```bash
|
|
@@ -40,6 +85,7 @@ This:
|
|
|
40
85
|
- reads local migrations
|
|
41
86
|
- compares them with applied migrations recorded in the database
|
|
42
87
|
- applies pending migrations
|
|
88
|
+
- refuses to run migrations whose plan still contains safety warnings
|
|
43
89
|
- updates the stored schema metadata tables
|
|
44
90
|
- writes the local snapshot and generated Drizzle file
|
|
45
91
|
|
|
@@ -53,6 +99,47 @@ URL resolution order:
|
|
|
53
99
|
- `--url`
|
|
54
100
|
- `LIBSQL_URL`
|
|
55
101
|
|
|
102
|
+
Example with environment variables:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
export LIBSQL_URL=libsql://my-db.turso.io
|
|
106
|
+
export LIBSQL_AUTH_TOKEN=secret
|
|
107
|
+
|
|
108
|
+
sedrino-db migrate apply --dir db
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Validate
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
sedrino-db migrate validate --dir db
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This:
|
|
118
|
+
|
|
119
|
+
- materializes the schema from local migrations
|
|
120
|
+
- reports planner warnings that would block apply
|
|
121
|
+
- checks whether `schema.snapshot.json` is up to date
|
|
122
|
+
- checks whether `schema.generated.ts` is up to date
|
|
123
|
+
|
|
124
|
+
The command exits non-zero if warnings are present or generated artifacts are stale.
|
|
125
|
+
|
|
126
|
+
### Status
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
sedrino-db migrate status --dir db --url libsql://...
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This:
|
|
133
|
+
|
|
134
|
+
- reports the local migration count and local schema hash
|
|
135
|
+
- optionally inspects the connected database
|
|
136
|
+
- reports applied, pending, and unexpected database migrations
|
|
137
|
+
- reports schema drift based on the stored schema hash
|
|
138
|
+
|
|
139
|
+
The command exits non-zero if drift is detected or the database contains migrations that are not present locally.
|
|
140
|
+
|
|
141
|
+
If `--url` or `LIBSQL_URL` is omitted, the command still prints local migration status and local schema hash.
|
|
142
|
+
|
|
56
143
|
### Print schema
|
|
57
144
|
|
|
58
145
|
```bash
|
|
@@ -68,3 +155,9 @@ sedrino-db schema drizzle --dir db --out db/schema/schema.generated.ts
|
|
|
68
155
|
```
|
|
69
156
|
|
|
70
157
|
Prints generated Drizzle source to stdout unless `--out` is provided.
|
|
158
|
+
|
|
159
|
+
Stdout example:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
sedrino-db schema drizzle --dir db
|
|
163
|
+
```
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# SQL Expressions And Transforms
|
|
2
|
+
|
|
3
|
+
The package exposes two layers for rebuild-time data movement:
|
|
4
|
+
|
|
5
|
+
- low-level SQL expression helpers
|
|
6
|
+
- higher-level migration intent transforms
|
|
7
|
+
|
|
8
|
+
## Low-level expression helpers
|
|
9
|
+
|
|
10
|
+
Use these when you want full control over the SQL fragment used in `backfill(...)` or `using(...)`.
|
|
11
|
+
|
|
12
|
+
Exports:
|
|
13
|
+
|
|
14
|
+
- `sqlExpression(...)`
|
|
15
|
+
- `raw(...)`
|
|
16
|
+
- `column(...)`
|
|
17
|
+
- `literal(...)`
|
|
18
|
+
- `lower(...)`
|
|
19
|
+
- `trim(...)`
|
|
20
|
+
- `replace(...)`
|
|
21
|
+
- `cast(...)`
|
|
22
|
+
- `unixepoch(...)`
|
|
23
|
+
- `date(...)`
|
|
24
|
+
- `multiply(...)`
|
|
25
|
+
- `coalesce(...)`
|
|
26
|
+
- `concat(...)`
|
|
27
|
+
- `sqlExpr.*`
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { sqlExpr } from "@sedrino/db-schema";
|
|
33
|
+
|
|
34
|
+
const expression = sqlExpr.multiply(
|
|
35
|
+
sqlExpr.cast(sqlExpr.unixepoch(sqlExpr.column("createdAt")), "INTEGER"),
|
|
36
|
+
1000,
|
|
37
|
+
);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This produces:
|
|
41
|
+
|
|
42
|
+
```sql
|
|
43
|
+
CAST(unixepoch("created_at") AS INTEGER) * 1000
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Raw migration examples
|
|
47
|
+
|
|
48
|
+
`backfill(...)` and `using(...)` accept either strings or expression objects:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { createMigration, sqlExpr } from "@sedrino/db-schema";
|
|
52
|
+
|
|
53
|
+
export default createMigration(
|
|
54
|
+
{
|
|
55
|
+
id: "2026-04-08-001",
|
|
56
|
+
name: "Backfill slug and convert createdAt",
|
|
57
|
+
},
|
|
58
|
+
(m) => {
|
|
59
|
+
m.alterTable("account", (t) => {
|
|
60
|
+
t.string("slug")
|
|
61
|
+
.required()
|
|
62
|
+
.backfill(sqlExpr.lower(sqlExpr.replace(sqlExpr.trim(sqlExpr.column("name")), " ", "-")));
|
|
63
|
+
|
|
64
|
+
t.alterField("createdAt", (f) => {
|
|
65
|
+
f.temporalInstant().using(
|
|
66
|
+
sqlExpr.multiply(
|
|
67
|
+
sqlExpr.cast(sqlExpr.unixepoch(sqlExpr.column("createdAt")), "INTEGER"),
|
|
68
|
+
1000,
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Higher-level transforms
|
|
78
|
+
|
|
79
|
+
Use `transforms.*` when you want the migration to read as intent instead of hand-built SQL.
|
|
80
|
+
|
|
81
|
+
Exports:
|
|
82
|
+
|
|
83
|
+
- `copy(...)`
|
|
84
|
+
- `lowercase(...)`
|
|
85
|
+
- `trimmed(...)`
|
|
86
|
+
- `slugFrom(...)`
|
|
87
|
+
- `concatFields(...)`
|
|
88
|
+
- `coalesceFields(...)`
|
|
89
|
+
- `epochMsFromIsoString(...)`
|
|
90
|
+
- `plainDateFromIsoString(...)`
|
|
91
|
+
- `integerFromText(...)`
|
|
92
|
+
- `realFromText(...)`
|
|
93
|
+
- `transforms`
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { createMigration, transforms } from "@sedrino/db-schema";
|
|
99
|
+
|
|
100
|
+
export default createMigration(
|
|
101
|
+
{
|
|
102
|
+
id: "2026-04-08-001",
|
|
103
|
+
name: "Normalize account fields",
|
|
104
|
+
},
|
|
105
|
+
(m) => {
|
|
106
|
+
m.alterTable("account", (t) => {
|
|
107
|
+
t.string("slug")
|
|
108
|
+
.required()
|
|
109
|
+
.backfill(transforms.slugFrom("name"));
|
|
110
|
+
|
|
111
|
+
t.string("searchName")
|
|
112
|
+
.required()
|
|
113
|
+
.backfill(transforms.lowercase("displayName"));
|
|
114
|
+
|
|
115
|
+
t.alterField("createdAt", (f) => {
|
|
116
|
+
f.temporalInstant().using(transforms.epochMsFromIsoString("createdAt"));
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Common patterns
|
|
124
|
+
|
|
125
|
+
### Copy a field directly
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
transforms.copy("accountId");
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Join fields together
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
transforms.concatFields(["firstName", "lastName"], { separator: " " });
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Use the first populated field
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
transforms.coalesceFields(["nickname", "name"], "Unknown");
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Parse numbers from legacy text columns
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
transforms.integerFromText("priority");
|
|
147
|
+
transforms.realFromText("amount");
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Convert ISO date/time strings
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
transforms.epochMsFromIsoString("createdAt");
|
|
154
|
+
transforms.plainDateFromIsoString("birthday");
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## When to use which layer
|
|
158
|
+
|
|
159
|
+
Use `transforms.*` first when a helper already matches the migration intent.
|
|
160
|
+
|
|
161
|
+
Drop down to `sqlExpr.*` or raw strings when:
|
|
162
|
+
|
|
163
|
+
- you need a custom SQL function
|
|
164
|
+
- you need a composition the built-in transforms do not cover yet
|
|
165
|
+
- you are prototyping a new reusable transform before promoting it into `transforms.*`
|
package/docs/index.md
CHANGED
|
@@ -6,6 +6,9 @@ Reference index for the migration-first schema planning package.
|
|
|
6
6
|
|
|
7
7
|
| Topic | File |
|
|
8
8
|
| --- | --- |
|
|
9
|
-
| Schema document model | [`schema-document.md`](./schema-document.md) |
|
|
10
|
-
| Migration DSL and
|
|
9
|
+
| Schema document model and validation helpers | [`schema-document.md`](./schema-document.md) |
|
|
10
|
+
| Migration DSL and authoring helpers | [`migrations.md`](./migrations.md) |
|
|
11
|
+
| Planning, apply, and project APIs | [`planning-and-apply.md`](./planning-and-apply.md) |
|
|
12
|
+
| SQL expression and transform helpers | [`expressions-and-transforms.md`](./expressions-and-transforms.md) |
|
|
13
|
+
| Drizzle relations generation | [`relations.md`](./relations.md) |
|
|
11
14
|
| CLI commands | [`cli.md`](./cli.md) |
|
package/docs/migrations.md
CHANGED
|
@@ -36,9 +36,64 @@ export default createMigration(
|
|
|
36
36
|
);
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## Core authoring surface
|
|
40
|
+
|
|
41
|
+
The migration builder supports:
|
|
42
|
+
|
|
43
|
+
- `createTable(...)`
|
|
44
|
+
- `createJunctionTable(...)`
|
|
45
|
+
- `dropTable(...)`
|
|
46
|
+
- `renameTable(...)`
|
|
47
|
+
- `alterTable(...)`
|
|
48
|
+
|
|
49
|
+
Inside `createTable(...)`, the table builder supports:
|
|
50
|
+
|
|
51
|
+
- field builders:
|
|
52
|
+
- `id(...)`
|
|
53
|
+
- `string(...)`
|
|
54
|
+
- `text(...)`
|
|
55
|
+
- `boolean(...)`
|
|
56
|
+
- `integer(...)`
|
|
57
|
+
- `number(...)`
|
|
58
|
+
- `enum(...)`
|
|
59
|
+
- `json(...)`
|
|
60
|
+
- `temporalInstant(...)`
|
|
61
|
+
- `temporalPlainDate(...)`
|
|
62
|
+
- `reference(...)`
|
|
63
|
+
- `belongsTo(...)`
|
|
64
|
+
- table metadata:
|
|
65
|
+
- `description(...)`
|
|
66
|
+
- `index(...)`
|
|
67
|
+
- `unique(...)`
|
|
68
|
+
|
|
69
|
+
Inside `alterTable(...)`, the alter builder supports:
|
|
70
|
+
|
|
71
|
+
- add field:
|
|
72
|
+
- `string(...)`
|
|
73
|
+
- `text(...)`
|
|
74
|
+
- `boolean(...)`
|
|
75
|
+
- `integer(...)`
|
|
76
|
+
- `number(...)`
|
|
77
|
+
- `enum(...)`
|
|
78
|
+
- `json(...)`
|
|
79
|
+
- `temporalInstant(...)`
|
|
80
|
+
- `temporalPlainDate(...)`
|
|
81
|
+
- `reference(...)`
|
|
82
|
+
- `belongsTo(...)`
|
|
83
|
+
- change structure:
|
|
84
|
+
- `dropField(...)`
|
|
85
|
+
- `renameField(...)`
|
|
86
|
+
- `alterField(...)`
|
|
87
|
+
- `addIndex(...)`
|
|
88
|
+
- `dropIndex(...)`
|
|
89
|
+
- `addUnique(...)`
|
|
90
|
+
- `dropUnique(...)`
|
|
91
|
+
|
|
39
92
|
## Planning flow
|
|
40
93
|
|
|
41
94
|
```ts
|
|
95
|
+
import { planMigration } from "@sedrino/db-schema";
|
|
96
|
+
|
|
42
97
|
const plan = planMigration({
|
|
43
98
|
currentSchema,
|
|
44
99
|
migration,
|
|
@@ -49,8 +104,14 @@ The result includes:
|
|
|
49
104
|
|
|
50
105
|
- ordered semantic operations
|
|
51
106
|
- next schema snapshot JSON
|
|
52
|
-
- SQLite migration statements
|
|
53
|
-
- warnings for operations that still
|
|
107
|
+
- SQLite migration statements, including rebuild-based statements for supported shape changes
|
|
108
|
+
- warnings for operations that still require explicit data transforms or multi-step backfills
|
|
109
|
+
|
|
110
|
+
The `nextSchema` output is then what you pass to:
|
|
111
|
+
|
|
112
|
+
- `compileSchemaToDrizzle(...)`
|
|
113
|
+
- `compileSchemaToDrizzleRelations(...)`
|
|
114
|
+
- `compileSchemaToSqlite(...)`
|
|
54
115
|
|
|
55
116
|
For project-style usage, the CLI wraps this flow:
|
|
56
117
|
|
|
@@ -58,8 +119,127 @@ For project-style usage, the CLI wraps this flow:
|
|
|
58
119
|
sedrino-db migrate plan --dir db
|
|
59
120
|
```
|
|
60
121
|
|
|
122
|
+
## Field alterations
|
|
123
|
+
|
|
124
|
+
The v1 migration DSL now supports conservative field alteration through `alterField(...)`.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
m.alterTable("account", (t) => {
|
|
128
|
+
t.alterField("nickname", (f) => {
|
|
129
|
+
f.required().default("unknown").description("Display nickname");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Supported `alterField(...)` changes are materialized into the schema snapshot and emitted as a SQLite table rebuild.
|
|
135
|
+
|
|
136
|
+
For rebuilds that need help moving data, the preferred API is higher-level transform helpers:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { transforms } from "@sedrino/db-schema";
|
|
140
|
+
|
|
141
|
+
m.alterTable("account", (t) => {
|
|
142
|
+
t.string("slug")
|
|
143
|
+
.required()
|
|
144
|
+
.backfill(transforms.slugFrom("name"));
|
|
145
|
+
|
|
146
|
+
t.alterField("createdAt", (f) => {
|
|
147
|
+
f.temporalInstant().using(transforms.epochMsFromIsoString("createdAt"));
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The lower-level `backfillSql(...)` and `usingSql(...)` methods are still available when a migration needs a custom expression.
|
|
153
|
+
|
|
154
|
+
Built-in helpers currently cover:
|
|
155
|
+
|
|
156
|
+
- `transforms.copy("fieldName")`
|
|
157
|
+
- `transforms.lowercase("fieldName")`
|
|
158
|
+
- `transforms.trimmed("fieldName")`
|
|
159
|
+
- `transforms.slugFrom("fieldName")`
|
|
160
|
+
- `transforms.concatFields(["firstName", "lastName"], { separator: " " })`
|
|
161
|
+
- `transforms.coalesceFields(["nickname", "name"], "Unknown")`
|
|
162
|
+
- `transforms.epochMsFromIsoString("createdAt")`
|
|
163
|
+
- `transforms.plainDateFromIsoString("birthday")`
|
|
164
|
+
- `transforms.integerFromText("priority")`
|
|
165
|
+
- `transforms.realFromText("amount")`
|
|
166
|
+
|
|
167
|
+
The planner intentionally warns and blocks apply for cases that still do not have an explicit safe path, for example:
|
|
168
|
+
|
|
169
|
+
- adding a required field without a default
|
|
170
|
+
- changing the underlying storage strategy without `using(...)`
|
|
171
|
+
|
|
172
|
+
## Table operations example
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { createMigration } from "@sedrino/db-schema";
|
|
176
|
+
|
|
177
|
+
export default createMigration(
|
|
178
|
+
{
|
|
179
|
+
id: "2026-04-08-002-reshape-crm",
|
|
180
|
+
name: "Reshape CRM tables",
|
|
181
|
+
},
|
|
182
|
+
(m) => {
|
|
183
|
+
m.renameTable("account", "organization");
|
|
184
|
+
|
|
185
|
+
m.alterTable("contact", (t) => {
|
|
186
|
+
t.renameField("fullName", "displayName");
|
|
187
|
+
t.dropField("legacyNotes");
|
|
188
|
+
t.string("slug").required().backfill("lower(replace(trim(\"display_name\"), ' ', '-'))");
|
|
189
|
+
t.addIndex(["slug"], { name: "contact_slug_idx" });
|
|
190
|
+
t.addUnique(["organizationId", "email"], {
|
|
191
|
+
name: "contact_org_email_unique",
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Relationship helpers
|
|
199
|
+
|
|
200
|
+
The DSL now includes higher-level helpers for common relational patterns.
|
|
201
|
+
|
|
202
|
+
### `belongsTo(...)`
|
|
203
|
+
|
|
204
|
+
Use `belongsTo(...)` when a table owns a foreign key to another table:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
m.createTable("contact", (t) => {
|
|
208
|
+
t.id("contactId", { prefix: "ct" });
|
|
209
|
+
t.belongsTo("account", {
|
|
210
|
+
required: true,
|
|
211
|
+
onDelete: "cascade",
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
By default this:
|
|
217
|
+
|
|
218
|
+
- creates a field like `accountId`
|
|
219
|
+
- references `account.accountId`
|
|
220
|
+
- can mark the field required or unique
|
|
221
|
+
- adds a single-column index unless the relation is unique
|
|
222
|
+
|
|
223
|
+
### `createJunctionTable(...)`
|
|
224
|
+
|
|
225
|
+
Use `createJunctionTable(...)` for many-to-many structures:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
m.createJunctionTable("userGroupMembership", {
|
|
229
|
+
left: { table: "user" },
|
|
230
|
+
right: { table: "group" },
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
By default this:
|
|
235
|
+
|
|
236
|
+
- creates required foreign keys like `userId` and `groupId`
|
|
237
|
+
- adds indexes on both foreign key fields
|
|
238
|
+
- adds a composite unique constraint across the pair
|
|
239
|
+
- enables inferred Drizzle `through(...)` many-to-many relations
|
|
240
|
+
|
|
61
241
|
## Current v1 limits
|
|
62
242
|
|
|
63
|
-
- no rebuild-aware SQL emission for destructive field changes
|
|
64
243
|
- no composite primary keys
|
|
65
244
|
- SQLite-only
|
|
245
|
+
- destructive/shape-changing migrations are expressed as rebuilds rather than native `ALTER` operations
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Planning And Apply
|
|
2
|
+
|
|
3
|
+
This guide covers the runtime-facing library APIs beyond the migration DSL itself.
|
|
4
|
+
|
|
5
|
+
## Plan a single migration
|
|
6
|
+
|
|
7
|
+
Use `planMigration(...)` when you already have a current schema and want:
|
|
8
|
+
|
|
9
|
+
- the semantic operation list
|
|
10
|
+
- the next schema snapshot
|
|
11
|
+
- emitted SQLite statements
|
|
12
|
+
- planner warnings
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { createMigration, planMigration } from "@sedrino/db-schema";
|
|
16
|
+
|
|
17
|
+
const migration = createMigration(
|
|
18
|
+
{
|
|
19
|
+
id: "2026-04-08-001-add-slug",
|
|
20
|
+
name: "Add account slug",
|
|
21
|
+
},
|
|
22
|
+
(m) => {
|
|
23
|
+
m.alterTable("account", (t) => {
|
|
24
|
+
t.string("slug").required().backfill(`lower(replace(trim("name"), ' ', '-'))`);
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const plan = planMigration({
|
|
30
|
+
currentSchema,
|
|
31
|
+
migration,
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Materialize a whole migration chain
|
|
36
|
+
|
|
37
|
+
Use `materializeSchema(...)` to replay every migration and get the current schema plus every intermediate plan:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { materializeSchema } from "@sedrino/db-schema";
|
|
41
|
+
|
|
42
|
+
const { schema, plans } = materializeSchema({
|
|
43
|
+
migrations,
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Apply semantic operations directly
|
|
48
|
+
|
|
49
|
+
If you already have a list of `MigrationOperation`s, use `applyOperationsToSchema(...)`:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { applyOperationsToSchema } from "@sedrino/db-schema";
|
|
53
|
+
|
|
54
|
+
const nextSchema = applyOperationsToSchema(currentSchema, operations);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Emit SQLite directly
|
|
58
|
+
|
|
59
|
+
The package can emit both full-schema DDL and incremental migration SQL:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import {
|
|
63
|
+
compileSchemaToSqlite,
|
|
64
|
+
createMigration,
|
|
65
|
+
materializeSchema,
|
|
66
|
+
renderSqliteMigration,
|
|
67
|
+
} from "@sedrino/db-schema";
|
|
68
|
+
|
|
69
|
+
const { schema } = materializeSchema({ migrations });
|
|
70
|
+
const ddl = compileSchemaToSqlite(schema);
|
|
71
|
+
|
|
72
|
+
const migration = createMigration(
|
|
73
|
+
{
|
|
74
|
+
id: "2026-04-08-002",
|
|
75
|
+
name: "Add slug",
|
|
76
|
+
},
|
|
77
|
+
(m) => {
|
|
78
|
+
m.alterTable("account", (t) => {
|
|
79
|
+
t.string("slug");
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const sql = renderSqliteMigration(migration.buildOperations(), {
|
|
85
|
+
currentSchema: schema,
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Emit Drizzle source
|
|
90
|
+
|
|
91
|
+
Use `compileSchemaToDrizzle(...)` for full table + relation source, or `compileSchemaToDrizzleRelations(...)` for relations only:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import {
|
|
95
|
+
compileSchemaToDrizzle,
|
|
96
|
+
compileSchemaToDrizzleRelations,
|
|
97
|
+
} from "@sedrino/db-schema";
|
|
98
|
+
|
|
99
|
+
const source = compileSchemaToDrizzle(schema);
|
|
100
|
+
const relationsOnly = compileSchemaToDrizzleRelations(schema);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Apply migrations to libSQL or Turso
|
|
104
|
+
|
|
105
|
+
Use `applyMigrations(...)` when you want the library to:
|
|
106
|
+
|
|
107
|
+
- ensure metadata tables exist
|
|
108
|
+
- compare local and database migration history
|
|
109
|
+
- detect drift
|
|
110
|
+
- apply pending migrations
|
|
111
|
+
- persist the updated schema snapshot and schema hash
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { applyMigrations, createLibsqlClient } from "@sedrino/db-schema";
|
|
115
|
+
|
|
116
|
+
const client = createLibsqlClient({
|
|
117
|
+
url: "file:./local.db",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const result = await applyMigrations({
|
|
121
|
+
client,
|
|
122
|
+
migrations,
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
You can also pass `connection` instead of a prebuilt client:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
await applyMigrations({
|
|
130
|
+
connection: {
|
|
131
|
+
url: process.env.LIBSQL_URL!,
|
|
132
|
+
authToken: process.env.LIBSQL_AUTH_TOKEN,
|
|
133
|
+
},
|
|
134
|
+
migrations,
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Inspect migration status
|
|
139
|
+
|
|
140
|
+
Use `inspectMigrationStatus(...)` for non-mutating status checks:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { inspectMigrationStatus } from "@sedrino/db-schema";
|
|
144
|
+
|
|
145
|
+
const status = await inspectMigrationStatus({
|
|
146
|
+
client,
|
|
147
|
+
migrations,
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The result includes:
|
|
152
|
+
|
|
153
|
+
- local migration ids
|
|
154
|
+
- applied migration ids
|
|
155
|
+
- pending migration ids
|
|
156
|
+
- unexpected database migration ids
|
|
157
|
+
- local schema hash
|
|
158
|
+
- database schema hash
|
|
159
|
+
- drift status
|
|
160
|
+
|
|
161
|
+
## Read metadata tables
|
|
162
|
+
|
|
163
|
+
Lower-level helpers are available if you want the raw metadata state:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { getSchemaState, listAppliedMigrations } from "@sedrino/db-schema";
|
|
167
|
+
|
|
168
|
+
const applied = await listAppliedMigrations(client);
|
|
169
|
+
const state = await getSchemaState(client);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Project layout helpers
|
|
173
|
+
|
|
174
|
+
For file-based projects, the package exports helpers around the default `db/` structure:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import {
|
|
178
|
+
loadMigrationDefinitionsFromDirectory,
|
|
179
|
+
materializeProjectMigrations,
|
|
180
|
+
resolveDbProjectLayout,
|
|
181
|
+
validateDbProject,
|
|
182
|
+
writeDrizzleSchema,
|
|
183
|
+
writeSchemaSnapshot,
|
|
184
|
+
} from "@sedrino/db-schema";
|
|
185
|
+
|
|
186
|
+
const layout = resolveDbProjectLayout("db");
|
|
187
|
+
const migrations = await loadMigrationDefinitionsFromDirectory(layout.migrationsDir);
|
|
188
|
+
const materialized = await materializeProjectMigrations(layout);
|
|
189
|
+
const validation = await validateDbProject(layout);
|
|
190
|
+
|
|
191
|
+
await writeSchemaSnapshot(materialized.schema, layout.snapshotPath);
|
|
192
|
+
await writeDrizzleSchema(materialized.schema, layout.drizzlePath);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`validateDbProject(...)` is especially useful in CI because it tells you whether:
|
|
196
|
+
|
|
197
|
+
- migrations are internally valid
|
|
198
|
+
- the generated schema snapshot is up to date
|
|
199
|
+
- the generated Drizzle source is up to date
|
|
200
|
+
- planner warnings would block apply
|