@bakery-framework/orm 1.0.0
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/LICENSE +19 -0
- package/README.md +88 -0
- package/package.json +59 -0
- package/src/adapters/base.ts +1128 -0
- package/src/adapters/mysql.ts +619 -0
- package/src/adapters/observe.ts +261 -0
- package/src/adapters/pgsql.ts +611 -0
- package/src/adapters/registry.ts +204 -0
- package/src/adapters/sqlite.ts +588 -0
- package/src/adapters.ts +72 -0
- package/src/backup.ts +37 -0
- package/src/connection.ts +69 -0
- package/src/define.ts +380 -0
- package/src/field.ts +595 -0
- package/src/globals.d.ts +22 -0
- package/src/index.ts +63 -0
- package/src/orm/index.ts +24 -0
- package/src/orm/mutation.ts +692 -0
- package/src/orm/query.ts +1680 -0
- package/src/pool.ts +83 -0
- package/src/schema-registry.ts +75 -0
- package/src/schema-util.ts +467 -0
- package/src/sync/builder.ts +618 -0
- package/src/sync/engine.ts +399 -0
- package/src/sync/helpers.ts +1119 -0
- package/src/sync/history.ts +169 -0
- package/src/sync/index.ts +113 -0
- package/src/sync/ledger.ts +335 -0
- package/src/sync/load.ts +368 -0
- package/src/sync/rollback.ts +200 -0
- package/src/sync/types.ts +101 -0
- package/src/sync/view-sql.ts +160 -0
- package/templates/schema.example.ts +94 -0
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
import { Try } from '@bakery-framework/core/utils'
|
|
2
|
+
import { throws } from '@bakery-framework/core/utils/common'
|
|
3
|
+
import { DEFAULT_MAX_QUERY_PARAMS, type SQLAdapter } from '../adapters'
|
|
4
|
+
import { getActiveDb, txStorage } from '../connection'
|
|
5
|
+
import type {
|
|
6
|
+
AppViews,
|
|
7
|
+
AppDBOptionals as DBOptionals,
|
|
8
|
+
AppDBSchema as DBSchema,
|
|
9
|
+
} from '../schema-registry'
|
|
10
|
+
import { evalOperands, qId } from '../schema-util'
|
|
11
|
+
import { DB } from './query'
|
|
12
|
+
|
|
13
|
+
export namespace Mutation {
|
|
14
|
+
/**
|
|
15
|
+
* What may be written to: a declared table that is not a view.
|
|
16
|
+
*
|
|
17
|
+
* There used to be a `| (string & {})` member here, for autocomplete on the
|
|
18
|
+
* literals while still accepting any string. It also made the rest of the
|
|
19
|
+
* type decorative — `Exclude<…, AppViews>` never rejected anything, so a
|
|
20
|
+
* `DB.Insert.into('some_view')` compiled and failed at the database instead,
|
|
21
|
+
* and so did a typo'd table name.
|
|
22
|
+
*
|
|
23
|
+
* Strict now, and it costs nothing when no schema is registered: `DBSchema`
|
|
24
|
+
* falls back to `MapOf<MapOf<any>>` there, whose `keyof` is `string`, so an
|
|
25
|
+
* unregistered app is exactly as permissive as before. Registering a schema
|
|
26
|
+
* is what opts you in.
|
|
27
|
+
*/
|
|
28
|
+
export type Tables = Exclude<keyof DBSchema, AppViews>
|
|
29
|
+
export type MapOf<T> = Record<string, T>
|
|
30
|
+
|
|
31
|
+
export type ValidOptionals<T extends keyof DBSchema> =
|
|
32
|
+
T extends keyof DBOptionals
|
|
33
|
+
? Extract<DBOptionals[T], keyof DBSchema[T]>
|
|
34
|
+
: never
|
|
35
|
+
|
|
36
|
+
export type Prettify<T> = { [K in keyof T]: T[K] } & {}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `RETURNING` takes an identifier list and is interpolated, not bound — the
|
|
40
|
+
* same position `orderBy` and `groupBy` guard with `safeColumn`. It was the
|
|
41
|
+
* one identifier writer on Insert/Update/Delete with no guard at all, so
|
|
42
|
+
* `.returning('* FROM users; DROP TABLE t --')` was emitted verbatim.
|
|
43
|
+
*
|
|
44
|
+
* Validated at the call site rather than in `parse()` so a bad list fails
|
|
45
|
+
* where it was written, which is what `orderBy` does with its direction.
|
|
46
|
+
*/
|
|
47
|
+
function safeReturning(cols: string): string {
|
|
48
|
+
return String(cols)
|
|
49
|
+
.split(',')
|
|
50
|
+
.map(part => DB.safeColumn(part.trim()))
|
|
51
|
+
.join(', ')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type InsertSchema<T extends Tables> = T extends keyof DBSchema
|
|
55
|
+
? Prettify<
|
|
56
|
+
Omit<DBSchema[T], ValidOptionals<T>> &
|
|
57
|
+
Partial<Pick<DBSchema[T], ValidOptionals<T>>>
|
|
58
|
+
>
|
|
59
|
+
: MapOf<unknown>
|
|
60
|
+
|
|
61
|
+
export type UpdateSchema<T extends Tables> = Partial<InsertSchema<T>>
|
|
62
|
+
|
|
63
|
+
export type ColumnTarget<T extends Tables> = T extends keyof DBSchema
|
|
64
|
+
? keyof DBSchema[T] & string
|
|
65
|
+
: string
|
|
66
|
+
|
|
67
|
+
export type QualifiedColumnTarget<T extends Tables> = T extends keyof DBSchema
|
|
68
|
+
?
|
|
69
|
+
| `${Extract<T, string>}.${Extract<keyof DBSchema[T], string>}`
|
|
70
|
+
| ColumnTarget<T>
|
|
71
|
+
: string
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* What a write returns — the adapter's own result, not a copy of it.
|
|
75
|
+
*
|
|
76
|
+
* It *was* a copy: an identical `{lastInsertRowid, changes}` declared here as
|
|
77
|
+
* well as on `SQLAdapter`. Identical today is the whole problem — that is the
|
|
78
|
+
* state `SQLAdapter.ColumnConstraint` was in before it fell behind
|
|
79
|
+
* `sync/types.ts` and started erasing fields at the cast.
|
|
80
|
+
*/
|
|
81
|
+
export type RunResult = SQLAdapter.RunResult
|
|
82
|
+
|
|
83
|
+
export class Insert<T extends Tables = any> {
|
|
84
|
+
constructor(private _table: string) {}
|
|
85
|
+
static into<T extends Tables>(table: T): Insert<T> {
|
|
86
|
+
return new Insert(table as string)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The rows to insert — as a spread, or as one array.
|
|
91
|
+
*
|
|
92
|
+
* Both forms exist because the variadic signature alone made the obvious
|
|
93
|
+
* call wrong in a way nothing announced: `values(rows)` bound the *array*
|
|
94
|
+
* as a single record, and the only symptom was
|
|
95
|
+
* `table big has no column named 0`. An array is never a record, so the two
|
|
96
|
+
* forms cannot be confused, and anything that is neither — a mixed
|
|
97
|
+
* `values(rows, extra)`, a primitive — is rejected by name rather than
|
|
98
|
+
* turned into columns called `0` and `1`.
|
|
99
|
+
*/
|
|
100
|
+
values(records: InsertSchema<T>[]): InsertExecutable
|
|
101
|
+
values(...records: InsertSchema<T>[]): InsertExecutable
|
|
102
|
+
values(...args: (InsertSchema<T> | InsertSchema<T>[])[]): InsertExecutable {
|
|
103
|
+
const records =
|
|
104
|
+
args.length === 1 && Array.isArray(args[0])
|
|
105
|
+
? (args[0] as InsertSchema<T>[])
|
|
106
|
+
: (args as InsertSchema<T>[])
|
|
107
|
+
|
|
108
|
+
for (const record of records) {
|
|
109
|
+
if (
|
|
110
|
+
record === null ||
|
|
111
|
+
typeof record !== 'object' ||
|
|
112
|
+
Array.isArray(record)
|
|
113
|
+
) {
|
|
114
|
+
throws(
|
|
115
|
+
'values() takes records: values(row), values(rowA, rowB) or ' +
|
|
116
|
+
'values(rows). Got a ' +
|
|
117
|
+
(Array.isArray(record) ? 'nested array' : typeof record) +
|
|
118
|
+
' — if you meant to pass an array of rows, pass it as the only ' +
|
|
119
|
+
'argument.',
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return new InsertExecutable(this._table, records as MapOf<unknown>[])
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export class InsertExecutable {
|
|
129
|
+
private _returning?: string
|
|
130
|
+
private _conflict?: { cols: string[]; update: string[] | null }
|
|
131
|
+
|
|
132
|
+
constructor(
|
|
133
|
+
private _table: string,
|
|
134
|
+
private _records: MapOf<unknown>[],
|
|
135
|
+
) {}
|
|
136
|
+
|
|
137
|
+
returning(cols: string = '*'): this {
|
|
138
|
+
this._returning = safeReturning(cols)
|
|
139
|
+
return this
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Insert, or update the row that is already there.
|
|
144
|
+
*
|
|
145
|
+
* `cols` names the unique columns that decide "already there" — a primary
|
|
146
|
+
* key or a unique index. Without an upsert the only way to express this is
|
|
147
|
+
* to check and then branch, which is a **race**: two requests both see no
|
|
148
|
+
* row and both insert.
|
|
149
|
+
*
|
|
150
|
+
* DB.Insert.into('users').values({ email, name }).upsert(['email'])
|
|
151
|
+
*
|
|
152
|
+
* By default every inserted column except the conflict columns is
|
|
153
|
+
* overwritten. Pass a second argument to narrow that — `upsert(['email'],
|
|
154
|
+
* ['name'])` leaves everything else as it was — or an empty array for
|
|
155
|
+
* insert-if-absent, which becomes `DO NOTHING`.
|
|
156
|
+
*
|
|
157
|
+
* MySQL ignores `cols` because `ON DUPLICATE KEY UPDATE` fires on *any*
|
|
158
|
+
* unique key and takes no conflict target. They are still required, since
|
|
159
|
+
* Postgres and SQLite cannot express the statement without them and a
|
|
160
|
+
* schema that works on one dialect should work on all three.
|
|
161
|
+
*/
|
|
162
|
+
upsert(cols: string[], update?: string[]): this {
|
|
163
|
+
if (!cols.length) throws('upsert() needs at least one conflict column')
|
|
164
|
+
this._conflict = { cols, update: update ?? null }
|
|
165
|
+
return this
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The column list: every record's keys, unioned, in first-seen order. */
|
|
169
|
+
private columnKeys(): string[] {
|
|
170
|
+
const keySet = new Set<string>()
|
|
171
|
+
for (const record of this._records) {
|
|
172
|
+
for (const key of Object.keys(record)) {
|
|
173
|
+
keySet.add(key)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return Array.from(keySet)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* How many records fit in one statement, under the adapter's ceiling.
|
|
181
|
+
*
|
|
182
|
+
* The ceiling comes from the adapter because only it knows its dialect, but
|
|
183
|
+
* a statement can be *rendered* without a connection — `parse()` is how the
|
|
184
|
+
* tests read the SQL, and the stub adapter in `orm.test.ts` implements two
|
|
185
|
+
* members. An unreachable or silent adapter falls back to the same default
|
|
186
|
+
* the base class publishes rather than making `parse()` require a database.
|
|
187
|
+
*/
|
|
188
|
+
private batchSize(columnCount: number): number {
|
|
189
|
+
const declared = Try.return(
|
|
190
|
+
() => Number(getActiveDb().maxQueryParams),
|
|
191
|
+
Number.NaN,
|
|
192
|
+
)
|
|
193
|
+
const ceiling =
|
|
194
|
+
Number.isFinite(declared) && declared > 0
|
|
195
|
+
? declared
|
|
196
|
+
: DEFAULT_MAX_QUERY_PARAMS
|
|
197
|
+
return Math.max(1, Math.floor(ceiling / Math.max(1, columnCount)))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private statement(
|
|
201
|
+
records: MapOf<unknown>[],
|
|
202
|
+
keys: string[],
|
|
203
|
+
): { sql: string; params: any[] } {
|
|
204
|
+
const columns = keys.map(k => qId(k)).join(', ')
|
|
205
|
+
const placeholderGroup = `(${Array(keys.length).fill('?').join(', ')})`
|
|
206
|
+
const placeholders = Array(records.length)
|
|
207
|
+
.fill(placeholderGroup)
|
|
208
|
+
.join(', ')
|
|
209
|
+
|
|
210
|
+
const params = records.flatMap(record => keys.map(k => record[k] ?? null))
|
|
211
|
+
|
|
212
|
+
const retSql = this._returning ? ` RETURNING ${this._returning}` : ''
|
|
213
|
+
// Rebuilt per batch, not hoisted: each batch is a whole statement, so an
|
|
214
|
+
// upsert whose conflict clause only rode on the first one would upsert
|
|
215
|
+
// 10,922 rows and then raise a unique violation on the next batch.
|
|
216
|
+
const conflictSql = this.conflictClause(keys)
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
sql: `INSERT INTO ${qId(this._table)} (${columns}) VALUES ${placeholders}${conflictSql}${retSql}`,
|
|
220
|
+
params,
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The insert as one statement per batch — the form the executors run.
|
|
226
|
+
*
|
|
227
|
+
* A single `INSERT … VALUES (…),(…),…` carries three parameters per row, so
|
|
228
|
+
* it stops being a legal statement somewhere around eleven thousand rows.
|
|
229
|
+
* Past that the drivers do not report a limit, they report a *wrapped*
|
|
230
|
+
* count (`expected 54464 values, received 120000`) or `too many SQL
|
|
231
|
+
* variables`, neither of which names the actual problem. Batching under the
|
|
232
|
+
* adapter's ceiling is the only way `values(rows)` can mean what it reads
|
|
233
|
+
* like for an arbitrary `rows`.
|
|
234
|
+
*
|
|
235
|
+
* The column list is computed once, across every record, so every batch
|
|
236
|
+
* inserts the same columns in the same order — a per-batch union would
|
|
237
|
+
* change the shape of the statement halfway through the insert.
|
|
238
|
+
*
|
|
239
|
+
* **`RETURNING` is accumulated, not refused.** Refusing is the
|
|
240
|
+
* safe-looking option and the wrong one: the batches run sequentially
|
|
241
|
+
* inside one transaction, so concatenating each batch's rows in batch order
|
|
242
|
+
* reproduces the sequence a single statement would have produced. Ordering
|
|
243
|
+
* *within* a batch is whatever the dialect gives — Postgres does not
|
|
244
|
+
* promise `RETURNING` follows `VALUES` order — but that is equally true
|
|
245
|
+
* unchunked, so batching neither adds nor removes a guarantee. Refusing
|
|
246
|
+
* would have cost the main reason to write `.values(rows).returning('id')`
|
|
247
|
+
* at all: getting the generated ids of a bulk import back.
|
|
248
|
+
*/
|
|
249
|
+
parseAll(): { sql: string; params: any[] }[] {
|
|
250
|
+
if (this._records.length === 0) throws('Empty insert')
|
|
251
|
+
const keys = this.columnKeys()
|
|
252
|
+
const size = this.batchSize(keys.length)
|
|
253
|
+
|
|
254
|
+
if (this._records.length <= size) {
|
|
255
|
+
return [this.statement(this._records, keys)]
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const batches: { sql: string; params: any[] }[] = []
|
|
259
|
+
for (let i = 0; i < this._records.length; i += size) {
|
|
260
|
+
batches.push(this.statement(this._records.slice(i, i + size), keys))
|
|
261
|
+
}
|
|
262
|
+
return batches
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The insert as one statement.
|
|
267
|
+
*
|
|
268
|
+
* Throws rather than returning the first batch when the records do not fit
|
|
269
|
+
* in one: a caller holding `{sql, params}` executes it themselves, and
|
|
270
|
+
* quietly handing back a third of their rows is the failure mode this whole
|
|
271
|
+
* change exists to remove. `parseAll()` is the honest answer for that case.
|
|
272
|
+
*/
|
|
273
|
+
parse(): { sql: string; params: any[] } {
|
|
274
|
+
const batches = this.parseAll()
|
|
275
|
+
if (batches.length > 1) {
|
|
276
|
+
throws(
|
|
277
|
+
`Insert of ${this._records.length} records needs ${batches.length} ` +
|
|
278
|
+
`statements to stay under the parameter ceiling; parse() returns ` +
|
|
279
|
+
`one. Use run()/array()/fetch(), which batch inside a single ` +
|
|
280
|
+
`transaction, or parseAll() for the statements themselves.`,
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
return batches[0]!
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Run every batch, in one transaction when there is more than one.
|
|
288
|
+
*
|
|
289
|
+
* The transaction is what keeps a batched insert meaning what the unbatched
|
|
290
|
+
* one meant: all rows or none. It is opened only when a batch boundary
|
|
291
|
+
* exists — one statement is already atomic — and only when the caller is
|
|
292
|
+
* not already inside `DB.transaction`, where the outer transaction is
|
|
293
|
+
* already the atomic unit.
|
|
294
|
+
*
|
|
295
|
+
* That second condition is now an economy rather than a requirement: since
|
|
296
|
+
* `SQLAdapter.transaction` nests through `SAVEPOINT`, wrapping anyway would
|
|
297
|
+
* work. It would just buy a savepoint per bulk insert that can never roll
|
|
298
|
+
* back independently of the transaction enclosing it.
|
|
299
|
+
*/
|
|
300
|
+
private async runBatches<R>(
|
|
301
|
+
batches: { sql: string; params: any[] }[],
|
|
302
|
+
each: (db: SQLAdapter, sql: string, params: any[]) => Promise<R>,
|
|
303
|
+
): Promise<R[]> {
|
|
304
|
+
const exec = async (db: SQLAdapter) => {
|
|
305
|
+
const results: R[] = []
|
|
306
|
+
// Sequential on purpose: they share one connection, and a batch that
|
|
307
|
+
// fails has to leave the ones after it unattempted.
|
|
308
|
+
for (const batch of batches) {
|
|
309
|
+
results.push(await each(db, batch.sql, batch.params))
|
|
310
|
+
}
|
|
311
|
+
return results
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const db = getActiveDb()
|
|
315
|
+
if (batches.length === 1 || txStorage.getStore()) return exec(db)
|
|
316
|
+
return db.transaction(tx => exec(tx))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// `execute` directly rather than `query(sql).run(...params)`: the
|
|
320
|
+
// statement-level API spreads its parameters as arguments, and a batch
|
|
321
|
+
// carries tens of thousands of them.
|
|
322
|
+
private static all(db: SQLAdapter, sql: string, params: any[]) {
|
|
323
|
+
return Promise.resolve(db.execute.all(sql, params))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The upsert clause, in the dialect of the active connection.
|
|
328
|
+
*
|
|
329
|
+
* Two shapes, not three: Postgres and SQLite share
|
|
330
|
+
* `ON CONFLICT (…) DO UPDATE SET col = excluded.col`, while MySQL spells it
|
|
331
|
+
* `ON DUPLICATE KEY UPDATE col = VALUES(col)` and takes no conflict target
|
|
332
|
+
* at all — it fires on whichever unique key was violated.
|
|
333
|
+
*
|
|
334
|
+
* Every identifier goes through `qId`, and no value is interpolated: the
|
|
335
|
+
* new row's values are already bound as the INSERT's parameters, and both
|
|
336
|
+
* dialects refer back to them by name rather than repeating them.
|
|
337
|
+
*/
|
|
338
|
+
private conflictClause(insertedKeys: string[]): string {
|
|
339
|
+
if (!this._conflict) return ''
|
|
340
|
+
const { cols, update } = this._conflict
|
|
341
|
+
|
|
342
|
+
// Default: everything inserted except the columns that identify the row.
|
|
343
|
+
const targets = (
|
|
344
|
+
update ?? insertedKeys.filter(k => !cols.includes(k))
|
|
345
|
+
).filter(k => insertedKeys.includes(k))
|
|
346
|
+
|
|
347
|
+
// The adapter spells it. This used to branch on `driver === 'mysql'`
|
|
348
|
+
// here, which put one dialect's syntax in the shared query builder — the
|
|
349
|
+
// thing every other difference (quote character, placeholder ceiling,
|
|
350
|
+
// date expression, foreign-key clause) is kept out of it for.
|
|
351
|
+
return getActiveDb().upsertClause(cols, targets)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async array<R = any>(): Promise<R[]> {
|
|
355
|
+
const perBatch = await this.runBatches(
|
|
356
|
+
this.parseAll(),
|
|
357
|
+
InsertExecutable.all,
|
|
358
|
+
)
|
|
359
|
+
// Batch order is insertion order, so the concatenation is the sequence a
|
|
360
|
+
// single statement would have returned.
|
|
361
|
+
return perBatch.flatMap(rows => (rows || []) as R[])
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async fetch<R = any>(): Promise<R | undefined> {
|
|
365
|
+
// Every batch still runs — `fetch()` means "insert, and hand me a row
|
|
366
|
+
// back", not "insert the first batch". `Executor.get` is defined as
|
|
367
|
+
// `all(…)[0]`, so this is the same row it would have produced.
|
|
368
|
+
const rows = await this.array<R>()
|
|
369
|
+
return (rows[0] as R) || undefined
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
first = this.fetch
|
|
373
|
+
|
|
374
|
+
async run(): Promise<RunResult> {
|
|
375
|
+
const results = await this.runBatches(
|
|
376
|
+
this.parseAll(),
|
|
377
|
+
(db, sql, params) => Promise.resolve(db.execute.run(sql, params)),
|
|
378
|
+
)
|
|
379
|
+
if (results.length === 1) return results[0]!
|
|
380
|
+
|
|
381
|
+
const changes = results.reduce((n, r) => n + Number(r?.changes ?? 0), 0)
|
|
382
|
+
// `changes` sums, because it answers "how many rows did this insert
|
|
383
|
+
// write". `lastInsertRowid` cannot sum, and the dialects do not agree
|
|
384
|
+
// what it means for a multi-row insert — so the adapter says which end of
|
|
385
|
+
// the batched run carries its answer, rather than this file knowing.
|
|
386
|
+
const pick =
|
|
387
|
+
getActiveDb().batchInsertIdPosition === 'first'
|
|
388
|
+
? results[0]!
|
|
389
|
+
: results[results.length - 1]!
|
|
390
|
+
return { lastInsertRowid: pick?.lastInsertRowid ?? null, changes }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
then<TR1 = RunResult, TR2 = never>(
|
|
394
|
+
onf?: ((v: RunResult) => TR1 | PromiseLike<TR1>) | null,
|
|
395
|
+
onr?: ((r: any) => TR2 | PromiseLike<TR2>) | null,
|
|
396
|
+
): Promise<TR1 | TR2> {
|
|
397
|
+
return this.run().then(onf, onr)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export class Update<T extends Tables = any> {
|
|
402
|
+
constructor(private _table: string) {}
|
|
403
|
+
static table<T extends Tables>(table: T): Update<T> {
|
|
404
|
+
return new Update(table as string)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
set(data: UpdateSchema<T>): UpdateWithWhere<T> {
|
|
408
|
+
return new UpdateWithWhere(this._table, data as MapOf<unknown>)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export class UpdateWithWhere<T extends Tables = any> {
|
|
413
|
+
constructor(
|
|
414
|
+
private _table: string,
|
|
415
|
+
private _data: MapOf<unknown>,
|
|
416
|
+
) {}
|
|
417
|
+
|
|
418
|
+
where<C extends QualifiedColumnTarget<T>>(
|
|
419
|
+
column: C,
|
|
420
|
+
valueOrRef?: DB.WhereValue<QualifiedColumnTarget<T>>,
|
|
421
|
+
): UpdateExecutable<T>
|
|
422
|
+
where(column: any, valueOrRef?: any): UpdateExecutable<T> {
|
|
423
|
+
const parsed = DB.parseWhereArgs(column, valueOrRef)
|
|
424
|
+
return new UpdateExecutable(this._table, this._data, parsed)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export class UpdateExecutable<T extends Tables = any> {
|
|
429
|
+
private _clauses: Array<{
|
|
430
|
+
connector: 'AND' | 'OR'
|
|
431
|
+
left: any
|
|
432
|
+
operator: string
|
|
433
|
+
right: any
|
|
434
|
+
isRightColumn?: boolean
|
|
435
|
+
}> = []
|
|
436
|
+
|
|
437
|
+
constructor(
|
|
438
|
+
private _table: string,
|
|
439
|
+
private _data: MapOf<unknown>,
|
|
440
|
+
initialWhere: {
|
|
441
|
+
left: any
|
|
442
|
+
operator: string
|
|
443
|
+
right: any
|
|
444
|
+
isRightColumn?: boolean
|
|
445
|
+
},
|
|
446
|
+
) {
|
|
447
|
+
this._clauses.push({ connector: 'AND', ...initialWhere })
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
and<C extends QualifiedColumnTarget<T>>(
|
|
451
|
+
column: C,
|
|
452
|
+
valueOrRef?: DB.WhereValue<QualifiedColumnTarget<T>>,
|
|
453
|
+
): this
|
|
454
|
+
and(column: any, valueOrRef?: any): this {
|
|
455
|
+
const parsed = DB.parseWhereArgs(column, valueOrRef)
|
|
456
|
+
this._clauses.push({ connector: 'AND', ...parsed })
|
|
457
|
+
return this
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
or<C extends QualifiedColumnTarget<T>>(
|
|
461
|
+
column: C,
|
|
462
|
+
valueOrRef?: DB.WhereValue<QualifiedColumnTarget<T>>,
|
|
463
|
+
): this
|
|
464
|
+
or(column: any, valueOrRef?: any): this {
|
|
465
|
+
const parsed = DB.parseWhereArgs(column, valueOrRef)
|
|
466
|
+
this._clauses.push({ connector: 'OR', ...parsed })
|
|
467
|
+
return this
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private evalWhere(params: any[]): string {
|
|
471
|
+
const parts: string[] = []
|
|
472
|
+
for (let i = 0; i < this._clauses.length; i++) {
|
|
473
|
+
const c = this._clauses[i]!
|
|
474
|
+
const left = evalOperands(c.left, params, true)
|
|
475
|
+
if (c.operator === '') {
|
|
476
|
+
parts.push(i === 0 ? left : `${c.connector} ${left}`)
|
|
477
|
+
} else {
|
|
478
|
+
const right = evalOperands(c.right, params, c.isRightColumn)
|
|
479
|
+
const clauseStr = `${left} ${c.operator} ${right}`
|
|
480
|
+
parts.push(i === 0 ? clauseStr : `${c.connector} ${clauseStr}`)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return parts.join(' ')
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private _returning?: string
|
|
487
|
+
|
|
488
|
+
returning(cols: string = '*'): this {
|
|
489
|
+
this._returning = safeReturning(cols)
|
|
490
|
+
return this
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
parse(): { sql: string; params: any[] } {
|
|
494
|
+
const params: any[] = []
|
|
495
|
+
const setClauses = Object.keys(this._data)
|
|
496
|
+
.map(key => {
|
|
497
|
+
params.push(this._data[key])
|
|
498
|
+
return `${qId(key)} = ?`
|
|
499
|
+
})
|
|
500
|
+
.join(', ')
|
|
501
|
+
|
|
502
|
+
const whereSql = this.evalWhere(params)
|
|
503
|
+
const retSql = this._returning ? ` RETURNING ${this._returning}` : ''
|
|
504
|
+
return {
|
|
505
|
+
sql: `UPDATE ${qId(this._table)} SET ${setClauses} WHERE ${whereSql}${retSql}`,
|
|
506
|
+
params,
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async array<R = any>(): Promise<R[]> {
|
|
511
|
+
const { sql, params } = this.parse()
|
|
512
|
+
const results = (await getActiveDb()
|
|
513
|
+
.query(sql)
|
|
514
|
+
.all(...params)) as R[]
|
|
515
|
+
return results || []
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async fetch<R = any>(): Promise<R | undefined> {
|
|
519
|
+
const { sql, params } = this.parse()
|
|
520
|
+
const result = await getActiveDb()
|
|
521
|
+
.query(sql)
|
|
522
|
+
.get(...params)
|
|
523
|
+
return (result as R) || undefined
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
first = this.fetch
|
|
527
|
+
|
|
528
|
+
async exists(): Promise<boolean> {
|
|
529
|
+
const params: any[] = []
|
|
530
|
+
const whereSql = this.evalWhere(params)
|
|
531
|
+
const result = await getActiveDb()
|
|
532
|
+
.query(`SELECT 1 FROM ${qId(this._table)} WHERE ${whereSql} LIMIT 1`)
|
|
533
|
+
.get(...params)
|
|
534
|
+
return !!result
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async run(): Promise<RunResult> {
|
|
538
|
+
const { sql, params } = this.parse()
|
|
539
|
+
return await getActiveDb()
|
|
540
|
+
.query(sql)
|
|
541
|
+
.run(...params)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
then<TR1 = RunResult, TR2 = never>(
|
|
545
|
+
onf?: ((v: RunResult) => TR1 | PromiseLike<TR1>) | null,
|
|
546
|
+
onr?: ((r: any) => TR2 | PromiseLike<TR2>) | null,
|
|
547
|
+
): Promise<TR1 | TR2> {
|
|
548
|
+
return this.run().then(onf, onr)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export class Delete<T extends Tables = any> {
|
|
553
|
+
constructor(private _table: string) {}
|
|
554
|
+
static from<T extends Tables>(table: T): Delete<T> {
|
|
555
|
+
return new Delete(table as string)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
where<C extends QualifiedColumnTarget<T>>(
|
|
559
|
+
column: C,
|
|
560
|
+
valueOrRef?: DB.WhereValue<QualifiedColumnTarget<T>>,
|
|
561
|
+
): DeleteExecutable<T>
|
|
562
|
+
where(column: any, valueOrRef?: any): DeleteExecutable<T> {
|
|
563
|
+
const parsed = DB.parseWhereArgs(column, valueOrRef)
|
|
564
|
+
return new DeleteExecutable(this._table, parsed)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export class DeleteExecutable<T extends Tables = any> {
|
|
569
|
+
private _returning?: string
|
|
570
|
+
|
|
571
|
+
private _clauses: Array<{
|
|
572
|
+
connector: 'AND' | 'OR'
|
|
573
|
+
left: any
|
|
574
|
+
operator: string
|
|
575
|
+
right: any
|
|
576
|
+
isRightColumn?: boolean
|
|
577
|
+
}> = []
|
|
578
|
+
|
|
579
|
+
constructor(
|
|
580
|
+
private _table: string,
|
|
581
|
+
initialWhere: {
|
|
582
|
+
left: any
|
|
583
|
+
operator: string
|
|
584
|
+
right: any
|
|
585
|
+
isRightColumn?: boolean
|
|
586
|
+
},
|
|
587
|
+
) {
|
|
588
|
+
this._clauses.push({ connector: 'AND', ...initialWhere })
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
returning(cols: string = '*'): this {
|
|
592
|
+
this._returning = safeReturning(cols)
|
|
593
|
+
return this
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
and<C extends QualifiedColumnTarget<T>>(
|
|
597
|
+
column: C,
|
|
598
|
+
valueOrRef?: DB.WhereValue<QualifiedColumnTarget<T>>,
|
|
599
|
+
): this
|
|
600
|
+
and(column: any, valueOrRef?: any): this {
|
|
601
|
+
const parsed = DB.parseWhereArgs(column, valueOrRef)
|
|
602
|
+
this._clauses.push({ connector: 'AND', ...parsed })
|
|
603
|
+
return this
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
or<C extends QualifiedColumnTarget<T>>(
|
|
607
|
+
column: C,
|
|
608
|
+
valueOrRef?: DB.WhereValue<QualifiedColumnTarget<T>>,
|
|
609
|
+
): this
|
|
610
|
+
or(column: any, valueOrRef?: any): this {
|
|
611
|
+
const parsed = DB.parseWhereArgs(column, valueOrRef)
|
|
612
|
+
this._clauses.push({ connector: 'OR', ...parsed })
|
|
613
|
+
return this
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private evalWhere(params: any[]): string {
|
|
617
|
+
const parts: string[] = []
|
|
618
|
+
for (let i = 0; i < this._clauses.length; i++) {
|
|
619
|
+
const c = this._clauses[i]!
|
|
620
|
+
const left = evalOperands(c.left, params, true)
|
|
621
|
+
// `parseWhereArgs` emits `operator: ''` for the one-argument form —
|
|
622
|
+
// `where(DB.raw`…`)` or `where(<subquery>)` — where the left operand
|
|
623
|
+
// is the whole condition and there is no right one. This branch is a
|
|
624
|
+
// copy of `UpdateExecutable.evalWhere` above, which is a copy of
|
|
625
|
+
// `formatClause` in query.ts; it was the copy that never got it. The
|
|
626
|
+
// failure was silent rather than loud: `evalOperands(undefined)` binds
|
|
627
|
+
// rather than throwing, so the clause came out as
|
|
628
|
+
// `(LOWER(email) = ?) ?` with a stray `undefined` pushed onto
|
|
629
|
+
// `params` *ahead* of every later clause's value, shifting them all.
|
|
630
|
+
if (c.operator === '') {
|
|
631
|
+
parts.push(i === 0 ? left : `${c.connector} ${left}`)
|
|
632
|
+
} else {
|
|
633
|
+
const right = evalOperands(c.right, params, c.isRightColumn)
|
|
634
|
+
const clauseStr = `${left} ${c.operator} ${right}`
|
|
635
|
+
parts.push(i === 0 ? clauseStr : `${c.connector} ${clauseStr}`)
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return parts.join(' ')
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
parse(): { sql: string; params: any[] } {
|
|
642
|
+
const params: any[] = []
|
|
643
|
+
const whereSql = this.evalWhere(params)
|
|
644
|
+
const retSql = this._returning ? ` RETURNING ${this._returning}` : ''
|
|
645
|
+
return {
|
|
646
|
+
sql: `DELETE FROM ${qId(this._table)} WHERE ${whereSql}${retSql}`,
|
|
647
|
+
params,
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async array<R = any>(): Promise<R[]> {
|
|
652
|
+
const { sql, params } = this.parse()
|
|
653
|
+
const results = (await getActiveDb()
|
|
654
|
+
.query(sql)
|
|
655
|
+
.all(...params)) as R[]
|
|
656
|
+
return results || []
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async fetch<R = any>(): Promise<R | undefined> {
|
|
660
|
+
const { sql, params } = this.parse()
|
|
661
|
+
const result = await getActiveDb()
|
|
662
|
+
.query(sql)
|
|
663
|
+
.get(...params)
|
|
664
|
+
return (result as R) || undefined
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
first = this.fetch
|
|
668
|
+
|
|
669
|
+
async exists(): Promise<boolean> {
|
|
670
|
+
const params: any[] = []
|
|
671
|
+
const whereSql = this.evalWhere(params)
|
|
672
|
+
const result = await getActiveDb()
|
|
673
|
+
.query(`SELECT 1 FROM ${qId(this._table)} WHERE ${whereSql} LIMIT 1`)
|
|
674
|
+
.get(...params)
|
|
675
|
+
return !!result
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async run(): Promise<RunResult> {
|
|
679
|
+
const { sql, params } = this.parse()
|
|
680
|
+
return await getActiveDb()
|
|
681
|
+
.query(sql)
|
|
682
|
+
.run(...params)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
then<TR1 = RunResult, TR2 = never>(
|
|
686
|
+
onf?: ((v: RunResult) => TR1 | PromiseLike<TR1>) | null,
|
|
687
|
+
onr?: ((r: any) => TR2 | PromiseLike<TR2>) | null,
|
|
688
|
+
): Promise<TR1 | TR2> {
|
|
689
|
+
return this.run().then(onf, onr)
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|