@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.
@@ -24,12 +24,12 @@ export type DataTypes = keyof TypeMap
24
24
  * Three parameters, where there were five (`type, default, nullable,
25
25
  * autoIncrement, primary`). The two that went were positional booleans nothing
26
26
  * outside `Field.Primary()` ever set, and `default` was carried only so
27
- * nullability and optionality could be *derived* from it — which is why
27
+ * nullability and optionality could be *derived* from it, which is why
28
28
  * "defaulted but not nullable" and "nullable" were indistinguishable to
29
29
  * anything downstream.
30
30
  *
31
31
  * **`T` is the row type, not a dialect name.** `number`, `string | null`, or an
32
- * enum's own union — so `ExtractTableTypes` is a lookup rather than a mapping
32
+ * enum's own union, so `ExtractTableTypes` is a lookup rather than a mapping
33
33
  * through `TypeMap`, and a type `TypeMap` cannot express (an enum, a branded
34
34
  * id) needs no special case.
35
35
  *
@@ -61,9 +61,8 @@ export const dateNow = '%dateNow%'
61
61
  * The row type of a column descriptor.
62
62
  *
63
63
  * Two forms reach here. A `Field.*` builder returns `TableDef<T, …>`, whose
64
- * `type` **is** the row type, so this is a lookup. A hand-written literal —
65
- * `{ type: 'integer', default: 0, nullable: true }`, the one shape `Field`
66
- * does not spell — carries a dialect name there instead, which still goes
64
+ * `type` **is** the row type, so this is a lookup. A hand-written literal ( * `{ type: 'integer', default: 0, nullable: true }`, the one shape `Field`
65
+ * does not spell) carries a dialect name there instead, which still goes
67
66
  * through `TypeMap`.
68
67
  *
69
68
  * Distinguished by whether `type` is one of `TypeMap`'s keys. That is
