@bakery-framework/orm 2.0.0-alpha.9 → 2.0.0-rc.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/src/field.ts CHANGED
@@ -7,7 +7,7 @@ import type * as SyncTypes from './sync/types'
7
7
  *
8
8
  * The former `value()` primitive, now private and two arguments shorter.
9
9
  * `autoIncrement` and `primary` were positional booleans that only
10
- * `Field.Primary()` ever set, and it can state them directly — which is the
10
+ * `Field.Primary()` ever set, and it can state them directly, which is the
11
11
  * whole reason `value('integer', undefined, false, true, true)` was worth
12
12
  * replacing.
13
13
  *
@@ -27,7 +27,7 @@ function column<T, N extends boolean = false, O extends boolean = false>(
27
27
  if (nullable === true || d === null) result.nullable = true
28
28
  // The runtime object is byte-for-byte what it was before `TableDef` changed
29
29
  // shape: `type` holds the dialect name, `optional` is type-level only. That
30
- // is the whole safety argument for this change — no adapter, no generated
30
+ // is the whole safety argument for this change: no adapter, no generated
31
31
  // file and no stored ledger payload sees any difference.
32
32
  return result as unknown as TableDef<T, N, O>
33
33
  }
@@ -52,9 +52,9 @@ const isColumnValue = (v: unknown): v is ColumnValue =>
52
52
  /**
53
53
  * Resolve either calling convention to `{ table, cols }`.
54
54
  *
55
- * The string form — `Field.Index('posts', ['authorId'])` — cannot catch a typo
56
- * in either argument until sync time, if then. The column form —
57
- * `Field.Index(posts.authorId)` — carries its own table, so that argument
55
+ * The string form: `Field.Index('posts', ['authorId'])`: cannot catch a typo
56
+ * in either argument until sync time, if then. The column form:
57
+ * `Field.Index(posts.authorId)`: carries its own table, so that argument
58
58
  * disappears and a mistake becomes a compile error.
59
59
  */
