@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 +9 -9
- package/package.json +3 -3
- package/src/adapters/base.ts +181 -62
- package/src/adapters/mysql.ts +29 -17
- package/src/adapters/observe.ts +11 -11
- package/src/adapters/pgsql.ts +54 -39
- package/src/adapters/registry.ts +13 -13
- package/src/adapters/sqlite.ts +175 -44
- package/src/adapters.ts +4 -4
- package/src/backup.ts +1 -1
- package/src/connection.ts +1 -1
- package/src/define.ts +22 -22
- package/src/field.ts +55 -40
- package/src/globals.d.ts +1 -1
- package/src/index.ts +4 -4
- package/src/orm/index.ts +2 -2
- package/src/orm/mutation.ts +36 -28
- package/src/orm/query.ts +92 -89
- package/src/pool.ts +5 -5
- package/src/schema-registry.ts +3 -3
- package/src/schema-util.ts +48 -17
- package/src/sync/builder.ts +32 -32
- package/src/sync/diff.ts +12 -12
- package/src/sync/engine.ts +19 -19
- package/src/sync/execute.ts +16 -16
- package/src/sync/history.ts +5 -5
- package/src/sync/index.ts +12 -12
- package/src/sync/ledger.ts +23 -23
- package/src/sync/load.ts +16 -16
- package/src/sync/plan.ts +1 -2
- package/src/sync/rename.ts +1 -1
- package/src/sync/rollback.ts +3 -3
- package/src/sync/types.ts +20 -4
- package/src/sync/view-sql.ts +9 -9
- package/templates/schema.example.ts +4 -4
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
|
|
27
|
+
runs: the columns are just permissive `any`:
|
|
28
28
|
|
|
29
|
-
```ts no-check
|
|
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
|
|
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
|
|
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
|
|
81
|
+
MIT with the Commons Clause v1.0. See [LICENSE](./LICENSE).
|
|
82
82
|
|
|
83
|
-
**Not an OSI-approved
|
|
84
|
-
the software
|
|
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
|
-
|
|
87
|
-
If your
|
|
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-
|
|
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.
|
|
54
|
+
"bun": ">=1.4.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@bakery-framework/core": "^2.0.0-
|
|
57
|
+
"@bakery-framework/core": "^2.0.0-rc.2"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/src/adapters/base.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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`
|
|
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`
|
|
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
|
|
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
|
|
134
|
-
* wrapping the connection Bun hands its callback
|
|
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
|
|
160
|
-
* streaming
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
338
|
-
* reasonable alone
|
|
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
|
|
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
|
|
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
|
|
400
|
-
* default
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
726
|
-
* `dateNowDefaults[0]`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
960
|
+
* The `LIKE` escape character: `!`, and deliberately **not** a backslash.
|
|
854
961
|
*
|
|
855
|
-
* A backslash is the obvious choice and is
|
|
856
|
-
* backslash escapes inside string literals where SQLite and
|
|
857
|
-
* so
|
|
858
|
-
*
|
|
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
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
867
|
-
*
|
|
868
|
-
*
|
|
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
|
|
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
|
|
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
|
|
957
|
-
// filter cannot be
|
|
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
|
}
|