kysely 0.29.4 → 0.30.0-beta.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua)
2
2
 
3
3
  [![NPM Version](https://img.shields.io/npm/v/kysely?style=flat&label=latest)](https://github.com/kysely-org/kysely/releases/latest)
4
- [![Socket Badge](https://badge.socket.dev/npm/package/kysely/0.29.4)](https://socket.dev/npm/package/kysely/overview/0.29.4)
4
+ [![Socket Badge](https://badge.socket.dev/npm/package/kysely/0.30.0-beta.1)](https://socket.dev/npm/package/kysely/overview/0.30.0-beta.1)
5
5
  [![Tests](https://github.com/kysely-org/kysely/actions/workflows/test.yml/badge.svg)](https://github.com/kysely-org/kysely)
6
6
  [![License](https://img.shields.io/github/license/kysely-org/kysely?style=flat)](https://github.com/kysely-org/kysely/blob/master/LICENSE)
7
7
  [![Issues](https://img.shields.io/github/issues-closed/kysely-org/kysely?logo=github)](https://github.com/kysely-org/kysely/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc)
@@ -5,23 +5,25 @@ import type { AbortableOperationOptions } from '../../util/abort.js';
5
5
  */
6
6
  export interface MysqlDialectConfig {
7
7
  /**
8
- * A `mysql2` `Client` constructor, to be used for connecting to the database
9
- * outside of the `pool` to avoid waiting for an idle connection.
8
+ * A `createConnection` function (from either `mysql2` or `mysql2/promise`),
9
+ * to be used for connecting to the database outside of the `pool` to avoid
10
+ * waiting for an idle connection.
10
11
  *
11
12
  * This is useful for cancelling queries on the database side.
12
13
  */
13
14
  controlConnection?: {
14
- (connectionUri: string): MysqlConnection;
15
- (config: object): MysqlConnection;
15
+ (connectionUri: string): MysqlConnection | Promise<MysqlPromiseConnection>;
16
+ (config: object): MysqlConnection | Promise<MysqlPromiseConnection>;
16
17
  };
17
18
  /**
18
- * A mysql2 Pool instance or a function that returns one.
19
+ * A mysql2 Pool instance (from either `mysql2` or `mysql2/promise`) or a
20
+ * function that returns one.
19
21
  *
20
22
  * If a function is provided, it's called once when the first query is executed.
21
23
  *
22
24
  * https://github.com/sidorares/node-mysql2#using-connection-pools
23
25
  */
24
- pool: MysqlPool | ((options?: AbortableOperationOptions) => Promise<MysqlPool>);
26
+ pool: MysqlPool | MysqlPromisePool | ((options?: AbortableOperationOptions) => Promise<MysqlPool | MysqlPromisePool>);
25
27
  /**
26
28
  * Called once for each created connection.
27
29
  */
@@ -43,6 +45,17 @@ export interface MysqlPool {
43
45
  getConnection(callback: (error: unknown, connection: MysqlPoolConnection) => void): void;
44
46
  end(callback: (error: unknown) => void): void;
45
47
  }
48
+ /**
49
+ * This interface is the subset of `mysql2/promise`'s `Pool` class that
50
+ * kysely needs.
51
+ *
52
+ * It's a thin promise wrapper around the callback pool, which it exposes
53
+ * as its `pool` property. The dialect unwraps it and uses the callback
54
+ * pool directly.
55
+ */
56
+ export interface MysqlPromisePool {
57
+ pool: MysqlPool;
58
+ }
46
59
  export interface MysqlConnection {
47
60
  config: object;
48
61
  connect(callback?: (error: unknown) => void): void;
@@ -52,6 +65,18 @@ export interface MysqlConnection {
52
65
  };
53
66
  threadId: number;
54
67
  }
68
+ /**
69
+ * This interface is the subset of `mysql2/promise`'s `Connection` class that
70
+ * kysely needs.
71
+ *
72
+ * It's a thin promise wrapper around the callback connection. It's already
73
+ * connected when the `createConnection` promise resolves, so the dialect
74
+ * uses its promise-based API directly.
75
+ */
76
+ export interface MysqlPromiseConnection {
77
+ destroy(): void;
78
+ query(sql: string, values?: unknown[]): Promise<unknown>;
79
+ }
55
80
  export type MysqlConectionConstructor = new (opts?: object) => MysqlConnection;
56
81
  export interface MysqlPoolConnection extends MysqlConnection {
57
82
  release(): void;
@@ -11,7 +11,7 @@ import type { MysqlDialectConfig } from './mysql-dialect-config.js';
11
11
  * The constructor takes an instance of {@link MysqlDialectConfig}.
12
12
  *
13
13
  * ```ts
14
- * import { createPool } from 'mysql2'
14
+ * import { createPool } from 'mysql2/promise' // or just 'mysql2' — both work
15
15
  *
16
16
  * new MysqlDialect({
17
17
  * pool: createPool({
@@ -25,7 +25,7 @@ import type { MysqlDialectConfig } from './mysql-dialect-config.js';
25
25
  * can be a function:
26
26
  *
27
27
  * ```ts
28
- * import { createPool } from 'mysql2'
28
+ * import { createPool } from 'mysql2/promise' // or just 'mysql2' — both work
29
29
  *
30
30
  * new MysqlDialect({
31
31
  * pool: async () => createPool({
@@ -9,7 +9,7 @@ import { MysqlAdapter } from './mysql-adapter.js';
9
9
  * The constructor takes an instance of {@link MysqlDialectConfig}.
10
10
  *
11
11
  * ```ts
12
- * import { createPool } from 'mysql2'
12
+ * import { createPool } from 'mysql2/promise' // or just 'mysql2' — both work
13
13
  *
14
14
  * new MysqlDialect({
15
15
  * pool: createPool({
@@ -23,7 +23,7 @@ import { MysqlAdapter } from './mysql-adapter.js';
23
23
  * can be a function:
24
24
  *
25
25
  * ```ts
26
- * import { createPool } from 'mysql2'
26
+ * import { createPool } from 'mysql2/promise' // or just 'mysql2' — both work
27
27
  *
28
28
  * new MysqlDialect({
29
29
  * pool: async () => createPool({
@@ -13,9 +13,12 @@ export class MysqlDriver {
13
13
  this.#config = freeze({ ...config });
14
14
  }
15
15
  async init(options) {
16
- this.#pool = isFunction(this.#config.pool)
16
+ const pool = isFunction(this.#config.pool)
17
17
  ? await this.#config.pool(options)
18
18
  : this.#config.pool;
19
+ // A `mysql2/promise` pool's `getConnection` ignores the callback we pass
20
+ // it, hanging any query forever. Unwrap the callback pool it decorates.
21
+ this.#pool = isPromisePool(pool) ? pool.pool : pool;
19
22
  }
20
23
  async acquireConnection(options) {
21
24
  const rawConnection = await this.#acquireConnection();
@@ -93,6 +96,12 @@ export class MysqlDriver {
93
96
  });
94
97
  }
95
98
  }
99
+ function isPromisePool(pool) {
100
+ return ('pool' in pool && isObject(pool.pool) && isFunction(pool.pool.getConnection));
101
+ }
102
+ function isPromiseConnection(connection) {
103
+ return isFunction(connection.then);
104
+ }
96
105
  function isOkPacket(obj) {
97
106
  return isObject(obj) && 'insertId' in obj && 'affectedRows' in obj;
98
107
  }
@@ -224,7 +233,24 @@ class MysqlConnection {
224
233
  const {
225
234
  // omitting these as they cause a warning and perhaps a future error when passed.
226
235
  clientFlags: _, maxPacketSize: __, ...cfg } = config;
227
- const controlConnection = this.#controlConnection(cfg);
236
+ const controlConnectionOrPromise = this.#controlConnection(cfg);
237
+ // `mysql2/promise`'s `createConnection` returns a promise of a connection
238
+ // that's already connected, with a promise-based API. Use it directly.
239
+ if (isPromiseConnection(controlConnectionOrPromise)) {
240
+ const controlConnection = await controlConnectionOrPromise;
241
+ try {
242
+ // by the time we get the connection, another query might have been executed.
243
+ // we need to ensure we're not canceling the wrong query.
244
+ if (queryIdToCancel.queryId === this.#queryId?.queryId) {
245
+ await controlConnection.query(query, []);
246
+ }
247
+ }
248
+ finally {
249
+ controlConnection.destroy();
250
+ }
251
+ return;
252
+ }
253
+ const controlConnection = controlConnectionOrPromise;
228
254
  try {
229
255
  await new Promise((resolve, reject) => {
230
256
  controlConnection.connect((connectError) => {
@@ -1,7 +1,14 @@
1
1
  import type { Expression } from '../expression/expression.js';
2
2
  import type { SelectQueryBuilderExpression } from '../query-builder/select-query-builder-expression.js';
3
3
  import type { RawBuilder } from '../raw-builder/raw-builder.js';
4
- import type { ShallowDehydrateObject, ShallowDehydrateValue, Simplify } from '../util/type-utils.js';
4
+ import type { KyselyTypeError } from '../util/type-error.js';
5
+ import type { IsNever, ShallowDehydrateObject, ShallowDehydrateValue, Simplify } from '../util/type-utils.js';
6
+ /**
7
+ * Used to check for `BLOB` column existence in result records.
8
+ */
9
+ export type ExtractBlobs<O> = {
10
+ [K in keyof O]: O[K] extends Expression<infer V> ? Extract<V, Uint8Array> : Extract<O[K], Uint8Array>;
11
+ }[keyof O];
5
12
  /**
6
13
  * A SQLite helper for aggregating a subquery into a JSON array.
7
14
  *
@@ -63,7 +70,7 @@ import type { ShallowDehydrateObject, ShallowDehydrateValue, Simplify } from '..
63
70
  * from "person"
64
71
  * ```
65
72
  */
66
- export declare function jsonArrayFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<ShallowDehydrateObject<O>>[]>;
73
+ export declare function jsonArrayFrom<O>(expr: IsNever<ExtractBlobs<O>> extends true ? SelectQueryBuilderExpression<O> : KyselyTypeError<'SQLite does not support passing `BLOB` values to `json_object`. Cast to `TEXT`.'>): RawBuilder<Simplify<ShallowDehydrateObject<O>>[]>;
67
74
  /**
68
75
  * A SQLite helper for turning a subquery into a JSON object.
69
76
  *
@@ -127,7 +134,7 @@ export declare function jsonArrayFrom<O>(expr: SelectQueryBuilderExpression<O>):
127
134
  * from "person";
128
135
  * ```
129
136
  */
130
- export declare function jsonObjectFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<ShallowDehydrateObject<O>> | null>;
137
+ export declare function jsonObjectFrom<O>(expr: IsNever<ExtractBlobs<O>> extends true ? SelectQueryBuilderExpression<O> : KyselyTypeError<'SQLite does not support passing `BLOB` values to `json_object`. Cast to `TEXT`.'>): RawBuilder<Simplify<ShallowDehydrateObject<O>> | null>;
131
138
  /**
132
139
  * The SQLite `json_object` function.
133
140
  *
@@ -184,6 +191,6 @@ export declare function jsonObjectFrom<O>(expr: SelectQueryBuilderExpression<O>)
184
191
  * from "person"
185
192
  * ```
186
193
  */
187
- export declare function jsonBuildObject<O extends Record<string, Expression<unknown>>>(obj: O): RawBuilder<Simplify<{
194
+ export declare function jsonBuildObject<O extends Record<string, Expression<unknown>>>(obj: IsNever<ExtractBlobs<O>> extends true ? O : KyselyTypeError<'SQLite does not support passing `BLOB` values to `json_object`. Cast to `TEXT`.'>): RawBuilder<Simplify<{
188
195
  [K in keyof O]: O[K] extends Expression<infer V> ? ShallowDehydrateValue<V> : never;
189
196
  }>>;
@@ -188,7 +188,10 @@ export function jsonObjectFrom(expr) {
188
188
  * ```
189
189
  */
190
190
  export function jsonBuildObject(obj) {
191
- return sql `json_object(${sql.join(Object.keys(obj).flatMap((k) => [sql.lit(k), obj[k]]))})`;
191
+ return sql `json_object(${sql.join(Object.keys(obj).flatMap((k) => [
192
+ sql.lit(k),
193
+ obj[k],
194
+ ]))})`;
192
195
  }
193
196
  function getSqliteJsonObjectArgs(node, table) {
194
197
  try {
@@ -1,11 +1,12 @@
1
- import type { Migration, MigrationProvider } from './migrator.js';
1
+ import type { Migration } from './migration.js';
2
+ import type { MigrationProvider } from './migrator.js';
2
3
  /**
3
4
  * Reads all migrations from a folder.
4
5
  *
5
6
  * ### Examples
6
7
  *
7
8
  * ```ts
8
- * import { promises as fs } from 'node:fs'
9
+ * import fs from 'node:fs/promises'
9
10
  * import path from 'node:path'
10
11
  *
11
12
  * new FileMigrationProvider({
@@ -6,7 +6,7 @@ import { isFunction, isObject } from '../util/object-utils.js';
6
6
  * ### Examples
7
7
  *
8
8
  * ```ts
9
- * import { promises as fs } from 'node:fs'
9
+ * import fs from 'node:fs/promises'
10
10
  * import path from 'node:path'
11
11
  *
12
12
  * new FileMigrationProvider({
@@ -1,2 +1,3 @@
1
1
  export * from './file-migration-provider.js';
2
+ export * from './migration.js';
2
3
  export * from './migrator.js';
@@ -1,3 +1,4 @@
1
1
  /// <reference types="./index.d.ts" />
2
2
  export * from './file-migration-provider.js';
3
+ export * from './migration.js';
3
4
  export * from './migrator.js';
@@ -0,0 +1,70 @@
1
+ import type { Kysely } from '../kysely.js';
2
+ /**
3
+ * The shape of a migration's module.
4
+ */
5
+ export interface Migration {
6
+ /**
7
+ * Configuration object for this migration module.
8
+ *
9
+ * See {@link MigrationConfig}.
10
+ */
11
+ config?: MigrationConfig;
12
+ /**
13
+ * The `down` method that reverts the changes made by it's sibling {@link Migration.up}
14
+ * method.
15
+ */
16
+ down?(db: Kysely<any>): Promise<void>;
17
+ /**
18
+ * The `up` method that changes the database schema.
19
+ *
20
+ * See {@link Migration.down}.
21
+ */
22
+ up(db: Kysely<any>): Promise<void>;
23
+ }
24
+ /**
25
+ * Configuration for a single migration.
26
+ */
27
+ export interface MigrationConfig {
28
+ /**
29
+ * Controls whether this migration runs inside its own transaction, when
30
+ * {@link MigrateOptions.transactionMode} is `'per-migration'` — the only
31
+ * mode that honors this property. Defaults to `true` in that mode: the
32
+ * migration runs in its own transaction unless this is set to `false`.
33
+ *
34
+ * In any other transaction mode, leaving this property out is the only
35
+ * valid state — providing a value, either one, results in an error: in
36
+ * `'per-run'` mode a single shared transaction cannot partially exclude a
37
+ * migration, and in `'none'` mode there are no transactions to configure.
38
+ * This is the one place an explicit `true` differs from the default —
39
+ * `true` asserts a requirement that non-`'per-migration'` modes refuse,
40
+ * while leaving the property out never errors.
41
+ *
42
+ * Setting `false` makes the migration run without a transaction. This is
43
+ * required for statements that cannot run inside a transaction, such as
44
+ * PostgreSQL's `CREATE INDEX CONCURRENTLY`. Note that without a
45
+ * transaction, a failure can leave the migration partially applied, and the
46
+ * migration table record is written separately after the migration
47
+ * completes. Prefer a single statement per non-transactional migration, and
48
+ * write it so it can be safely retried — for example, drop a
49
+ * possibly-leftover invalid index before creating it.
50
+ */
51
+ readonly transaction?: boolean;
52
+ }
53
+ /**
54
+ * Helps define a single migration by taking over types duties and enforcing the contract.
55
+ *
56
+ * ### Examples
57
+ *
58
+ * ```ts
59
+ * import { defineMigration } from 'kysely/migration'
60
+ *
61
+ * export default defineMigration({
62
+ * async up(db) {
63
+ * // ...
64
+ * },
65
+ *
66
+ * config: { transaction: false },
67
+ * })
68
+ * ```
69
+ */
70
+ export declare function defineMigration(migration: Migration): Migration;
@@ -0,0 +1,21 @@
1
+ /// <reference types="./migration.d.ts" />
2
+ /**
3
+ * Helps define a single migration by taking over types duties and enforcing the contract.
4
+ *
5
+ * ### Examples
6
+ *
7
+ * ```ts
8
+ * import { defineMigration } from 'kysely/migration'
9
+ *
10
+ * export default defineMigration({
11
+ * async up(db) {
12
+ * // ...
13
+ * },
14
+ *
15
+ * config: { transaction: false },
16
+ * })
17
+ * ```
18
+ */
19
+ export function defineMigration(migration) {
20
+ return migration;
21
+ }
@@ -1,19 +1,10 @@
1
1
  import type { Kysely } from '../kysely.js';
2
+ import type { Migration } from './migration.js';
2
3
  export declare const DEFAULT_MIGRATION_TABLE = "kysely_migration";
3
4
  export declare const DEFAULT_MIGRATION_LOCK_TABLE = "kysely_migration_lock";
4
5
  export declare const DEFAULT_ALLOW_UNORDERED_MIGRATIONS = false;
5
6
  export declare const MIGRATION_LOCK_ID = "migration_lock";
6
7
  export declare const NO_MIGRATIONS: NoMigrations;
7
- export interface Migration {
8
- up(db: Kysely<any>): Promise<void>;
9
- /**
10
- * An optional down method.
11
- *
12
- * If you don't provide a down method, the migration is skipped when
13
- * migrating down.
14
- */
15
- down?(db: Kysely<any>): Promise<void>;
16
- }
17
8
  /**
18
9
  * A class for running migrations.
19
10
  *
@@ -25,7 +16,7 @@ export interface Migration {
25
16
  * other way.
26
17
  *
27
18
  * ```ts
28
- * import { promises as fs } from 'node:fs'
19
+ * import fs from 'node:fs/promises'
29
20
  * import path from 'node:path'
30
21
  * import * as Sqlite from 'better-sqlite3'
31
22
  * import { Kysely, SqliteDialect } from 'kysely'
@@ -73,7 +64,7 @@ export declare class Migrator {
73
64
  * ### Examples
74
65
  *
75
66
  * ```ts
76
- * import { promises as fs } from 'node:fs'
67
+ * import fs from 'node:fs/promises'
77
68
  * import path from 'node:path'
78
69
  * import * as Sqlite from 'better-sqlite3'
79
70
  * import { FileMigrationProvider, Migrator } from 'kysely/migration'
@@ -115,7 +106,7 @@ export declare class Migrator {
115
106
  * ### Examples
116
107
  *
117
108
  * ```ts
118
- * import { promises as fs } from 'node:fs'
109
+ * import fs from 'node:fs/promises'
119
110
  * import path from 'node:path'
120
111
  * import { FileMigrationProvider, Migrator } from 'kysely/migration'
121
112
  *
@@ -138,7 +129,7 @@ export declare class Migrator {
138
129
  * you can use a special constant `NO_MIGRATIONS`:
139
130
  *
140
131
  * ```ts
141
- * import { promises as fs } from 'node:fs'
132
+ * import fs from 'node:fs/promises'
142
133
  * import path from 'node:path'
143
134
  * import { FileMigrationProvider, Migrator, NO_MIGRATIONS } from 'kysely/migration'
144
135
  *
@@ -154,8 +145,33 @@ export declare class Migrator {
154
145
  *
155
146
  * await migrator.migrateTo(NO_MIGRATIONS)
156
147
  * ```
148
+ *
149
+ * By default, this method migrates up or down depending on which side of the
150
+ * current migration state the target migration is on. Pass
151
+ * {@link MigrateToOptions.direction} to enforce a direction — if reaching the
152
+ * target would require migrating the other way, nothing is executed and
153
+ * {@link MigrationResultSet.error} holds an error:
154
+ *
155
+ * ```ts
156
+ * import fs from 'node:fs/promises'
157
+ * import path from 'node:path'
158
+ * import { FileMigrationProvider, Migrator } from 'kysely/migration'
159
+ *
160
+ * const migrator = new Migrator({
161
+ * db,
162
+ * provider: new FileMigrationProvider({
163
+ * fs,
164
+ * // Path to the folder that contains all your migrations.
165
+ * migrationFolder: 'some/path/to/migrations',
166
+ * path,
167
+ * })
168
+ * })
169
+ *
170
+ * // Errors if `some_migration` is already executed.
171
+ * await migrator.migrateTo('some_migration', { direction: 'Up' })
172
+ * ```
157
173
  */
158
- migrateTo(targetMigrationName: string | NoMigrations, options?: MigrateOptions): Promise<MigrationResultSet>;
174
+ migrateTo(targetMigrationName: string | NoMigrations, options?: MigrateToOptions): Promise<MigrationResultSet>;
159
175
  /**
160
176
  * Migrate one step up.
161
177
  *
@@ -167,7 +183,7 @@ export declare class Migrator {
167
183
  * ### Examples
168
184
  *
169
185
  * ```ts
170
- * import { promises as fs } from 'node:fs'
186
+ * import fs from 'node:fs/promises'
171
187
  * import path from 'node:path'
172
188
  * import { FileMigrationProvider, Migrator } from 'kysely/migration'
173
189
  *
@@ -196,7 +212,7 @@ export declare class Migrator {
196
212
  * ### Examples
197
213
  *
198
214
  * ```ts
199
- * import { promises as fs } from 'node:fs'
215
+ * import fs from 'node:fs/promises'
200
216
  * import path from 'node:path'
201
217
  * import { FileMigrationProvider, Migrator } from 'kysely/migration'
202
218
  *
@@ -221,9 +237,65 @@ export interface MigrateOptions {
221
237
  *
222
238
  * Default is `false`.
223
239
  *
224
- * This is useful when some migrations include queries that would fail otherwise.
240
+ * @deprecated Use {@link transactionMode} instead — `disableTransactions: true`
241
+ * is equivalent to `transactionMode: 'none'`. Setting both to contradicting
242
+ * values results in an error.
225
243
  */
226
244
  readonly disableTransactions?: boolean;
245
+ /**
246
+ * Controls how migrations are wrapped in transactions, when the dialect
247
+ * supports transactional DDL ({@link DialectAdapter.supportsTransactionalDdl}).
248
+ *
249
+ * - `'per-run'` — the entire migration run is wrapped in a single transaction.
250
+ * Either every migration in the run is applied, or none are. Any migration
251
+ * with a per-migration `transaction` configuration ({@link MigrationConfig.transaction})
252
+ * results in an error, since a single shared transaction cannot partially
253
+ * exclude a migration. Requires transactional DDL — results in an error on
254
+ * dialects without it (e.g. MySQL).
255
+ *
256
+ * - `'per-migration'` — each migration runs in its own transaction, together
257
+ * with the insertion/deletion of its migration table record. A failure rolls
258
+ * back only the failing migration; previously completed migrations stay
259
+ * applied. Individual migrations can opt out of their transaction with
260
+ * `config: { transaction: false }` — required for statements that cannot
261
+ * run inside a transaction, such as PostgreSQL's `CREATE INDEX CONCURRENTLY`
262
+ * or `ALTER TYPE ... ADD VALUE` (pre-12). Requires transactional DDL —
263
+ * results in an error on dialects without it.
264
+ *
265
+ * - `'none'` — migrations run without any transactions. Any migration with a
266
+ * per-migration `transaction` configuration results in an error.
267
+ *
268
+ * When not provided, the current default is `'per-run'` on dialects that
269
+ * support transactional DDL and `'none'` on dialects that don't. This
270
+ * default might change to `'per-migration'` in a future version of Kysely —
271
+ * running multiple migrations without an explicit `transactionMode` logs a
272
+ * message about this. Provide an explicit `transactionMode` to make the
273
+ * migrator's behavior independent of the default.
274
+ *
275
+ * Note that transactions spanning multiple migrations (`'per-run'`) can make
276
+ * otherwise-valid migration sequences fail. For example, a new PostgreSQL enum
277
+ * value cannot be used in the same transaction that added it, so a migration
278
+ * that runs `ALTER TYPE ... ADD VALUE` followed by a migration that uses the
279
+ * new value works when the two run in separate transactions, but fails when
280
+ * both are pending in a single `'per-run'` mode run.
281
+ */
282
+ readonly transactionMode?: MigratorTransactionMode;
283
+ }
284
+ export type MigratorTransactionMode = 'per-run' | 'per-migration' | 'none';
285
+ export interface MigrateToOptions extends MigrateOptions {
286
+ /**
287
+ * Enforces a migration direction.
288
+ *
289
+ * When provided and reaching the target migration would require migrating
290
+ * the other way, nothing is executed and {@link MigrationResultSet.error}
291
+ * holds an error. For example, `direction: 'Up'` errors when the target
292
+ * migration is already executed.
293
+ *
294
+ * By default, {@link Migrator.migrateTo | migrateTo} migrates up or down
295
+ * depending on which side of the current migration state the target
296
+ * migration is on.
297
+ */
298
+ readonly direction?: MigrationDirection;
227
299
  }
228
300
  export interface MigratorProps extends MigrateOptions {
229
301
  readonly db: Kysely<any>;
@@ -327,7 +399,7 @@ export interface MigrationResultSet {
327
399
  */
328
400
  readonly results?: MigrationResult[];
329
401
  }
330
- type MigrationDirection = 'Up' | 'Down';
402
+ export type MigrationDirection = 'Up' | 'Down';
331
403
  export interface MigrationResult {
332
404
  readonly migrationName: string;
333
405
  /**
@@ -390,4 +462,3 @@ export interface MigrationInfo {
390
462
  */
391
463
  executedAt?: Date;
392
464
  }
393
- export {};
@@ -1,7 +1,8 @@
1
1
  /// <reference types="./migrator.d.ts" />
2
2
  import { NoopPlugin } from '../plugin/noop-plugin.js';
3
3
  import { WithSchemaPlugin } from '../plugin/with-schema/with-schema-plugin.js';
4
- import { freeze, getLast, isObject } from '../util/object-utils.js';
4
+ import { logOnce } from '../util/log-once.js';
5
+ import { freeze, isObject } from '../util/object-utils.js';
5
6
  export const DEFAULT_MIGRATION_TABLE = 'kysely_migration';
6
7
  export const DEFAULT_MIGRATION_LOCK_TABLE = 'kysely_migration_lock';
7
8
  export const DEFAULT_ALLOW_UNORDERED_MIGRATIONS = false;
@@ -18,7 +19,7 @@ export const NO_MIGRATIONS = freeze({ __noMigrations__: true });
18
19
  * other way.
19
20
  *
20
21
  * ```ts
21
- * import { promises as fs } from 'node:fs'
22
+ * import fs from 'node:fs/promises'
22
23
  * import path from 'node:path'
23
24
  * import * as Sqlite from 'better-sqlite3'
24
25
  * import { Kysely, SqliteDialect } from 'kysely'
@@ -87,7 +88,7 @@ export class Migrator {
87
88
  * ### Examples
88
89
  *
89
90
  * ```ts
90
- * import { promises as fs } from 'node:fs'
91
+ * import fs from 'node:fs/promises'
91
92
  * import path from 'node:path'
92
93
  * import * as Sqlite from 'better-sqlite3'
93
94
  * import { FileMigrationProvider, Migrator } from 'kysely/migration'
@@ -131,7 +132,7 @@ export class Migrator {
131
132
  * ### Examples
132
133
  *
133
134
  * ```ts
134
- * import { promises as fs } from 'node:fs'
135
+ * import fs from 'node:fs/promises'
135
136
  * import path from 'node:path'
136
137
  * import { FileMigrationProvider, Migrator } from 'kysely/migration'
137
138
  *
@@ -154,7 +155,7 @@ export class Migrator {
154
155
  * you can use a special constant `NO_MIGRATIONS`:
155
156
  *
156
157
  * ```ts
157
- * import { promises as fs } from 'node:fs'
158
+ * import fs from 'node:fs/promises'
158
159
  * import path from 'node:path'
159
160
  * import { FileMigrationProvider, Migrator, NO_MIGRATIONS } from 'kysely/migration'
160
161
  *
@@ -170,11 +171,39 @@ export class Migrator {
170
171
  *
171
172
  * await migrator.migrateTo(NO_MIGRATIONS)
172
173
  * ```
174
+ *
175
+ * By default, this method migrates up or down depending on which side of the
176
+ * current migration state the target migration is on. Pass
177
+ * {@link MigrateToOptions.direction} to enforce a direction — if reaching the
178
+ * target would require migrating the other way, nothing is executed and
179
+ * {@link MigrationResultSet.error} holds an error:
180
+ *
181
+ * ```ts
182
+ * import fs from 'node:fs/promises'
183
+ * import path from 'node:path'
184
+ * import { FileMigrationProvider, Migrator } from 'kysely/migration'
185
+ *
186
+ * const migrator = new Migrator({
187
+ * db,
188
+ * provider: new FileMigrationProvider({
189
+ * fs,
190
+ * // Path to the folder that contains all your migrations.
191
+ * migrationFolder: 'some/path/to/migrations',
192
+ * path,
193
+ * })
194
+ * })
195
+ *
196
+ * // Errors if `some_migration` is already executed.
197
+ * await migrator.migrateTo('some_migration', { direction: 'Up' })
198
+ * ```
173
199
  */
174
200
  async migrateTo(targetMigrationName, options) {
175
201
  return this.#migrate(({ migrations, executedMigrations, pendingMigrations, }) => {
176
202
  if (isObject(targetMigrationName) &&
177
203
  targetMigrationName.__noMigrations__ === true) {
204
+ if (options?.direction === 'Up') {
205
+ throw new Error(`can't migrate up to NO_MIGRATIONS`);
206
+ }
178
207
  return { direction: 'Down', step: Infinity };
179
208
  }
180
209
  if (!migrations.find((m) => m.name === targetMigrationName)) {
@@ -183,12 +212,18 @@ export class Migrator {
183
212
  const executedIndex = executedMigrations.indexOf(targetMigrationName);
184
213
  const pendingIndex = pendingMigrations.findIndex((m) => m.name === targetMigrationName);
185
214
  if (executedIndex !== -1) {
215
+ if (options?.direction === 'Up') {
216
+ throw new Error(`migration "${targetMigrationName}" is already executed; can't migrate up to it`);
217
+ }
186
218
  return {
187
219
  direction: 'Down',
188
220
  step: executedMigrations.length - executedIndex - 1,
189
221
  };
190
222
  }
191
223
  if (pendingIndex !== -1) {
224
+ if (options?.direction === 'Down') {
225
+ throw new Error(`migration "${targetMigrationName}" isn't executed; can't migrate down to it`);
226
+ }
192
227
  return { direction: 'Up', step: pendingIndex + 1 };
193
228
  }
194
229
  throw new Error(`migration "${targetMigrationName}" isn't executed or pending`);
@@ -205,7 +240,7 @@ export class Migrator {
205
240
  * ### Examples
206
241
  *
207
242
  * ```ts
208
- * import { promises as fs } from 'node:fs'
243
+ * import fs from 'node:fs/promises'
209
244
  * import path from 'node:path'
210
245
  * import { FileMigrationProvider, Migrator } from 'kysely/migration'
211
246
  *
@@ -236,7 +271,7 @@ export class Migrator {
236
271
  * ### Examples
237
272
  *
238
273
  * ```ts
239
- * import { promises as fs } from 'node:fs'
274
+ * import fs from 'node:fs/promises'
240
275
  * import path from 'node:path'
241
276
  * import { FileMigrationProvider, Migrator } from 'kysely/migration'
242
277
  *
@@ -403,6 +438,7 @@ export class Migrator {
403
438
  lockRowId: MIGRATION_LOCK_ID,
404
439
  lockTableSchema: this.#props.migrationTableSchema,
405
440
  });
441
+ const transactionMode = this.#resolveTransactionMode(options, adapter);
406
442
  const run = async (db) => {
407
443
  const state = await this.#getState(db);
408
444
  if (state.migrations.length === 0) {
@@ -412,13 +448,34 @@ export class Migrator {
412
448
  if (step <= 0) {
413
449
  return { results: [] };
414
450
  }
415
- if (direction === 'Down') {
416
- return await this.#migrateDown(db, state, step);
451
+ const migrationsToRun = this.#getMigrationsToRun(state, direction, step);
452
+ const [orchestrator, runner] = direction === 'Down'
453
+ ? [
454
+ this.#rollbackMigrations.bind(this),
455
+ this.#rollbackMigration.bind(this),
456
+ ]
457
+ : [this.#applyMigrations.bind(this), this.#applyMigration.bind(this)];
458
+ if (transactionMode.value === 'per-migration') {
459
+ return await orchestrator(migrationsToRun, async (migration) => {
460
+ if (migration.config?.transaction === false) {
461
+ return await runner(db, migration);
462
+ }
463
+ await db.transaction().execute((trx) => runner(trx, migration));
464
+ });
465
+ }
466
+ const migrationWithTransactionConfig = migrationsToRun.find((migration) => migration.config?.transaction != null);
467
+ if (migrationWithTransactionConfig) {
468
+ const modeDescription = transactionMode.explicit
469
+ ? `\`transactionMode\` is '${transactionMode.value}'`
470
+ : `\`transactionMode\` was not provided and defaults to '${transactionMode.value}'`;
471
+ throw new Error(`Migration "${migrationWithTransactionConfig.name}" has a \`transaction\` configuration but ${modeDescription}. Per-migration transaction configuration is only supported when \`transactionMode\` is 'per-migration'. Set \`transactionMode: 'per-migration'\` on the migrator or the migrate call to enable it.`);
417
472
  }
418
- else if (direction === 'Up') {
419
- return await this.#migrateUp(db, state, step);
473
+ if (!transactionMode.explicit &&
474
+ adapter.supportsTransactionalDdl &&
475
+ migrationsToRun.length > 1) {
476
+ logOnce("kysely:migrator: multiple migrations were run in a single transaction ('per-run'). This default might change in a future version of Kysely. Pass an explicit `transactionMode` to the migrator to pin the behavior and silence this message: 'per-run' to keep the current behavior, or 'per-migration' to run each migration in its own transaction.");
420
477
  }
421
- return { results: [] };
478
+ return await orchestrator(migrationsToRun, (migration) => runner(db, migration));
422
479
  };
423
480
  const runWithLock = async (db, cb) => {
424
481
  try {
@@ -429,51 +486,92 @@ export class Migrator {
429
486
  await adapter.releaseMigrationLock(db, lockOptions);
430
487
  }
431
488
  };
432
- const disableTransactions = options?.disableTransactions ?? this.#props.disableTransactions;
433
489
  if (this.#props.db.isTransaction) {
434
490
  if (!adapter.supportsTransactionalDdl) {
435
491
  throw new Error('Transactional DDL is not supported in this dialect. Passing a transaction to this migrator would result in failure or unexpected behavior.');
436
492
  }
437
- if (disableTransactions) {
438
- throw new Error('`disableTransactions` is true but the migrator was given a transaction. Passing a transaction to this migrator would result in failure or unexpected behavior.');
493
+ if (transactionMode.value !== 'per-run') {
494
+ throw new Error(`The transaction mode is '${transactionMode.value}' but the migrator was given a transaction. Passing a transaction to this migrator is only supported when \`transactionMode\` is 'per-run'.`);
439
495
  }
440
496
  return runWithLock(this.#props.db, run);
441
497
  }
442
- if (adapter.supportsTransactionalDdl && !disableTransactions) {
498
+ if (transactionMode.value === 'per-run') {
443
499
  return this.#props.db
444
500
  .connection()
445
- .execute((db) => runWithLock(db, (db) => db.transaction().execute((trx) => run(trx))));
501
+ .execute((db) => runWithLock(db, (db) => db.transaction().execute(run)));
446
502
  }
447
503
  return this.#props.db.connection().execute((db) => runWithLock(db, run));
448
504
  }
505
+ #resolveTransactionMode(options, adapter) {
506
+ const resolveLevel = (options, source) => {
507
+ if (!options) {
508
+ return;
509
+ }
510
+ const { disableTransactions, transactionMode } = options;
511
+ if (disableTransactions != null &&
512
+ transactionMode &&
513
+ (transactionMode === 'none') !== disableTransactions) {
514
+ throw new Error(`\`transactionMode\` is '${transactionMode}' but \`disableTransactions\` is ${disableTransactions} in the ${source}. These options contradict each other. Prefer \`transactionMode\` — \`disableTransactions\` is deprecated.`);
515
+ }
516
+ if (transactionMode) {
517
+ if (transactionMode !== 'none' && !adapter.supportsTransactionalDdl) {
518
+ throw new Error(`\`transactionMode\` is '${transactionMode}' but transactional DDL is not supported in this dialect. Migrations would fail or behave unexpectedly.`);
519
+ }
520
+ return { explicit: true, value: transactionMode };
521
+ }
522
+ if (disableTransactions == null) {
523
+ return;
524
+ }
525
+ if (disableTransactions === true) {
526
+ return { explicit: true, value: 'none' };
527
+ }
528
+ return {
529
+ explicit: false,
530
+ value: adapter.supportsTransactionalDdl ? 'per-run' : 'none',
531
+ };
532
+ };
533
+ return (resolveLevel(options, 'migrate call options') ||
534
+ resolveLevel(this.#props, 'migrator properties') || {
535
+ explicit: false,
536
+ value: adapter.supportsTransactionalDdl ? 'per-run' : 'none',
537
+ });
538
+ }
539
+ #getMigrationsToRun(state, direction, step) {
540
+ if (direction === 'Down') {
541
+ return state.executedMigrations
542
+ .toReversed()
543
+ .slice(0, step)
544
+ .map((name) => {
545
+ return state.migrations.find((it) => it.name === name);
546
+ });
547
+ }
548
+ return state.pendingMigrations.slice(0, step);
549
+ }
449
550
  async #getState(db) {
450
- const migrations = await this.#resolveMigrations();
451
- const executedMigrations = await this.#getExecutedMigrations(db);
551
+ const [migrations, executedMigrations] = await Promise.all([
552
+ this.#resolveMigrations(),
553
+ this.#getExecutedMigrations(db),
554
+ ]);
452
555
  this.#ensureNoMissingMigrations(migrations, executedMigrations);
453
556
  if (!this.#allowUnorderedMigrations) {
454
557
  this.#ensureMigrationsInOrder(migrations, executedMigrations);
455
558
  }
456
559
  const pendingMigrations = this.#getPendingMigrations(migrations, executedMigrations);
457
560
  return freeze({
458
- migrations,
459
561
  executedMigrations,
460
- lastMigration: getLast(executedMigrations),
562
+ lastMigration: executedMigrations.at(-1),
563
+ migrations,
461
564
  pendingMigrations,
462
565
  });
463
566
  }
464
567
  #getPendingMigrations(migrations, executedMigrations) {
465
- return migrations.filter((migration) => {
466
- return !executedMigrations.includes(migration.name);
467
- });
568
+ return migrations.filter((migration) => !executedMigrations.includes(migration.name));
468
569
  }
469
570
  async #resolveMigrations() {
470
571
  const allMigrations = await this.#props.provider.getMigrations();
471
572
  return Object.keys(allMigrations)
472
573
  .sort()
473
- .map((name) => ({
474
- ...allMigrations[name],
475
- name,
476
- }));
574
+ .map((name) => ({ ...allMigrations[name], name }));
477
575
  }
478
576
  async #getExecutedMigrations(db) {
479
577
  const executedMigrations = await db
@@ -509,92 +607,63 @@ export class Migrator {
509
607
  }
510
608
  }
511
609
  }
512
- async #migrateDown(db, state, step) {
513
- const migrationsToRollback = state.executedMigrations
514
- .toReversed()
515
- .slice(0, step)
516
- .map((name) => {
517
- return state.migrations.find((it) => it.name === name);
518
- });
519
- const results = migrationsToRollback.map((migration) => {
520
- return {
521
- migrationName: migration.name,
522
- direction: 'Down',
523
- status: 'NotExecuted',
524
- };
525
- });
610
+ async #rollbackMigrations(migrationsToRollback, rollbacker) {
611
+ const results = migrationsToRollback.map((migration) => ({
612
+ direction: 'Down',
613
+ migrationName: migration.name,
614
+ status: 'NotExecuted',
615
+ }));
526
616
  for (let i = 0; i < results.length; ++i) {
527
617
  const migration = migrationsToRollback[i];
618
+ if (!migration.down) {
619
+ continue;
620
+ }
528
621
  try {
529
- if (migration.down) {
530
- await migration.down(db);
531
- await db
532
- .withPlugin(this.#schemaPlugin)
533
- .deleteFrom(this.#migrationTable)
534
- .where('name', '=', migration.name)
535
- .execute();
536
- results[i] = {
537
- migrationName: migration.name,
538
- direction: 'Down',
539
- status: 'Success',
540
- };
541
- }
622
+ await rollbacker(migration);
623
+ results[i].status = 'Success';
542
624
  }
543
625
  catch (error) {
544
- results[i] = {
545
- migrationName: migration.name,
546
- direction: 'Down',
547
- status: 'Error',
548
- };
549
- throw new MigrationResultSetError({
550
- error,
551
- results,
552
- });
626
+ results[i].status = 'Error';
627
+ throw new MigrationResultSetError({ error, results });
553
628
  }
554
629
  }
555
630
  return { results };
556
631
  }
557
- async #migrateUp(db, state, step) {
558
- const migrationsToRun = state.pendingMigrations.slice(0, step);
559
- const results = migrationsToRun.map((migration) => {
560
- return {
561
- migrationName: migration.name,
562
- direction: 'Up',
563
- status: 'NotExecuted',
564
- };
565
- });
632
+ async #applyMigrations(migrationsToRun, applier) {
633
+ const results = migrationsToRun.map((migration) => ({
634
+ direction: 'Up',
635
+ migrationName: migration.name,
636
+ status: 'NotExecuted',
637
+ }));
566
638
  for (let i = 0; i < results.length; i++) {
567
- const migration = state.pendingMigrations[i];
639
+ const migration = migrationsToRun[i];
568
640
  try {
569
- await migration.up(db);
570
- await db
571
- .withPlugin(this.#schemaPlugin)
572
- .insertInto(this.#migrationTable)
573
- .values({
574
- name: migration.name,
575
- timestamp: new Date().toISOString(),
576
- })
577
- .execute();
578
- results[i] = {
579
- migrationName: migration.name,
580
- direction: 'Up',
581
- status: 'Success',
582
- };
641
+ await applier(migration);
642
+ results[i].status = 'Success';
583
643
  }
584
644
  catch (error) {
585
- results[i] = {
586
- migrationName: migration.name,
587
- direction: 'Up',
588
- status: 'Error',
589
- };
590
- throw new MigrationResultSetError({
591
- error,
592
- results,
593
- });
645
+ results[i].status = 'Error';
646
+ throw new MigrationResultSetError({ error, results });
594
647
  }
595
648
  }
596
649
  return { results };
597
650
  }
651
+ async #applyMigration(db, migration) {
652
+ await migration.up(db);
653
+ await db
654
+ .withPlugin(this.#schemaPlugin)
655
+ .insertInto(this.#migrationTable)
656
+ .values({ name: migration.name, timestamp: new Date().toISOString() })
657
+ .execute();
658
+ }
659
+ async #rollbackMigration(db, migration) {
660
+ await migration.down(db);
661
+ await db
662
+ .withPlugin(this.#schemaPlugin)
663
+ .deleteFrom(this.#migrationTable)
664
+ .where('name', '=', migration.name)
665
+ .execute();
666
+ }
598
667
  async #createIfNotExists(qb) {
599
668
  if (this.#props.db.getExecutor().adapter.supportsCreateIfNotExists) {
600
669
  qb = qb.ifNotExists();
@@ -14,7 +14,6 @@ export declare function isFunction(obj: unknown): obj is Function;
14
14
  export declare function isObject(obj: unknown): obj is ShallowRecord<string, unknown>;
15
15
  export declare function isArrayBufferOrView(obj: unknown): obj is ArrayBuffer | ArrayBufferView;
16
16
  export declare function isPlainObject(obj: unknown): obj is Record<string, unknown>;
17
- export declare function getLast<T>(arr: ArrayLike<T>): T | undefined;
18
17
  export declare function freeze<T>(obj: T): Readonly<T>;
19
18
  export declare function asArray<T>(arg: T | ReadonlyArray<T>): ReadonlyArray<T>;
20
19
  export declare function asReadonlyArray<T>(arg: T | ReadonlyArray<T>): ReadonlyArray<T>;
@@ -56,9 +56,6 @@ export function isPlainObject(obj) {
56
56
  }
57
57
  return Object.getPrototypeOf(obj) === proto;
58
58
  }
59
- export function getLast(arr) {
60
- return arr[arr.length - 1];
61
- }
62
59
  export function freeze(obj) {
63
60
  return Object.freeze(obj);
64
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kysely",
3
- "version": "0.29.4",
3
+ "version": "0.30.0-beta.1",
4
4
  "description": "Type safe SQL query builder",
5
5
  "repository": {
6
6
  "type": "git",