@bakery-framework/orm 2.0.0-alpha.9 → 2.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,9 +24,9 @@ export const posts = table('posts', {
24
24
  ```
25
25
 
26
26
  Register it so the query builder is typed. Without this block everything still
27
- runs — the columns are just permissive `any`:
27
+ runs: the columns are just permissive `any`:
28
28
 
29
- ```ts no-check — ./schema is the sibling file from the block above, which only exists in a real app
29
+ ```ts no-check: ./schema is the sibling file from the block above, which only exists in a real app
30
30
  // orm/index.ts
31
31
  import type { InferOptionals, InferSchema, InferViews } from '@bakery-framework/orm'
32
32
  import * as model from './schema'
@@ -54,7 +54,7 @@ const rows = await DB.from('posts').selectAll('posts').array()
54
54
  await DB.Insert.into('posts').values({ title: 'Hello', body: 'World' }).run()
55
55
  ```
56
56
 
57
- Identifiers are snake_cased on the way to SQL — `createdAt` becomes
57
+ Identifiers are snake_cased on the way to SQL: `createdAt` becomes
58
58
  `created_at`. Values always bind as parameters.
59
59
 
60
60
  ## Schema sync
@@ -68,7 +68,7 @@ process.exit(0)
68
68
 
69
69
  Diffs the database against your schema and applies the difference, prompting
70
70
  before anything destructive. `NODE_ENV=production` makes destructive changes
71
- refuse rather than prompt — set it on any deployed host.
71
+ refuse rather than prompt. Set it on any deployed host.
72
72
 
73
73
  ## Adapters
74
74
 
@@ -78,11 +78,11 @@ round-trips in CI, which is not the same as being battle-tested.
78
78
 
79
79
  ## License
80
80
 
81
- MIT with the Commons Clause v1.0 — see [LICENSE](./LICENSE).
81
+ MIT with the Commons Clause v1.0. See [LICENSE](./LICENSE).
82
82
 
83
- **Not an OSI-approved licence.** The Commons Clause removes the right to *sell*
84
- the software — meaning to charge for a product or service whose value derives
83
+ **Not an OSI-approved license.** The Commons Clause removes the right to *sell*
84
+ the software: meaning to charge for a product or service whose value derives
85
85
  substantially from it, hosting and support included. Everything else the MIT
86
- licence grants is unchanged: use it, modify it, ship it inside your own product.
87
- If your organisation only permits OSI-approved dependencies, this will not pass
86
+ license grants is unchanged: use it, modify it, ship it inside your own product.
87
+ If your organization only permits OSI-approved dependencies, this will not pass
88
88
  that check.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakery-framework/orm",
3
- "version": "2.0.0-alpha.9",
3
+ "version": "2.0.0-rc.2",
4
4
  "description": "Bakery database layer: adapters, query builder, schema sync and backup.",
5
5
  "keywords": [
6
6
  "bakery",
@@ -51,9 +51,9 @@
51
51
  "!src/tests"
52
52
  ],
53
53
  "engines": {
54
- "bun": ">=1.3.14"
54
+ "bun": ">=1.4.0"
55
55
  },
56
56
  "dependencies": {
57
- "@bakery-framework/core": "^2.0.0-alpha.9"
57
+ "@bakery-framework/core": "^2.0.0-rc.2"
58
58
  }
59
59
  }
@@ -10,7 +10,7 @@ import type { Driver as RegisteredDriver } from './registry'
10
10
  export namespace SQLAdapter {
11
11
  /**
12
12
  * Kept as `SQLAdapter.Driver` because that is what the ~40 existing call
13
- * sites say, but the list itself now lives in `registry.ts` — a namespace
13
+ * sites say, but the list itself now lives in `registry.ts`: a namespace
14
14
  * member cannot be declaration-merged from another package, and adding a
15
15
  * driver has to be possible from outside.
16
16
  */
@@ -35,10 +35,21 @@ export namespace SQLAdapter {
35
35
  }
36
36
  export interface TableDetails {
37
37
  name: string
38
- rowCount: number
38
+ /**
39
+ * `null` unless the caller asked for counts. A `COUNT(*)` is a full scan
40
+ * on SQLite and Postgres, and `getSchema()` is on the write path (every
41
+ * explorer write introspects to resolve row identity), so the scan is paid
42
+ * only where a number is actually displayed. `null`, not `0`: a count that
43
+ * was never taken must not read as an empty table.
44
+ */
45
+ rowCount: number | null
39
46
  columns: TableColumnInfo[]
40
47
  indexes: TableIndexInfo[]
41
48
  }
49
+ export interface SchemaOptions {
50
+ /** Take the per-table `COUNT(*)`. Off by default. See `rowCount`. */
51
+ rowCounts?: boolean
52
+ }
42
53
  export interface TableDataResult {
43
54
  rows: any[]
44
55
  totalRows: number
@@ -50,7 +61,7 @@ export namespace SQLAdapter {
50
61
  // as in `sync/types.ts`, and this copy had fallen behind: no `length`, no
51
62
  // `_enum`, no `_oldColumn`/`_transform`, and a `type` union missing `bigint`
52
63
  // and `json`. The three sync call sites that cast to it were therefore
53
- // *erasing* the fields at the exact point they are read — harmless only
64
+ // *erasing* the fields at the exact point they are read: harmless only
54
65
  // because `colDef` takes `unknown`. One declaration now, in `sync/types.ts`.
55
66
 
56
67
  export type RowRecord = MapOf<any>
@@ -62,7 +73,33 @@ export namespace SQLAdapter {
62
73
  export interface TableDataOptions extends FilterSortOptions {
63
74
  page: number
64
75
  pageSize: number
76
+ /**
77
+ * A total the caller already has, so the `COUNT(*)` can be skipped.
78
+ *
79
+ * **The count is almost the whole cost of a page.** Measured on a
80
+ * 200,000-row SQLite table with a page size of 50, reading page 101:
81
+ *
82
+ * count, no filter 11.7 ms rows, no filter 0.4 ms
83
+ * count, filtered 51.3 ms rows, filtered 1.6 ms
84
+ *
85
+ * 97% of the work either way. The rows come off an index and stop at the
86
+ * page; the count has to visit every row that matches, and a filtered
87
+ * count visits them a second time after the rows query already did.
88
+ *
89
+ * Opt-in and caller-asserted, because it trades a guarantee for that.
90
+ * Passing a total says "I counted, with exactly these filters" - an
91
+ * adapter cannot check it, and a wrong one shows a wrong row count and a
92
+ * wrong page count. It never affects which rows come back, so the
93
+ * failure mode is a stale readout rather than wrong data. Paging through
94
+ * a table while another writer inserts is exactly when it goes stale, and
95
+ * exactly when re-counting on every page is least worth it.
96
+ *
97
+ * Ignored unless it is a non-negative finite number, so a bad value
98
+ * degrades to the count rather than to a negative page total.
99
+ */
100
+ knownTotal?: number
65
101
  }
102
+
66
103
  export interface NameRow {
67
104
  name: string
68
105
  }
@@ -79,7 +116,7 @@ export namespace SQLAdapter {
79
116
  * The slice of a Bun `SQL` handle that transaction nesting needs.
80
117
  *
81
118
  * Structural rather than `SQL`, because the base class holds `sql` as
82
- * `unknown` — each adapter narrows it — and because both members are
119
+ * `unknown` (each adapter narrows it), and because both members are
83
120
  * verified by probe rather than by the type: `savepoint` is present on the
84
121
  * root connection and on every transaction and savepoint handle in Bun
85
122
  * 1.3.14, on all three dialects.
@@ -112,7 +149,7 @@ export namespace SQLAdapter {
112
149
  * 32,766 for every dialect, deliberately below the two that could go higher.
113
150
  * The counter that overflows is 16 bits and it does not *fail* on overflow, it
114
151
  * wraps: a 120,000-parameter insert reported `expected 54464 values, received
115
- * 120000` — 120000 − 65536 — which reads like memory corruption rather than a
152
+ * 120000` (120000 − 65536), which reads like memory corruption rather than a
116
153
  * limit being hit. Half the 16-bit ceiling leaves room for whatever bookkeeping
117
154
  * a driver adds on top of the parameters we counted, and the cost of the extra
118
155
  * round trips is nothing next to the bytes those parameters weigh.
@@ -124,14 +161,14 @@ export const DEFAULT_MAX_QUERY_PARAMS = 32766
124
161
  * open one from?
125
162
  *
126
163
  * A duck check, because `value instanceof SQL` is unusable: Bun's `SQL` export
127
- * has no `prototype` property at all — it is `undefined` as of 1.3.14 — and
164
+ * has no `prototype` property at all. It is `undefined` as of 1.3.14, and
128
165
  * `instanceof` against it does not return false, it **throws**
129
166
  * `instanceof called on an object with an invalid prototype property` for every
130
167
  * object operand. The MySQL and Postgres constructors read as if they tested
131
168
  * this and did not: `instanceof` short-circuits to false for a primitive
132
169
  * *before* it touches the right operand, so the string form never reached the
133
- * throw and the only caller that passes a real handle — `transaction()`,
134
- * wrapping the connection Bun hands its callback — failed every time it ran.
170
+ * throw and the only caller that passes a real handle (`transaction()`,
171
+ * wrapping the connection Bun hands its callback) failed every time it ran.
135
172
  *
136
173
  * `typeof` covers `function` as well as `object` because a Bun connection is
137
174
  * callable: it is the tagged-template entry point, with `unsafe` hung off it.
@@ -156,8 +193,8 @@ export function quoteIdentifier(name: string, quoteChar: string): string {
156
193
  /**
157
194
  * How many rows one `iterate()` chunk fetches. See {@link pagedIterate}.
158
195
  *
159
- * Big enough that the round trips are amortised, small enough that the point of
160
- * streaming — never holding the whole result — survives. Not a tuning knob
196
+ * Big enough that the round trips are amortized, small enough that the point of
197
+ * streaming (never holding the whole result) survives. Not a tuning knob
161
198
  * anyone has measured; it is a default, and `createExecutor` takes an override.
162
199
  */
163
200
  export const DEFAULT_STREAM_CHUNK = 500
@@ -166,7 +203,7 @@ export const DEFAULT_STREAM_CHUNK = 500
166
203
  * `iterate()`, built out of `all()` by paging.
167
204
  *
168
205
  * **Bun cannot stream.** As of 1.3.14 an `SQLQuery` is a thenable and nothing
169
- * more — no `Symbol.asyncIterator`, no `Symbol.iterator`, and its own methods
206
+ * more, no `Symbol.asyncIterator`, no `Symbol.iterator`, and its own methods
170
207
  * (`raw`, `simple`, `values`, `execute`, `run`) all resolve the whole result.
171
208
  * The adapters used to hand their raw query object to `for await`, which is why
172
209
  * `iterate` threw `… .iterate is not a function` on **every** dialect and why
@@ -180,17 +217,45 @@ export const DEFAULT_STREAM_CHUNK = 500
180
217
  * ```
181
218
  *
182
219
  * Verified on all three dialects against live servers, including a statement
183
- * that already carries its own `ORDER BY` or `LIMIT` — the derived table
220
+ * that already carries its own `ORDER BY` or `LIMIT`: the derived table
184
221
  * contains it, so the window composes rather than colliding. MySQL needs the
185
222
  * alias; the other two tolerate it.
186
223
  *
187
224
  * **What this is not.** It is not a server-side cursor, and the difference is
188
225
  * observable, so say it plainly: the statement is re-executed once per chunk,
189
226
  * and chunk boundaries are only stable under a total order. Rows inserted or
190
- * deleted mid-walk can therefore be seen twice or missed — the same hazard
227
+ * deleted mid-walk can therefore be seen twice or missed: the same hazard
191
228
  * offset pagination has, and exactly what `seek()` exists to avoid. What it
192
229
  * does buy is the thing people reach for streaming to get: memory bounded by
193
230
  * the chunk instead of by the result.
231
+ *
232
+ * **What it costs, and why it is not one number.** The statement is
233
+ * re-executed per chunk, so the total is the chunk count times whatever one
234
+ * window costs, and what one window costs depends entirely on whether the
235
+ * ordering can be served from an index. If it cannot, every chunk sorts the
236
+ * whole result and throws away the offset, which is quadratic in the chunk
237
+ * count. Measured on 100,000 rows, a chunk size of 500, so 200 statements:
238
+ *
239
+ * sqlite ORDER BY the primary key all 180 ms iterate 593 ms 3x
240
+ * sqlite ORDER BY an unindexed column all 275 ms iterate 35,687 ms 130x
241
+ * postgres ORDER BY the primary key all 83 ms iterate 1,590 ms 19x
242
+ * postgres ORDER BY an unindexed column all 126 ms iterate 12,767 ms 101x
243
+ *
244
+ * Thirty-five seconds to walk what `all()` returns in 275 ms. The backlog
245
+ * recorded a flat "393x", which is the right neighborhood for the unindexed
246
+ * case and says nothing about the case a caller can fix: **order by something
247
+ * indexed and the cost collapses by 40x.**
248
+ *
249
+ * The two dialects differ for different reasons. Postgres pays a network round
250
+ * trip per chunk, which is why its indexed case is 19x where SQLite's is 3x.
251
+ * SQLite pays nothing for the trip and everything for the sort, which is why
252
+ * its unindexed case is the worst of the four.
253
+ *
254
+ * So: reach for this when the result does not fit in memory, not when it is
255
+ * merely large. If it fits, `all()` is between 3 and 130 times faster. If it
256
+ * does not, index the column you order by, and prefer `seek()` where the shape
257
+ * allows it: that is keyset pagination and it has neither the cost nor the
258
+ * skipped-row hazard described above.
194
259
  */
195
260
  export function pagedIterate(
196
261
  all: SQLAdapter.Executor['all'],
@@ -215,16 +280,16 @@ export function createExecutor(
215
280
  run: SQLAdapter.Executor['run'],
216
281
  driver: SQLAdapter.Driver,
217
282
  options: {
218
- /** Replace the paging walker — for a driver that can genuinely stream. */
283
+ /** Replace the paging walker: for a driver that can genuinely stream. */
219
284
  iterate?: SQLAdapter.Executor['iterate']
220
285
  chunkSize?: number
221
286
  } = {},
222
287
  ): SQLAdapter.Executor {
223
288
  const iterate = options.iterate ?? pagedIterate(all, options.chunkSize)
224
289
  // `get` and `values` call the raw `all` rather than `exec.all`, which is a
225
- // behavioural detail worth stating: it is what keeps one executed statement
290
+ // behavioral detail worth stating: it is what keeps one executed statement
226
291
  // to exactly one observer event. Routing them through the observed `exec.all`
227
- // would report every `.get()` twice — once as `get`, once as `all` — and a
292
+ // would report every `.get()` twice (once as `get`, once as `all`), and a
228
293
  // "slowest queries" panel built on double-counted rows is worse than none.
229
294
  return {
230
295
  all: observe(driver, 'all', all, rows =>
@@ -263,7 +328,7 @@ export abstract class SQLAdapter {
263
328
  * The database this connection is pointed at, or `undefined`.
264
329
  *
265
330
  * Parsed from the URL rather than asked of the server, because the one caller
266
- * — normalising a stored view body — runs inside the diff and must not add a
331
+ * (normalizing a stored view body) runs inside the diff and must not add a
267
332
  * round trip to it. SQLite has no such name and needs none: it does not
268
333
  * qualify a view's tables in the first place.
269
334
  *
@@ -281,7 +346,7 @@ export abstract class SQLAdapter {
281
346
  }
282
347
 
283
348
  /**
284
- * Placeholder ceiling for a single statement — see
349
+ * Placeholder ceiling for a single statement. See
285
350
  * {@link DEFAULT_MAX_QUERY_PARAMS} for why it is one number and not three.
286
351
  *
287
352
  * A getter rather than a field so a dialect that genuinely differs can
@@ -292,6 +357,47 @@ export abstract class SQLAdapter {
292
357
  return DEFAULT_MAX_QUERY_PARAMS
293
358
  }
294
359
 
360
+ /**
361
+ * Whether a caller-supplied total can be used in place of a `COUNT(*)`.
362
+ *
363
+ * One rule in one place, because all three adapters answer it and a dialect
364
+ * that disagreed would be a bug rather than a capability. A static on the
365
+ * class rather than a function in the namespace: the namespace is declared
366
+ * above the class it merges with, so it can hold types and not values.
367
+ */
368
+ static usableTotal(value: unknown): value is number {
369
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0
370
+ }
371
+
372
+ /**
373
+ * A cheap value that changes whenever the schema changes, or `null`.
374
+ *
375
+ * The capability is expressed by the return, not by a boolean beside it: a
376
+ * dialect that cannot answer this cheaply returns `null` and its callers
377
+ * fall back to asking the schema directly. That is the default here, so a
378
+ * new adapter is correct without implementing anything.
379
+ *
380
+ * **It must not move for ordinary DML.** A value that changed on every
381
+ * `INSERT` would still be *correct* as a cache key and useless as one,
382
+ * invalidating on the busiest thing a database does. SQLite's
383
+ * `PRAGMA schema_version` has exactly the right semantics, verified rather
384
+ * than assumed: it advances on `CREATE TABLE`, on `ALTER TABLE ... ADD
385
+ * COLUMN` and on `DROP TABLE`, and stays put across an `INSERT`.
386
+ *
387
+ * A method rather than one of the capability *getters* above because it
388
+ * costs a round trip. Those describe the dialect and are answered from
389
+ * nothing; this one asks the server.
390
+ *
391
+ * Postgres and MySQL return `null`. Neither exposes a single counter for
392
+ * this - Postgres has no schema-level equivalent of `schema_version`, and
393
+ * MySQL's `information_schema` would have to be aggregated, which is the
394
+ * expense being avoided. Adding one later is a per-adapter override and
395
+ * needs no change here or in any caller.
396
+ */
397
+ async schemaFingerprint(): Promise<string | null> {
398
+ return null
399
+ }
400
+
295
401
  constructor(
296
402
  public readonly driver: SQLAdapter.Driver,
297
403
  public readonly filename?: string,
@@ -315,7 +421,7 @@ export abstract class SQLAdapter {
315
421
  *
316
422
  * Set by {@link transaction} on the child it just built, never by a
317
423
  * constructor. Inferring it from "was I handed an open connection?" would be
318
- * a guess — a pooled handle is open too, and issuing `SAVEPOINT` outside a
424
+ * a guess: a pooled handle is open too, and issuing `SAVEPOINT` outside a
319
425
  * transaction block is an error on Postgres and a silent no-op on MySQL.
320
426
  * Only `transaction` knows for certain, because it is what opened the thing.
321
427
  */
@@ -328,18 +434,18 @@ export abstract class SQLAdapter {
328
434
  protected abstract withConnection(sql: unknown): SQLAdapter
329
435
 
330
436
  /**
331
- * Run `callback` atomically — `BEGIN` at the top level, `SAVEPOINT` within an
437
+ * Run `callback` atomically: `BEGIN` at the top level, `SAVEPOINT` within an
332
438
  * enclosing transaction.
333
439
  *
334
440
  * The dispatch is the whole point. Bun refuses a nested `BEGIN` outright
335
441
  * (`cannot call begin inside a transaction use savepoint() instead`, verbatim
336
442
  * on all three dialects), so before this, any two transactional functions
337
- * that composed — `createUser()` called from `importUsers()`, both perfectly
338
- * reasonable alone — crashed the moment they met.
443
+ * that composed (`createUser()` called from `importUsers()`, both perfectly
444
+ * reasonable alone) crashed the moment they met.
339
445
  *
340
446
  * A failed inner block rolls back to its own savepoint and nothing more, so
341
447
  * an outer transaction that catches the error keeps its own work. It only
342
- * gets the whole thing if it lets the error propagate — which is the same
448
+ * gets the whole thing if it lets the error propagate, which is the same
343
449
  * rule a single transaction already follows.
344
450
  */
345
451
  async transaction<T>(
@@ -366,7 +472,7 @@ export abstract class SQLAdapter {
366
472
  * A table-level `FOREIGN KEY` clause, for inclusion in `CREATE TABLE`.
367
473
  *
368
474
  * Emitted inline rather than by `ALTER` wherever possible, because SQLite has
369
- * no `ALTER TABLE ADD FOREIGN KEY` at all — inline is the only spelling all
475
+ * no `ALTER TABLE ADD FOREIGN KEY` at all: inline is the only spelling all
370
476
  * three dialects share.
371
477
  */
372
478
  foreignKeyClause(fk: SyncTypes.ForeignKeyInfo): string {
@@ -396,8 +502,8 @@ export abstract class SQLAdapter {
396
502
  * One spelling for a referential action, whatever the dialect called it.
397
503
  *
398
504
  * Postgres reports a single character rather than a word; MySQL and SQLite
399
- * report the word. Anything unrecognised becomes `NO ACTION` — the SQL
400
- * default — so a dialect that grows a new code cannot make the diff churn.
505
+ * report the word. Anything unrecognized becomes `NO ACTION` (the SQL
506
+ * default), so a dialect that grows a new code cannot make the diff churn.
401
507
  */
402
508
  static normalizeForeignKeyAction(raw: unknown): SyncTypes.ForeignKeyAction {
403
509
  const v = String(raw ?? '')
@@ -434,7 +540,7 @@ export abstract class SQLAdapter {
434
540
  *
435
541
  * `cols` names the unique columns that decide "already there"; `targets` are
436
542
  * the columns to overwrite, empty meaning "insert if absent". Both arrive as
437
- * declared — snake-casing and quoting happen here, so the caller never has to
543
+ * declared: snake-casing and quoting happen here, so the caller never has to
438
544
  * know which dialect it is talking to.
439
545
  */
440
546
  upsertClause(cols: string[], targets: string[]): string {
@@ -451,7 +557,7 @@ export abstract class SQLAdapter {
451
557
  * Which end of a batched multi-row insert `lastInsertRowid` refers to.
452
558
  *
453
559
  * A large insert is split into batches, so the id has to be taken from one of
454
- * them — and the dialects do not agree which. SQLite and Postgres report the
560
+ * them, and the dialects do not agree which. SQLite and Postgres report the
455
561
  * *last* row written; MySQL's `insertId` reports the *first* of the block.
456
562
  * Taking the matching end keeps each dialect's own answer true rather than
457
563
  * inventing a third one.
@@ -477,7 +583,7 @@ export abstract class SQLAdapter {
477
583
  * A rebuild is `CREATE t_temp` → copy → `DROP TABLE t` →
478
584
  * `RENAME t_temp TO t`,
479
585
  * and two of the three dialects refuse to run it while a view still names `t`
480
- * — at different steps, and with different messages:
586
+ *: at different steps, and with different messages:
481
587
  *
482
588
  * - **SQLite** refuses the *rename*: `error in view v: no such table: main.t`
483
589
  * - **Postgres** refuses the *drop*: `cannot drop table t because other
@@ -486,7 +592,7 @@ export abstract class SQLAdapter {
486
592
  * it resolves a view's tables at query time rather than binding them at
487
593
  * creation.
488
594
  *
489
- * So this is `true` by default and MySQL is the exception — the reverse of
595
+ * So this is `true` by default and MySQL is the exception: the reverse of
490
596
  * how it first reads. Where it holds, the planner drops declared views before
491
597
  * the rebuild phase and recreates them a moment later; where it does not, the
492
598
  * drop is skipped and the views are simply left alone.
@@ -496,7 +602,7 @@ export abstract class SQLAdapter {
496
602
  }
497
603
 
498
604
  /**
499
- * `INTERSECT ALL` and `EXCEPT ALL` — the duplicate-preserving forms.
605
+ * `INTERSECT ALL` and `EXCEPT ALL`: the duplicate-preserving forms.
500
606
  *
501
607
  * `UNION ALL` is universal and is not covered by this; only the other two
502
608
  * are. MySQL grew them in 8.0.31 and Postgres has always had them; SQLite
@@ -511,7 +617,7 @@ export abstract class SQLAdapter {
511
617
  * `FULL OUTER JOIN`.
512
618
  *
513
619
  * Postgres has it; SQLite gained it in 3.39 and the version Bun bundles has
514
- * it. **MySQL has never had it**, at any version — the workaround there is a
620
+ * it. **MySQL has never had it**, at any version: the workaround there is a
515
621
  * `LEFT JOIN` unioned with a `RIGHT JOIN`, which is a different query rather
516
622
  * than a flag, so the builder refuses instead of rewriting silently.
517
623
  */
@@ -534,7 +640,9 @@ export abstract class SQLAdapter {
534
640
  ).run()
535
641
  }
536
642
 
537
- abstract getSchema(): Promise<SQLAdapter.TableDetails[]>
643
+ abstract getSchema(
644
+ options?: SQLAdapter.SchemaOptions,
645
+ ): Promise<SQLAdapter.TableDetails[]>
538
646
 
539
647
  /**
540
648
  * Foreign keys as the database has them, keyed by the tuple that identifies
@@ -553,7 +661,7 @@ export abstract class SQLAdapter {
553
661
  *
554
662
  * Names are deliberately excluded. SQLite's `PRAGMA foreign_key_list` does
555
663
  * not report one, so a name-keyed diff would see every SQLite foreign key as
556
- * new on every sync — the perpetual-rebuild failure this project has hit
664
+ * new on every sync: the perpetual-rebuild failure this project has hit
557
665
  * repeatedly. The tuple is the same on all three dialects.
558
666
  */
559
667
  static foreignKeyId(fk: {
@@ -582,7 +690,7 @@ export abstract class SQLAdapter {
582
690
  refTable: String(r.parent),
583
691
  refCols: [] as string[],
584
692
  name: key,
585
- // Normalised here, not at the call site: Postgres reports a single
693
+ // Normalized here, not at the call site: Postgres reports a single
586
694
  // character where MySQL reports a word, and the diff has to compare one
587
695
  // vocabulary or it replaces every key on every sync.
588
696
  onDelete: SQLAdapter.normalizeForeignKeyAction(r.on_delete),
@@ -713,7 +821,7 @@ export abstract class SQLAdapter {
713
821
  protected async preSync(_tx: SQLAdapter): Promise<void> {}
714
822
  protected async postSync(_tx: SQLAdapter): Promise<void> {}
715
823
  /**
716
- * Patterns recognised when reading a default back *out* of the database.
824
+ * Patterns recognized when reading a default back *out* of the database.
717
825
  * Matched loosely (parens stripped, uppercased, `includes`), so a prefix
718
826
  * fragment is a perfectly good entry here.
719
827
  */
@@ -722,8 +830,8 @@ export abstract class SQLAdapter {
722
830
  /**
723
831
  * The complete SQL expression emitted *into* DDL for a `%dateNow%` default.
724
832
  *
725
- * Deliberately separate from `dateNowDefaults`. Conflating the two — emitting
726
- * `dateNowDefaults[0]` — is what produced `DEFAULT (EXTRACT(EPOCH FROM)` on
833
+ * Deliberately separate from `dateNowDefaults`. Conflating the two (emitting
834
+ * `dateNowDefaults[0]`) is what produced `DEFAULT (EXTRACT(EPOCH FROM)` on
727
835
  * Postgres and `DEFAULT (UNIX_TIMESTAMP)` on MySQL: a prefix is fine to match
728
836
  * against and fatal to emit. SQLite only escaped because its match pattern
729
837
  * happened to be a complete expression.
@@ -752,7 +860,7 @@ export abstract class SQLAdapter {
752
860
 
753
861
  /**
754
862
  * Loose match: parens stripped, uppercased, substring. A prefix fragment is a
755
- * perfectly good entry in the pattern lists — and is fatal to *emit*, which
863
+ * perfectly good entry in the pattern lists, and is fatal to *emit*, which
756
864
  * is the whole reason the emitted expression is a separate field.
757
865
  */
758
866
  private matchesMarker(
@@ -771,12 +879,12 @@ export abstract class SQLAdapter {
771
879
  /**
772
880
  * The declared width of a **sized** text column, or `undefined`.
773
881
  *
774
- * The guard is the entire point, and it is not defensive coding — it is a
882
+ * The guard is the entire point, and it is not defensive coding: it is a
775
883
  * measured result. MySQL reports `character_maximum_length = 65535` for an
776
884
  * unsized `TEXT` column, where Postgres reports `null`. Take the number
777
885
  * unconditionally and every `Field.Text()` column reads back as
778
886
  * `length: 65535`, the schema says nothing, the two never agree, and MySQL
779
- * rebuilds the table on every sync forever — the exact failure that kept
887
+ * rebuilds the table on every sync forever, the exact failure that kept
780
888
  * `length` out of the diff until it could be checked against real servers.
781
889
  *
782
890
  * So: a width counts only when the dialect also calls the column a *sized*
@@ -799,8 +907,7 @@ export abstract class SQLAdapter {
799
907
  /**
800
908
  * A `CHECK (col IN (…))` clause restricting a column to a set of values.
801
909
  *
802
- * Takes the column name because a CHECK has to name it. Values bind nowhere —
803
- * this is DDL — so they are quoted the same way `formatDefault` quotes a
910
+ * Takes the column name because a CHECK has to name it. Values bind nowhere ( * this is DDL), so they are quoted the same way `formatDefault` quotes a
804
911
  * string default, by doubling the single quote.
805
912
  */
806
913
  protected enumClause(column: string, values: string[]): string {
@@ -815,7 +922,7 @@ export abstract class SQLAdapter {
815
922
  const isStr = typeof def === 'string'
816
923
  if (isStr && def.toUpperCase() === 'NULL') return null
817
924
  // `def.trim() !== ''` first, because `Number('')` is `0` and `Number(' ')`
818
- // is `0` — so an empty-string default came back as the *number* zero. The
925
+ // is `0`, so an empty-string default came back as the *number* zero. The
819
926
  // schema then said `''`, the database said `0`, and the column was rebuilt
820
927
  // on every single sync, forever.
821
928
  //
@@ -850,22 +957,21 @@ export abstract class SQLAdapter {
850
957
  }
851
958
 
852
959
  /**
853
- * The `LIKE` escape character — `!`, and deliberately **not** a backslash.
960
+ * The `LIKE` escape character: `!`, and deliberately **not** a backslash.
854
961
  *
855
- * A backslash is the obvious choice and is unusable here. MySQL processes
856
- * backslash escapes inside string literals where SQLite and Postgres do not,
857
- * so the clause would need to differ per dialect; and worse, Postgres never
858
- * receives what is written. `normalizePostgresSQL` treats `\'` as an escaped
859
- * quote — MySQL's rule, applied to every dialect — so `ESCAPE '\'` leaves the
860
- * string literal *open*, swallows the rest of the statement, and the driver
861
- * reports `syntax error at or near "OFFSET"` from a clause several tokens
862
- * later.
962
+ * A backslash is the obvious choice and is still the wrong one. MySQL
963
+ * processes backslash escapes inside string literals where SQLite and
964
+ * Postgres do not, so `ESCAPE '\'` would need a per-dialect spelling, and
965
+ * the whole point of one escape character is one clause for all three.
863
966
  *
864
- * That normalizer behaviour is a real defect and is not fixed here: a test
865
- * pins it (`a backslash escape keeps the character it escapes`), and changing
866
- * it would change every literal the framework emits. Choosing a character
867
- * with no meaning to any of the three parsers sidesteps it entirely, and one
868
- * spelling for all dialects beats a capability getter per dialect.
967
+ * The Postgres normalizer used to make this worse: it applied MySQL's rule
968
+ * to every dialect, so `'\'` swallowed its own closing quote and the driver
969
+ * reported a syntax error several tokens later. That defect is fixed (the
970
+ * scanner now has no opinion about backslashes, matching the server), and
971
+ * `a backslash in a literal is a character, not an escape` pins it. `!` stays
972
+ * regardless, because MySQL's literal-level escaping is the server's real
973
+ * behavior, not a scanner bug, and a character with no meaning to any of the
974
+ * three parsers needs no capability getter.
869
975
  */
870
976
  protected readonly likeEscape = '!'
871
977
 
@@ -877,7 +983,7 @@ export abstract class SQLAdapter {
877
983
  * Make a value match literally under `LIKE`.
878
984
  *
879
985
  * Without this a search for `50%` matches every row and one for `a_b` matches
880
- * `axb` — `%` and `_` are the wildcards, and the filter passed user input
986
+ * `axb`: `%` and `_` are the wildcards, and the filter passed user input
881
987
  * through untouched. The escape character itself goes first, or escaping it
882
988
  * afterwards would double the ones this method just added.
883
989
  */
@@ -933,7 +1039,7 @@ export abstract class SQLAdapter {
933
1039
  * One filter, as SQL. Returns `null` when the filter says nothing.
934
1040
  *
935
1041
  * `params` is appended to rather than returned, because an operator may bind
936
- * one value, two, or none at all — `IS NULL` has nothing to bind, and
1042
+ * one value, two, or none at all: `IS NULL` has nothing to bind, and
937
1043
  * pretending otherwise is how a placeholder count drifts from its arguments.
938
1044
  */
939
1045
  private filterClause(
@@ -953,8 +1059,8 @@ export abstract class SQLAdapter {
953
1059
 
954
1060
  const { op, value } = raw as { op?: string; value?: unknown }
955
1061
 
956
- // No value to bind, and none expected — these two are the whole reason a
957
- // filter cannot be modelled as a plain column/value pair.
1062
+ // No value to bind, and none expected: these two are the whole reason a
1063
+ // filter cannot be modeled as a plain column/value pair.
958
1064
  if (op === 'null') return `${quoted} IS NULL`
959
1065
  if (op === 'notnull') return `${quoted} IS NOT NULL`
960
1066
 
@@ -1231,6 +1337,19 @@ export class DatabaseStatement {
1231
1337
  values(...params: any[]) {
1232
1338
  return this.connection.execute.values(this.sql, params)
1233
1339
  }
1340
+ /**
1341
+ * Walk the result a chunk at a time, holding only the chunk.
1342
+ *
1343
+ * **Not a cursor, and not cheap.** Bun cannot stream, so this pages: the
1344
+ * statement becomes a derived table and is re-executed once per 500 rows.
1345
+ * On 100,000 rows that is 200 statements, and the total depends on whether
1346
+ * the ordering is indexed: 3x `all()` on SQLite ordered by a primary key,
1347
+ * **130x** ordered by a column with no index (35.7 seconds against 275 ms).
1348
+ * The full table and the reasoning are on {@link pagedIterate}.
1349
+ *
1350
+ * Use it when the result does not fit in memory. When it does, `all()` is
1351
+ * faster by between 3 and 130 times.
1352
+ */
1234
1353
  iterate(...params: any[]) {
1235
1354
  return this.connection.execute.iterate(this.sql, params)
1236
1355
  }