@bakery-framework/orm 2.0.0-alpha.9 → 2.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +35 -35
- 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/src/adapters/mysql.ts
CHANGED
|
@@ -13,11 +13,11 @@ export class MySQLAdapter extends SQLAdapter {
|
|
|
13
13
|
? connectionTarget.toString().replace(/^(mysqli?s?:\/\/)/, 'mysql://')
|
|
14
14
|
: undefined
|
|
15
15
|
super('mysql', undefined, target)
|
|
16
|
-
// `isOpenConnection`, not `instanceof SQL
|
|
16
|
+
// `isOpenConnection`, not `instanceof SQL`. See the helper for why the
|
|
17
17
|
// latter throws rather than answering.
|
|
18
18
|
//
|
|
19
19
|
// Pool options apply only when this opens its own connection. A handle
|
|
20
|
-
// handed in is already someone else's pool
|
|
20
|
+
// handed in is already someone else's pool: notably a transaction's, where
|
|
21
21
|
// re-sizing anything would be meaningless.
|
|
22
22
|
this.sql = isOpenConnection(connectionTarget)
|
|
23
23
|
? (connectionTarget as SQL)
|
|
@@ -40,7 +40,7 @@ export class MySQLAdapter extends SQLAdapter {
|
|
|
40
40
|
// `affectedRows` first, and the order is the whole fix.
|
|
41
41
|
//
|
|
42
42
|
// Bun's MySQL driver sets `count` to **0** for every write and puts the
|
|
43
|
-
// real number in `affectedRows
|
|
43
|
+
// real number in `affectedRows`: the reverse of SQLite, where `count`
|
|
44
44
|
// is authoritative and `affectedRows` is null. Reading `count` first
|
|
45
45
|
// through a `??` chain therefore never fell through: zero is not
|
|
46
46
|
// nullish, so it was taken and returned. Every insert, update and
|
|
@@ -251,7 +251,7 @@ export class MySQLAdapter extends SQLAdapter {
|
|
|
251
251
|
/**
|
|
252
252
|
* MySQL binds a view's tables at query time, not at `CREATE VIEW`, so a table
|
|
253
253
|
* can be dropped and rebuilt underneath a view that names it. SQLite and
|
|
254
|
-
* Postgres both refuse
|
|
254
|
+
* Postgres both refuse. See the base declaration for the two messages.
|
|
255
255
|
*/
|
|
256
256
|
override get viewsBlockTableRebuild(): boolean {
|
|
257
257
|
return false
|
|
@@ -270,7 +270,9 @@ export class MySQLAdapter extends SQLAdapter {
|
|
|
270
270
|
return new MySQLAdapter(sql as SQL)
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
async getSchema(
|
|
273
|
+
async getSchema(
|
|
274
|
+
options?: SQLAdapter.SchemaOptions,
|
|
275
|
+
): Promise<SQLAdapter.TableDetails[]> {
|
|
274
276
|
const res = (await this.query(
|
|
275
277
|
'SELECT table_name AS name' +
|
|
276
278
|
' FROM information_schema.tables' +
|
|
@@ -280,7 +282,11 @@ export class MySQLAdapter extends SQLAdapter {
|
|
|
280
282
|
const tablesWithDetails: SQLAdapter.TableDetails[] = []
|
|
281
283
|
for (const t of res) {
|
|
282
284
|
const [countRes, cols, idxs] = (await Promise.all([
|
|
283
|
-
|
|
285
|
+
options?.rowCounts
|
|
286
|
+
? this.query(
|
|
287
|
+
`SELECT COUNT(*) as count FROM ${this.quote(t.name)}`,
|
|
288
|
+
).get()
|
|
289
|
+
: null,
|
|
284
290
|
this.query(
|
|
285
291
|
'SELECT column_name AS name, data_type AS type,' +
|
|
286
292
|
' is_nullable AS is_nullable, column_key AS column_key' +
|
|
@@ -293,13 +299,13 @@ export class MySQLAdapter extends SQLAdapter {
|
|
|
293
299
|
' FROM information_schema.statistics' +
|
|
294
300
|
' WHERE table_name = ? AND table_schema = DATABASE()',
|
|
295
301
|
).all(t.name),
|
|
296
|
-
])) as [SQLAdapter.CountRow, any[], any[]]
|
|
302
|
+
])) as [SQLAdapter.CountRow | null, any[], any[]]
|
|
297
303
|
const uniqueIdxs = Array.from(
|
|
298
304
|
new Map(idxs.map(i => [i.name, i.non_unique === 0])).entries(),
|
|
299
305
|
).map(([name, unique]) => ({ name, unique }))
|
|
300
306
|
tablesWithDetails.push({
|
|
301
307
|
name: t.name,
|
|
302
|
-
rowCount: countRes?.count || 0,
|
|
308
|
+
rowCount: options?.rowCounts ? countRes?.count || 0 : null,
|
|
303
309
|
columns: cols.map(c => ({
|
|
304
310
|
name: c.name,
|
|
305
311
|
type: c.type,
|
|
@@ -326,10 +332,15 @@ export class MySQLAdapter extends SQLAdapter {
|
|
|
326
332
|
new Set(cols.map(c => c.name)),
|
|
327
333
|
)
|
|
328
334
|
const tName = this.quote(tableName)
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
335
|
+
// See `TableDataOptions.knownTotal`: the count is almost the whole cost
|
|
336
|
+
// of a page, and a caller that has already counted can say so.
|
|
337
|
+
const reuse = SQLAdapter.usableTotal(options.knownTotal)
|
|
338
|
+
const countRes = reuse
|
|
339
|
+
? null
|
|
340
|
+
: ((await this.query(
|
|
341
|
+
`SELECT COUNT(*) as count FROM ${tName}${whereSql}`,
|
|
342
|
+
).get(...whereParams)) as SQLAdapter.CountRow)
|
|
343
|
+
const totalRows = reuse ? options.knownTotal! : countRes?.count || 0
|
|
333
344
|
const rows = await this.query(
|
|
334
345
|
`SELECT * FROM ${tName}${whereSql}${orderSql} LIMIT ? OFFSET ?`,
|
|
335
346
|
).all(
|
|
@@ -352,7 +363,7 @@ export class MySQLAdapter extends SQLAdapter {
|
|
|
352
363
|
*
|
|
353
364
|
* This deliberately does *not* go through `getSchema()`. That lists every
|
|
354
365
|
* table in the database and then issues `COUNT(*)` plus two
|
|
355
|
-
* information_schema queries per table
|
|
366
|
+
* information_schema queries per table, so deleting one row from a
|
|
356
367
|
* twenty-table schema ran sixty-one statements, twenty of them full row
|
|
357
368
|
* counts, to learn one column name. The pk is one lookup against the same
|
|
358
369
|
* `column_key = 'PRI'` that getSchema itself derived the flag from, in the
|
|
@@ -400,8 +411,8 @@ export class MySQLAdapter extends SQLAdapter {
|
|
|
400
411
|
* Every information_schema column is aliased to its own lowercase name, and
|
|
401
412
|
* the aliases are load-bearing despite looking redundant.
|
|
402
413
|
*
|
|
403
|
-
* MySQL 8 returns information_schema field names **uppercase**
|
|
404
|
-
* `SELECT table_name` yields a row keyed `TABLE_NAME`
|
|
414
|
+
* MySQL 8 returns information_schema field names **uppercase** (a bare
|
|
415
|
+
* `SELECT table_name` yields a row keyed `TABLE_NAME`), while an alias is
|
|
405
416
|
* returned exactly as written. Without them `t.table_name` is `undefined`
|
|
406
417
|
* and this method throws `undefined is not an object` inside `Case.camel`,
|
|
407
418
|
* which takes `db:sync` against MySQL down entirely. Postgres was never
|
|
@@ -505,6 +516,7 @@ export class MySQLAdapter extends SQLAdapter {
|
|
|
505
516
|
type: info.non_unique === 0 ? 'unique' : 'index',
|
|
506
517
|
table: Case.camel(info.table),
|
|
507
518
|
cols: info.cols.map(Case.camel),
|
|
519
|
+
rawCols: info.cols,
|
|
508
520
|
},
|
|
509
521
|
]),
|
|
510
522
|
)
|
|
@@ -512,7 +524,7 @@ export class MySQLAdapter extends SQLAdapter {
|
|
|
512
524
|
// Match pattern only. MySQL re-renders an expression default before storing
|
|
513
525
|
// it in information_schema (`unix_timestamp()`, case and parens not
|
|
514
526
|
// guaranteed), and isDateNowDefault() strips parens and uppercases before
|
|
515
|
-
// comparing
|
|
527
|
+
// comparing, so the bare prefix is the right entry here. It is *not*
|
|
516
528
|
// emittable SQL; see below.
|
|
517
529
|
override readonly dateNowDefaults: string[] = ['UNIX_TIMESTAMP']
|
|
518
530
|
|
|
@@ -539,7 +551,7 @@ export class MySQLAdapter extends SQLAdapter {
|
|
|
539
551
|
),
|
|
540
552
|
}
|
|
541
553
|
// `column_type` ('varchar(64)'), not `data_type` ('varchar'), so the guard
|
|
542
|
-
// sees the same string a human would read
|
|
554
|
+
// sees the same string a human would read, and so TEXT, whose
|
|
543
555
|
// character_maximum_length MySQL reports as 65535, is excluded.
|
|
544
556
|
const length = this.sizedTextLength(
|
|
545
557
|
String(col.column_type || col.data_type || ''),
|
package/src/adapters/observe.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { SQLAdapter } from './base'
|
|
|
6
6
|
*
|
|
7
7
|
* Deliberately **not** an emitter and **not** a listener array. This sits in
|
|
8
8
|
* the hot path of every statement the ORM runs, so the cost of having the
|
|
9
|
-
* feature at all has to be a single null read when nobody is watching
|
|
9
|
+
* feature at all has to be a single null read when nobody is watching. See
|
|
10
10
|
* `observe()` below, which returns the driver's own value untouched in that
|
|
11
11
|
* case. An emitter would allocate an event object and walk a list per query
|
|
12
12
|
* whether or not anything subscribed.
|
|
@@ -22,12 +22,12 @@ import type { SQLAdapter } from './base'
|
|
|
22
22
|
export type QueryMethod = 'all' | 'run' | 'get' | 'values' | 'iterate'
|
|
23
23
|
|
|
24
24
|
export interface QueryEvent {
|
|
25
|
-
/** The statement as handed to the driver, before dialect
|
|
25
|
+
/** The statement as handed to the driver, before dialect normalization. */
|
|
26
26
|
sql: string
|
|
27
27
|
/** Wall-clock duration in milliseconds, fractional. */
|
|
28
28
|
ms: number
|
|
29
29
|
/**
|
|
30
|
-
* Rows the statement produced, or `null` when it cannot be known
|
|
30
|
+
* Rows the statement produced, or `null` when it cannot be known: a failed
|
|
31
31
|
* statement, or a result shape the adapter does not describe as an array.
|
|
32
32
|
* For `run` this is the *affected* row count, not a result set.
|
|
33
33
|
*/
|
|
@@ -47,7 +47,7 @@ export interface QueryEvent {
|
|
|
47
47
|
* observer receives is one `logger.info` away from a log file, an analytics
|
|
48
48
|
* table or a dashboard panel. Defaulting this on would turn "add a slow
|
|
49
49
|
* query panel" into a credential leak that nobody reviewed. Callers that
|
|
50
|
-
* genuinely need bindings
|
|
50
|
+
* genuinely need bindings (a local query profiler, say) must ask, and are
|
|
51
51
|
* then responsible for what they do with them.
|
|
52
52
|
*/
|
|
53
53
|
params?: readonly unknown[]
|
|
@@ -73,7 +73,7 @@ let includeParams = false
|
|
|
73
73
|
* call installed, so a test that forgets to restore cannot silently unhook a
|
|
74
74
|
* later one.
|
|
75
75
|
*
|
|
76
|
-
* ```ts no-check
|
|
76
|
+
* ```ts no-check: illustrative: the app decides where slow queries go
|
|
77
77
|
* import { setQueryObserver } from '@bakery-framework/orm'
|
|
78
78
|
*
|
|
79
79
|
* setQueryObserver(event => {
|
|
@@ -150,7 +150,7 @@ async function timed<R>(
|
|
|
150
150
|
rows: null,
|
|
151
151
|
driver,
|
|
152
152
|
method,
|
|
153
|
-
//
|
|
153
|
+
// Normalized so `error` is a reliable "did this throw" signal: a driver
|
|
154
154
|
// that rejects with `undefined` would otherwise be indistinguishable
|
|
155
155
|
// from success.
|
|
156
156
|
error: error ?? new Error('query failed with no error value'),
|
|
@@ -165,8 +165,8 @@ async function timed<R>(
|
|
|
165
165
|
* Wrap one executor entry point so it reports to the observer.
|
|
166
166
|
*
|
|
167
167
|
* The unobserved path is the `if` below and nothing else: no timer read, no
|
|
168
|
-
* event object, and the underlying call's own return value
|
|
169
|
-
* and `run` may legitimately be synchronous
|
|
168
|
+
* event object, and the underlying call's own return value (which for `all`
|
|
169
|
+
* and `run` may legitimately be synchronous) passes straight through.
|
|
170
170
|
*/
|
|
171
171
|
export function observe<R>(
|
|
172
172
|
driver: SQLAdapter.Driver,
|
|
@@ -182,7 +182,7 @@ export function observe<R>(
|
|
|
182
182
|
): (sqlText: string, params?: unknown[]) => Promise<R> | R
|
|
183
183
|
// Two overloads because the executor's five entry points are not one shape.
|
|
184
184
|
// `get` and `values` are declared to return a promise unconditionally, while
|
|
185
|
-
// `all` and `run` may legitimately be synchronous
|
|
185
|
+
// `all` and `run` may legitimately be synchronous, and the implementation
|
|
186
186
|
// signature, which has to admit both, cannot narrow to the former on its own.
|
|
187
187
|
// Without the first overload `createExecutor` fails to satisfy `Executor`.
|
|
188
188
|
export function observe<R>(
|
|
@@ -201,7 +201,7 @@ export function observe<R>(
|
|
|
201
201
|
* `iterate` is a stream, so its duration means something different from the
|
|
202
202
|
* other four, and the choice here is deliberate:
|
|
203
203
|
*
|
|
204
|
-
* - `ms` spans from the call to `iterate()` until iteration **ends
|
|
204
|
+
* - `ms` spans from the call to `iterate()` until iteration **ends**: the
|
|
205
205
|
* generator is exhausted, throws, or the consumer breaks out of the loop.
|
|
206
206
|
* That includes whatever the consumer did between yields, so an `iterate`
|
|
207
207
|
* event is not comparable with an `all` event and must not be averaged into
|
|
@@ -254,7 +254,7 @@ export function observeIterate(
|
|
|
254
254
|
// The source is created here rather than inside `drain`, and `started` with
|
|
255
255
|
// it: an async generator body does not run until the first `next()`, so
|
|
256
256
|
// both would otherwise be deferred to whenever the consumer got round to
|
|
257
|
-
// pulling
|
|
257
|
+
// pulling, and the statement would be issued later than it is today.
|
|
258
258
|
const started = performance.now()
|
|
259
259
|
return drain(driver, iterate(sqlText, params), sqlText, params, started)
|
|
260
260
|
}
|
package/src/adapters/pgsql.ts
CHANGED
|
@@ -22,11 +22,11 @@ export class PGAdapter extends SQLAdapter {
|
|
|
22
22
|
? connectionTarget.toString()
|
|
23
23
|
: undefined
|
|
24
24
|
super('postgres', undefined, target)
|
|
25
|
-
// `isOpenConnection`, not `instanceof SQL
|
|
25
|
+
// `isOpenConnection`, not `instanceof SQL`. See the helper for why the
|
|
26
26
|
// latter throws rather than answering.
|
|
27
27
|
//
|
|
28
28
|
// Pool options apply only when this opens its own connection. A handle
|
|
29
|
-
// handed in is already someone else's pool
|
|
29
|
+
// handed in is already someone else's pool: notably a transaction's, where
|
|
30
30
|
// re-sizing anything would be meaningless.
|
|
31
31
|
this.sql = isOpenConnection(connectionTarget)
|
|
32
32
|
? (connectionTarget as SQL)
|
|
@@ -57,16 +57,23 @@ export class PGAdapter extends SQLAdapter {
|
|
|
57
57
|
|
|
58
58
|
private static handleSpecial(
|
|
59
59
|
char: string,
|
|
60
|
-
|
|
60
|
+
// Underscored because it is deliberately unread: the branch that would
|
|
61
|
+
// have used it is the backslash branch, and the comment below is why
|
|
62
|
+
// there is not one. Kept in the signature so the shape matches
|
|
63
|
+
// `handleSpecial` in the other two adapters.
|
|
64
|
+
_nextChar: string | undefined,
|
|
61
65
|
state: PGSQLParserState,
|
|
62
66
|
): string | null {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
// No backslash branch, deliberately. Postgres with
|
|
68
|
+
// `standard_conforming_strings`: the server default since 9.1, and what
|
|
69
|
+
// every connection here runs under: treats a backslash inside a `'…'`
|
|
70
|
+
// literal as an ordinary character. This scanner used to apply MySQL's
|
|
71
|
+
// rule instead: `\` consumed the next character, so the `'` closing a
|
|
72
|
+
// `'\'` literal was eaten, the scanner stayed "inside" a literal the
|
|
73
|
+
// server had already closed, and every `?` after it was left unrewritten,
|
|
74
|
+
// which is how `ESCAPE '\'` produced a syntax error several tokens
|
|
75
|
+
// downstream. The server's parse is the only one that matters; the scanner
|
|
76
|
+
// now agrees with it by having no opinion about backslashes at all.
|
|
70
77
|
if (char === '`') {
|
|
71
78
|
return !state.inSingleQuote && !state.inDoubleQuote ? '"' : char
|
|
72
79
|
}
|
|
@@ -89,7 +96,7 @@ export class PGAdapter extends SQLAdapter {
|
|
|
89
96
|
skipNext: false,
|
|
90
97
|
paramsLength: params.length,
|
|
91
98
|
}
|
|
92
|
-
// `skipNext` is consumed in exactly one place
|
|
99
|
+
// `skipNext` is consumed in exactly one place (the `i++` below), and
|
|
93
100
|
// cleared there.
|
|
94
101
|
//
|
|
95
102
|
// It used to be consumed twice: a top-of-loop
|
|
@@ -100,9 +107,8 @@ export class PGAdapter extends SQLAdapter {
|
|
|
100
107
|
// default containing an apostrophe silently lost the rest of its value, and
|
|
101
108
|
// the same applied to a backslash escape. Values bind as parameters, so
|
|
102
109
|
// this
|
|
103
|
-
// only ever reached literals the framework itself emits
|
|
104
|
-
//
|
|
105
|
-
// normalisation, and no live server ran until now.
|
|
110
|
+
// only ever reached literals the framework itself emits (DDL defaults), // which is why it survived: `ddl.test.ts` asserts the SQL *before*
|
|
111
|
+
// normalization, and no live server ran until now.
|
|
106
112
|
for (let i = 0; i < sql.length; i++) {
|
|
107
113
|
const char = sql[i]
|
|
108
114
|
const nextChar = sql[i + 1]
|
|
@@ -142,15 +148,15 @@ export class PGAdapter extends SQLAdapter {
|
|
|
142
148
|
PGAdapter.normalizePostgresSQL(sql, params),
|
|
143
149
|
params,
|
|
144
150
|
)) as any
|
|
145
|
-
// `count` is authoritative on Postgres for every command
|
|
146
|
-
// row count from the command tag
|
|
151
|
+
// `count` is authoritative on Postgres for every command (it is the
|
|
152
|
+
// row count from the command tag), so it is read first and `rows.length`
|
|
147
153
|
// is only a fallback.
|
|
148
154
|
//
|
|
149
155
|
// It used to be the other way round, behind an `Array.isArray` check
|
|
150
156
|
// that was always true: Bun returns an *array* for a write too, just an
|
|
151
157
|
// empty one, so the `count` branch was unreachable and `changes` was
|
|
152
158
|
// `rows.length`. `UPDATE` and `DELETE` return no rows, so both reported
|
|
153
|
-
// 0. `INSERT` was right only by accident
|
|
159
|
+
// 0. `INSERT` was right only by accident: the `RETURNING *` appended
|
|
154
160
|
// above happens to make `rows.length` the number inserted.
|
|
155
161
|
const changes = Number(
|
|
156
162
|
rows?.count ?? (Array.isArray(rows) ? rows.length : 0),
|
|
@@ -246,7 +252,9 @@ export class PGAdapter extends SQLAdapter {
|
|
|
246
252
|
return new PGAdapter(sql as SQL)
|
|
247
253
|
}
|
|
248
254
|
|
|
249
|
-
async getSchema(
|
|
255
|
+
async getSchema(
|
|
256
|
+
options?: SQLAdapter.SchemaOptions,
|
|
257
|
+
): Promise<SQLAdapter.TableDetails[]> {
|
|
250
258
|
const res = (await this.query(
|
|
251
259
|
'SELECT table_name AS name, table_type AS type' +
|
|
252
260
|
' FROM information_schema.tables' +
|
|
@@ -261,7 +269,9 @@ export class PGAdapter extends SQLAdapter {
|
|
|
261
269
|
res.map(async t => {
|
|
262
270
|
const qName = this.quote(t.name)
|
|
263
271
|
const [countRes, cols, pkCols, idxs] = (await Promise.all([
|
|
264
|
-
|
|
272
|
+
options?.rowCounts
|
|
273
|
+
? this.query(`SELECT COUNT(*)::int as count FROM ${qName}`).get()
|
|
274
|
+
: null,
|
|
265
275
|
this.query(
|
|
266
276
|
'SELECT column_name AS name, data_type AS type,' +
|
|
267
277
|
' is_nullable AS is_nullable' +
|
|
@@ -271,8 +281,8 @@ export class PGAdapter extends SQLAdapter {
|
|
|
271
281
|
' ORDER BY ordinal_position',
|
|
272
282
|
).all(t.name),
|
|
273
283
|
// `::regclass` casts a **string**, so the table name binds as a
|
|
274
|
-
// parameter. It used to interpolate `qName`
|
|
275
|
-
// *identifier*
|
|
284
|
+
// parameter. It used to interpolate `qName` (a double-quoted
|
|
285
|
+
// *identifier*), which Postgres reads as a column reference, so this
|
|
276
286
|
// threw `column "<table>" does not exist` for every table and took the
|
|
277
287
|
// whole of `getSchema()` down with it on this dialect.
|
|
278
288
|
//
|
|
@@ -292,10 +302,10 @@ export class PGAdapter extends SQLAdapter {
|
|
|
292
302
|
" WHERE schemaname NOT IN ('pg_catalog', 'information_schema')" +
|
|
293
303
|
' AND tablename = ?',
|
|
294
304
|
).all(t.name),
|
|
295
|
-
])) as [SQLAdapter.CountRow, any[], any[], any[]]
|
|
305
|
+
])) as [SQLAdapter.CountRow | null, any[], any[], any[]]
|
|
296
306
|
return {
|
|
297
307
|
name: t.name,
|
|
298
|
-
rowCount: countRes?.count || 0,
|
|
308
|
+
rowCount: options?.rowCounts ? countRes?.count || 0 : null,
|
|
299
309
|
columns: cols.map(c => ({
|
|
300
310
|
name: c.name,
|
|
301
311
|
type: c.type,
|
|
@@ -326,10 +336,15 @@ export class PGAdapter extends SQLAdapter {
|
|
|
326
336
|
new Set(cols.map(c => c.name)),
|
|
327
337
|
)
|
|
328
338
|
const tName = this.quote(tableName)
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
339
|
+
// See `TableDataOptions.knownTotal`: the count is almost the whole cost
|
|
340
|
+
// of a page, and a caller that has already counted can say so.
|
|
341
|
+
const reuse = SQLAdapter.usableTotal(options.knownTotal)
|
|
342
|
+
const countRes = reuse
|
|
343
|
+
? null
|
|
344
|
+
: ((await this.query(
|
|
345
|
+
`SELECT COUNT(*) as count FROM ${tName}${whereSql}`,
|
|
346
|
+
).get(...whereParams)) as SQLAdapter.CountRow)
|
|
347
|
+
const totalRows = reuse ? options.knownTotal! : countRes?.count || 0
|
|
333
348
|
const rows = (await this.query(
|
|
334
349
|
`SELECT ctid::text AS rowid, * FROM ${tName}` +
|
|
335
350
|
`${whereSql}${orderSql} LIMIT ? OFFSET ?`,
|
|
@@ -438,7 +453,7 @@ export class PGAdapter extends SQLAdapter {
|
|
|
438
453
|
}
|
|
439
454
|
|
|
440
455
|
// is_identity/identity_generation are selected because an identity column
|
|
441
|
-
//
|
|
456
|
+
// (what colDef() emits) carries a NULL column_default; only the legacy
|
|
442
457
|
// serial style leaves a nextval(...) marker there.
|
|
443
458
|
const cols = (await this.query(
|
|
444
459
|
'SELECT column_name, data_type, is_nullable, column_default,' +
|
|
@@ -473,24 +488,24 @@ export class PGAdapter extends SQLAdapter {
|
|
|
473
488
|
)
|
|
474
489
|
continue
|
|
475
490
|
const m = r.indexdef.match(/\(([^)]+)\)/)
|
|
491
|
+
const raw: string[] = m
|
|
492
|
+
? m[1].split(',').map((c: string) => c.trim().replace(/"/g, ''))
|
|
493
|
+
: []
|
|
476
494
|
dbIndexes[Case.camel(r.indexname)] = {
|
|
477
495
|
type: /UNIQUE/i.test(r.indexdef) ? 'unique' : 'index',
|
|
478
496
|
table: Case.camel(r.tablename),
|
|
479
|
-
cols:
|
|
480
|
-
|
|
481
|
-
.split(',')
|
|
482
|
-
.map((c: string) => Case.camel(c.trim().replace(/"/g, '')))
|
|
483
|
-
: [],
|
|
497
|
+
cols: raw.map(Case.camel),
|
|
498
|
+
rawCols: raw,
|
|
484
499
|
}
|
|
485
500
|
}
|
|
486
501
|
return dbIndexes
|
|
487
502
|
}
|
|
488
|
-
// Two patterns because Postgres does not store DDL text
|
|
503
|
+
// Two patterns because Postgres does not store DDL text: it stores a parsed
|
|
489
504
|
// expression and re-renders it, and how it renders depends on the version.
|
|
490
505
|
// PG 14+ reports `(EXTRACT(epoch FROM now()))::integer`; PG <= 13 parses
|
|
491
506
|
// EXTRACT into `date_part('epoch'::text, now())`, which the EXTRACT pattern
|
|
492
507
|
// does not match. Missing it means the column diffs dirty on every single
|
|
493
|
-
// sync
|
|
508
|
+
// sync: the same perpetual-rebuild failure the SQLite quote bug caused.
|
|
494
509
|
override readonly dateNowDefaults: string[] = [
|
|
495
510
|
'EXTRACT(EPOCH FROM',
|
|
496
511
|
'DATE_PART',
|
|
@@ -506,7 +521,7 @@ export class PGAdapter extends SQLAdapter {
|
|
|
506
521
|
// `gen_random_uuid()` is built in from Postgres 13; before that it lived in
|
|
507
522
|
// the pgcrypto extension. Postgres reports it back as
|
|
508
523
|
// `gen_random_uuid()`, so unlike the epoch expression the emitted form and
|
|
509
|
-
// the match pattern coincide
|
|
524
|
+
// the match pattern coincide: stated rather than assumed, because the two
|
|
510
525
|
// being equal here is a coincidence of this expression, not a rule.
|
|
511
526
|
override readonly uuidDefaults: string[] = ['GEN_RANDOM_UUID']
|
|
512
527
|
override readonly uuidExpression: string = 'gen_random_uuid()'
|
|
@@ -521,7 +536,7 @@ export class PGAdapter extends SQLAdapter {
|
|
|
521
536
|
),
|
|
522
537
|
}
|
|
523
538
|
// Postgres calls it 'character varying' and reports null for TEXT, so the
|
|
524
|
-
// guard has less to do here than on MySQL
|
|
539
|
+
// guard has less to do here than on MySQL, but it is the same guard.
|
|
525
540
|
const length = this.sizedTextLength(
|
|
526
541
|
String(col.data_type || col.udt_name || ''),
|
|
527
542
|
col.character_maximum_length,
|
|
@@ -542,11 +557,11 @@ export class PGAdapter extends SQLAdapter {
|
|
|
542
557
|
// Postgres does not store the DDL text of a default; it re-renders the
|
|
543
558
|
// parsed expression and appends the column's type. `''` comes back as
|
|
544
559
|
// `''::character varying` and `'x'` as `'x'::text`, neither of which
|
|
545
|
-
// equals what the schema says
|
|
560
|
+
// equals what the schema says, so the column diffs on every sync.
|
|
546
561
|
//
|
|
547
562
|
// The cast is only stripped when it ends the string, which leaves
|
|
548
563
|
// `nextval('seq'::regclass)` and the `%dateNow%` expressions alone: both
|
|
549
|
-
// end in `)`, and both are
|
|
564
|
+
// end in `)`, and both are recognized elsewhere.
|
|
550
565
|
if (typeof def === 'string') {
|
|
551
566
|
const bare = def.replace(/::[\w ]+$/, '').trim()
|
|
552
567
|
const quoted = /^'([\s\S]*)'$/.exec(bare)
|
package/src/adapters/registry.ts
CHANGED
|
@@ -6,11 +6,11 @@ import type { SQLAdapter } from './base'
|
|
|
6
6
|
* add its own.
|
|
7
7
|
*
|
|
8
8
|
* A union type cannot be extended from outside the package, and widening it to
|
|
9
|
-
* `string` would give up every place the compiler currently catches a typo
|
|
9
|
+
* `string` would give up every place the compiler currently catches a typo, so
|
|
10
10
|
* this is an interface and a new name arrives by declaration merging, the same
|
|
11
11
|
* mechanism `DBSchema` already uses for an app's tables:
|
|
12
12
|
*
|
|
13
|
-
* ```ts no-check
|
|
13
|
+
* ```ts no-check: a third-party package's own declaration file
|
|
14
14
|
* declare module '@bakery-framework/orm/adapters' {
|
|
15
15
|
* interface DriverRegistry {
|
|
16
16
|
* mssql: true
|
|
@@ -18,7 +18,7 @@ import type { SQLAdapter } from './base'
|
|
|
18
18
|
* }
|
|
19
19
|
* ```
|
|
20
20
|
*
|
|
21
|
-
* `@bakery-framework/orm/adapters`
|
|
21
|
+
* `@bakery-framework/orm/adapters` (the public subpath), and not this file, which is
|
|
22
22
|
* private and does not resolve from outside the package. The merge reaches
|
|
23
23
|
* through the barrel's `export *`; aiming it at an unresolvable specifier
|
|
24
24
|
* instead declares a second, unrelated interface and leaves every driver name
|
|
@@ -47,14 +47,14 @@ export interface AdapterSpec {
|
|
|
47
47
|
/**
|
|
48
48
|
* URL schemes this adapter answers to, without `://`.
|
|
49
49
|
*
|
|
50
|
-
* Checked first and exactly
|
|
50
|
+
* Checked first and exactly: `mysql://…` reaches the adapter that listed
|
|
51
51
|
* `mysql`, and nothing else is consulted. List every spelling you accept;
|
|
52
52
|
* the built-in MySQL adapter lists four.
|
|
53
53
|
*/
|
|
54
54
|
protocols?: readonly string[]
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
* Last resort, for a target no scheme matched
|
|
57
|
+
* Last resort, for a target no scheme matched: a bare file path, say.
|
|
58
58
|
*
|
|
59
59
|
* Consulted in reverse registration order, so an adapter registered later can
|
|
60
60
|
* claim a target one of the built-ins would otherwise have taken. Return
|
|
@@ -66,7 +66,7 @@ export interface AdapterSpec {
|
|
|
66
66
|
* Open a connection. `target` is the raw `DB_URL`, or `undefined` when none
|
|
67
67
|
* was set and this adapter is the default.
|
|
68
68
|
*
|
|
69
|
-
* Async so the implementation can `await import()` its own driver module
|
|
69
|
+
* Async so the implementation can `await import()` its own driver module,
|
|
70
70
|
* which is how the three built-ins stay lazy: registering all of them costs
|
|
71
71
|
* three object literals, and only the one that wins is ever loaded.
|
|
72
72
|
*/
|
|
@@ -77,7 +77,7 @@ export interface AdapterSpec {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
* What to fall back to when the target names no adapter
|
|
80
|
+
* What to fall back to when the target names no adapter: split in two because
|
|
81
81
|
* the two cases have never had the same answer. See {@link resolveDriver}.
|
|
82
82
|
*/
|
|
83
83
|
export interface DriverFallback {
|
|
@@ -96,8 +96,8 @@ export const DEFAULT_FALLBACK: DriverFallback = {
|
|
|
96
96
|
* A stack per driver name, not one entry per name.
|
|
97
97
|
*
|
|
98
98
|
* Registering over an existing driver is temporary far more often than it is
|
|
99
|
-
* permanent
|
|
100
|
-
* environment
|
|
99
|
+
* permanent (a test swapping in a fake, an app overriding a built-in for one
|
|
100
|
+
* environment), so "undo" has to give back exactly what was displaced. One
|
|
101
101
|
* entry per name cannot: release two registrations out of order and the second
|
|
102
102
|
* disposer restores the first's spec, quietly leaving a stub adapter installed
|
|
103
103
|
* for the rest of the process. A stack makes the order irrelevant, which is the
|
|
@@ -149,14 +149,14 @@ export function listAdapters(): AdapterSpec[] {
|
|
|
149
149
|
* 1. **No target at all** → `fallback` (`sqlite`), so a fresh app with no
|
|
150
150
|
* `DB_URL` gets a file database rather than an error.
|
|
151
151
|
* 2. **A URL scheme** that some adapter declared → that adapter, exactly.
|
|
152
|
-
* 3. **`matches()`**, in reverse registration order
|
|
152
|
+
* 3. **`matches()`**, in reverse registration order: later registrations get
|
|
153
153
|
* first refusal, which is what makes overriding a built-in possible.
|
|
154
154
|
* 4. **`fallback`** otherwise, which for a target that looked like a host is
|
|
155
|
-
* `postgres`. Long-standing
|
|
155
|
+
* `postgres`. Long-standing behavior, kept deliberately: a bare
|
|
156
156
|
* `db.internal:5432/app` has been read as Postgres since before the
|
|
157
157
|
* registry existed.
|
|
158
158
|
*
|
|
159
|
-
* Throws only if the resolved driver has no adapter registered
|
|
159
|
+
* Throws only if the resolved driver has no adapter registered, which means
|
|
160
160
|
* something registered a `protocols` entry and then unregistered itself, not
|
|
161
161
|
* anything a user can reach by typing a URL.
|
|
162
162
|
*/
|
|
@@ -175,7 +175,7 @@ export function resolveAdapter(
|
|
|
175
175
|
return spec
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
/** Step 1
|
|
178
|
+
/** Step 1 to 4 of {@link resolveAdapter}, without the lookup. */
|
|
179
179
|
export function resolveDriver(
|
|
180
180
|
target: string,
|
|
181
181
|
fallback: DriverFallback = DEFAULT_FALLBACK,
|