@bakery-framework/orm 2.0.0-alpha.8 → 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.
@@ -1,10 +1,20 @@
1
1
  import path from 'node:path'
2
2
  import { Bakery } from '@bakery-framework/core/core/bakery'
3
+ import { Logger, messageLogger } from '@bakery-framework/core/logger'
3
4
  import { Case, Try } from '@bakery-framework/core/utils'
4
5
  import { SQL } from 'bun'
5
6
  import type * as SyncTypes from '../sync/types'
6
7
  import { createExecutor, SQLAdapter } from './base'
7
8
 
9
+ // Convention 4: logging is data, declared in a table rather than formatted at
10
+ // the call site.
11
+ const MESSAGES = messageLogger(new Logger('database'), {
12
+ JOURNAL_WAL_REFUSED:
13
+ 'W SQLite refused WAL for %y{file}%* (answered %y{answer}%*): running the %y{mode}%* journal instead. Expected on a network path; on a local disk it costs every write.',
14
+ PRAGMA_FAILED:
15
+ 'W A SQLite performance pragma was rejected: %r{error}%*. The database works; it is not tuned.',
16
+ } as const)
17
+
8
18
  export class SQLiteAdapter extends SQLAdapter {
9
19
  // SQLite's standard identifier quote. The base class defaults to MySQL's
10
20
  // backtick, which SQLite only tolerates as a compatibility extension.
@@ -12,12 +22,26 @@ export class SQLiteAdapter extends SQLAdapter {
12
22
 
13
23
  protected readonly sql: SQL
14
24
 
25
+ /**
26
+ * Resolves when the performance pragmas have finished, or failed loudly.
27
+ *
28
+ * They are deliberately not awaited by every query the way `ready` is:
29
+ * tuning is not correctness, and making the first statement of every process
30
+ * wait on six round trips to gain nothing is the wrong trade. But `close()`
31
+ * has to wait, because a handle closed underneath an in-flight pragma
32
+ * rejects with `Connection closed`, and that is indistinguishable at the
33
+ * catch from a filesystem genuinely refusing one. A short-lived adapter (a
34
+ * test, a one-shot script) would otherwise warn on every close about a
35
+ * failure that never happened.
36
+ */
37
+ protected readonly tuned: Promise<unknown> = Promise.resolve()
38
+
15
39
  /**
16
40
  * Resolves once `foreign_keys` is on, and every query awaits it.
17
41
  *
18
42
  * SQLite defaults the pragma OFF and it is per-connection, so without this a
19
43
  * FOREIGN KEY is stored, reported by `PRAGMA foreign_key_list`, shown in the
20
- * dashboard — and enforces nothing.
44
+ * dashboard, and enforces nothing.
21
45
  *
22
46
  * Deliberately *not* in the performance pragma chain above it. That chain is
23
47
  * fire-and-forget by design (its own comment calls the statements
@@ -124,7 +148,7 @@ export class SQLiteAdapter extends SQLAdapter {
124
148
  filename,
125
149
  typeof connectionTarget === 'string' ? connectionTarget : undefined,
126
150
  )
127
- // `sql` is supplied when wrapping an existing connection — notably once per
151
+ // `sql` is supplied when wrapping an existing connection: notably once per
128
152
  // transaction. Setup below belongs only to a connection we open ourselves;
129
153
  // re-running it per transaction meant a mkdirSync plus six unawaited
130
154
  // PRAGMAs
@@ -143,7 +167,7 @@ export class SQLiteAdapter extends SQLAdapter {
143
167
  (filename === ':memory:'
144
168
  ? new SQL('sqlite://:memory:')
145
169
  : new SQL(filename, { adapter: 'sqlite' }))
146
- // Every owned connection, `:memory:` included — the perf pragmas below
170
+ // Every owned connection, `:memory:` included, the perf pragmas below
147
171
  // skip in-memory databases, but correctness does not get to.
148
172
  this.ready = ownsConnection
149
173
  ? this.sql.unsafe('PRAGMA foreign_keys = ON;')
@@ -151,18 +175,105 @@ export class SQLiteAdapter extends SQLAdapter {
151
175
 
152
176
  if (ownsConnection && filename !== ':memory:') {
153
177
  const cacheSize = import.meta.env.THREAD_WORKER ? -1000 : -10000
154
- const journalMode = process.platform === 'win32' ? 'DELETE' : 'WAL'
155
- this.sql
156
- .unsafe(`PRAGMA journal_mode = ${journalMode};`)
178
+ this.tuned = this.sql
179
+ .unsafe('PRAGMA journal_mode = WAL;')
180
+ .then((rows: unknown) => this.confirmWAL(rows, filename))
157
181
  .then(() => this.sql.unsafe('PRAGMA synchronous = NORMAL;'))
158
182
  .then(() => this.sql.unsafe('PRAGMA temp_store = memory;'))
159
183
  .then(() => this.sql.unsafe(`PRAGMA cache_size = ${cacheSize};`))
160
184
  .then(() => this.sql.unsafe('PRAGMA busy_timeout = 5000;'))
161
185
  .then(() => this.sql.unsafe('PRAGMA mmap_size = 0;'))
162
- .catch(() => {})
186
+ // Not `catch(() => {})`. Every performance pragma failing silently is
187
+ // how a database ends up running at a fraction of its speed with
188
+ // nothing anywhere saying so, and convention 3 bans the bare form for
189
+ // exactly this. These are tuning, not correctness (`foreign_keys`
190
+ // above is awaited and is allowed to reject), so a failure is a
191
+ // warning rather than a throw.
192
+ .catch((error: Error) => {
193
+ MESSAGES.PRAGMA_FAILED({ error: error.message })
194
+ })
163
195
  }
164
196
  }
165
197
 
198
+ /**
199
+ * Waits for the pragma chain before closing the handle. See `tuned`.
200
+ */
201
+ override async close(): Promise<void> {
202
+ await Try.catch(this.tuned)
203
+ await super.close()
204
+ }
205
+
206
+ /**
207
+ * **WAL first, DELETE only where WAL is actually refused.**
208
+ *
209
+ * The journal mode used to be `platform === 'win32' ? 'DELETE' : 'WAL'`, a
210
+ * rule with no recorded reason. The hypothesis behind it was that a Windows
211
+ * network path cannot host WAL, which is true (WAL needs a shared `-shm`
212
+ * mapping that SMB and WebDAV do not provide), but the rule applied to every
213
+ * Windows install, local disks included, where WAL works.
214
+ *
215
+ * Measured on this machine, four interleaved rounds against a CPU-bound
216
+ * control that stayed flat at 29-34 ms:
217
+ *
218
+ * single autocommit write DELETE 3629 us WAL 36 us 100x
219
+ * 100-row transaction DELETE 3.91 ms WAL 0.13 ms 29x
220
+ *
221
+ * An attempt and a check replaces the rule, which is what makes it safe to
222
+ * change without knowing why it was there. SQLite refuses WAL in two
223
+ * observable ways and both are handled: it returns the *unchanged* mode in
224
+ * the pragma's own result row (an in-memory database answers `memory`), or
225
+ * it throws. Both shapes were reproduced before this was written, so the
226
+ * fallback rests on a measurement rather than on the documentation.
227
+ *
228
+ * The same logic runs on the cache database in
229
+ * `@bakery-framework/core/cache/shared-db`. It is deliberately not shared:
230
+ * that site is synchronous `bun:sqlite` and this one is an async
231
+ * `Bun.SQL`, so the control flow has nothing in common and only the
232
+ * predicate would move: at the cost of a new published export subpath on a
233
+ * surface that was closed on purpose before 2.0.0.
234
+ */
235
+ private async confirmWAL(rows: unknown, filename: string): Promise<void> {
236
+ const first = Array.isArray(rows) ? rows[0] : undefined
237
+ const answer = String(
238
+ (first as { journal_mode?: unknown } | undefined)?.journal_mode ?? '',
239
+ )
240
+ if (answer.toLowerCase() === 'wal') return
241
+
242
+ const fell = await this.sql.unsafe('PRAGMA journal_mode = DELETE;')
243
+ const mode = Array.isArray(fell)
244
+ ? String((fell[0] as { journal_mode?: unknown })?.journal_mode ?? 'unknown')
245
+ : 'unknown'
246
+ MESSAGES.JOURNAL_WAL_REFUSED({
247
+ file: filename,
248
+ answer: answer || '(no row)',
249
+ mode,
250
+ })
251
+ }
252
+
253
+ /**
254
+ * `PRAGMA schema_version`, which is a counter SQLite bumps on every schema
255
+ * change and leaves alone for every row change.
256
+ *
257
+ * Measured on a 50-table database: the full introspection the explorer runs
258
+ * per write is **250 statements and 8.28 ms**, and this is **10.4 us** - 795x.
259
+ * The semantics were checked rather than taken from the documentation: the
260
+ * counter advances on `CREATE TABLE`, on `ALTER TABLE ... ADD COLUMN` and on
261
+ * `DROP TABLE`, and does not move for an `INSERT`.
262
+ *
263
+ * Prefixed with the driver and the file so two different SQLite databases
264
+ * cannot collide on the same small integer. A fresh database starts near
265
+ * zero, so "version 3" is a value many of them hold at once.
266
+ */
267
+ override async schemaFingerprint(): Promise<string | null> {
268
+ const row = (await this.query('PRAGMA schema_version').get()) as
269
+ | { schema_version?: number }
270
+ | null
271
+ | undefined
272
+ const version = row?.schema_version
273
+ if (typeof version !== 'number') return null
274
+ return `sqlite:${this.filename ?? ''}:${version}`
275
+ }
276
+
166
277
  private static resolveFilename(rawValue?: string | null): string {
167
278
  const envVal = process.env.DATABASE_URL || process.env.SQLITE_PATH
168
279
  // `Bakery.dataDir`, not a literal: the default database file has to follow
@@ -212,7 +323,7 @@ export class SQLiteAdapter extends SQLAdapter {
212
323
  )
213
324
 
214
325
  // `PRAGMA table_info('${table}')` interpolated the table name into a
215
- // string literal — a second SQL writer on a public adapter method, outside
326
+ // string literal, a second SQL writer on a public adapter method, outside
216
327
  // the `qId`/`qRef`/`safeColumn` guards convention 8 makes the only ones.
217
328
  // The `pragma_table_info` table-valued function takes the same argument as a
218
329
  // bound parameter, so there is nothing left to quote; MySQL and Postgres
@@ -236,8 +347,7 @@ export class SQLiteAdapter extends SQLAdapter {
236
347
  bigint: 'BIGINT',
237
348
  json: 'JSON',
238
349
  }[d.type as string] || 'TEXT'
239
- // SQLite has no VARCHAR of its own — every text column is TEXT affinity —
240
- // but it stores the *declared* type verbatim and hands it back through
350
+ // SQLite has no VARCHAR of its own (every text column is TEXT affinity), // but it stores the *declared* type verbatim and hands it back through
241
351
  // `pragma table_info`. Emitting the width is therefore free here and is
242
352
  // what lets one schema round-trip on all three dialects.
243
353
  let out =
@@ -282,7 +392,9 @@ export class SQLiteAdapter extends SQLAdapter {
282
392
  return new SQLiteAdapter(this.filename, sql as SQL)
283
393
  }
284
394
 
285
- async getSchema(): Promise<SQLAdapter.TableDetails[]> {
395
+ async getSchema(
396
+ options?: SQLAdapter.SchemaOptions,
397
+ ): Promise<SQLAdapter.TableDetails[]> {
286
398
  const res = (await this.query(
287
399
  'SELECT name FROM sqlite_master' +
288
400
  " WHERE type='table' AND name NOT LIKE 'sqlite_%'",
@@ -292,30 +404,31 @@ export class SQLiteAdapter extends SQLAdapter {
292
404
  // 20-table schema cost 20 sequential round-trip waves. They share nothing,
293
405
  // so the whole fan-out is one wave now.
294
406
  //
295
- // Worth knowing before this grows: the `COUNT(*)` is a full scan on this
296
- // dialect and on Postgres, and it is paid on every caller of `getSchema()`
297
- // even though only a schema *listing* displays the number. Making it
298
- // optional means `rowCount` has to admit it can be absent, which is a type
299
- // change across every consumer — recorded rather than done here.
407
+ // The `COUNT(*)` is a full scan on this dialect and on Postgres, so it is
408
+ // opt-in: only a schema *listing* displays the number, and `getSchema()`
409
+ // also sits on the explorer's write path, where a count nobody shows cost
410
+ // a scan of every table per write.
300
411
  return await Promise.all(
301
412
  res.map(async t => {
302
413
  const tableName = this.quote(t.name)
303
414
 
304
415
  const [countRes, cols, idxs] = (await Promise.all([
305
- this.query(`SELECT COUNT(*) as count FROM ${tableName}`).get(),
416
+ options?.rowCounts
417
+ ? this.query(`SELECT COUNT(*) as count FROM ${tableName}`).get()
418
+ : null,
306
419
  this.query(`PRAGMA table_info(${tableName})`).all(),
307
420
  this.query(`PRAGMA index_list(${tableName})`).all(),
308
- ])) as [SQLAdapter.CountRow, any[], any[]]
421
+ ])) as [SQLAdapter.CountRow | null, any[], any[]]
309
422
 
310
423
  return {
311
424
  name: t.name,
312
- rowCount: countRes?.count || 0,
425
+ rowCount: options?.rowCounts ? countRes?.count || 0 : null,
313
426
  columns: cols.map(c => ({
314
427
  name: c.name,
315
428
  type: c.type,
316
429
  notnull: c.notnull === 1,
317
430
  // `pk` is the column's **1-based position within the primary key**,
318
- // not a boolean — `PRAGMA table_info` reports 0 for "not part of the
431
+ // not a boolean: `PRAGMA table_info` reports 0 for "not part of the
319
432
  // key", 1 for the first key column, 2 for the second. So `=== 1`
320
433
  // reported a composite `PRIMARY KEY (a, b)` as a single-column key on
321
434
  // `a`, silently, and only on SQLite: MySQL reads `column_key = 'PRI'`
@@ -348,17 +461,23 @@ export class SQLiteAdapter extends SQLAdapter {
348
461
 
349
462
  const { page, pageSize } = options
350
463
 
464
+ // See `TableDataOptions.knownTotal`: the count is 97% of a page's cost,
465
+ // and a caller that has already counted can say so.
466
+ const reuse = SQLAdapter.usableTotal(options.knownTotal)
467
+
351
468
  const [countRes, rows] = (await Promise.all([
352
- this.query(`SELECT COUNT(*) as count FROM ${tname}${whereSql}`).get(
353
- ...whereParams,
354
- ),
469
+ reuse
470
+ ? Promise.resolve(null)
471
+ : this.query(`SELECT COUNT(*) as count FROM ${tname}${whereSql}`).get(
472
+ ...whereParams,
473
+ ),
355
474
  this.query(
356
475
  `SELECT rowid AS rowid, * FROM ${tname}` +
357
476
  `${whereSql}${orderSql} LIMIT ? OFFSET ?`,
358
477
  ).all(...whereParams, pageSize, (page - 1) * pageSize),
359
- ])) as [SQLAdapter.CountRow, any[]]
478
+ ])) as [SQLAdapter.CountRow | null, any[]]
360
479
 
361
- const totalRows = countRes?.count || 0
480
+ const totalRows = reuse ? options.knownTotal! : countRes?.count || 0
362
481
  return {
363
482
  rows,
364
483
  totalRows,
@@ -446,7 +565,7 @@ export class SQLiteAdapter extends SQLAdapter {
446
565
  }
447
566
 
448
567
  /**
449
- * SQLite has `UNION ALL` but neither `INTERSECT ALL` nor `EXCEPT ALL` — the
568
+ * SQLite has `UNION ALL` but neither `INTERSECT ALL` nor `EXCEPT ALL`: the
450
569
  * `ALL` modifier is only accepted after `UNION`.
451
570
  */
452
571
  override get supportsSetOperationAll(): boolean {
@@ -466,9 +585,13 @@ export class SQLiteAdapter extends SQLAdapter {
466
585
  const tName = Case.camel(table.name)
467
586
  dbConstraints[tName] = {} as SyncTypes.TableConstraints
468
587
 
588
+ // Bound, not interpolated: the same conversion `hasCol` above
589
+ // records. This one survived it: `getConstraints` runs over every table
590
+ // the database reports, so a name carrying an apostrophe closed the
591
+ // literal and the rest of the statement went with it.
469
592
  const cols = (await this.query(
470
- `PRAGMA table_info('${table.name}')`,
471
- ).all()) as any[]
593
+ 'SELECT * FROM pragma_table_info(?)',
594
+ ).all(table.name)) as any[]
472
595
 
473
596
  if (table.type === 'view') {
474
597
  const match = table.sql.match(/AS\s+(.*)/is)
@@ -505,18 +628,26 @@ export class SQLiteAdapter extends SQLAdapter {
505
628
  ).all()) as any[]
506
629
  return Object.fromEntries(
507
630
  await Promise.all(
508
- indexes.map(async idx => [
509
- Case.camel(idx.name),
510
- {
511
- type: idx.sql.toUpperCase().includes('UNIQUE') ? 'unique' : 'index',
512
- table: Case.camel(idx.tbl_name),
513
- cols: (
514
- (await this.query(
515
- `PRAGMA index_info('${idx.name}')`,
516
- ).all()) as any[]
517
- ).map(c => Case.camel(c.name)),
518
- },
519
- ]),
631
+ indexes.map(async idx => {
632
+ // Bound for the same reason as `getConstraints` above: an index
633
+ // name comes from `sqlite_master`, not from this codebase.
634
+ const raw = (
635
+ (await this.query('SELECT * FROM pragma_index_info(?)').all(
636
+ idx.name,
637
+ )) as any[]
638
+ ).map(c => String(c.name))
639
+ return [
640
+ Case.camel(idx.name),
641
+ {
642
+ type: idx.sql.toUpperCase().includes('UNIQUE')
643
+ ? 'unique'
644
+ : 'index',
645
+ table: Case.camel(idx.tbl_name),
646
+ cols: raw.map(Case.camel),
647
+ rawCols: raw,
648
+ },
649
+ ]
650
+ }),
520
651
  ),
521
652
  )
522
653
  }
@@ -536,7 +667,7 @@ export class SQLiteAdapter extends SQLAdapter {
536
667
  /**
537
668
  * SQLite has no UUID function, so the canonical 8-4-4-4-12 form is assembled
538
669
  * from `randomblob(16)`. Version and variant nibbles are **not** forced, so
539
- * this is a random 128-bit value in UUID shape rather than a conforming v4 —
670
+ * this is a random 128-bit value in UUID shape rather than a conforming v4:
540
671
  * unique, but do not hand it to something that validates the version field.
541
672
  *
542
673
  * SQLite stores the default expression verbatim and hands it back the same
@@ -559,7 +690,7 @@ export class SQLiteAdapter extends SQLAdapter {
559
690
 
560
691
  // SQLite has no catalog column for width; it stores the *declared* type
561
692
  // verbatim and hands it back through `pragma table_info`, so the number is
562
- // in the type string — 'VARCHAR(64)'. That is also why emitting the width
693
+ // in the type string, 'VARCHAR(64)'. That is also why emitting the width
563
694
  // is worth doing on a dialect with no real VARCHAR: it is what lets one
564
695
  // schema round-trip on all three.
565
696
  const declared = String(col.type || '')
@@ -587,7 +718,7 @@ export class SQLiteAdapter extends SQLAdapter {
587
718
  }
588
719
 
589
720
  // `protected override`, not `private`: narrowing a base member's visibility
590
- // is TS2415, and it made SQLiteAdapter fail to satisfy SQLAdapter — so the
721
+ // is TS2415, and it made SQLiteAdapter fail to satisfy SQLAdapter, so the
591
722
  // one adapter with real test coverage did not typecheck against the contract
592
723
  // the two untested ones share.
593
724
  protected override parseDefault(def: any): any {
@@ -600,7 +731,7 @@ export class SQLiteAdapter extends SQLAdapter {
600
731
  // sqlite_master hands back the literal exactly as formatDefault wrote it,
601
732
  // so the quote it doubled per SQL rules is still doubled. Stripping the
602
733
  // delimiters without collapsing it leaves `it''s fine` where the schema
603
- // says `it's fine` — drift diffColumnMismatch can never resolve, and a
734
+ // says `it's fine`: drift diffColumnMismatch can never resolve, and a
604
735
  // table rebuild on every db:sync as a result.
605
736
  return unquoted.replaceAll(quote + quote, quote)
606
737
  }
package/src/adapters.ts CHANGED
@@ -6,7 +6,7 @@ import { registerAdapter, resolveAdapter } from './adapters/registry'
6
6
  /**
7
7
  * Does this target name a SQLite file rather than a server?
8
8
  *
9
- * Deliberately greedy — anything with a path separator lands here — because it
9
+ * Deliberately greedy (anything with a path separator lands here), because it
10
10
  * is consulted only *after* every registered scheme has had its say. Before
11
11
  * the registry that ordering was implicit and this heuristic ran second by
12
12
  * luck; now `protocols` is checked first by contract, so an adapter that
@@ -27,7 +27,7 @@ function isSQLite(val: string) {
27
27
  /**
28
28
  * The three built-in adapters, registered at module load.
29
29
  *
30
- * Registering is cheap — three object literals — because `open` is what pulls
30
+ * Registering is cheap (three object literals), because `open` is what pulls
31
31
  * the driver in. An app on SQLite never loads `mysql.ts` or `pgsql.ts`, which
32
32
  * was true of the `switch` this replaced and had to stay true of the registry.
33
33
  */
@@ -64,8 +64,8 @@ registerAdapter({
64
64
  export async function createDbAdapter() {
65
65
  const url = process.env.DB_URL || process.env.DATABASE_URL || ''
66
66
  const { poolOptionsFromEnv } = await import('./pool')
67
- // Read once and handed to every adapter. SQLite ignores it — a single file
68
- // handle has no pool to size — but the registry cannot know which does what,
67
+ // Read once and handed to every adapter. SQLite ignores it (a single file
68
+ // handle has no pool to size), but the registry cannot know which does what,
69
69
  // so the option goes to all of them and each takes what it needs.
70
70
  const pool = poolOptionsFromEnv()
71
71
  return await resolveAdapter(url).open(url || undefined, pool)
package/src/backup.ts CHANGED
@@ -12,7 +12,7 @@ const MESSAGES = messageLogger(new Logger('db-backup'), {
12
12
 
13
13
  /**
14
14
  * Returns whether a backup file was actually written. Callers about to run a
15
- * destructive migration must check this — a thrown error, a `:memory:` database,
15
+ * destructive migration must check this: a thrown error, a `:memory:` database,
16
16
  * or a missing `pg_dump`/`mysqldump` all previously looked identical to success.
17
17
  */
18
18
  export async function backupDatabase(adapter?: any): Promise<boolean> {
package/src/connection.ts CHANGED
@@ -8,7 +8,7 @@ let testDb: any = null
8
8
 
9
9
  /**
10
10
  * Test seam. Unit tests that only need a stub adapter should call this instead
11
- * of `mock.module('./connection', …)` — Bun's module mocks are process-global
11
+ * of `mock.module('./connection', …)`: Bun's module mocks are process-global
12
12
  * and are never restored, so one mocking test file silently breaks every later
13
13
  * test that needs a real connection.
14
14
  */
package/src/define.ts CHANGED
@@ -8,15 +8,15 @@ import type {
8
8
  * Prototype: tables as values.
9
9
  *
10
10
  * Today a schema is one `constraints` object plus a hand-written `DBSchema`
11
- * type, and everything that references a table — indexes, foreign keys, query
12
- * columns — does so by string. Typos surface at sync time, or not at all.
11
+ * type, and everything that references a table (indexes, foreign keys, query
12
+ * columns) does so by string. Typos surface at sync time, or not at all.
13
13
  *
14
14
  * Here a table is a value carrying its own name and columns, so `indexes.ts`
15
15
  * and `foreign.ts` can import it, rename-symbol works across files, and an
16
16
  * identifier reaching SQL is one the framework constructed rather than one a
17
17
  * caller typed.
18
18
  *
19
- * The column descriptors are unchanged — the `Field` builders and the
19
+ * The column descriptors are unchanged: the `Field` builders and the
20
20
  * `ExtractTableTypes` mapping are reused as-is, so this is a restructuring of
21
21
  * how tables are *declared*, not of how their types are computed. That is what
22
22
  * keeps `DB.table('users')` autocompleting exactly as it does now: the derived
@@ -31,7 +31,7 @@ export type ColumnMap = Record<string, unknown>
31
31
  * from, and the columns themselves.
32
32
  *
33
33
  * **Was `TableDef`, and the rename is the point.** `schema-util.ts` exports a
34
- * `TableDef<T, N, O>` describing a *column* — and that is the one every
34
+ * `TableDef<T, N, O>` describing a *column*, and that is the one every
35
35
  * internal call site, every test and every schema file means. Both were public,
36
36
  * and the root barrel re-exported *this* one, so `import type { TableDef } from
37
37
  * '@bakery-framework/orm'` handed you a table where you asked for a column. Nothing
@@ -43,7 +43,7 @@ export interface TableRef<
43
43
  N extends string = string,
44
44
  C extends ColumnMap = ColumnMap,
45
45
  > {
46
- /** The name columns qualify against — the alias, when aliased. */
46
+ /** The name columns qualify against: the alias, when aliased. */
47
47
  readonly __table: N
48
48
  /** The real table to read FROM. Differs from `__table` only for an alias. */
49
49
  readonly __source: string
@@ -52,7 +52,7 @@ export interface TableRef<
52
52
 
53
53
  /**
54
54
  * A reference to one column, carrying its table. This is the value that
55
- * replaces the `'users.id'` string — `qId` receives structured data rather
55
+ * replaces the `'users.id'` string: `qId` receives structured data rather
56
56
  * than text that has to be validated before it can be trusted.
57
57
  */
58
58
  export interface TableColumn<
@@ -84,7 +84,7 @@ export function table<N extends string, C extends ColumnMap>(
84
84
  }
85
85
 
86
86
  /**
87
- * Declare a view — a stored `SELECT` the database treats as a table.
87
+ * Declare a view: a stored `SELECT` the database treats as a table.
88
88
  *
89
89
  * export const activeUsers = view(
90
90
  * 'active_users',
@@ -94,15 +94,15 @@ export function table<N extends string, C extends ColumnMap>(
94
94
  *
95
95
  * The columns are the shape the `SELECT` returns. They are declared rather than
96
96
  * inferred because nothing here parses SQL, and they are what gives the view a
97
- * row type — reading from it is typed exactly like reading a table.
97
+ * row type: reading from it is typed exactly like reading a table.
98
98
  *
99
99
  * **Writes are rejected at compile time.** `InferViews` collects these names and
100
100
  * `Mutation.Tables` excludes them, so `DB.Insert.into('active_users')` does not
101
101
  * typecheck. A view is a `SELECT`; the database would refuse the write anyway,
102
102
  * and refusing it earlier is strictly better.
103
103
  *
104
- * `db:sync` emits `CREATE VIEW`, diffs the body as normalised text, and drops
105
- * and recreates the view when it changes — views hold no data, so recreating is
104
+ * `db:sync` emits `CREATE VIEW`, diffs the body as normalized text, and drops
105
+ * and recreates the view when it changes: views hold no data, so recreating is
106
106
  * free and there is no migration to plan.
107
107
  *
108
108
  * Previously declarable only in the older `DBInfo` layout, or here by writing
@@ -122,7 +122,7 @@ export function view<N extends string, C extends ColumnMap>(
122
122
  * export const activeUsers = view('active_users', users, 'SELECT * FROM users WHERE active = 1')
123
123
  *
124
124
  * The filtered-view case, which is the common one: the shape is the source
125
- * table's, so restating it is duplication that nothing checks — declare a
125
+ * table's, so restating it is duplication that nothing checks. Declare a
126
126
  * column the `SELECT` does not return and you find out at query time.
127
127
  *
128
128
  * The source is a **value**, not a type argument, and that is forced rather
@@ -130,7 +130,7 @@ export function view<N extends string, C extends ColumnMap>(
130
130
  * inferring the *remaining* type parameters as soon as one is supplied
131
131
  * explicitly, so `N` would fall back to `string` and the view's name would stop
132
132
  * being a literal. `__table` is what `InferConstraints` keys the schema map on,
133
- * so that name degrading takes `InferViews` with it — and since
133
+ * so that name degrading takes `InferViews` with it, and since
134
134
  * `Mutation.Tables` now excludes views, `Exclude<…, string>` is `never` and
135
135
  * *every* mutation stops compiling. Passing the table keeps both inferred.
136
136
  *
@@ -158,7 +158,7 @@ export function view<N extends string, C extends ColumnMap>(
158
158
  *
159
159
  * This is what `db:sync --choose=db` writes into `orm/views.ts`, and it is the
160
160
  * honest shape for a view: **a view has no column DDL.** `CREATE VIEW x AS
161
- * SELECT …` declares no types, and the sync engine only ever reads the body —
161
+ * SELECT …` declares no types, and the sync engine only ever reads the body.
162
162
  * `createView(name, sql)` takes nothing else, and the diff compares the two
163
163
  * bodies as text. So a view's columns exist purely to give it a row type, and
164
164
  * writing `Field.Varchar(64)` there would imply a width the database neither
@@ -166,12 +166,12 @@ export function view<N extends string, C extends ColumnMap>(
166
166
  *
167
167
  * **Both type arguments are given, and that is forced.** TypeScript stops
168
168
  * inferring the remaining type parameters as soon as one is supplied, so
169
- * `view<ActiveUsersView>(name, body)` would leave `N` as `string` — and `N` is
169
+ * `view<ActiveUsersView>(name, body)` would leave `N` as `string`, and `N` is
170
170
  * what `TablesOf` re-keys the schema map on, so the whole map collapses to an
171
171
  * index signature and every mutation stops compiling. Naming both keeps it a
172
172
  * literal. In generated code the repetition costs nothing.
173
173
  *
174
- * Column references still work — `activeUsers.id` — even though the keys are
174
+ * Column references still work: `activeUsers.id`: even though the keys are
175
175
  * known only to the type. See the implementation.
176
176
  */
177
177
  export function view<N extends string, T>(
@@ -203,8 +203,8 @@ export function view(
203
203
  /**
204
204
  * A row interface, as the descriptor map `ExtractTableTypes` reads.
205
205
  *
206
- * One `{ type: T[K] }` per property — which is all a descriptor needs now that
207
- * `type` carries the row type — plus the `_view` marker that makes
206
+ * One `{ type: T[K] }` per property (which is all a descriptor needs now that
207
+ * `type` carries the row type) plus the `_view` marker that makes
208
208
  * `ExtractViews` classify it as a view.
209
209
  */
210
210
  type ViewColumns<T> = { [K in keyof T]-?: { type: T[K] } } & { _view: string }
@@ -255,7 +255,7 @@ function viewImpl<N extends string, C extends ColumnMap>(
255
255
  // It has to be in `__columns`'s *declared type* too, not only at runtime:
256
256
  // `ExtractViews` is what `InferViews` reads and what `Mutation.Tables`
257
257
  // excludes, so erasing `_view` from the type left writes to a view
258
- // compiling — the exact thing declaring one is supposed to prevent.
258
+ // compiling: the exact thing declaring one is supposed to prevent.
259
259
  // `ExtractTableTypes` filters the key out of the row type separately.
260
260
  return Object.assign(table(name, columns), {
261
261
  __columns: { _view: body, ...columns },
@@ -308,7 +308,7 @@ export type InferConstraints<M> = {
308
308
  : never
309
309
  }
310
310
 
311
- /** Row types — the derived replacement for a hand-written `DBSchema`. */
311
+ /** Row types: the derived replacement for a hand-written `DBSchema`. */
312
312
  export type InferSchema<M> = {
313
313
  [N in keyof InferConstraints<M>]: ExtractTableTypes<InferConstraints<M>, N>
314
314
  }
@@ -332,7 +332,7 @@ export function collectConstraints(module: Record<string, unknown>) {
332
332
 
333
333
  // Skip aliases. An alias is a query-time view of an existing table, and
334
334
  // collecting one would tell the sync engine to CREATE a table named after
335
- // it — so `alias(users, 'author')` would try to build an `author` table.
335
+ // it, so `alias(users, 'author')` would try to build an `author` table.
336
336
  if (def.__source !== def.__table) continue
337
337
 
338
338
  constraints[def.__table] = def.__columns
@@ -347,8 +347,8 @@ export function collectConstraints(module: Record<string, unknown>) {
347
347
  * export type ActiveUsersView = RowOf<typeof activeUsers>
348
348
  * // ^ { id: number; name: string }
349
349
  *
350
- * TypeScript cannot mint a *named* interface from a value — a name has to be
351
- * written somewhere — so this is the one line that does it, and it stays
350
+ * TypeScript cannot mint a *named* interface from a value (a name has to be
351
+ * written somewhere), so this is the one line that does it, and it stays
352
352
  * correct when the declaration changes because it is derived rather than
353
353
  * copied. A hand-written `interface ActiveUsersView` would be a second source
354
354
  * of truth that nothing checks against the first.