60
60
  function resolveTarget(
@@ -86,8 +86,8 @@ export interface ForeignKeyActions {
86
86
  /**
87
87
  * The shape `Field.Foreign()` contributes to a row type.
88
88
  *
89
- * Always `integer`, because that is what `Field.Primary()` always is — an
90
- * `INTEGER PRIMARY KEY AUTOINCREMENT` — and a foreign key exists to point at
89
+ * Always `integer`, because that is what `Field.Primary()` always is (an
90
+ * `INTEGER PRIMARY KEY AUTOINCREMENT`), and a foreign key exists to point at
91
91
  * one. Nullable adds `| null`, which is the only variation worth having.
92
92
  *
93
93
  * The runtime object still resolves its type from the referenced column (see
@@ -102,14 +102,14 @@ type ForeignDef<N extends true | undefined> = TableDef<
102
102
  N extends true ? true : false
103
103
  >
104
104
 
105
- /** What `Field.Index` / `Field.Unique` accept — a column produced by `table()`. */
105
+ /** What `Field.Index` / `Field.Unique` accept: a column produced by `table()`. */
106
106
  type ColumnValue = { __table: string; __column: string }
107
107
 
108
108
  /**
109
109
  * What `Field.Enum` accepts: a literal array, or a TypeScript string enum.
110
110
  *
111
111
  * A string enum is an ordinary object at runtime (`{ Draft: 'draft' }`), so
112
- * both forms reduce to the same list of members — `Object.values` for the
112
+ * both forms reduce to the same list of members, `Object.values` for the
113
113
  * object, the tuple itself for the array.
114
114
  *
115
115
  * **String enums only.** A *numeric* enum compiles to an object with a reverse
@@ -127,7 +127,7 @@ type EnumValues<V> = V extends readonly (infer U extends string)[]
127
127
  : V[keyof V]
128
128
 
129
129
  /**
130
- * `Field` — the column vocabulary, namespaced so it is discoverable.
130
+ * `Field`: the column vocabulary, namespaced so it is discoverable.
131
131
  *
132
132
  * This replaced `value('string', null, true, false, false)`, which required
133
133
  * remembering a type string *and* the meaning of four positional booleans.
@@ -137,8 +137,8 @@ type EnumValues<V> = V extends readonly (infer U extends string)[]
137
137
  * **`Field` is now the whole vocabulary, not sugar over one.** `value`,
138
138
  * `primary`, `index`, `unique` and `foreign` are gone; the construction they
139
139
  * did lives in `column()` and `resolveTarget()` above, private to this file.
140
- * What the sync engine and the type inference consume is unchanged — plain
141
- * descriptor objects — so this moved the API without moving the contract.
140
+ * What the sync engine and the type inference consume is unchanged (plain
141
+ * descriptor objects), so this moved the API without moving the contract.
142
142
  *
143
143
  * The one shape deliberately left unspellable is **nullable *and* defaulted to
144
144
  * something other than null**, because a null default is how you say nullable.
@@ -158,7 +158,7 @@ type EnumValues<V> = V extends readonly (infer U extends string)[]
158
158
  */
159
159
  export const Field = {
160
160
  /**
161
- * `INTEGER PRIMARY KEY AUTOINCREMENT` — the id column, spelled once.
161
+ * `INTEGER PRIMARY KEY AUTOINCREMENT`: the id column, spelled once.
162
162
  *
163
163
  * The single most repeated line in any schema, and the one most likely to be
164
164
  * written wrong by hand: `value('integer', undefined, false, true, true)`.
@@ -185,7 +185,7 @@ export const Field = {
185
185
  >('integer', d),
186
186
 
187
187
  /**
188
- * A fractional number — `DOUBLE` on MySQL, `DOUBLE PRECISION` on Postgres,
188
+ * A fractional number: `DOUBLE` on MySQL, `DOUBLE PRECISION` on Postgres,
189
189
  * `REAL` on SQLite.
190
190
  *
191
191
  * Named `Float` rather than mirroring the underlying `'number'` type string,
@@ -208,12 +208,12 @@ export const Field = {
208
208
  >('string', d),
209
209
 
210
210
  /**
211
- * Unbounded text — `TEXT` on every dialect.
211
+ * Unbounded text: `TEXT` on every dialect.
212
212
  *
213
213
  * **MySQL rejects a literal `DEFAULT` on TEXT**, so this takes no default.
214
214
  * That is not an omission: `Field.String('')` emits
215
215
  * `TEXT NOT NULL DEFAULT ''`, which MySQL refuses outright with "BLOB, TEXT,
216
- * GEOMETRY or JSON column can't have a default value" — the shipped schema
216
+ * GEOMETRY or JSON column can't have a default value", the shipped schema
217
217
  * template could not `db:sync` against MySQL because of exactly this. Use
218
218
  * `Varchar` when you need a default.
219
219
  */
@@ -227,12 +227,12 @@ export const Field = {
227
227
  },
228
228
 
229
229
  /**
230
- * Sized text — `VARCHAR(n)`, and the answer to TEXT's default problem, since
230
+ * Sized text: `VARCHAR(n)`, and the answer to TEXT's default problem, since
231
231
  * every dialect accepts a default on a sized column.
232
232
  *
233
233
  * slug: Field.Varchar(255, ''),
234
234
  *
235
- * SQLite has no real `VARCHAR` — all text is TEXT affinity — but it stores
235
+ * SQLite has no real `VARCHAR` (all text is TEXT affinity), but it stores
236
236
  * the declared type verbatim and reads it back, so one schema round-trips on
237
237
  * all three.
238
238
  *
@@ -254,7 +254,7 @@ export const Field = {
254
254
  ),
255
255
 
256
256
  /**
257
- * A 64-bit integer — `BIGINT` everywhere.
257
+ * A 64-bit integer: `BIGINT` everywhere.
258
258
  *
259
259
  * Reads back as a **string** on MySQL and Postgres, which is how they avoid
260
260
  * losing precision, and as a **number** on SQLite, which does not: values
@@ -269,11 +269,11 @@ export const Field = {
269
269
  >('bigint' as any, d),
270
270
 
271
271
  /**
272
- * A JSON document — `JSON` on MySQL, `JSONB` on Postgres, a `JSON`-declared
272
+ * A JSON document: `JSON` on MySQL, `JSONB` on Postgres, a `JSON`-declared
273
273
  * text column on SQLite.
274
274
  *
275
275
  * MySQL and Postgres parse it into an object on read; SQLite hands back the
276
- * raw string. The row type is therefore `unknown` — narrow it where you use
276
+ * raw string. The row type is therefore `unknown`: narrow it where you use
277
277
  * it rather than trusting a type that would be wrong on one of the three.
278
278
  *
279
279
  * Takes no default, for the same reason `Text` does not: MySQL refuses a
@@ -286,7 +286,7 @@ export const Field = {
286
286
  (nullable: true): TableDef<unknown, true, true>
287
287
  },
288
288
 
289
- /** True/false — `BOOLEAN` on Postgres, `TINYINT(1)` on MySQL. */
289
+ /** True/false: `BOOLEAN` on Postgres, `TINYINT(1)` on MySQL. */
290
290
  Bool: <D extends boolean | null | undefined = undefined>(d?: D) =>
291
291
  column<
292
292
  Nullable<boolean, D extends null ? true : false>,
@@ -312,8 +312,8 @@ export const Field = {
312
312
  *
313
313
  * **The column's type is copied from the target, not declared here**, and
314
314
  * that is the real reason to prefer this form. MySQL refuses a foreign key
315
- * whose column type does not match the referenced key *exactly* — an
316
- * `INT` child against a `BIGINT` parent is rejected outright — and that
315
+ * whose column type does not match the referenced key *exactly* (an
316
+ * `INT` child against a `BIGINT` parent is rejected outright), and that
317
317
  * mismatch is invisible in a schema where the two columns are declared pages
318
318
  * apart. Resolution happens in `resolveColumnForeignKeys()`, where the whole
319
319
  * schema is in scope, so the two cannot disagree.
@@ -332,11 +332,11 @@ export const Field = {
332
332
  } = {},
333
333
  ) =>
334
334
  ({
335
- // `integer` in the row type, always — see `ForeignDef`. At *runtime* the
335
+ // `integer` in the row type, always. See `ForeignDef`. At *runtime* the
336
336
  // type is still overwritten from the referenced column by
337
337
  // `resolveColumnForeignKeys()`, because MySQL refuses a key whose column
338
- // type does not match the target exactly. For the ordinary case — a key
339
- // pointing at a `Field.Primary()` — the two agree and there is nothing to
338
+ // type does not match the target exactly. For the ordinary case: a key
339
+ // pointing at a `Field.Primary()`: the two agree and there is nothing to
340
340
  // reconcile.
341
341
  type: 'integer',
342
342
  ...(options.nullable ? { nullable: true, default: null } : {}),
@@ -429,7 +429,7 @@ export const Field = {
429
429
  * The column form carries its own table, so there is no separate table
430
430
  * argument to get wrong; the string form exists because the `DBInfo`
431
431
  * namespace layout has no `table()` values to point at. Several columns make
432
- * one composite index, in the order given — which is the order that decides
432
+ * one composite index, in the order given, which is the order that decides
433
433
  * which queries it can serve.
434
434
  *
435
435
  * A direct alias of `index()` rather than a wrapper, so the two cannot drift
@@ -460,7 +460,7 @@ export const Field = {
460
460
  },
461
461
 
462
462
  /**
463
- * A UUID, generated by the database — `CHAR(36)` sized text with a
463
+ * A UUID, generated by the database: `CHAR(36)` sized text with a
464
464
  * per-dialect default expression.
465
465
  *
466
466
  * id: Field.Uuid(),
@@ -468,7 +468,7 @@ export const Field = {
468
468
  * `gen_random_uuid()` on Postgres, `UUID()` on MySQL, and
469
469
  * `lower(hex(randomblob(16)))` shaped into the canonical form on SQLite,
470
470
  * which has no UUID function of its own. All three round-trip through the
471
- * `%uuid%` marker, the same way `Field.Date.now()` round-trips `%dateNow%` —
471
+ * `%uuid%` marker, the same way `Field.Date.now()` round-trips `%dateNow%`,
472
472
  * without the read-back half the database reports its own expression, the
473
473
  * schema says `%uuid%`, and the column rebuilds on every sync forever.
474
474
  *
@@ -496,11 +496,11 @@ export const Field = {
496
496
  *
497
497
  * Enforced in the database too, and **the same way on all three dialects**:
498
498
  * a `CHECK (col IN (…))`. MySQL has a native `ENUM` and this deliberately
499
- * does not use it — a value rejected by MySQL and accepted by SQLite means
499
+ * does not use it: a value rejected by MySQL and accepted by SQLite means
500
500
  * an app that behaves differently depending on where it runs, which is worse
501
501
  * than either choice made consistently.
502
502
  *
503
- * The members are **not part of the column diff** — see
503
+ * The members are **not part of the column diff**. See
504
504
  * `ColumnConstraint._enum`. Adding or removing one does not migrate on its
505
505
  * own; the table has to be rebuilt for the CHECK to change.
506
506
  */
@@ -520,7 +520,7 @@ export const Field = {
520
520
  throws(
521
521
  'Field.Enum() takes a string enum or an array of strings. A numeric ' +
522
522
  'enum reverse-maps its members, so its values are half names and ' +
523
- 'half numbers — use Field.Int() and validate in your code, or give ' +
523
+ 'half numbers. Use Field.Int() and validate in your code, or give ' +
524
524
  'the enum string values.',
525
525
  )
526
526
  }
@@ -555,7 +555,7 @@ export const Field = {
555
555
  * **`updatedAt` is not auto-maintained on write, and that is a deliberate
556
556
  * limit rather than an oversight.** Doing it silently needs the query layer
557
557
  * to know which tables have the column, and the query layer has no runtime
558
- * view of the schema at all — `schema.ts` is loaded by the sync engine, not
558
+ * view of the schema at all: `schema.ts` is loaded by the sync engine, not
559
559
  * by the ORM. The alternatives were a `hasCol` probe on every UPDATE, or
560
560
  * stamping a column that might not exist. Stamp it yourself:
561
561
  *
@@ -570,7 +570,7 @@ export const Field = {
570
570
  * The current time as Unix **seconds**, for a value position.
571
571
  *
572
572
  * A plain number, so it binds as an ordinary parameter and needs no dialect
573
- * handling — unlike `%dateNow%`, which is a DDL default marker and means
573
+ * handling: unlike `%dateNow%`, which is a DDL default marker and means
574
574
  * nothing in an INSERT or UPDATE.
575
575
  */
576
576
  now: () => Math.floor(Date.now() / 1000),
package/src/globals.d.ts CHANGED
@@ -4,7 +4,7 @@ import type { AppDBOptionals, AppDBSchema } from './schema-registry'
4
4
  * Ambient database types.
5
5
  *
6
6
  * These live with the orm package rather than in core's `global.d.ts`: they are
7
- * derived from this package's schema registry, and core cannot reference orm —
7
+ * derived from this package's schema registry, and core cannot reference orm,
8
8
  * orm depends on core, so the reverse would be circular.
9
9
  *
10
10
  * Both are read only inside this package (`orm/query.ts`, `orm/mutation.ts`,
package/src/index.ts CHANGED
@@ -12,7 +12,7 @@ export type {
12
12
  *
13
13
  * Re-exported from the root rather than given a `./observe` subpath. The
14
14
  * export map is closed and every entry in it is public API from the moment it
15
- * ships, so a new subpath needs a reason — and there is none here: this is one
15
+ * ships, so a new subpath needs a reason, and there is none here: this is one
16
16
  * function and three types, the observer is process-wide, and an app sets it
17
17
  * once at boot next to where it already imports `DB`.
18
18
  *
@@ -34,15 +34,15 @@ export type {
34
34
  * Schema authoring, from one place.
35
35
  *
36
36
  * `table`/`alias` live in `define.ts` and the column and constraint helpers in
37
- * `schema-util.ts`, but that split is an implementation detail — someone
37
+ * `schema-util.ts`, but that split is an implementation detail: someone
38
38
  * writing `orm/schema.ts` should import from `@bakery-framework/orm` without having to
39
39
  * know which file a helper happens to sit in.
40
40
  */
41
41
  export { alias, table, view } from './define'
42
42
  export { Field } from './field'
43
43
  /**
44
- * `TableDef` is the **column** descriptor — `TableDef<TYPE, nullable, optional>`
45
- * — and it now comes from the root barrel, which is where someone writing a
44
+ * `TableDef` is the **column** descriptor (`TableDef<TYPE, nullable, optional>`
45
+ *), and it now comes from the root barrel, which is where someone writing a
46
46
  * schema would look for it.
47
47
  *
48
48
  * It did not, and that was a defect rather than an omission: the barrel used to
package/src/orm/index.ts CHANGED
@@ -8,14 +8,14 @@
8
8
  *
9
9
  * `./query` first works. `./mutation` first does not: `mutation.ts` pulls in
10
10
  * `query.ts`, whose top-level `Mutation.Insert` runs while `Mutation` is still
11
- * initialising, and the whole ORM dies with
11
+ * initializing, and the whole ORM dies with
12
12
  * `TypeError: undefined is not an object (evaluating 'Mutation.Insert')`.
13
13
  *
14
14
  * Biome's `organizeImports` sorts these alphabetically and puts `./mutation`
15
15
  * first. The compiler cannot see the breakage, because the types are fine
16
16
  * either way. Hence the suppression.
17
17
  */
18
- // biome-ignore-all assist/source/organizeImports: cycle — see above
18
+ // biome-ignore-all assist/source/organizeImports: cycle. See above
19
19
  import { DB } from './query'
20
20
  import { Mutation } from './mutation'
21
21
 
@@ -7,7 +7,7 @@ import type {
7
7
  AppDBOptionals as DBOptionals,
8
8
  AppDBSchema as DBSchema,
9
9
  } from '../schema-registry'
10
- import { evalOperands, qId } from '../schema-util'
10
+ import { evalOperands, nullComparison, qId } from '../schema-util'
11
11
  import { DB } from './query'
12
12
 
13
13
  export namespace Mutation {
@@ -16,7 +16,7 @@ export namespace Mutation {
16
16
  *
17
17
  * There used to be a `| (string & {})` member here, for autocomplete on the
18
18
  * literals while still accepting any string. It also made the rest of the
19
- * type decorative — `Exclude<…, AppViews>` never rejected anything, so a
19
+ * type decorative: `Exclude<…, AppViews>` never rejected anything, so a
20
20
  * `DB.Insert.into('some_view')` compiled and failed at the database instead,
21
21
  * and so did a typo'd table name.
22
22
  *
@@ -36,7 +36,7 @@ export namespace Mutation {
36
36
  export type Prettify<T> = { [K in keyof T]: T[K] } & {}
37
37
 
38
38
  /**
39
- * `RETURNING` takes an identifier list and is interpolated, not bound — the
39
+ * `RETURNING` takes an identifier list and is interpolated, not bound: the
40
40
  * same position `orderBy` and `groupBy` guard with `safeColumn`. It was the
41
41
  * one identifier writer on Insert/Update/Delete with no guard at all, so
42
42
  * `.returning('* FROM users; DROP TABLE t --')` was emitted verbatim.
@@ -71,10 +71,10 @@ export namespace Mutation {
71
71
  : string
72
72
 
73
73
  /**
74
- * What a write returns — the adapter's own result, not a copy of it.
74
+ * What a write returns: the adapter's own result, not a copy of it.
75
75
  *
76
76
  * It *was* a copy: an identical `{lastInsertRowid, changes}` declared here as
77
- * well as on `SQLAdapter`. Identical today is the whole problem — that is the
77
+ * well as on `SQLAdapter`. Identical today is the whole problem: that is the
78
78
  * state `SQLAdapter.ColumnConstraint` was in before it fell behind
79
79
  * `sync/types.ts` and started erasing fields at the cast.
80
80
  */
@@ -87,14 +87,14 @@ export namespace Mutation {
87
87
  }
88
88
 
89
89
  /**
90
- * The rows to insert — as a spread, or as one array.
90
+ * The rows to insert: as a spread, or as one array.
91
91
  *
92
92
  * Both forms exist because the variadic signature alone made the obvious
93
93
  * call wrong in a way nothing announced: `values(rows)` bound the *array*
94
94
  * as a single record, and the only symptom was
95
95
  * `table big has no column named 0`. An array is never a record, so the two
96
- * forms cannot be confused, and anything that is neither — a mixed
97
- * `values(rows, extra)`, a primitive — is rejected by name rather than
96
+ * forms cannot be confused, and anything that is neither (a mixed
97
+ * `values(rows, extra)`, a primitive) is rejected by name rather than
98
98
  * turned into columns called `0` and `1`.
99
99
  */
100
100
  values(records: InsertSchema<T>[]): InsertExecutable
@@ -115,7 +115,7 @@ export namespace Mutation {
115
115
  'values() takes records: values(row), values(rowA, rowB) or ' +
116
116
  'values(rows). Got a ' +
117
117
  (Array.isArray(record) ? 'nested array' : typeof record) +
118
- ' — if you meant to pass an array of rows, pass it as the only ' +
118
+ ': if you meant to pass an array of rows, pass it as the only ' +
119
119
  'argument.',
120
120
  )
121
121
  }
@@ -142,7 +142,7 @@ export namespace Mutation {
142
142
  /**
143
143
  * Insert, or update the row that is already there.
144
144
  *
145
- * `cols` names the unique columns that decide "already there" — a primary
145
+ * `cols` names the unique columns that decide "already there": a primary
146
146
  * key or a unique index. Without an upsert the only way to express this is
147
147
  * to check and then branch, which is a **race**: two requests both see no
148
148
  * row and both insert.
@@ -150,8 +150,8 @@ export namespace Mutation {
150
150
  * DB.Insert.into('users').values({ email, name }).upsert(['email'])
151
151
  *
152
152
  * By default every inserted column except the conflict columns is
153
- * overwritten. Pass a second argument to narrow that — `upsert(['email'],
154
- * ['name'])` leaves everything else as it was — or an empty array for
153
+ * overwritten. Pass a second argument to narrow that (`upsert(['email'],
154
+ * ['name'])` leaves everything else as it was), or an empty array for
155
155
  * insert-if-absent, which becomes `DO NOTHING`.
156
156
  *
157
157
  * MySQL ignores `cols` because `ON DUPLICATE KEY UPDATE` fires on *any*
@@ -180,7 +180,7 @@ export namespace Mutation {
180
180
  * How many records fit in one statement, under the adapter's ceiling.
181
181
  *
182
182
  * The ceiling comes from the adapter because only it knows its dialect, but
183
- * a statement can be *rendered* without a connection — `parse()` is how the
183
+ * a statement can be *rendered* without a connection: `parse()` is how the
184
184
  * tests read the SQL, and the stub adapter in `orm.test.ts` implements two
185
185
  * members. An unreachable or silent adapter falls back to the same default
186
186
  * the base class publishes rather than making `parse()` require a database.
@@ -222,7 +222,7 @@ export namespace Mutation {
222
222
  }
223
223
 
224
224
  /**
225
- * The insert as one statement per batch — the form the executors run.
225
+ * The insert as one statement per batch: the form the executors run.
226
226
  *
227
227
  * A single `INSERT … VALUES (…),(…),…` carries three parameters per row, so
228
228
  * it stops being a legal statement somewhere around eleven thousand rows.
@@ -233,15 +233,15 @@ export namespace Mutation {
233
233
  * like for an arbitrary `rows`.
234
234
  *
235
235
  * The column list is computed once, across every record, so every batch
236
- * inserts the same columns in the same order — a per-batch union would
236
+ * inserts the same columns in the same order: a per-batch union would
237
237
  * change the shape of the statement halfway through the insert.
238
238
  *
239
239
  * **`RETURNING` is accumulated, not refused.** Refusing is the
240
240
  * safe-looking option and the wrong one: the batches run sequentially
241
241
  * inside one transaction, so concatenating each batch's rows in batch order
242
242
  * reproduces the sequence a single statement would have produced. Ordering
243
- * *within* a batch is whatever the dialect gives — Postgres does not
244
- * promise `RETURNING` follows `VALUES` order — but that is equally true
243
+ * *within* a batch is whatever the dialect gives (Postgres does not
244
+ * promise `RETURNING` follows `VALUES` order), but that is equally true
245
245
  * unchunked, so batching neither adds nor removes a guarantee. Refusing
246
246
  * would have cost the main reason to write `.values(rows).returning('id')`
247
247
  * at all: getting the generated ids of a bulk import back.
@@ -288,7 +288,7 @@ export namespace Mutation {
288
288
  *
289
289
  * The transaction is what keeps a batched insert meaning what the unbatched
290
290
  * one meant: all rows or none. It is opened only when a batch boundary
291
- * exists — one statement is already atomic — and only when the caller is
291
+ * exists (one statement is already atomic), and only when the caller is
292
292
  * not already inside `DB.transaction`, where the outer transaction is
293
293
  * already the atomic unit.
294
294
  *
@@ -329,7 +329,7 @@ export namespace Mutation {
329
329
  * Two shapes, not three: Postgres and SQLite share
330
330
  * `ON CONFLICT (…) DO UPDATE SET col = excluded.col`, while MySQL spells it
331
331
  * `ON DUPLICATE KEY UPDATE col = VALUES(col)` and takes no conflict target
332
- * at all — it fires on whichever unique key was violated.
332
+ * at all, it fires on whichever unique key was violated.
333
333
  *
334
334
  * Every identifier goes through `qId`, and no value is interpolated: the
335
335
  * new row's values are already bound as the INSERT's parameters, and both
@@ -345,7 +345,7 @@ export namespace Mutation {
345
345
  ).filter(k => insertedKeys.includes(k))
346
346
 
347
347
  // The adapter spells it. This used to branch on `driver === 'mysql'`
348
- // here, which put one dialect's syntax in the shared query builder — the
348
+ // here, which put one dialect's syntax in the shared query builder: the
349
349
  // thing every other difference (quote character, placeholder ceiling,
350
350
  // date expression, foreign-key clause) is kept out of it for.
351
351
  return getActiveDb().upsertClause(cols, targets)
@@ -362,7 +362,7 @@ export namespace Mutation {
362
362
  }
363
363
 
364
364
  async fetch<R = any>(): Promise<R | undefined> {
365
- // Every batch still runs — `fetch()` means "insert, and hand me a row
365
+ // Every batch still runs: `fetch()` means "insert, and hand me a row
366
366
  // back", not "insert the first batch". `Executor.get` is defined as
367
367
  // `all(…)[0]`, so this is the same row it would have produced.
368
368
  const rows = await this.array<R>()
@@ -381,7 +381,7 @@ export namespace Mutation {
381
381
  const changes = results.reduce((n, r) => n + Number(r?.changes ?? 0), 0)
382
382
  // `changes` sums, because it answers "how many rows did this insert
383
383
  // write". `lastInsertRowid` cannot sum, and the dialects do not agree
384
- // what it means for a multi-row insert — so the adapter says which end of
384
+ // what it means for a multi-row insert, so the adapter says which end of
385
385
  // the batched run carries its answer, rather than this file knowing.
386
386
  const pick =
387
387
  getActiveDb().batchInsertIdPosition === 'first'
@@ -475,8 +475,12 @@ export namespace Mutation {
475
475
  if (c.operator === '') {
476
476
  parts.push(i === 0 ? left : `${c.connector} ${left}`)
477
477
  } else {
478
- const right = evalOperands(c.right, params, c.isRightColumn)
479
- const clauseStr = `${left} ${c.operator} ${right}`
478
+ // `nullComparison` before `evalOperands`, and the order is the point:
479
+ // a null that becomes `IS NULL` must not also bind a parameter.
480
+ const nullOp = nullComparison(c.operator, c.right, c.isRightColumn)
481
+ const clauseStr = nullOp
482
+ ? `${left} ${nullOp}`
483
+ : `${left} ${c.operator} ${evalOperands(c.right, params, c.isRightColumn)}`
480
484
  parts.push(i === 0 ? clauseStr : `${c.connector} ${clauseStr}`)
481
485
  }
482
486
  }
@@ -618,8 +622,8 @@ export namespace Mutation {
618
622
  for (let i = 0; i < this._clauses.length; i++) {
619
623
  const c = this._clauses[i]!
620
624
  const left = evalOperands(c.left, params, true)
621
- // `parseWhereArgs` emits `operator: ''` for the one-argument form —
622
- // `where(DB.raw`…`)` or `where(<subquery>)` — where the left operand
625
+ // `parseWhereArgs` emits `operator: ''` for the one-argument form,
626
+ // `where(DB.raw`…`)` or `where(<subquery>)`, where the left operand
623
627
  // is the whole condition and there is no right one. This branch is a
624
628
  // copy of `UpdateExecutable.evalWhere` above, which is a copy of
625
629
  // `formatClause` in query.ts; it was the copy that never got it. The
@@ -630,8 +634,12 @@ export namespace Mutation {
630
634
  if (c.operator === '') {
631
635
  parts.push(i === 0 ? left : `${c.connector} ${left}`)
632
636
  } else {
633
- const right = evalOperands(c.right, params, c.isRightColumn)
634
- const clauseStr = `${left} ${c.operator} ${right}`
637
+ // `nullComparison` before `evalOperands`, and the order is the point:
638
+ // a null that becomes `IS NULL` must not also bind a parameter.
639
+ const nullOp = nullComparison(c.operator, c.right, c.isRightColumn)
640
+ const clauseStr = nullOp
641
+ ? `${left} ${nullOp}`
642
+ : `${left} ${c.operator} ${evalOperands(c.right, params, c.isRightColumn)}`
635
643
  parts.push(i === 0 ? clauseStr : `${c.connector} ${clauseStr}`)
636
644
  }
637
645
  }