kysely-ddl 0.1.0 → 0.3.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 +64 -0
- package/README.md +128 -52
- package/dist/generator/diff.d.ts +2 -0
- package/dist/generator/diff.js +5 -2
- package/dist/generator/generate.d.ts +9 -3
- package/dist/generator/generate.js +8 -2
- package/dist/generator/render.d.ts +11 -2
- package/dist/generator/render.js +33 -6
- package/dist/generator/snapshot.d.ts +6 -0
- package/dist/generator/snapshot.js +1 -0
- package/dist/index.d.ts +18 -8
- package/dist/index.js +15 -8
- package/dist/kysely/infer.d.ts +40 -12
- package/dist/migrator/index.d.ts +15 -0
- package/dist/migrator/index.js +13 -0
- package/dist/{kysely → migrator}/provider.js +21 -9
- package/dist/{kysely → migrator}/runner.d.ts +18 -6
- package/dist/{kysely → migrator}/runner.js +46 -13
- package/dist/migrator/store.d.ts +34 -2
- package/dist/migrator/store.js +104 -19
- package/dist/table/columns.d.ts +28 -6
- package/dist/table/columns.js +5 -1
- package/dist/table/define.d.ts +12 -1
- package/dist/table/define.js +2 -1
- package/package.json +4 -4
- package/dist/kysely/index.d.ts +0 -12
- package/dist/kysely/index.js +0 -3
- /package/dist/{kysely → migrator}/provider.d.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,70 @@
|
|
|
2
2
|
|
|
3
3
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versions follow [SemVer](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.3.0] — 2026-09-12
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `{ bigint: true }` in the infer options: `bigint()` columns as `bigint` and
|
|
12
|
+
`bigint().array()` columns as `bigint[]`, for a driver that returns int8 that
|
|
13
|
+
way (`Bun.SQL` with `{ bigint: true }`, `pg` with a type parser for oid 20 and,
|
|
14
|
+
for arrays, 1016). A column with `$type<T>()` keeps `T`. The `InferOptions`
|
|
15
|
+
type is exported.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Breaking:** the second type parameter of `inferKyselyTable` and
|
|
20
|
+
`inferKyselyDatabase` is an options object instead of a boolean:
|
|
21
|
+
`inferKyselyDatabase<typeof schema, true>` becomes
|
|
22
|
+
`inferKyselyDatabase<typeof schema, { camelCase: true }>`.
|
|
23
|
+
|
|
24
|
+
## [0.2.0] — 2026-09-12
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `concurrently: true` on an index in `defineTable`: the index is built and
|
|
29
|
+
dropped with `CONCURRENTLY`. Postgres refuses that inside a transaction and
|
|
30
|
+
inside a multi-statement query, so the generator writes such statements into a
|
|
31
|
+
migration of their own, `<stamp>_<name>_concurrently.sql`, marked
|
|
32
|
+
`--> no-transaction` and split by `--> statement-breakpoint`, with a
|
|
33
|
+
`DROP INDEX CONCURRENTLY IF EXISTS` in front of every create so that a rerun
|
|
34
|
+
after a failure is clean. `GenerateResult.concurrently` holds these
|
|
35
|
+
statements. Toggling the flag on an existing index is not a change; the
|
|
36
|
+
snapshot records it, and older snapshots read as `false`.
|
|
37
|
+
- `--> no-transaction`: a header marker in a `.sql` migration. The runner honors
|
|
38
|
+
it under `transaction: 'each'` and `'none'` and rejects it under `'all'`
|
|
39
|
+
before applying anything. `sqlFileMigrationProvider` turns it into
|
|
40
|
+
`config: { transaction: false }` for Kysely 0.30+
|
|
41
|
+
(`transactionMode: 'per-migration'`); on older Kysely the marked migration
|
|
42
|
+
fails with a clear error when the `Migrator` runs it inside a transaction.
|
|
43
|
+
- `MigrationError.line`: the line postgres pointed at inside the failing query.
|
|
44
|
+
- `readMigration`, `renderConcurrentStatements`, `NO_TRANSACTION_MARKER`,
|
|
45
|
+
`CONCURRENTLY_SUFFIX` and the `MigrationFile` type.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **Breaking:** entry points. `kysely-ddl/kysely` is gone: `inferKyselyTable`,
|
|
50
|
+
`inferKyselyDatabase`, `jsonb`, `jsonbArray` and the `Jsonb` type now come
|
|
51
|
+
from `kysely-ddl`, which therefore imports `kysely`, the peer dependency, at
|
|
52
|
+
runtime. The runner (`createMigrator`, `migrateToLatest`, `MigrationError`,
|
|
53
|
+
`DEFAULT_JOURNAL_TABLE`, `MIGRATION_LOCK_ID` and their types) and
|
|
54
|
+
`sqlFileMigrationProvider` moved to the new `kysely-ddl/migrator`, so that a
|
|
55
|
+
project which applies its migrations some other way does not pull them in.
|
|
56
|
+
- **Breaking:** the `defineTable` option `tableName` is now `name`.
|
|
57
|
+
- **Breaking:** the runner's default `transaction` mode is `'each'` instead of
|
|
58
|
+
`'all'`, so that a generated `_concurrently` migration runs out of the box.
|
|
59
|
+
Pass `transaction: 'all'` to keep the whole run atomic.
|
|
60
|
+
- **Breaking:** `writeMigration` returns the names of the files written, an
|
|
61
|
+
array, instead of one name.
|
|
62
|
+
- **Breaking:** a `Change` of kind `dropIndex` carries `concurrently`.
|
|
63
|
+
- Ordinary migrations are written as plain SQL, without
|
|
64
|
+
`--> statement-breakpoint`, and the runner sends such a file to postgres as
|
|
65
|
+
one query: postgres runs it as one implicit transaction and reports the
|
|
66
|
+
failing line. Files with breakpoints, hand-written or `_concurrently`, are
|
|
67
|
+
still sent one chunk at a time.
|
|
68
|
+
|
|
5
69
|
## [0.1.0] — 2026-09-10
|
|
6
70
|
|
|
7
71
|
First release.
|
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/hehmonke/kysely-ddl/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/kysely-ddl)
|
|
5
|
+
[](LICENSE)
|
|
5
6
|
|
|
6
7
|
PostgreSQL schema as TypeScript code, SQL migrations generated from snapshot
|
|
7
8
|
diffs, Kysely table types, and a migration runner on top of your `Kysely`.
|
|
@@ -28,8 +29,8 @@ Bun, or any PostgreSQL dialect for Kysely. Runtime: Node >= 20 or Bun >= 1.2.
|
|
|
28
29
|
|
|
29
30
|
| import | contents |
|
|
30
31
|
|---|---|
|
|
31
|
-
| `kysely-ddl` | table definitions, migration generation, migration files on disk
|
|
32
|
-
| `kysely-ddl/
|
|
32
|
+
| `kysely-ddl` | table definitions, migration generation, migration files on disk, `inferKyselyTable` / `inferKyselyDatabase`, `jsonb` / `jsonbArray` |
|
|
33
|
+
| `kysely-ddl/migrator` | running migrations: `createMigrator` / `migrateToLatest` and `sqlFileMigrationProvider`. A separate entry point, so that a project which applies its migrations some other way does not pull the runner in |
|
|
33
34
|
|
|
34
35
|
## Quick start
|
|
35
36
|
|
|
@@ -39,7 +40,7 @@ Bun, or any PostgreSQL dialect for Kysely. Runtime: Node >= 20 or Bun >= 1.2.
|
|
|
39
40
|
import { defineTable, ref, sql } from 'kysely-ddl';
|
|
40
41
|
|
|
41
42
|
export const userTable = defineTable({
|
|
42
|
-
|
|
43
|
+
name: 'user',
|
|
43
44
|
// builders arrive as an argument; they are not exported one by one
|
|
44
45
|
columns: t => ({
|
|
45
46
|
// no explicit column name -> derived from the property: created_at, apple_id, ...
|
|
@@ -56,7 +57,7 @@ export const userTable = defineTable({
|
|
|
56
57
|
});
|
|
57
58
|
|
|
58
59
|
export const sessionTable = defineTable({
|
|
59
|
-
|
|
60
|
+
name: 'session',
|
|
60
61
|
columns: t => ({
|
|
61
62
|
id: t.uuid().notNull().default(sql`gen_random_uuid()`),
|
|
62
63
|
userId: t.uuid().notNull(),
|
|
@@ -75,17 +76,19 @@ import * as schema from './schema';
|
|
|
75
76
|
const dir = './migrations';
|
|
76
77
|
const result = generateMigration([schema.userTable, schema.sessionTable], readLatestSnapshot(dir));
|
|
77
78
|
|
|
78
|
-
if (result.
|
|
79
|
+
if (result.changes.length === 0) {
|
|
79
80
|
console.log('no changes');
|
|
80
81
|
} else {
|
|
81
|
-
|
|
82
|
+
// one file, or two when the diff has indexes built CONCURRENTLY:
|
|
83
|
+
// [ '20260910123045_migration', '20260910123046_migration_concurrently' ]
|
|
84
|
+
console.log(writeMigration(dir, process.argv[2] ?? 'migration', result));
|
|
82
85
|
}
|
|
83
86
|
```
|
|
84
87
|
|
|
85
88
|
`migrate.ts`, applying migrations:
|
|
86
89
|
|
|
87
90
|
```ts
|
|
88
|
-
import { migrateToLatest } from 'kysely-ddl/
|
|
91
|
+
import { migrateToLatest } from 'kysely-ddl/migrator';
|
|
89
92
|
import { Kysely, PostgresDialect } from 'kysely';
|
|
90
93
|
import pg from 'pg';
|
|
91
94
|
|
|
@@ -102,7 +105,7 @@ try {
|
|
|
102
105
|
Types for queries:
|
|
103
106
|
|
|
104
107
|
```ts
|
|
105
|
-
import type { inferKyselyDatabase } from 'kysely-ddl
|
|
108
|
+
import type { inferKyselyDatabase } from 'kysely-ddl';
|
|
106
109
|
import * as schema from './schema';
|
|
107
110
|
|
|
108
111
|
type DB = inferKyselyDatabase<typeof schema>;
|
|
@@ -120,7 +123,7 @@ A hybrid: columns as chains, everything else as a declarative block.
|
|
|
120
123
|
|---|---|
|
|
121
124
|
| column types | `t.uuid` `t.varchar({ length })` `t.integer` `t.bigint` `t.boolean` `t.numeric({ precision, scale })` `t.timestamp({ withTimezone, precision })` `t.jsonb` `t.enum([...])` |
|
|
122
125
|
| modifiers | `.notNull()` `.default(v \| sql)` `.defaultNow()` `.array()` `.$type<T>()` `.generatedAlwaysAsIdentity()` |
|
|
123
|
-
| table | `primaryKey` (composite too), `uniques`, `indexes` (unique
|
|
126
|
+
| table | `primaryKey` (composite too), `uniques`, `indexes` (unique, partial via `where`, built online via `concurrently`), `foreignKeys` (`onDelete` / `onUpdate`), `checks`; names are optional everywhere |
|
|
124
127
|
| expressions | `` sql`...` `` with column and literal interpolation, `inArray(c.status, [...])` |
|
|
125
128
|
|
|
126
129
|
When a column name is not given, it is derived from the property name with the
|
|
@@ -131,8 +134,9 @@ config second: `t.varchar('title', { length: 200 })`.
|
|
|
131
134
|
|
|
132
135
|
TypeScript value types match what the driver returns, without modes like
|
|
133
136
|
`bigint({ mode })`: `bigint` and `numeric` are strings (no precision loss),
|
|
134
|
-
`timestamp` is `Date`, `jsonb` is `unknown`.
|
|
135
|
-
|
|
137
|
+
`timestamp` is `Date`, `jsonb` is `unknown`. A driver told to return int8 as
|
|
138
|
+
`bigint` is matched by the `bigint: true` infer option, see "Types for Kysely".
|
|
139
|
+
For another type use `$type<T>()` and convert on your side.
|
|
136
140
|
|
|
137
141
|
### `enum([...])`
|
|
138
142
|
|
|
@@ -183,17 +187,16 @@ Collisions are caught too: two objects on the same columns, an index named like
|
|
|
183
187
|
a unique constraint, a check with neither columns nor a name, two properties
|
|
184
188
|
mapping to the same database name.
|
|
185
189
|
|
|
186
|
-
###
|
|
190
|
+
### Limitations
|
|
187
191
|
|
|
188
192
|
Native enums, stored generated columns, schemas other than `public`, views,
|
|
189
|
-
`DEFERRABLE`, covering indexes, index methods other than btree.
|
|
190
|
-
|
|
191
|
-
in the diff -> a branch in the renderer.
|
|
193
|
+
`DEFERRABLE`, covering indexes, index methods other than btree. Pull requests are
|
|
194
|
+
welcome; [CONTRIBUTING.md](CONTRIBUTING.md) describes how such a feature is added.
|
|
192
195
|
|
|
193
196
|
## Types for Kysely
|
|
194
197
|
|
|
195
198
|
```ts
|
|
196
|
-
import type { inferKyselyDatabase, inferKyselyTable } from 'kysely-ddl
|
|
199
|
+
import type { inferKyselyDatabase, inferKyselyTable } from 'kysely-ddl';
|
|
197
200
|
import type { Insertable, Selectable } from 'kysely';
|
|
198
201
|
|
|
199
202
|
type UserTable = inferKyselyTable<typeof userTable>;
|
|
@@ -204,9 +207,10 @@ type NewUser = Insertable<UserTable>;
|
|
|
204
207
|
```
|
|
205
208
|
|
|
206
209
|
Keys are **the column names in the database**: they are known at the type level
|
|
207
|
-
because `defineTable` keeps them as literals.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
+
because `defineTable` keeps them as literals. The second parameter of both types
|
|
211
|
+
is an options object, `InferOptions`: `{ camelCase: true }` for Kysely's
|
|
212
|
+
`CamelCasePlugin`, `{ bigint: true }` for a driver that returns int8 as `bigint`;
|
|
213
|
+
both are described below, and they combine. Write rules:
|
|
210
214
|
|
|
211
215
|
| column | `Insertable` |
|
|
212
216
|
|---|---|
|
|
@@ -219,13 +223,13 @@ camelCase, see below. Write rules:
|
|
|
219
223
|
### CamelCasePlugin
|
|
220
224
|
|
|
221
225
|
`CamelCasePlugin` rewrites camelCase in code to snake_case in SQL and back in
|
|
222
|
-
result row keys. To have the types reflect that,
|
|
223
|
-
|
|
226
|
+
result row keys. To have the types reflect that, pass `{ camelCase: true }` to
|
|
227
|
+
either `infer` type:
|
|
224
228
|
|
|
225
229
|
```ts
|
|
226
230
|
import { CamelCasePlugin, Kysely } from 'kysely';
|
|
227
231
|
|
|
228
|
-
type DB = inferKyselyDatabase<typeof schema, true>;
|
|
232
|
+
type DB = inferKyselyDatabase<typeof schema, { camelCase: true }>;
|
|
229
233
|
const db = new Kysely<DB>({ dialect, plugins: [new CamelCasePlugin()] });
|
|
230
234
|
|
|
231
235
|
await db.selectFrom('auditLog').select(['userId', 'happenedAt']).execute();
|
|
@@ -244,6 +248,35 @@ leading underscore. Plugin options (`upperCase`, `underscoreBeforeDigits`,
|
|
|
244
248
|
conversions themselves are exported too: the types `SnakeCase`, `CamelCase` and
|
|
245
249
|
the functions `toSnakeCase`, `toCamelCase`.
|
|
246
250
|
|
|
251
|
+
### bigint: when the driver returns `bigint`
|
|
252
|
+
|
|
253
|
+
`bigint()` columns are strings in the types because that is what `pg` and
|
|
254
|
+
`Bun.SQL` return for int8 by default. Both can return `bigint` instead, and then
|
|
255
|
+
the types follow with `{ bigint: true }`: `bigint()` columns read and write as
|
|
256
|
+
`bigint`, `bigint().array()` columns as `bigint[]`, nullable ones as
|
|
257
|
+
`bigint | null`. A column with `$type<T>()` stays `T`, and `numeric` stays a
|
|
258
|
+
string either way.
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
// Bun.SQL: its own option
|
|
262
|
+
const sql = new SQL(process.env.DATABASE_URL, { bigint: true });
|
|
263
|
+
|
|
264
|
+
// pg: a parser for int8 (oid 20); int8[] (oid 1016) is parsed separately and has no entry in pg.types.builtins
|
|
265
|
+
pg.types.setTypeParser(pg.types.builtins.INT8, BigInt);
|
|
266
|
+
const INT8_ARRAY = 1016 as Parameters<typeof pg.types.getTypeParser>[0];
|
|
267
|
+
const parseInt8Array = pg.types.getTypeParser(INT8_ARRAY) as (value: string) => (string | null)[];
|
|
268
|
+
pg.types.setTypeParser(INT8_ARRAY, value => parseInt8Array(value).map(item => (item === null ? null : BigInt(item))));
|
|
269
|
+
|
|
270
|
+
type DB = inferKyselyDatabase<typeof schema, { bigint: true }>;
|
|
271
|
+
// together with CamelCasePlugin: { camelCase: true, bigint: true }
|
|
272
|
+
|
|
273
|
+
const row = await db.selectFrom('ledger').select(['amount', 'history']).executeTakeFirstOrThrow();
|
|
274
|
+
// ^? { amount: bigint; history: bigint[] }
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Values past 2^53 stay exact on both drivers, in parameters as well as in
|
|
278
|
+
results, which is the point of `bigint` over `number`.
|
|
279
|
+
|
|
247
280
|
### jsonb: values through `jsonb()` and `jsonbArray()`
|
|
248
281
|
|
|
249
282
|
Drivers disagree on how a jsonb parameter should be passed, and there is no raw
|
|
@@ -255,7 +288,7 @@ and the `Jsonb<T>` brand in the write types keeps raw objects and strings from
|
|
|
255
288
|
slipping past them:
|
|
256
289
|
|
|
257
290
|
```ts
|
|
258
|
-
import { jsonb, jsonbArray } from 'kysely-ddl
|
|
291
|
+
import { jsonb, jsonbArray } from 'kysely-ddl';
|
|
259
292
|
|
|
260
293
|
await db.insertInto('user').values({ settings: jsonb({ theme: 'dark', tags: ['a'] }) }).execute();
|
|
261
294
|
await db.updateTable('user').set({ settings: jsonb({ theme: 'light' }) }).where('id', '=', id).execute();
|
|
@@ -294,15 +327,56 @@ migrations/
|
|
|
294
327
|
snapshot.json
|
|
295
328
|
```
|
|
296
329
|
|
|
297
|
-
`writeMigration(dir, name, result)` writes the `.sql`
|
|
298
|
-
`snapshot.json`,
|
|
330
|
+
`writeMigration(dir, name, result)` writes the `.sql` files and updates
|
|
331
|
+
`snapshot.json`, returning the names in application order.
|
|
332
|
+
`readLatestSnapshot(dir)` reads the snapshot for the next diff, and
|
|
299
333
|
`listMigrations(dir)` returns names in the order the runner applies them (by
|
|
300
334
|
character codes, like Kysely). There is no separate journal on disk: the table in
|
|
301
335
|
the database knows what has been applied.
|
|
302
336
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
337
|
+
An ordinary migration is plain SQL. The runner sends the whole file to postgres
|
|
338
|
+
as one query, and postgres runs a multi-statement query as one implicit
|
|
339
|
+
transaction, so even under `transaction: 'none'` a failing statement rolls the
|
|
340
|
+
file back. `MigrationError.line` says which line postgres pointed at.
|
|
341
|
+
|
|
342
|
+
### Indexes built `CONCURRENTLY`
|
|
343
|
+
|
|
344
|
+
`CREATE INDEX CONCURRENTLY` does not lock the table against writes, but postgres
|
|
345
|
+
refuses it inside a transaction and inside a multi-statement query alike. An
|
|
346
|
+
index declared with `concurrently: true` is therefore written as a migration of
|
|
347
|
+
its own, one second after the ordinary one:
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
indexes: [{ columns: ['userId'], concurrently: true }],
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
migrations/
|
|
355
|
+
20260910120000_add_tickets.sql <- the table and its fk, one transaction
|
|
356
|
+
20260910120001_add_tickets_concurrently.sql <- the index, outside any transaction
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
```sql
|
|
360
|
+
--> no-transaction
|
|
361
|
+
DROP INDEX CONCURRENTLY IF EXISTS "ticket_user_id_idx";
|
|
362
|
+
--> statement-breakpoint
|
|
363
|
+
CREATE INDEX CONCURRENTLY "ticket_user_id_idx" ON "ticket" ("user_id");
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
The `--> no-transaction` marker in the header tells the runner to skip the
|
|
367
|
+
transaction, and `--> statement-breakpoint` makes it send the statements one at a
|
|
368
|
+
time, which `CONCURRENTLY` also demands. Both are comments for `psql`. Dropping
|
|
369
|
+
such an index is `DROP INDEX CONCURRENTLY` in the same kind of file. A failed
|
|
370
|
+
`CREATE INDEX CONCURRENTLY` leaves an invalid index behind while the migration
|
|
371
|
+
stays unrecorded, hence the `DROP INDEX CONCURRENTLY IF EXISTS` in front: the
|
|
372
|
+
rerun is clean. Toggling `concurrently` on an existing index changes nothing.
|
|
373
|
+
|
|
374
|
+
The runner applies such a migration under the default `transaction: 'each'` and
|
|
375
|
+
under `'none'`, and rejects it under `'all'` before touching the database; through the provider,
|
|
376
|
+
Kysely's `Migrator` needs `transactionMode: 'per-migration'` (Kysely 0.30+).
|
|
377
|
+
Both markers may also be written by hand. A `--> no-transaction` below the
|
|
378
|
+
header is an error: postgres would take it for a comment. `readMigration(dir, name)`
|
|
379
|
+
returns the execution chunks together with the `transaction` flag.
|
|
306
380
|
|
|
307
381
|
Names are monotonic: if the previous migration was created in the same second,
|
|
308
382
|
the timestamp is bumped forward, otherwise the suffix would decide the order.
|
|
@@ -340,13 +414,13 @@ The runner takes a ready `Kysely` with any PostgreSQL dialect and works through
|
|
|
340
414
|
single connection (`db.connection()`): the lock and the transaction live on it.
|
|
341
415
|
|
|
342
416
|
```ts
|
|
343
|
-
import { createMigrator, migrateToLatest } from 'kysely-ddl/
|
|
417
|
+
import { createMigrator, migrateToLatest } from 'kysely-ddl/migrator';
|
|
344
418
|
|
|
345
419
|
const migrator = createMigrator({
|
|
346
420
|
db, // a Kysely instance, not a Transaction
|
|
347
421
|
migrationsDir: './migrations',
|
|
348
422
|
journalTable: 'kysely_migration', // the default, same as Kysely
|
|
349
|
-
transaction: '
|
|
423
|
+
transaction: 'each', // the default; 'all' | 'each' | 'none'
|
|
350
424
|
allowUnordered: false,
|
|
351
425
|
});
|
|
352
426
|
|
|
@@ -358,13 +432,15 @@ await migrateToLatest({ db, migrationsDir: './migrations' }); // the same in one
|
|
|
358
432
|
|
|
359
433
|
| option | effect |
|
|
360
434
|
|---|---|
|
|
361
|
-
| `transaction: '
|
|
362
|
-
| `transaction: '
|
|
363
|
-
| `transaction: 'none'` | no
|
|
435
|
+
| `transaction: 'each'` | the default: one transaction per migration, the ones before the failure stay applied. A `--> no-transaction` migration runs without one, its journal row in autocommit right after it |
|
|
436
|
+
| `transaction: 'all'` | the whole run in one transaction, like Kysely: a failing migration rolls back the earlier ones from the same run. A `--> no-transaction` migration is an error before anything is applied: one shared transaction cannot leave it out |
|
|
437
|
+
| `transaction: 'none'` | no `BEGIN` at all. A plain file still runs as one implicit transaction; only a file split by `--> statement-breakpoint` fails statement by statement. `--> no-transaction` changes nothing here |
|
|
364
438
|
| `allowUnordered` | apply migrations that sort before already applied ones (branch merges). An error by default |
|
|
365
439
|
|
|
366
|
-
A failing
|
|
367
|
-
|
|
440
|
+
A failing query throws `MigrationError` with `migration`, `statement` (the text
|
|
441
|
+
sent: a whole file, or one chunk between breakpoints), `line` (where postgres
|
|
442
|
+
pointed inside it), `applied` (what this run applied before the failure) and the
|
|
443
|
+
driver's `cause`.
|
|
368
444
|
A migration present in the journal but missing on disk is an error: restore the
|
|
369
445
|
file from history or delete the row by hand.
|
|
370
446
|
|
|
@@ -378,13 +454,22 @@ lock is the same `pg_advisory_lock`. So `.sql` migrations can also be run by the
|
|
|
378
454
|
built-in `Migrator`, and the two runners can alternate on one database:
|
|
379
455
|
|
|
380
456
|
```ts
|
|
381
|
-
import { sqlFileMigrationProvider } from 'kysely-ddl/
|
|
457
|
+
import { sqlFileMigrationProvider } from 'kysely-ddl/migrator';
|
|
382
458
|
import { Migrator } from 'kysely/migration';
|
|
383
459
|
|
|
384
460
|
const migrator = new Migrator({ db, provider: sqlFileMigrationProvider('./migrations') });
|
|
385
461
|
const { error, results } = await migrator.migrateToLatest();
|
|
386
462
|
```
|
|
387
463
|
|
|
464
|
+
`--> no-transaction` reaches the `Migrator` as `config: { transaction: false }`
|
|
465
|
+
on the migration, the field Kysely 0.30+ honors under
|
|
466
|
+
`new Migrator({ ..., transactionMode: 'per-migration' })`: every migration in
|
|
467
|
+
its own transaction, the marked one without. Under the default `'per-run'`
|
|
468
|
+
Kysely itself reports the marked migration as an error and applies nothing.
|
|
469
|
+
Older Kysely ignores `config`; there the marked migration checks that it is not
|
|
470
|
+
inside a transaction and fails with an error naming the fix, and the only way to
|
|
471
|
+
run it is `disableTransactions: true` for the whole run.
|
|
472
|
+
|
|
388
473
|
There are no rollbacks: the generator only writes forward. Without `down`,
|
|
389
474
|
Kysely skips a migration on `migrateDown` (`NotExecuted`) and leaves it in the
|
|
390
475
|
journal, so by default the provider supplies a `down` that fails with a clear
|
|
@@ -411,27 +496,18 @@ What the checks showed on `pg` 8.23 and `Bun.SQL` 1.4 (parameters via Kysely):
|
|
|
411
496
|
| array of objects -> `jsonb[]` | ok | error: arrays are not encoded | `jsonbArray([...])` |
|
|
412
497
|
| array of strings -> `varchar[]` | ok, including commas, quotes, `null` | error: elements joined with commas | a Bun dialect must encode arrays into literals itself, as the test one does |
|
|
413
498
|
| `numeric` = 0 on read | `'0.00'` | `'0'` | compare as numbers |
|
|
499
|
+
| `int8` on read | `'42'`; `42n` with a parser for oid 20, and 1016 for `int8[]` | `'42'`; `42n` with `{ bigint: true }` | the `bigint: true` infer option once the driver returns `bigint` |
|
|
414
500
|
|
|
415
501
|
Reading jsonb, JSON arrays, `jsonb[]` and `varchar[]` yields parsed JS values
|
|
416
502
|
with both drivers.
|
|
417
503
|
|
|
418
|
-
##
|
|
419
|
-
|
|
420
|
-
```bash
|
|
421
|
-
bun install
|
|
422
|
-
bun run db:up # postgres:18-alpine on 54329
|
|
423
|
-
DATABASE_URL=postgres://postgres:postgres@localhost:54329/kysely_ddl bun test
|
|
424
|
-
bun run typecheck && bun run lint && bun run build
|
|
425
|
-
DATABASE_URL=... bun run smoke:node # the built dist under Node with pg
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
Without `DATABASE_URL` the integration tests are skipped, the unit tests always
|
|
429
|
-
run. Type-level checks live in `test/types.test-d.ts` and are read by `tsc` only.
|
|
504
|
+
## Contributing
|
|
430
505
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
506
|
+
Bug reports and pull requests are welcome on
|
|
507
|
+
[GitHub](https://github.com/hehmonke/kysely-ddl/issues). [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
508
|
+
covers setting up the repository, running the tests and cutting a release.
|
|
509
|
+
Changes between versions are listed in [CHANGELOG.md](CHANGELOG.md).
|
|
434
510
|
|
|
435
511
|
## License
|
|
436
512
|
|
|
437
|
-
MIT
|
|
513
|
+
[MIT](LICENSE)
|
package/dist/generator/diff.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ export type Change = {
|
|
|
63
63
|
} | {
|
|
64
64
|
readonly kind: 'dropIndex';
|
|
65
65
|
readonly index: string;
|
|
66
|
+
/** `DROP INDEX CONCURRENTLY`, in the `--> no-transaction` migration together with the concurrent creates. */
|
|
67
|
+
readonly concurrently: boolean;
|
|
66
68
|
};
|
|
67
69
|
export declare function diffSnapshots(prev: Snapshot, next: Snapshot): Change[];
|
|
68
70
|
export {};
|
package/dist/generator/diff.js
CHANGED
|
@@ -24,6 +24,7 @@ function sameConstraint(a, b) {
|
|
|
24
24
|
}
|
|
25
25
|
return false;
|
|
26
26
|
}
|
|
27
|
+
/** `concurrently` is left out on purpose: it says how to build the index, not what the index is. */
|
|
27
28
|
function sameIndex(a, b) {
|
|
28
29
|
return a.unique === b.unique && sameArray(a.columns, b.columns) && a.where === b.where;
|
|
29
30
|
}
|
|
@@ -137,13 +138,15 @@ export function diffSnapshots(prev, next) {
|
|
|
137
138
|
createIndexes.push({ kind: 'createIndex', table: after.name, index });
|
|
138
139
|
}
|
|
139
140
|
else if (!sameIndex(old, index)) {
|
|
140
|
-
|
|
141
|
+
// the drop follows the NEW flag: both statements must land in the same
|
|
142
|
+
// migration, or the create would run before the drop
|
|
143
|
+
dropIndexes.push({ kind: 'dropIndex', index: index.name, concurrently: index.concurrently });
|
|
141
144
|
createIndexes.push({ kind: 'createIndex', table: after.name, index });
|
|
142
145
|
}
|
|
143
146
|
}
|
|
144
147
|
for (const index of before.indexes) {
|
|
145
148
|
if (!afterIndexes.has(index.name)) {
|
|
146
|
-
dropIndexes.push({ kind: 'dropIndex', index: index.name });
|
|
149
|
+
dropIndexes.push({ kind: 'dropIndex', index: index.name, concurrently: index.concurrently });
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
152
|
}
|
|
@@ -7,13 +7,19 @@ import type { AnyTable } from '../table/define.ts';
|
|
|
7
7
|
import { type Change } from './diff.ts';
|
|
8
8
|
import { type Snapshot } from './snapshot.ts';
|
|
9
9
|
export interface GenerateResult {
|
|
10
|
-
/**
|
|
10
|
+
/** The migration text, without the `CONCURRENTLY` statements; an empty string means none. */
|
|
11
11
|
readonly sql: string;
|
|
12
|
-
/** The same SQL
|
|
12
|
+
/** The same SQL statement by statement. */
|
|
13
13
|
readonly statements: readonly string[];
|
|
14
|
+
/**
|
|
15
|
+
* The `CONCURRENTLY` index statements. Postgres refuses them inside a
|
|
16
|
+
* transaction and inside a multi-statement query alike, so `writeMigration`
|
|
17
|
+
* puts them into a migration of their own, marked `--> no-transaction`.
|
|
18
|
+
*/
|
|
19
|
+
readonly concurrently: readonly string[];
|
|
14
20
|
/** The snapshot to store next to the migration. */
|
|
15
21
|
readonly snapshot: Snapshot;
|
|
16
|
-
/** Parsed changes; handy for tests and for a "what changed" summary. */
|
|
22
|
+
/** Parsed changes; handy for tests and for a "what changed" summary. Empty means nothing to write. */
|
|
17
23
|
readonly changes: readonly Change[];
|
|
18
24
|
}
|
|
19
25
|
export declare function generateMigration(tables: readonly AnyTable[], previous?: Snapshot): GenerateResult;
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { diffSnapshots } from './diff.js';
|
|
2
|
-
import { renderChanges, renderStatements } from './render.js';
|
|
2
|
+
import { renderChanges, renderConcurrentStatements, renderStatements } from './render.js';
|
|
3
3
|
import { buildSnapshot, EMPTY_SNAPSHOT } from './snapshot.js';
|
|
4
4
|
export function generateMigration(tables, previous = EMPTY_SNAPSHOT) {
|
|
5
5
|
const snapshot = buildSnapshot(tables);
|
|
6
6
|
const changes = diffSnapshots(previous, snapshot);
|
|
7
|
-
return {
|
|
7
|
+
return {
|
|
8
|
+
sql: renderChanges(changes),
|
|
9
|
+
statements: renderStatements(changes),
|
|
10
|
+
concurrently: renderConcurrentStatements(changes),
|
|
11
|
+
snapshot,
|
|
12
|
+
changes,
|
|
13
|
+
};
|
|
8
14
|
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { Change } from './diff.ts';
|
|
2
2
|
export declare function renderChange(change: Change): string[];
|
|
3
|
-
/**
|
|
3
|
+
/** The statements of the ordinary migration, that is, everything but the `CONCURRENTLY` ones. */
|
|
4
4
|
export declare function renderStatements(changes: readonly Change[]): string[];
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* The statements of the `--> no-transaction` migration: `CONCURRENTLY` drops and
|
|
7
|
+
* creates. A replaced index yields its drop twice, as a change of its own and as
|
|
8
|
+
* the guard in front of the create, so duplicates are folded.
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderConcurrentStatements(changes: readonly Change[]): string[];
|
|
11
|
+
/**
|
|
12
|
+
* The ordinary migration as one text: multi-line statements are separated by a
|
|
13
|
+
* blank line, single-line ones follow each other. `writeMigration` writes it as is.
|
|
14
|
+
*/
|
|
6
15
|
export declare function renderChanges(changes: readonly Change[]): string;
|
package/dist/generator/render.js
CHANGED
|
@@ -67,8 +67,19 @@ function createTable(table) {
|
|
|
67
67
|
}
|
|
68
68
|
function createIndex(table, index) {
|
|
69
69
|
const unique = index.unique ? 'UNIQUE ' : '';
|
|
70
|
+
const concurrently = index.concurrently ? 'CONCURRENTLY ' : '';
|
|
70
71
|
const where = index.where === null ? '' : ` WHERE ${index.where}`;
|
|
71
|
-
return `CREATE ${unique}INDEX ${q(index.name)} ON ${q(table)} (${columns(index.columns)})${where};`;
|
|
72
|
+
return `CREATE ${unique}INDEX ${concurrently}${q(index.name)} ON ${q(table)} (${columns(index.columns)})${where};`;
|
|
73
|
+
}
|
|
74
|
+
function dropIndexConcurrently(name) {
|
|
75
|
+
return `DROP INDEX CONCURRENTLY IF EXISTS ${q(name)};`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Changes whose SQL postgres refuses inside a transaction, and inside a
|
|
79
|
+
* multi-statement query too. `writeMigration` gives them a migration of their own.
|
|
80
|
+
*/
|
|
81
|
+
function isConcurrent(change) {
|
|
82
|
+
return (change.kind === 'createIndex' && change.index.concurrently) || (change.kind === 'dropIndex' && change.concurrently);
|
|
72
83
|
}
|
|
73
84
|
function alterColumn(table, from, to) {
|
|
74
85
|
const head = `ALTER TABLE ${q(table)} ALTER COLUMN ${q(to.name)}`;
|
|
@@ -111,18 +122,34 @@ export function renderChange(change) {
|
|
|
111
122
|
`ADD ${constraintClause(change.constraint)};`,
|
|
112
123
|
];
|
|
113
124
|
case 'createIndex':
|
|
114
|
-
|
|
125
|
+
// a failed CREATE INDEX CONCURRENTLY leaves an invalid index behind, and the
|
|
126
|
+
// migration stays unrecorded; the drop in front makes the rerun clean
|
|
127
|
+
return change.index.concurrently
|
|
128
|
+
? [dropIndexConcurrently(change.index.name), createIndex(change.table, change.index)]
|
|
129
|
+
: [createIndex(change.table, change.index)];
|
|
115
130
|
case 'dropIndex':
|
|
116
|
-
return [`DROP INDEX ${q(change.index)};`];
|
|
131
|
+
return [change.concurrently ? dropIndexConcurrently(change.index) : `DROP INDEX ${q(change.index)};`];
|
|
117
132
|
default:
|
|
118
133
|
return unreachable(change);
|
|
119
134
|
}
|
|
120
135
|
}
|
|
121
|
-
/**
|
|
136
|
+
/** The statements of the ordinary migration, that is, everything but the `CONCURRENTLY` ones. */
|
|
122
137
|
export function renderStatements(changes) {
|
|
123
|
-
return changes.flatMap(renderChange);
|
|
138
|
+
return changes.filter(change => !isConcurrent(change)).flatMap(renderChange);
|
|
124
139
|
}
|
|
125
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* The statements of the `--> no-transaction` migration: `CONCURRENTLY` drops and
|
|
142
|
+
* creates. A replaced index yields its drop twice, as a change of its own and as
|
|
143
|
+
* the guard in front of the create, so duplicates are folded.
|
|
144
|
+
*/
|
|
145
|
+
export function renderConcurrentStatements(changes) {
|
|
146
|
+
const statements = changes.filter(isConcurrent).flatMap(renderChange);
|
|
147
|
+
return statements.filter((statement, i) => statements.indexOf(statement) === i);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The ordinary migration as one text: multi-line statements are separated by a
|
|
151
|
+
* blank line, single-line ones follow each other. `writeMigration` writes it as is.
|
|
152
|
+
*/
|
|
126
153
|
export function renderChanges(changes) {
|
|
127
154
|
const statements = renderStatements(changes);
|
|
128
155
|
let out = '';
|
|
@@ -25,6 +25,12 @@ export interface TableSnapshot {
|
|
|
25
25
|
readonly unique: boolean;
|
|
26
26
|
readonly columns: readonly string[];
|
|
27
27
|
readonly where: string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Built and dropped with `CONCURRENTLY`. How the index is built, not what it
|
|
30
|
+
* is: the diff ignores it when comparing indexes. Snapshots written before
|
|
31
|
+
* the field existed simply lack it, which reads as `false`.
|
|
32
|
+
*/
|
|
33
|
+
readonly concurrently: boolean;
|
|
28
34
|
}[];
|
|
29
35
|
readonly foreignKeys: readonly {
|
|
30
36
|
readonly name: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,27 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* kysely-ddl: PostgreSQL schema as code
|
|
3
|
-
*
|
|
2
|
+
* kysely-ddl: PostgreSQL schema as code, SQL migration generation and the
|
|
3
|
+
* Kysely side of it, row types and jsonb values. Imports `kysely`, the peer
|
|
4
|
+
* dependency, and no driver.
|
|
4
5
|
*
|
|
5
6
|
* Layers and dependency direction:
|
|
6
7
|
*
|
|
7
|
-
* table ◄── generator ◄── migrator ◄──
|
|
8
|
+
* table ◄── generator ◄── migrator/store ◄── migrator (runner, provider)
|
|
9
|
+
* table ◄── kysely (row types, jsonb values)
|
|
8
10
|
*
|
|
9
11
|
* Entry points:
|
|
10
12
|
*
|
|
11
|
-
* kysely-ddl
|
|
12
|
-
*
|
|
13
|
+
* kysely-ddl — this file: tables, generation, migration files on disk,
|
|
14
|
+
* `inferKyselyTable` / `inferKyselyDatabase`, `jsonb` / `jsonbArray`
|
|
15
|
+
* kysely-ddl/migrator — running migrations: `createMigrator` / `migrateToLatest`
|
|
16
|
+
* and `sqlFileMigrationProvider`; separate, so that a project
|
|
17
|
+
* which applies migrations some other way does not pull it in
|
|
13
18
|
*
|
|
14
19
|
* Exports are sorted by module path: generator (snapshot, diff, render,
|
|
15
|
-
* facade), migrator (files on disk), table
|
|
20
|
+
* facade), kysely (row types, jsonb values), migrator (files on disk), table
|
|
21
|
+
* (table definitions).
|
|
16
22
|
*/
|
|
17
23
|
export { diffSnapshots } from './generator/diff.ts';
|
|
18
24
|
export type { Change, Constraint } from './generator/diff.ts';
|
|
19
25
|
export { generateMigration } from './generator/generate.ts';
|
|
20
26
|
export type { GenerateResult } from './generator/generate.ts';
|
|
21
|
-
export { renderChange, renderChanges, renderStatements } from './generator/render.ts';
|
|
27
|
+
export { renderChange, renderChanges, renderConcurrentStatements, renderStatements } from './generator/render.ts';
|
|
22
28
|
export { buildSnapshot, EMPTY_SNAPSHOT, SNAPSHOT_VERSION } from './generator/snapshot.ts';
|
|
23
29
|
export type { ColumnSnapshot, Snapshot, TableSnapshot } from './generator/snapshot.ts';
|
|
24
|
-
export {
|
|
30
|
+
export type { inferKyselyDatabase, inferKyselyTable, InferOptions } from './kysely/infer.ts';
|
|
31
|
+
export { jsonb, jsonbArray } from './kysely/json.ts';
|
|
32
|
+
export type { Jsonb } from './kysely/json.ts';
|
|
33
|
+
export { CONCURRENTLY_SUFFIX, listMigrations, MIGRATION_EXTENSION, migrationTimestamp, NO_TRANSACTION_MARKER, readLatestSnapshot, readMigration, readStatements, SNAPSHOT_FILE, STATEMENT_SEPARATOR, writeMigration, } from './migrator/store.ts';
|
|
34
|
+
export type { MigrationFile } from './migrator/store.ts';
|
|
25
35
|
export { toCamelCase, toSnakeCase } from './table/casing.ts';
|
|
26
36
|
export type { CamelCase, SnakeCase } from './table/casing.ts';
|
|
27
37
|
export { ColumnBuilder, columnBuilders } from './table/columns.ts';
|