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 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
  [![CI](https://github.com/hehmonke/kysely-ddl/actions/workflows/ci.yml/badge.svg)](https://github.com/hehmonke/kysely-ddl/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/kysely-ddl)](https://www.npmjs.com/package/kysely-ddl)
5
+ [![license](https://img.shields.io/npm/l/kysely-ddl)](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. Does not import Kysely |
32
- | `kysely-ddl/kysely` | `inferKyselyTable`, `inferKyselyDatabase`, `jsonb` / `jsonbArray`, `createMigrator` / `migrateToLatest`, `sqlFileMigrationProvider` |
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
- tableName: 'user',
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
- tableName: 'session',
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.statements.length === 0) {
79
+ if (result.changes.length === 0) {
79
80
  console.log('no changes');
80
81
  } else {
81
- console.log(writeMigration(dir, process.argv[2] ?? 'migration', result)); // 20260910123045_migration
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/kysely';
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/kysely';
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 and partial via `where`), `foreignKeys` (`onDelete` / `onUpdate`), `checks`; names are optional everywhere |
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`. For another type use `$type<T>()`
135
- and convert on your side.
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
- ### What is missing
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. Everything is
190
- added the same way: a field in `TableSpec` -> a field in the snapshot -> a branch
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/kysely';
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. With Kysely's `CamelCasePlugin`
208
- pass `true` as the second parameter and the column and table keys become
209
- camelCase, see below. Write rules:
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, both `infer` types take `true`
223
- as the second parameter:
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/kysely';
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` file and updates
298
- `snapshot.json`, `readLatestSnapshot(dir)` reads it for the next diff, and
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
- Inside a `.sql` file, statements are separated by the `--> statement-breakpoint`
304
- comment: the file stays valid for `psql`, and the runner splits on it to execute
305
- statements one at a time so that an error points at a specific statement.
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/kysely';
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: 'all', // 'all' | 'each' | 'none'
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: 'all'` | the whole run in one transaction, like Kysely: a failing migration rolls back the earlier ones from the same run |
362
- | `transaction: 'each'` | one transaction per migration: the ones before the failure stay applied |
363
- | `transaction: 'none'` | no transactions, for `CREATE INDEX CONCURRENTLY` and the like |
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 statement throws `MigrationError` with `migration`, `statement`,
367
- `applied` (what this run applied before the failure) and the driver's `cause`.
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/kysely';
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
- ## Development
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
- Releases are tagged `vX.Y.Z`, matching `version` in `package.json`: GitHub
432
- Actions runs the checks and does `npm publish` with provenance. It needs the
433
- `NPM_TOKEN` secret in the repository settings.
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)
@@ -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 {};
@@ -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
- dropIndexes.push({ kind: 'dropIndex', index: index.name });
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
- /** Migration SQL; an empty string means no changes. */
10
+ /** The migration text, without the `CONCURRENTLY` statements; an empty string means none. */
11
11
  readonly sql: string;
12
- /** The same SQL as individual statements; the runner executes these. */
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 { sql: renderChanges(changes), statements: renderStatements(changes), snapshot, changes };
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
- /** Individual statements; `writeMigration` writes these to disk. */
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
- /** Multi-line statements are separated by a blank line, single-line ones follow each other. */
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;
@@ -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
- return [createIndex(change.table, change.index)];
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
- /** Individual statements; `writeMigration` writes these to disk. */
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
- /** Multi-line statements are separated by a blank line, single-line ones follow each other. */
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;
@@ -29,6 +29,7 @@ function tableSnapshot(spec) {
29
29
  unique: i.unique,
30
30
  columns: [...i.columns],
31
31
  where: i.where !== undefined ? renderSql(i.where) : null,
32
+ concurrently: i.concurrently,
32
33
  })),
33
34
  foreignKeys: spec.foreignKeys.map(f => ({
34
35
  name: f.name,
package/dist/index.d.ts CHANGED
@@ -1,27 +1,37 @@
1
1
  /**
2
- * kysely-ddl: PostgreSQL schema as code and SQL migration generation.
3
- * Has no runtime dependency on Kysely or on any driver.
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 ◄── kysely
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 — this file: tables, generation, migration files
12
- * kysely-ddl/kysely — row types, migration runner, provider for Migrator
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 (table definitions).
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 { listMigrations, MIGRATION_EXTENSION, migrationTimestamp, readLatestSnapshot, readStatements, SNAPSHOT_FILE, STATEMENT_SEPARATOR, writeMigration, } from './migrator/store.ts';
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';