@@ -85,12 +84,12 @@ export type ExtractTableTypes<C, K extends keyof C> = {
85
84
  * Which columns may be omitted from an `INSERT`.
86
85
  *
87
86
  * `optional: true` when a `Field.*` builder said so, which is the whole check
88
- * for anything `Field` produces — it states optionality rather than leaving it
87
+ * for anything `Field` produces, it states optionality rather than leaving it
89
88
  * to be reconstructed. The three derived conditions below it are the fallback
90
89
  * for a hand-written literal, which has no `optional` key.
91
90
  *
92
91
  * Stating it also makes a shape expressible that derivation could not:
93
- * **optional but neither nullable nor defaulted** — a column the database fills
92
+ * **optional but neither nullable nor defaulted**: a column the database fills
94
93
  * in, such as a generated key.
95
94
  */
96
95
  export type ExtractOptionals<C, T extends keyof C> = {
@@ -149,7 +148,7 @@ export const SQL_FUNCTIONS = new Set([
149
148
  * `ROW_NUMBER()` outside an `OVER` clause is a syntax error on all three
150
149
  * dialects, so putting it in the general list would let the builder emit SQL
151
150
  * that cannot run. The aggregates in `SQL_FUNCTIONS` work in *both* positions
152
- * and stay where they are — `evalOperands` accepts either list inside a window.
151
+ * and stay where they are: `evalOperands` accepts either list inside a window.
153
152
  *
154
153
  * Verified present on all three servers: SQLite has had window functions since
155
154
  * 3.25, MySQL since 8.0, Postgres throughout.
@@ -171,15 +170,15 @@ export const WINDOW_FUNCTIONS = new Set([
171
170
  /**
172
171
  * A function call with an `OVER (…)` clause.
173
172
  *
174
- * `spec` is the inside of the parentheses — `PARTITION BY "a" ORDER BY "b" ASC`
175
- * — and arrives **already validated and quoted**, exactly like `_joins[].on`.
173
+ * `spec` is the inside of the parentheses (`PARTITION BY "a" ORDER BY "b" ASC`
174
+ *), and arrives **already validated and quoted**, exactly like `_joins[].on`.
176
175
  * That is deliberate: `safeColumn` lives in the query builder, and duplicating
177
176
  * the identifier rules here would make two writers of the same SQL, which is
178
177
  * the thing convention 8 exists to prevent. This class stores; the builder
179
178
  * validates.
180
179
  *
181
180
  * No frame clause (`ROWS BETWEEN …`). It is legal everywhere and would fit, but
182
- * it has its own grammar and nobody has asked — an empty frame is the SQL
181
+ * it has its own grammar and nobody has asked: an empty frame is the SQL
183
182
  * default and is what every call here gets.
184
183
  */
185
184
  export class WindowRef {
@@ -205,13 +204,13 @@ function activeQuoteChar(): string {
205
204
 
206
205
  /**
207
206
  * Memo for `Case.snake`, which is the single most expensive step in quoting an
208
- * identifier — measurably more than the quoting itself — and is re-run for the
207
+ * identifier (measurably more than the quoting itself), and is re-run for the
209
208
  * same handful of table and column names on every clause of every query.
210
209
  *
211
210
  * A capped `Map` rather than `LRUCache`: an LRU `get` deletes and re-inserts on
212
211
  * every hit to maintain recency, which measured ~38x slower than `Map.get` and
213
212
  * would cost more than the `Case.snake` call it is meant to replace. Convention
214
- * 6 asks for bounded, and this is bounded by construction — identifiers are
213
+ * 6 asks for bounded, and this is bounded by construction: identifiers are
215
214
  * schema-derived in practice, but `DB.col()` takes a caller-supplied string, so
216
215
  * the bound cannot rest on that. Dropping the whole map on overflow is correct
217
216
  * because every entry is recomputable: it is a memo, not state.
@@ -281,7 +280,7 @@ export class OperatorRef<R = any> {
281
280
 
282
281
  // Convention 8 makes this the single writer of SQL identifiers, so the arms
283
282
  // belong together: splitting by operand kind spreads that responsibility.
284
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: recursive operand evaluator — one arm per operand shape
283
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: recursive operand evaluator, one arm per operand shape
285
284
  export function evalOperands(
286
285
  where: unknown,
287
286
  params: unknown[],
@@ -324,7 +323,7 @@ export function evalOperands(
324
323
  if (typeof where === 'object' && where !== null) {
325
324
  if (where instanceof WindowRef) {
326
325
  // The function half only. `spec` was built by the query builder out of
327
- // `safeColumn` output and is inserted as-is — see the class comment.
326
+ // `safeColumn` output and is inserted as-is. See the class comment.
328
327
  let call: string
329
328
  if (where.fn instanceof SQLFunctionRef) {
330
329
  call = evalOperands(where.fn, params, true)
@@ -348,7 +347,7 @@ export function evalOperands(
348
347
  const colArg = where.col
349
348
  const extraArgs = where.extraArgs || []
350
349
 
351
- // fnName is interpolated, not bound — only known functions may pass.
350
+ // fnName is interpolated, not bound. Only known functions may pass.
352
351
  if (!SQL_FUNCTIONS.has(fnName)) {
353
352
  throws(`Unsupported SQL function: ${where.fnName}`)
354
353
  }
@@ -436,7 +435,7 @@ export function old<TSchema extends SyncTypes.DBConstraints>(
436
435
  * position, so it is where the two vocabularies meet: `ColumnConstraint.type`
437
436
  * is the dialect name the sync engine reads, while a `Field.*` builder declares
438
437
  * `type` as the **row type**. Requiring the sync-side shape here rejected every
439
- * `Field` value — `old('title', Field.Varchar(255))` stopped compiling.
438
+ * `Field` value: `old('title', Field.Varchar(255))` stopped compiling.
440
439
  *
441
440
  * `old()` only copies the object and adds two keys, so it needs no more than
442
441
  * "something with a `type`", and returning `T` unchanged means the row type and
@@ -465,3 +464,35 @@ export function old(
465
464
  _transform: transform,
466
465
  })
467
466
  }
467
+
468
+ /**
469
+ * The SQL spelling of a comparison against an explicit `null`, or `null` when
470
+ * the comparison is an ordinary one.
471
+ *
472
+ * A bound NULL can never satisfy `=` or `<>`: three-valued logic makes both
473
+ * UNKNOWN for every row, so `where(col, null)` matched nothing while reporting
474
+ * success. On a `SELECT` that is an empty result; on a `DELETE` or an
475
+ * `UPDATE` it is a write that silently does nothing and answers zero changes,
476
+ * which a caller cannot tell from a genuine conflict.
477
+ *
478
+ * **This lives here, not in a `WHERE` builder, because there are three of
479
+ * them.** `formatClause` in `orm/query.ts` and the two `evalWhere` copies in
480
+ * `orm/mutation.ts` each compile a clause, and the copies have now diverged
481
+ * twice: once over the one-argument `where(DB.raw\`…\`)` form, which the
482
+ * comment in `DeleteExecutable.evalWhere` records, and once over this rule,
483
+ * which reached the builder and neither mutation path. Both were silent. One
484
+ * function three callers share cannot drift a third time.
485
+ *
486
+ * A column reference on the right is a different comparison and is left alone.
487
+ */
488
+ export function nullComparison(
489
+ operator: string,
490
+ right: unknown,
491
+ isRightColumn?: boolean,
492
+ ): string | null {
493
+ if (right !== null || isRightColumn) return null
494
+ const op = operator.toUpperCase()
495
+ if (op === '=') return 'IS NULL'
496
+ if (op === '!=' || op === '<>') return 'IS NOT NULL'
497
+ return null
498
+ }
@@ -9,8 +9,8 @@ import { formatViewBody } from './view-sql'
9
9
  /**
10
10
  * Names that cannot bind a `const` in a module (modules are always strict), so
11
11
  * they cannot be used as the export name for a generated `table()`. The export
12
- * name is cosmetic — `InferConstraints` and `collectConstraints` both key off
13
- * the string passed to `table()`, not the binding — so renaming one is safe.
12
+ * name is cosmetic (`InferConstraints` and `collectConstraints` both key off
13
+ * the string passed to `table()`, not the binding), so renaming one is safe.
14
14
  */
15
15
  const RESERVED_WORDS = new Set([
16
16
  'await',
@@ -84,7 +84,7 @@ export class SchemaBuilder {
84
84
  // Fall back to what introspection reported, not to `false`.
85
85
  //
86
86
  // The old `: false` flattened every view column to NOT NULL whenever
87
- // there was no previous schema to copy from — which is exactly the
87
+ // there was no previous schema to copy from, which is exactly the
88
88
  // seeding path, the one a project with an existing database takes. On
89
89
  // a real view that made `category` and `images` non-nullable in the
90
90
  // generated interface while the database reports both nullable, so the
@@ -120,7 +120,7 @@ export class SchemaBuilder {
120
120
 
121
121
  // **A null default is only spellable on a nullable column**, and conflating
122
122
  // the two turned every NOT NULL column without a default into a nullable
123
- // one. `Field`'s convention — stated in `asFieldCall` below — is that a null
123
+ // one. `Field`'s convention (stated in `asFieldCall` below) is that a null
124
124
  // default *means* nullable, so `Field.Int(null)` does not describe
125
125
  // `author_id integer NOT NULL`; it redefines it.
126
126
  //
@@ -158,7 +158,7 @@ export class SchemaBuilder {
158
158
  const named = SchemaBuilder.asFieldCall(cons, d, n)
159
159
  if (named) return `${indent}${colName}: ${named},\n`
160
160
 
161
- // Nothing in the `Field` vocabulary spells this column — nullable *and*
161
+ // Nothing in the `Field` vocabulary spells this column, nullable *and*
162
162
  // defaulted to something other than null, or an explicit `primary` that is
163
163
  // not auto-increment.
164
164
  //
@@ -168,7 +168,7 @@ export class SchemaBuilder {
168
168
  // column, rather than dropping one it cannot spell.
169
169
  const parts = [`type: '${cons.type}'`]
170
170
  if (typeof cons.length === 'number') parts.push(`length: ${cons.length}`)
171
- // `d` is already source text from `getDefaultValue` — a quoted literal, a
171
+ // `d` is already source text from `getDefaultValue`, a quoted literal, a
172
172
  // bare number, `null`, or the identifier `dateNow`. The marker is spelled
173
173
  // out here instead so the emitted file needs no import for it.
174
174
  if (d !== undefined) {
@@ -187,10 +187,10 @@ export class SchemaBuilder {
187
187
  // `as const`, and without it the folder layout loses these columns' types.
188
188
  //
189
189
  // `table()` takes `C extends ColumnMap`, which is `Record<string, unknown>`
190
- // — a constraint that does not preserve literals, so `{ type: 'integer' }`
190
+ //: a constraint that does not preserve literals, so `{ type: 'integer' }`
191
191
  // widens to `{ type: string }` and `InferSchema` has no `'integer'` left to
192
- // match. Every column the `Field` vocabulary cannot name — which since
193
- // foreign keys round-trip includes *every referencing column* — then infers
192
+ // match. Every column the `Field` vocabulary cannot name (which since
193
+ // foreign keys round-trip includes *every referencing column*), then infers
194
194
  // as a string.
195
195
  //
196
196
  // Invisible in the single-file layout, where `DBInfo.constraints` is already
@@ -204,14 +204,14 @@ export class SchemaBuilder {
204
204
  * Copy introspected foreign keys onto the columns that carry them.
205
205
  *
206
206
  * `getConstraints()` describes columns and `getForeignKeys()` describes
207
- * references, and the generator only ever read the first — so a database whose
207
+ * references, and the generator only ever read the first, so a database whose
208
208
  * `posts.author_id` references `users.id ON DELETE CASCADE` regenerated as a
209
209
  * plain integer. The constraint stayed in the database, the schema stopped
210
210
  * mentioning it, and the next sync therefore planned to rebuild the table to
211
211
  * *remove* a key nobody asked to remove.
212
212
  *
213
213
  * Single-column keys only. A composite key cannot be a property of one column
214
- * — it is declared with `foreign()` alongside the indexes — and inventing a
214
+ * (it is declared with `foreign()` alongside the indexes), and inventing a
215
215
  * per-column half of one would be worse than omitting it, so it is counted and
216
216
  * reported rather than silently dropped.
217
217
  */
@@ -265,7 +265,7 @@ export class SchemaBuilder {
265
265
  * anything else falls through to `value()` above rather than being
266
266
  * approximated into a column that means something slightly different.
267
267
  *
268
- * `_enum` is deliberately absent — the members are not part of the column diff
268
+ * `_enum` is deliberately absent: the members are not part of the column diff
269
269
  * and are not introspected, so the database cannot tell us an enum from a
270
270
  * `VARCHAR`, and guessing would silently invent a constraint.
271
271
  */
@@ -277,7 +277,7 @@ export class SchemaBuilder {
277
277
  ): string | null {
278
278
  if (cons.primary || cons.autoIncrement) return null
279
279
  // A referencing column has no `Field.*` spelling that also carries the
280
- // reference — `Field.Foreign` needs the parent table's column *value* in
280
+ // reference, `Field.Foreign` needs the parent table's column *value* in
281
281
  // scope, which the generated file cannot guarantee (introspection order is
282
282
  // not declaration order, and the single-file layout has no table values at
283
283
  // all). Fall through to the object literal, which spells `_references`
@@ -335,7 +335,7 @@ export class SchemaBuilder {
335
335
  * The TypeScript type of a column, for a generated view interface.
336
336
  *
337
337
  * A view has no column DDL, so this is the only place its columns appear.
338
- * Straight through `TypeMap`'s mapping, with `| null` for a nullable column —
338
+ * Straight through `TypeMap`'s mapping, with `| null` for a nullable column:
339
339
  * introspection reports that faithfully for a view's projected columns.
340
340
  */
341
341
  private static tsTypeFor(cons: any): string {
@@ -365,13 +365,13 @@ export class SchemaBuilder {
365
365
  * `SELECT ...`,
366
366
  * )
367
367
  *
368
- * The interface is what a view *is* — `CREATE VIEW` declares no column types,
368
+ * The interface is what a view *is*, `CREATE VIEW` declares no column types,
369
369
  * so emitting `Field.Varchar(64)` here would state a width the database
370
370
  * neither stores nor enforces. It is also the thing worth exporting: the row
371
371
  * type gets a name you can use in a signature.
372
372
  *
373
373
  * Both type arguments are written out because TypeScript stops inferring the
374
- * rest once one is supplied, and the *name* has to stay a literal — it is
374
+ * rest once one is supplied, and the *name* has to stay a literal: it is
375
375
  * what the schema map is keyed on. Generated code, so the repetition is free.
376
376
  */
377
377
  private static buildViewModule(
@@ -406,14 +406,14 @@ export class SchemaBuilder {
406
406
  *
407
407
  * A view is a stored SELECT; it has no column DDL, so each one is described by
408
408
  * an interface rather than by column builders. Edit the SELECT here and the
409
- * next sync recreates the view — views hold no data, so there is nothing to
409
+ * next sync recreates the view: views hold no data, so there is nothing to
410
410
  * migrate.
411
411
  *
412
412
  * **This file is yours from now on. The generator writes it once and never
413
413
  * overwrites it**, because the interfaces are the part worth editing by hand:
414
414
  * introspection can only report a JSON column as \`unknown\`, and the shape it
415
- * actually holds — \`{ id: number; name: string }[]\` for a
416
- * \`json_arrayagg(json_object(...))\` — is knowledge only you have. Regenerating
415
+ * actually holds: \`{ id: number; name: string }[]\` for a
416
+ * \`json_arrayagg(json_object(...))\`. Is knowledge only you have. Regenerating
417
417
  * over that would throw away the reason for writing it down.
418
418
  *
419
419
  * A view added to the database later will not appear here on its own; add it,
@@ -472,7 +472,7 @@ ${body}`
472
472
  return `/**
473
473
  * Generated from the database by \`db:sync\`.
474
474
  *
475
- * Seeded once and never overwritten — unlike \`tables.ts\`. An index this file
475
+ * Seeded once and never overwritten, unlike \`tables.ts\`. An index this file
476
476
  * does not declare is dropped by the next TS-wins sync, so add new ones here
477
477
  * rather than only in the database.
478
478
  *
@@ -495,7 +495,7 @@ ${body}`
495
495
  // `Field.Index` / `Field.Unique`, capitalised from the stored `index` /
496
496
  // `unique` type. This emitted the bare `index(…)` / `unique(…)` names
497
497
  // until they were removed, at which point the generated file referenced
498
- // two identifiers it did not import — invisible here because the *tables*
498
+ // two identifiers it did not import: invisible here because the *tables*
499
499
  // are what the round-trip test imports, not the index block.
500
500
  const fn = idx.type === 'unique' ? 'Field.Unique' : 'Field.Index'
501
501
  result += ` ${idxName}: ${fn}('${idx.table}', ${colsStr}),\n`
@@ -523,7 +523,7 @@ ${body}`
523
523
  *
524
524
  * A depth-first walk with a visiting set, which is also the cycle break: a
525
525
  * table already on the stack is left where it is, and the column pointing back
526
- * at it falls through to the object literal — correct, just not pretty.
526
+ * at it falls through to the object literal, correct, just not pretty.
527
527
  * Circular references between tables are legal SQL and this must not fail on
528
528
  * them or, worse, emit a forward reference that is `undefined` at module
529
529
  * evaluation.
@@ -561,7 +561,7 @@ ${body}`
561
561
  /**
562
562
  * `Field.Foreign(parent.column, …)` when the parent is already in scope.
563
563
  *
564
- * Returns null when it is not — the cycle case above — so the caller falls
564
+ * Returns null when it is not (the cycle case above), so the caller falls
565
565
  * back to the literal rather than emitting a reference to a `const` declared
566
566
  * further down the file, which is a TDZ error at import time.
567
567
  */
@@ -630,7 +630,7 @@ ${body}`
630
630
  colName,
631
631
  cons,
632
632
  adapter,
633
- // Never a view here — those were skipped above.
633
+ // Never a view here. Those were skipped above.
634
634
  false,
635
635
  ' ',
636
636
  )
@@ -653,7 +653,7 @@ ${body}`
653
653
  *
654
654
  * Tables only. In the orm/ folder layout \`index.ts\` owns the re-exports and
655
655
  * the schema registration, \`indexes.ts\` owns the index and unique
656
- * declarations, and \`views.ts\` owns the views — none of which is written here.
656
+ * declarations, and \`views.ts\` owns the views: none of which is written here.
657
657
  *
658
658
  * The cost of that separation, worth knowing: an index or a view the database
659
659
  * has and its file does not declare is dropped by the next TS-wins sync. It
@@ -672,7 +672,7 @@ ${body}`
672
672
  *
673
673
  * `Field` is imported from the package root and the types from
674
674
  * `schema-util`, in two statements rather than one, because `schema-util`
675
- * cannot re-export `Field` without closing a cycle — `field.ts` calls
675
+ * cannot re-export `Field` without closing a cycle: `field.ts` calls
676
676
  * `value`/`primary`/`index`/`unique`, and this repo has already paid once for
677
677
  * a cycle that typechecked and then failed at runtime.
678
678
  */
@@ -733,7 +733,7 @@ declare module '@bakery-framework/orm/schema-registry' {
733
733
  messages.GEN_TYPES()
734
734
 
735
735
  // Stripped, or `--choose=db` writes `__bakery_schema` into the app's own
736
- // schema as an ordinary table — after which sync manages the ledger, the
736
+ // schema as an ordinary table, after which sync manages the ledger, the
737
737
  // ledger records itself, and the shape check never matches again. The diff
738
738
  // path strips it in `resolveCurrentState`; this path reads the adapter
739
739
  // directly and was missing it.
@@ -768,7 +768,7 @@ declare module '@bakery-framework/orm/schema-registry' {
768
768
 
769
769
  await Bun.write(schemaPath, source)
770
770
 
771
- // `views.ts` beside `tables.ts`, and only in the folder layout — the
771
+ // `views.ts` beside `tables.ts`, and only in the folder layout: the
772
772
  // single-file layout already carries views inside its `DBInfo` namespace.
773
773
  //
774
774
  // Written only when the database *has* views: creating an empty file, and
@@ -781,7 +781,7 @@ declare module '@bakery-framework/orm/schema-registry' {
781
781
  )
782
782
  if (viewsSource) {
783
783
  const viewsPath = `${schemaPath.replace(/[^/\\]+$/, '')}views.ts`
784
- // Seeded once, then never overwritten — unlike `tables.ts`, which the
784
+ // Seeded once, then never overwritten, unlike `tables.ts`, which the
785
785
  // generator owns outright.
786
786
  //
787
787
  // The interfaces are the part worth editing by hand. Introspection can
@@ -804,7 +804,7 @@ declare module '@bakery-framework/orm/schema-registry' {
804
804
  // way the single-file layout is not: `DBInfo` carries an `indexes` block,
805
805
  // so `--choose=db` there round-trips them, while the folder layout left
806
806
  // every index undeclared. A TS-wins sync then drops what the schema does
807
- // not mention — so regenerating a folder-layout schema from a database
807
+ // not mention, so regenerating a folder-layout schema from a database
808
808
  // armed the *next* sync to delete every index in it.
809
809
  const indexes = await adapter.getIndexes()
810
810
  if (Object.keys(indexes).length) {
@@ -828,13 +828,13 @@ declare module '@bakery-framework/orm/schema-registry' {
828
828
  * Copy the current schema aside before it is overwritten.
829
829
  *
830
830
  * `--choose=db` regenerates the schema wholesale, and anything hand-written
831
- * in it — a comment, a `_view`, an `old()` rename wrapper — is gone. The
831
+ * in it (a comment, a `_view`, an `old()` rename wrapper) is gone. The
832
832
  * database is backed up before a destructive sync; the schema, which is
833
833
  * source, was not. It is also gitignored, so `git checkout` cannot recover
834
834
  * it either: this is the one file with no other safety net.
835
835
  *
836
836
  * Stored under `bakery/backups` (`Bakery.dataDir`) rather than the cache,
837
- * because a cache is defined as safe to delete and this is not — the
837
+ * because a cache is defined as safe to delete and this is not: the
838
838
  * framework itself deletes the cache directory on every version bump.
839
839
  */
840
840
  private static async preserveExisting(
package/src/sync/diff.ts CHANGED
@@ -34,7 +34,7 @@ function diffColumnMismatch(
34
34
  // TEXT trap that made this dangerous to add blind.
35
35
  //
36
36
  // Driven by the *schema* side only. When the schema declares a width, any
37
- // other answer from the database differs — including no width at all, which
37
+ // other answer from the database differs, including no width at all, which
38
38
  // is a real `TEXT` column that should become `VARCHAR(n)`. Requiring both
39
39
  // sides to be sized meant sizing an existing TEXT column silently did
40
40
  // nothing, and `db:sync` then reported a perfectly synced database whose
@@ -44,16 +44,16 @@ function diffColumnMismatch(
44
44
  // (measured, see `SQLAdapter.sizedTextLength`): after one rebuild the two
45
45
  // agree. A column that reports *no* width really is unsized.
46
46
  //
47
- // When the schema declares no width, nothing differs — `Field.Text()` against
47
+ // When the schema declares no width, nothing differs: `Field.Text()` against
48
48
  // an existing `VARCHAR` is not a request to shrink it.
49
49
  const lengthDiffers =
50
50
  typeof tsCol.length === 'number' && tsCol.length !== dbCol.length
51
51
 
52
52
  // Enum members join the diff, so changing them migrates instead of silently
53
- // doing nothing — but **only when the current state came from the ledger**.
53
+ // doing nothing, but **only when the current state came from the ledger**.
54
54
  //
55
55
  // `_enum` is emitted as an inline `CHECK (col IN (...))` by all three
56
- // dialects, and all three *will* report that constraint back — in three
56
+ // dialects, and all three *will* report that constraint back: in three
57
57
  // incompatible shapes. Measured:
58
58
  //
59
59
  // sqlite CHECK (status IN ('draft','live')) in the table DDL
@@ -61,14 +61,14 @@ function diffColumnMismatch(
61
61
  // pgsql CHECK (((status)::text = ANY ((ARRAY[...]))) re-rendered
62
62
  //
63
63
  // Postgres does not store the text it was given, it re-renders a parsed
64
- // expression — the same trap that turned `EXTRACT` into `date_part` and
64
+ // expression: the same trap that turned `EXTRACT` into `date_part` and
65
65
  // rebuilt a table on every sync forever. Three parsers, each an opportunity
66
66
  // for that bug, is the wrong trade when the ledger already holds the members
67
67
  // exactly as declared.
68
68
  //
69
69
  // So under introspection this stays out of the diff. A schema-side-only
70
70
  // comparison would find `_enum` on one side and nothing on the other, differ
71
- // every time, and rebuild the table on every sync — which is precisely what
71
+ // every time, and rebuild the table on every sync, which is precisely what
72
72
  // the `length` note above says it waited to rule out before shipping.
73
73
  const enumDiffers =
74
74
  plan.ledgerSource === 'ledger' &&
@@ -238,12 +238,12 @@ function diffViewStrings(
238
238
  constraints: any,
239
239
  database?: string,
240
240
  ): boolean {
241
- // Both sides through the same canonicaliser, which is the only thing that
241
+ // Both sides through the same canonicalizer, which is the only thing that
242
242
  // makes a text comparison viable: you write `SELECT id FROM users` and MySQL
243
243
  // returns it fully qualified, fully quoted and aliased column by column.
244
244
  //
245
- // Symmetry is the whole requirement. Normalising the *generated file* while
246
- // comparing raw — or stripping the schema on one side only — recreates the
245
+ // Symmetry is the whole requirement. Normalizing the *generated file* while
246
+ // comparing raw (or stripping the schema on one side only) recreates the
247
247
  // view on every sync, which is the same churn the column diff has hit twice.
248
248
  const tsViewStr = normalizeViewBody(
249
249
  String(constraints[camelTable]._view || ''),
@@ -265,7 +265,7 @@ function diffViewStrings(
265
265
  * The view lifecycle: create, recreate, drop.
266
266
  *
267
267
  * Views were invisible to the planner. `initDbTablesMap` skips `_view` entries,
268
- * and every existing comparison iterates that map — so a declared view never
268
+ * and every existing comparison iterates that map, so a declared view never
269
269
  * reached `diffViewStrings`, and nothing about a view ever reached
270
270
  * `hasChanges`. The consequences, all three measured:
271
271
  *
@@ -279,11 +279,11 @@ function diffViewStrings(
279
279
  * reported a perfectly synced database and left the view alone.
280
280
  *
281
281
  * Bodies are compared through `normalizeViewBody` on both sides. That converges
282
- * on SQLite, which stores the text verbatim, and via the ledger everywhere —
282
+ * on SQLite, which stores the text verbatim, and via the ledger everywhere:
283
283
  * the ledger records what was *applied*, so it holds the authored SELECT.
284
284
  * Diffing against live introspection on MySQL or Postgres will still see a
285
285
  * difference, because both re-render the body (MySQL re-qualifies every column,
286
- * Postgres adds parentheses), and no amount of text normalisation short of a
286
+ * Postgres adds parentheses), and no amount of text normalization short of a
287
287
  * parser fixes that. It costs a recreate, and a view holds no data.
288
288
  */
289
289
  export function diffViews(
@@ -17,17 +17,17 @@ import type * as SyncTypes from './types'
17
17
  export const syncMsgs = {
18
18
  GEN_TYPES: 'I Generating types...',
19
19
  // Counted rather than silently dropped: a composite key is not a property of
20
- // one column, so the generator cannot spell it — but a reader who is not told
20
+ // one column, so the generator cannot spell it, but a reader who is not told
21
21
  // will believe the generated schema is complete.
22
22
  GEN_COMPOSITE_FK:
23
- 'W %y{count}%* composite foreign key(s) could not be generated as column references — declare them with %yforeign()%* beside the indexes.',
23
+ 'W %y{count}%* composite foreign key(s) could not be generated as column references. Declare them with %yforeign()%* beside the indexes.',
24
24
  INDEXES_SEEDED: 'I Seeded %y{file}%* from the database.',
25
25
  INDEXES_KEPT:
26
26
  'I Left %y{file}%* alone: index declarations are hand-owned once seeded. Delete it and re-run to reseed.',
27
27
  MIGRATE_DONE:
28
28
  'I %gAdopted the existing database%*. No tables were changed; run %ydb:sync%* to confirm nothing is pending.',
29
29
  MIGRATE_NO_LEDGER:
30
- 'W Schema written, but the ledger could not be recorded — the next sync will diff against live introspection instead.',
30
+ 'W Schema written, but the ledger could not be recorded: the next sync will diff against live introspection instead.',
31
31
  SYNC_SUCCESS: 'I %gschema.ts successfully synced%* to Database!',
32
32
  INVALID_SCHEMA: 'W %yschema.ts is invalid or corrupt. Treating as new.%*',
33
33
  NO_DBINFO: 'W %yDBInfo namespace not found in schema.ts!%*',
@@ -70,7 +70,7 @@ export const syncMsgs = {
70
70
  EXEC_ADD_INDEX: 'I Creating %y{type}%* index: %g{name}%*...',
71
71
  // Declared late. `executeSyncPlan` has always called these two, and
72
72
  // `messageLogger`'s proxy answers an undeclared key with a live emitter that
73
- // prints `E Error message not found: EXEC_ADD_FK` — so every foreign key
73
+ // prints `E Error message not found: EXEC_ADD_FK`, so every foreign key
74
74
  // added or dropped on a dialect that can ALTER one logged an *error* where
75
75
  // its ten sibling operations logged progress. Only reachable with
76
76
  // `supportsAlterForeignKey`, which is why SQLite-only local runs never
@@ -89,11 +89,11 @@ export const syncMsgs = {
89
89
  FATAL_ERROR:
90
90
  'E %rFATAL ERROR: Sync failed! All changes have been safely rolled back. Detail: {error}%*',
91
91
  VIEWS_SEEDED:
92
- 'I Seeded %y{file}%* from the database. It is yours from now on — the generator will not overwrite it.',
92
+ 'I Seeded %y{file}%* from the database. It is yours from now on: the generator will not overwrite it.',
93
93
  VIEWS_KEPT:
94
94
  'I Left %y{file}%* alone: view interfaces are hand-owned. Delete it and re-run to reseed.',
95
95
  LEDGER_DRIFT:
96
- 'W %yDatabase has drifted from the last schema Bakery applied%*: {reason}. Diffing against live introspection instead of the ledger. Something changed this database outside Bakery — if that was not deliberate, check it before syncing.',
96
+ 'W %yDatabase has drifted from the last schema Bakery applied%*: {reason}. Diffing against live introspection instead of the ledger. Something changed this database outside Bakery: if that was not deliberate, check it before syncing.',
97
97
  } as const
98
98
 
99
99
  const logger = new Logger('db-sync')
@@ -102,7 +102,7 @@ export const MESSAGES = messageLogger(logger, syncMsgs)
102
102
  /**
103
103
  * Whether a destructive sync in this process needs an explicit `--force-sync`.
104
104
  *
105
- * **`NODE_ENV` is the only term, deliberately — do not add a `PROD` check back.**
105
+ * **`NODE_ENV` is the only term, deliberately. Do not add a `PROD` check back.**
106
106
  * There used to be a `process.env.PROD === 'true'` half that could never be
107
107
  * true, since `core/init.ts` installs `PROD` as a getter returning a *boolean*.
108
108
  * Repairing it rather than deleting it would have been worse: `PROD` means only
@@ -136,7 +136,7 @@ export class SyncEngine {
136
136
  /**
137
137
  * Record the adopted database as the schema Bakery last applied.
138
138
  *
139
- * Without this, the first sync after an adoption falls back to introspection —
139
+ * Without this, the first sync after an adoption falls back to introspection:
140
140
  * safe, but it withdraws the thing the ledger is for, and enum member changes
141
141
  * only migrate when the diff runs against the ledger.
142
142
  *
@@ -201,7 +201,7 @@ export class SyncEngine {
201
201
  // Creating or recreating a view cannot lose anything: a view holds no data,
202
202
  // and `createView` drops and recreates in one step. Counting it as
203
203
  // destructive made a schema with a view demand `--force-sync` in production
204
- // for no reason — and it matters more now that views are diffed at all,
204
+ // for no reason, and it matters more now that views are diffed at all,
205
205
  // because MySQL and Postgres re-render a stored body, so a run that falls
206
206
  // back to introspection sees one as changed every time.
207
207
  //
@@ -298,11 +298,11 @@ export class SyncEngine {
298
298
  // After the transaction commits, never inside it: MySQL commits DDL
299
299
  // implicitly, so there is no unit of work these two could share anyway, and
300
300
  // writing the ledger first would claim a migration that had not happened.
301
- // Best-effort by design — a sync that succeeded still succeeded, and a
301
+ // Best-effort by design: a sync that succeeded still succeeded, and a
302
302
  // missing ledger only costs the next run its fast path.
303
303
  // `tsIndexes`, not `adapter.getIndexes()`: the ledger records what was
304
304
  // *applied*, and re-reading the database here would record whatever the
305
- // dialect reports back — the exact normalisation problem the ledger exists
305
+ // dialect reports back, the exact normalization problem the ledger exists
306
306
  // to route around.
307
307
  await writeLedger(adapter, constraints, tsIndexes)
308
308
 
@@ -328,7 +328,7 @@ export class SyncEngine {
328
328
  //
329
329
  // It is not `--choose=db` with a nicer name. `--choose=db` sits *after* the
330
330
  // "no changes" early return, so on a database that already matches it does
331
- // nothing — which is the common case when adopting one, since the schema
331
+ // nothing, which is the common case when adopting one, since the schema
332
332
  // being written is derived from that same database. And it never writes the
333
333
  // ledger, so the first real sync afterwards diffs against introspection
334
334
  // instead: the one place enum changes are invisible.
@@ -337,7 +337,7 @@ export class SyncEngine {
337
337
  //
338
338
  // Passing the loaded schema in makes `syncNullableConstraints` reconcile
339
339
  // view column nullability against it, which is right for a regeneration of
340
- // a schema you are keeping and wrong for this — the point of `--migrate`
340
+ // a schema you are keeping and wrong for this: the point of `--migrate`
341
341
  // is that the database is the source of truth, and quietly carrying a
342
342
  // detail over from the file being replaced would make the result depend on
343
343
  // what happened to be there.
@@ -359,13 +359,13 @@ export class SyncEngine {
359
359
  // column rename existed, which dated from SQLite before 3.25 (2018) having
360
360
  // no ALTER TABLE … RENAME COLUMN. Bun ships 3.53. Measured against a real
361
361
  // database, the special case lost a rename in a non-renamed table silently
362
- // — reporting a perfect sync — and threw outright in the two cases where it
362
+ // (reporting a perfect sync), and threw outright in the two cases where it
363
363
  // did fire. See sync/engine.test.ts, which pins all four.
364
364
  const plan = await buildSyncPlan(adapter, constraints, logger, MESSAGES)
365
365
 
366
366
  // `buildSyncPlan` has always recorded which state it diffed against and
367
- // nothing ever read it. Falling back to introspection is *correct* — see
368
- // sync/ledger.ts — but doing it silently means a column somebody added by
367
+ // nothing ever read it. Falling back to introspection is *correct*. See
368
+ // sync/ledger.ts, but doing it silently means a column somebody added by
369
369
  // hand in production looks exactly like an ordinary run. "No ledger yet" is
370
370
  // the normal state of a fresh database and is not drift.
371
371
  if (
@@ -394,8 +394,8 @@ export class SyncEngine {
394
394
  plan.tablesToRebuild,
395
395
  beingCreated,
396
396
  )
397
- // SQLite cannot ALTER a foreign key in or out — the constraint is part of
398
- // the table definition — so those become table rebuilds, which recreate the
397
+ // SQLite cannot ALTER a foreign key in or out (the constraint is part of
398
+ // the table definition), so those become table rebuilds, which recreate the
399
399
  // table with the keys inline. Decided here rather than in the adapter so
400
400
  // the printed plan states the work that will actually happen.
401
401
  if (!adapter.supportsAlterForeignKey) {
@@ -424,7 +424,7 @@ export class SyncEngine {
424
424
  logPlannedChanges(plan, indexesToDrop, indexesToAdd, isDangerous, MESSAGES)
425
425
  if (argv.includes('--dry-run')) {
426
426
  // `logger.log` takes the level as its second argument. A leading 'D ' is
427
- // `messageLogger` table syntax, and this is not a table — so the letter
427
+ // `messageLogger` table syntax, and this is not a table, so the letter
428
428
  // was printed verbatim and the level defaulted.
429
429
  //
430
430
  // `info`, not the `debug` the stray 'D' was reaching for: this line is