@remix-run/data-table 0.0.0 → 0.1.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 +21 -0
- package/README.md +298 -2
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/adapter.d.ts +180 -0
- package/dist/lib/adapter.d.ts.map +1 -0
- package/dist/lib/adapter.js +1 -0
- package/dist/lib/database.d.ts +361 -0
- package/dist/lib/database.d.ts.map +1 -0
- package/dist/lib/database.js +1368 -0
- package/dist/lib/errors.d.ts +50 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +67 -0
- package/dist/lib/inflection.d.ts +3 -0
- package/dist/lib/inflection.d.ts.map +1 -0
- package/dist/lib/inflection.js +56 -0
- package/dist/lib/operators.d.ts +151 -0
- package/dist/lib/operators.d.ts.map +1 -0
- package/dist/lib/operators.js +218 -0
- package/dist/lib/references.d.ts +42 -0
- package/dist/lib/references.d.ts.map +1 -0
- package/dist/lib/references.js +33 -0
- package/dist/lib/sql.d.ts +28 -0
- package/dist/lib/sql.d.ts.map +1 -0
- package/dist/lib/sql.js +51 -0
- package/dist/lib/table.d.ts +254 -0
- package/dist/lib/table.d.ts.map +1 -0
- package/dist/lib/table.js +496 -0
- package/dist/lib/types.d.ts +4 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +1 -0
- package/package.json +41 -7
- package/src/index.ts +115 -0
- package/src/lib/adapter.ts +209 -0
- package/src/lib/database.ts +2458 -0
- package/src/lib/errors.ts +109 -0
- package/src/lib/inflection.ts +69 -0
- package/src/lib/operators.ts +433 -0
- package/src/lib/references.ts +79 -0
- package/src/lib/sql.ts +67 -0
- package/src/lib/table.ts +981 -0
- package/src/lib/types.ts +3 -0
|
@@ -0,0 +1,2458 @@
|
|
|
1
|
+
import { parseSafe } from '@remix-run/data-schema'
|
|
2
|
+
import type { Schema } from '@remix-run/data-schema'
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
AdapterResult,
|
|
6
|
+
CountStatement,
|
|
7
|
+
DatabaseAdapter,
|
|
8
|
+
DeleteStatement,
|
|
9
|
+
ExistsStatement,
|
|
10
|
+
InsertManyStatement,
|
|
11
|
+
InsertStatement,
|
|
12
|
+
JoinClause,
|
|
13
|
+
JoinType,
|
|
14
|
+
ReturningSelection,
|
|
15
|
+
SelectColumn,
|
|
16
|
+
SelectStatement,
|
|
17
|
+
TransactionOptions,
|
|
18
|
+
TransactionToken,
|
|
19
|
+
UpdateStatement,
|
|
20
|
+
UpsertStatement,
|
|
21
|
+
} from './adapter.ts'
|
|
22
|
+
import { DataTableAdapterError, DataTableQueryError, DataTableValidationError } from './errors.ts'
|
|
23
|
+
import type {
|
|
24
|
+
AnyRelation,
|
|
25
|
+
AnyTable,
|
|
26
|
+
LoadedRelationMap,
|
|
27
|
+
OrderByClause,
|
|
28
|
+
OrderDirection,
|
|
29
|
+
PrimaryKeyInput,
|
|
30
|
+
Relation,
|
|
31
|
+
TableName,
|
|
32
|
+
TablePrimaryKey,
|
|
33
|
+
TableRow,
|
|
34
|
+
TableRowWith,
|
|
35
|
+
TimestampConfig,
|
|
36
|
+
tableMetadataKey,
|
|
37
|
+
} from './table.ts'
|
|
38
|
+
import {
|
|
39
|
+
getCompositeKey,
|
|
40
|
+
getPrimaryKeyObject,
|
|
41
|
+
getTableColumns,
|
|
42
|
+
getTableName,
|
|
43
|
+
getTablePrimaryKey,
|
|
44
|
+
getTableTimestamps,
|
|
45
|
+
validatePartialRow,
|
|
46
|
+
} from './table.ts'
|
|
47
|
+
import type { Predicate, WhereInput } from './operators.ts'
|
|
48
|
+
import { and, eq, inList, normalizeWhereInput, or } from './operators.ts'
|
|
49
|
+
import type { SqlStatement } from './sql.ts'
|
|
50
|
+
import { rawSql, isSqlStatement } from './sql.ts'
|
|
51
|
+
import type { AdapterStatement } from './adapter.ts'
|
|
52
|
+
import type { Pretty } from './types.ts'
|
|
53
|
+
import { normalizeColumnInput } from './references.ts'
|
|
54
|
+
import type { ColumnInput, NormalizeColumnInput, TableMetadataLike } from './references.ts'
|
|
55
|
+
|
|
56
|
+
type QueryState = {
|
|
57
|
+
select: '*' | SelectColumn[]
|
|
58
|
+
distinct: boolean
|
|
59
|
+
joins: JoinClause[]
|
|
60
|
+
where: Predicate<string>[]
|
|
61
|
+
groupBy: string[]
|
|
62
|
+
having: Predicate<string>[]
|
|
63
|
+
orderBy: OrderByClause[]
|
|
64
|
+
limit?: number
|
|
65
|
+
offset?: number
|
|
66
|
+
with: Record<string, AnyRelation>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type TableColumnName<table extends AnyTable> = keyof TableRow<table> & string
|
|
70
|
+
type QualifiedTableColumnName<table extends AnyTable> =
|
|
71
|
+
`${TableName<table>}.${TableColumnName<table>}`
|
|
72
|
+
type QueryColumnName<table extends AnyTable> =
|
|
73
|
+
| TableColumnName<table>
|
|
74
|
+
| QualifiedTableColumnName<table>
|
|
75
|
+
|
|
76
|
+
type RowColumnName<row extends Record<string, unknown>> = keyof row & string
|
|
77
|
+
type QualifiedRowColumnName<
|
|
78
|
+
tableName extends string,
|
|
79
|
+
row extends Record<string, unknown>,
|
|
80
|
+
> = `${tableName}.${RowColumnName<row>}`
|
|
81
|
+
|
|
82
|
+
type QueryColumnTypeMapFromRow<tableName extends string, row extends Record<string, unknown>> = {
|
|
83
|
+
[column in
|
|
84
|
+
| RowColumnName<row>
|
|
85
|
+
| QualifiedRowColumnName<tableName, row>]: column extends RowColumnName<row>
|
|
86
|
+
? row[column]
|
|
87
|
+
: column extends `${tableName}.${infer name extends RowColumnName<row>}`
|
|
88
|
+
? row[name]
|
|
89
|
+
: never
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type QueryColumnTypeMap<table extends AnyTable> = Pretty<
|
|
93
|
+
QueryColumnTypeMapFromRow<TableName<table>, TableRow<table>>
|
|
94
|
+
>
|
|
95
|
+
|
|
96
|
+
type MergeColumnTypeMaps<
|
|
97
|
+
left extends Record<string, unknown>,
|
|
98
|
+
right extends Record<string, unknown>,
|
|
99
|
+
> = Pretty<{
|
|
100
|
+
[column in Extract<keyof left | keyof right, string>]: column extends keyof right
|
|
101
|
+
? column extends keyof left
|
|
102
|
+
? left[column] | right[column]
|
|
103
|
+
: right[column]
|
|
104
|
+
: column extends keyof left
|
|
105
|
+
? left[column]
|
|
106
|
+
: never
|
|
107
|
+
}>
|
|
108
|
+
|
|
109
|
+
type QueryColumns<columnTypes extends Record<string, unknown>> = Extract<keyof columnTypes, string>
|
|
110
|
+
|
|
111
|
+
type QueryColumnInput<columnTypes extends Record<string, unknown>> = ColumnInput<
|
|
112
|
+
QueryColumns<columnTypes>
|
|
113
|
+
>
|
|
114
|
+
|
|
115
|
+
type SelectedAliasRow<
|
|
116
|
+
columnTypes extends Record<string, unknown>,
|
|
117
|
+
selection extends Record<string, QueryColumnInput<columnTypes>>,
|
|
118
|
+
> = Pretty<{
|
|
119
|
+
[alias in keyof selection]: NormalizeColumnInput<selection[alias]> extends keyof columnTypes
|
|
120
|
+
? columnTypes[NormalizeColumnInput<selection[alias]>]
|
|
121
|
+
: never
|
|
122
|
+
}>
|
|
123
|
+
|
|
124
|
+
type SavepointCounter = {
|
|
125
|
+
value: number
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const executeStatement = Symbol('executeStatement')
|
|
129
|
+
|
|
130
|
+
type RelationMapForSourceName<tableName extends string> = Record<
|
|
131
|
+
string,
|
|
132
|
+
AnyRelation & {
|
|
133
|
+
sourceTable: {
|
|
134
|
+
[tableMetadataKey]: {
|
|
135
|
+
name: tableName
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
>
|
|
140
|
+
|
|
141
|
+
type PrimaryKeyInputForRow<
|
|
142
|
+
row extends Record<string, unknown>,
|
|
143
|
+
primaryKey extends readonly string[],
|
|
144
|
+
> = primaryKey extends readonly [infer column extends keyof row & string]
|
|
145
|
+
? row[column]
|
|
146
|
+
: {
|
|
147
|
+
[column in primaryKey[number] & keyof row]: row[column]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
type ReturningInput<row extends Record<string, unknown>> = '*' | (keyof row & string)[]
|
|
151
|
+
|
|
152
|
+
export type QueryTableInput<
|
|
153
|
+
tableName extends string,
|
|
154
|
+
row extends Record<string, unknown>,
|
|
155
|
+
primaryKey extends readonly (keyof row & string)[],
|
|
156
|
+
> = TableMetadataLike<
|
|
157
|
+
tableName,
|
|
158
|
+
{
|
|
159
|
+
[column in keyof row & string]: Schema<any, row[column]>
|
|
160
|
+
},
|
|
161
|
+
primaryKey,
|
|
162
|
+
TimestampConfig | null
|
|
163
|
+
> & {
|
|
164
|
+
'~standard': Schema<unknown, Partial<row>>['~standard']
|
|
165
|
+
} & Record<string, unknown>
|
|
166
|
+
|
|
167
|
+
export type QueryBuilderFor<
|
|
168
|
+
tableName extends string,
|
|
169
|
+
row extends Record<string, unknown>,
|
|
170
|
+
primaryKey extends readonly (keyof row & string)[],
|
|
171
|
+
loaded extends Record<string, unknown> = {},
|
|
172
|
+
> = QueryBuilder<
|
|
173
|
+
Pretty<QueryColumnTypeMapFromRow<tableName, row>>,
|
|
174
|
+
row,
|
|
175
|
+
loaded,
|
|
176
|
+
tableName,
|
|
177
|
+
primaryKey
|
|
178
|
+
>
|
|
179
|
+
|
|
180
|
+
export type QueryMethod = <
|
|
181
|
+
tableName extends string,
|
|
182
|
+
row extends Record<string, unknown>,
|
|
183
|
+
primaryKey extends readonly (keyof row & string)[],
|
|
184
|
+
>(
|
|
185
|
+
table: QueryTableInput<tableName, row, primaryKey>,
|
|
186
|
+
) => QueryBuilderFor<tableName, row, primaryKey>
|
|
187
|
+
|
|
188
|
+
export type WriteResult = {
|
|
189
|
+
affectedRows: number
|
|
190
|
+
insertId?: unknown
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export type WriteRowsResult<row> = {
|
|
194
|
+
affectedRows: number
|
|
195
|
+
insertId?: unknown
|
|
196
|
+
rows: row[]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export type WriteRowResult<row> = {
|
|
200
|
+
affectedRows: number
|
|
201
|
+
insertId?: unknown
|
|
202
|
+
row: row | null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export type QueryColumnTypesForTable<table extends AnyTable> = QueryColumnTypeMap<table>
|
|
206
|
+
|
|
207
|
+
export type QueryForTable<
|
|
208
|
+
table extends AnyTable,
|
|
209
|
+
loaded extends Record<string, unknown> = {},
|
|
210
|
+
> = QueryBuilder<
|
|
211
|
+
QueryColumnTypesForTable<table>,
|
|
212
|
+
TableRow<table>,
|
|
213
|
+
loaded,
|
|
214
|
+
TableName<table>,
|
|
215
|
+
TablePrimaryKey<table>
|
|
216
|
+
>
|
|
217
|
+
|
|
218
|
+
export type SingleTableColumn<table extends AnyTable> = QueryColumns<QueryColumnTypeMap<table>>
|
|
219
|
+
|
|
220
|
+
export type SingleTableWhere<table extends AnyTable> = WhereInput<SingleTableColumn<table>>
|
|
221
|
+
|
|
222
|
+
export type OrderByTuple<table extends AnyTable> = [
|
|
223
|
+
column: SingleTableColumn<table>,
|
|
224
|
+
direction?: OrderDirection,
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
export type OrderByInput<table extends AnyTable> = OrderByTuple<table> | OrderByTuple<table>[]
|
|
228
|
+
|
|
229
|
+
export type FindManyOptions<
|
|
230
|
+
table extends AnyTable,
|
|
231
|
+
relations extends RelationMapForSourceName<TableName<table>> = {},
|
|
232
|
+
> = {
|
|
233
|
+
where?: SingleTableWhere<table>
|
|
234
|
+
orderBy?: OrderByInput<table>
|
|
235
|
+
limit?: number
|
|
236
|
+
offset?: number
|
|
237
|
+
with?: relations
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export type FindOneOptions<
|
|
241
|
+
table extends AnyTable,
|
|
242
|
+
relations extends RelationMapForSourceName<TableName<table>> = {},
|
|
243
|
+
> = Omit<FindManyOptions<table, relations>, 'limit' | 'offset'> & {
|
|
244
|
+
where: SingleTableWhere<table>
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export type UpdateOptions<
|
|
248
|
+
table extends AnyTable,
|
|
249
|
+
relations extends RelationMapForSourceName<TableName<table>> = {},
|
|
250
|
+
> = {
|
|
251
|
+
touch?: boolean
|
|
252
|
+
with?: relations
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export type UpdateManyOptions<table extends AnyTable> = {
|
|
256
|
+
where: SingleTableWhere<table>
|
|
257
|
+
orderBy?: OrderByInput<table>
|
|
258
|
+
limit?: number
|
|
259
|
+
offset?: number
|
|
260
|
+
touch?: boolean
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export type DeleteManyOptions<table extends AnyTable> = {
|
|
264
|
+
where: SingleTableWhere<table>
|
|
265
|
+
orderBy?: OrderByInput<table>
|
|
266
|
+
limit?: number
|
|
267
|
+
offset?: number
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export type CountOptions<table extends AnyTable> = {
|
|
271
|
+
where?: SingleTableWhere<table>
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export type CreateResultOptions = {
|
|
275
|
+
touch?: boolean
|
|
276
|
+
returnRow?: false
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export type CreateRowOptions<
|
|
280
|
+
table extends AnyTable,
|
|
281
|
+
relations extends RelationMapForSourceName<TableName<table>> = {},
|
|
282
|
+
> = {
|
|
283
|
+
touch?: boolean
|
|
284
|
+
with?: relations
|
|
285
|
+
returnRow: true
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export type CreateManyResultOptions = {
|
|
289
|
+
touch?: boolean
|
|
290
|
+
returnRows?: false
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export type CreateManyRowsOptions = {
|
|
294
|
+
touch?: boolean
|
|
295
|
+
returnRows: true
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export type Database = {
|
|
299
|
+
adapter: DatabaseAdapter
|
|
300
|
+
now(): unknown
|
|
301
|
+
query: QueryMethod
|
|
302
|
+
create<table extends AnyTable>(
|
|
303
|
+
table: table,
|
|
304
|
+
values: Partial<TableRow<table>>,
|
|
305
|
+
options?: CreateResultOptions,
|
|
306
|
+
): Promise<WriteResult>
|
|
307
|
+
create<table extends AnyTable, relations extends RelationMapForSourceName<TableName<table>> = {}>(
|
|
308
|
+
table: table,
|
|
309
|
+
values: Partial<TableRow<table>>,
|
|
310
|
+
options: CreateRowOptions<table, relations>,
|
|
311
|
+
): Promise<TableRowWith<table, LoadedRelationMap<relations>>>
|
|
312
|
+
createMany<table extends AnyTable>(
|
|
313
|
+
table: table,
|
|
314
|
+
values: Array<Partial<TableRow<table>>>,
|
|
315
|
+
options?: CreateManyResultOptions,
|
|
316
|
+
): Promise<WriteResult>
|
|
317
|
+
createMany<table extends AnyTable>(
|
|
318
|
+
table: table,
|
|
319
|
+
values: Array<Partial<TableRow<table>>>,
|
|
320
|
+
options: CreateManyRowsOptions,
|
|
321
|
+
): Promise<TableRow<table>[]>
|
|
322
|
+
find<table extends AnyTable, relations extends RelationMapForSourceName<TableName<table>> = {}>(
|
|
323
|
+
table: table,
|
|
324
|
+
value: PrimaryKeyInput<table>,
|
|
325
|
+
options?: { with?: relations },
|
|
326
|
+
): Promise<TableRowWith<table, LoadedRelationMap<relations>> | null>
|
|
327
|
+
findOne<
|
|
328
|
+
table extends AnyTable,
|
|
329
|
+
relations extends RelationMapForSourceName<TableName<table>> = {},
|
|
330
|
+
>(
|
|
331
|
+
table: table,
|
|
332
|
+
options: FindOneOptions<table, relations>,
|
|
333
|
+
): Promise<TableRowWith<table, LoadedRelationMap<relations>> | null>
|
|
334
|
+
findMany<
|
|
335
|
+
table extends AnyTable,
|
|
336
|
+
relations extends RelationMapForSourceName<TableName<table>> = {},
|
|
337
|
+
>(
|
|
338
|
+
table: table,
|
|
339
|
+
options?: FindManyOptions<table, relations>,
|
|
340
|
+
): Promise<Array<TableRowWith<table, LoadedRelationMap<relations>>>>
|
|
341
|
+
count<table extends AnyTable>(table: table, options?: CountOptions<table>): Promise<number>
|
|
342
|
+
update<table extends AnyTable, relations extends RelationMapForSourceName<TableName<table>> = {}>(
|
|
343
|
+
table: table,
|
|
344
|
+
value: PrimaryKeyInput<table>,
|
|
345
|
+
changes: Partial<TableRow<table>>,
|
|
346
|
+
options?: UpdateOptions<table, relations>,
|
|
347
|
+
): Promise<TableRowWith<table, LoadedRelationMap<relations>>>
|
|
348
|
+
updateMany<table extends AnyTable>(
|
|
349
|
+
table: table,
|
|
350
|
+
changes: Partial<TableRow<table>>,
|
|
351
|
+
options: UpdateManyOptions<table>,
|
|
352
|
+
): Promise<WriteResult>
|
|
353
|
+
delete<table extends AnyTable>(table: table, value: PrimaryKeyInput<table>): Promise<boolean>
|
|
354
|
+
deleteMany<table extends AnyTable>(
|
|
355
|
+
table: table,
|
|
356
|
+
options: DeleteManyOptions<table>,
|
|
357
|
+
): Promise<WriteResult>
|
|
358
|
+
exec(statement: string | SqlStatement, values?: unknown[]): Promise<AdapterResult>
|
|
359
|
+
transaction<result>(
|
|
360
|
+
callback: (database: Database) => Promise<result>,
|
|
361
|
+
options?: TransactionOptions,
|
|
362
|
+
): Promise<result>
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
class DatabaseRuntime implements Database {
|
|
366
|
+
#adapter: DatabaseAdapter
|
|
367
|
+
#token?: TransactionToken
|
|
368
|
+
#now: () => unknown
|
|
369
|
+
#savepointCounter: SavepointCounter
|
|
370
|
+
|
|
371
|
+
constructor(options: {
|
|
372
|
+
adapter: DatabaseAdapter
|
|
373
|
+
token?: TransactionToken
|
|
374
|
+
now: () => unknown
|
|
375
|
+
savepointCounter: SavepointCounter
|
|
376
|
+
}) {
|
|
377
|
+
this.#adapter = options.adapter
|
|
378
|
+
this.#token = options.token
|
|
379
|
+
this.#now = options.now
|
|
380
|
+
this.#savepointCounter = options.savepointCounter
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
get adapter(): DatabaseAdapter {
|
|
384
|
+
return this.#adapter
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
now(): unknown {
|
|
388
|
+
return this.#now()
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
query: QueryMethod = <
|
|
392
|
+
tableName extends string,
|
|
393
|
+
row extends Record<string, unknown>,
|
|
394
|
+
primaryKey extends readonly (keyof row & string)[],
|
|
395
|
+
>(
|
|
396
|
+
table: QueryTableInput<tableName, row, primaryKey>,
|
|
397
|
+
): QueryBuilderFor<tableName, row, primaryKey> =>
|
|
398
|
+
new QueryBuilder(this, table, createInitialQueryState())
|
|
399
|
+
|
|
400
|
+
create<table extends AnyTable>(
|
|
401
|
+
table: table,
|
|
402
|
+
values: Partial<TableRow<table>>,
|
|
403
|
+
options?: CreateResultOptions,
|
|
404
|
+
): Promise<WriteResult>
|
|
405
|
+
create<table extends AnyTable, relations extends RelationMapForSourceName<TableName<table>> = {}>(
|
|
406
|
+
table: table,
|
|
407
|
+
values: Partial<TableRow<table>>,
|
|
408
|
+
options: CreateRowOptions<table, relations>,
|
|
409
|
+
): Promise<TableRowWith<table, LoadedRelationMap<relations>>>
|
|
410
|
+
async create<
|
|
411
|
+
table extends AnyTable,
|
|
412
|
+
relations extends RelationMapForSourceName<TableName<table>> = {},
|
|
413
|
+
>(
|
|
414
|
+
table: table,
|
|
415
|
+
values: Partial<TableRow<table>>,
|
|
416
|
+
options?: CreateResultOptions | CreateRowOptions<table, relations>,
|
|
417
|
+
): Promise<WriteResult | TableRowWith<table, LoadedRelationMap<relations>>> {
|
|
418
|
+
let touch = options?.touch
|
|
419
|
+
let query: QueryForTable<table> = this.query(asQueryTableInput(table))
|
|
420
|
+
|
|
421
|
+
if (options?.returnRow !== true) {
|
|
422
|
+
let result = await query.insert(values, { touch })
|
|
423
|
+
return toWriteResult(result)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (this.#adapter.capabilities.returning) {
|
|
427
|
+
let result = (await query.insert(values, {
|
|
428
|
+
returning: '*',
|
|
429
|
+
touch,
|
|
430
|
+
})) as WriteRowResult<TableRow<table>>
|
|
431
|
+
let row = result.row
|
|
432
|
+
|
|
433
|
+
if (!row) {
|
|
434
|
+
throw new DataTableQueryError(
|
|
435
|
+
'create({ returnRow: true }) failed to return an inserted row',
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!options.with) {
|
|
440
|
+
return row as TableRowWith<table, LoadedRelationMap<relations>>
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let where = getPrimaryKeyWhereFromRow(table, row)
|
|
444
|
+
let loaded = await this.findOne(table, {
|
|
445
|
+
where,
|
|
446
|
+
with: options.with,
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
if (!loaded) {
|
|
450
|
+
throw new DataTableQueryError('create({ returnRow: true }) failed to load inserted row')
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return loaded
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let insertResult = await query.insert(values, { touch })
|
|
457
|
+
let where = resolveCreateRowWhere(table, values, toWriteResult(insertResult).insertId)
|
|
458
|
+
let loaded = await this.findOne(table, {
|
|
459
|
+
where,
|
|
460
|
+
with: options.with,
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
if (!loaded) {
|
|
464
|
+
throw new DataTableQueryError('create({ returnRow: true }) failed to load inserted row')
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return loaded
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
createMany<table extends AnyTable>(
|
|
471
|
+
table: table,
|
|
472
|
+
values: Array<Partial<TableRow<table>>>,
|
|
473
|
+
options?: CreateManyResultOptions,
|
|
474
|
+
): Promise<WriteResult>
|
|
475
|
+
createMany<table extends AnyTable>(
|
|
476
|
+
table: table,
|
|
477
|
+
values: Array<Partial<TableRow<table>>>,
|
|
478
|
+
options: CreateManyRowsOptions,
|
|
479
|
+
): Promise<TableRow<table>[]>
|
|
480
|
+
async createMany<table extends AnyTable>(
|
|
481
|
+
table: table,
|
|
482
|
+
values: Array<Partial<TableRow<table>>>,
|
|
483
|
+
options?: CreateManyResultOptions | CreateManyRowsOptions,
|
|
484
|
+
): Promise<WriteResult | TableRow<table>[]> {
|
|
485
|
+
let query: QueryForTable<table> = this.query(asQueryTableInput(table))
|
|
486
|
+
|
|
487
|
+
if (options?.returnRows === true) {
|
|
488
|
+
if (!this.#adapter.capabilities.returning) {
|
|
489
|
+
throw new DataTableQueryError(
|
|
490
|
+
'createMany({ returnRows: true }) is not supported by this adapter',
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
let result = (await query.insertMany(values, {
|
|
495
|
+
returning: '*',
|
|
496
|
+
touch: options.touch,
|
|
497
|
+
})) as WriteRowsResult<TableRow<table>>
|
|
498
|
+
|
|
499
|
+
return result.rows
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
let result = await query.insertMany(values, {
|
|
503
|
+
touch: options?.touch,
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
return toWriteResult(result)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async find<
|
|
510
|
+
table extends AnyTable,
|
|
511
|
+
relations extends RelationMapForSourceName<TableName<table>> = {},
|
|
512
|
+
>(
|
|
513
|
+
table: table,
|
|
514
|
+
value: PrimaryKeyInput<table>,
|
|
515
|
+
options?: { with?: relations },
|
|
516
|
+
): Promise<TableRowWith<table, LoadedRelationMap<relations>> | null> {
|
|
517
|
+
if (value == null) {
|
|
518
|
+
return null
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
let query: QueryForTable<table> = this.query(asQueryTableInput(table))
|
|
522
|
+
|
|
523
|
+
if (options?.with) {
|
|
524
|
+
return query
|
|
525
|
+
.with(options.with)
|
|
526
|
+
.find(
|
|
527
|
+
value as PrimaryKeyInputForRow<TableRow<table>, TablePrimaryKey<table>>,
|
|
528
|
+
) as Promise<TableRowWith<table, LoadedRelationMap<relations>> | null>
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return query.find(
|
|
532
|
+
value as PrimaryKeyInputForRow<TableRow<table>, TablePrimaryKey<table>>,
|
|
533
|
+
) as Promise<TableRowWith<table, LoadedRelationMap<relations>> | null>
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async findOne<
|
|
537
|
+
table extends AnyTable,
|
|
538
|
+
relations extends RelationMapForSourceName<TableName<table>> = {},
|
|
539
|
+
>(
|
|
540
|
+
table: table,
|
|
541
|
+
options: FindOneOptions<table, relations>,
|
|
542
|
+
): Promise<TableRowWith<table, LoadedRelationMap<relations>> | null> {
|
|
543
|
+
let query: QueryForTable<table> = this.query(asQueryTableInput(table)).where(options.where)
|
|
544
|
+
let orderBy = normalizeOrderByInput(options.orderBy)
|
|
545
|
+
|
|
546
|
+
for (let [column, direction] of orderBy) {
|
|
547
|
+
query = query.orderBy(column, direction)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (options.with) {
|
|
551
|
+
return query.with(options.with).first() as Promise<TableRowWith<
|
|
552
|
+
table,
|
|
553
|
+
LoadedRelationMap<relations>
|
|
554
|
+
> | null>
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return query.first() as Promise<TableRowWith<table, LoadedRelationMap<relations>> | null>
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async findMany<
|
|
561
|
+
table extends AnyTable,
|
|
562
|
+
relations extends RelationMapForSourceName<TableName<table>> = {},
|
|
563
|
+
>(
|
|
564
|
+
table: table,
|
|
565
|
+
options?: FindManyOptions<table, relations>,
|
|
566
|
+
): Promise<Array<TableRowWith<table, LoadedRelationMap<relations>>>> {
|
|
567
|
+
let query: QueryForTable<table> = this.query(asQueryTableInput(table))
|
|
568
|
+
|
|
569
|
+
if (options?.where) {
|
|
570
|
+
query = query.where(options.where)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
let orderBy = normalizeOrderByInput(options?.orderBy)
|
|
574
|
+
for (let [column, direction] of orderBy) {
|
|
575
|
+
query = query.orderBy(column, direction)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (options?.limit !== undefined) {
|
|
579
|
+
query = query.limit(options.limit)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (options?.offset !== undefined) {
|
|
583
|
+
query = query.offset(options.offset)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (options?.with) {
|
|
587
|
+
return query.with(options.with).all() as Promise<
|
|
588
|
+
Array<TableRowWith<table, LoadedRelationMap<relations>>>
|
|
589
|
+
>
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return query.all() as Promise<Array<TableRowWith<table, LoadedRelationMap<relations>>>>
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async count<table extends AnyTable>(
|
|
596
|
+
table: table,
|
|
597
|
+
options?: CountOptions<table>,
|
|
598
|
+
): Promise<number> {
|
|
599
|
+
let query: QueryForTable<table> = this.query(asQueryTableInput(table))
|
|
600
|
+
|
|
601
|
+
if (options?.where) {
|
|
602
|
+
query = query.where(options.where)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return query.count()
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async update<
|
|
609
|
+
table extends AnyTable,
|
|
610
|
+
relations extends RelationMapForSourceName<TableName<table>> = {},
|
|
611
|
+
>(
|
|
612
|
+
table: table,
|
|
613
|
+
value: PrimaryKeyInput<table>,
|
|
614
|
+
changes: Partial<TableRow<table>>,
|
|
615
|
+
options?: UpdateOptions<table, relations>,
|
|
616
|
+
): Promise<TableRowWith<table, LoadedRelationMap<relations>>> {
|
|
617
|
+
let where = getPrimaryKeyWhere(table, value)
|
|
618
|
+
|
|
619
|
+
if (this.#adapter.capabilities.returning) {
|
|
620
|
+
let updateResult = (await this.query(asQueryTableInput(table)).where(where).update(changes, {
|
|
621
|
+
touch: options?.touch,
|
|
622
|
+
returning: '*',
|
|
623
|
+
})) as WriteRowsResult<TableRow<table>>
|
|
624
|
+
let updatedRow = updateResult.rows[0]
|
|
625
|
+
|
|
626
|
+
if (!updatedRow) {
|
|
627
|
+
throw new DataTableQueryError(
|
|
628
|
+
'update() failed to find row for table "' + getTableName(table) + '"',
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (!options?.with) {
|
|
633
|
+
return updatedRow as TableRowWith<table, LoadedRelationMap<relations>>
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
let loaded = await this.findOne(table, {
|
|
637
|
+
where: getPrimaryKeyWhereFromRow(table, updatedRow),
|
|
638
|
+
with: options.with,
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
if (!loaded) {
|
|
642
|
+
throw new DataTableQueryError(
|
|
643
|
+
'update() failed to find row for table "' + getTableName(table) + '"',
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return loaded
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
await this.query(asQueryTableInput(table)).where(where).update(changes, {
|
|
651
|
+
touch: options?.touch,
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
let loaded = await this.find(table, value, { with: options?.with })
|
|
655
|
+
|
|
656
|
+
if (!loaded) {
|
|
657
|
+
throw new DataTableQueryError(
|
|
658
|
+
'update() failed to find row for table "' + getTableName(table) + '"',
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return loaded
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async updateMany<table extends AnyTable>(
|
|
666
|
+
table: table,
|
|
667
|
+
changes: Partial<TableRow<table>>,
|
|
668
|
+
options: UpdateManyOptions<table>,
|
|
669
|
+
): Promise<WriteResult> {
|
|
670
|
+
let query: QueryForTable<table> = this.query(asQueryTableInput(table)).where(options.where)
|
|
671
|
+
let orderBy = normalizeOrderByInput(options.orderBy)
|
|
672
|
+
|
|
673
|
+
for (let [column, direction] of orderBy) {
|
|
674
|
+
query = query.orderBy(column, direction)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (options.limit !== undefined) {
|
|
678
|
+
query = query.limit(options.limit)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (options.offset !== undefined) {
|
|
682
|
+
query = query.offset(options.offset)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
let result = await query.update(changes, { touch: options.touch })
|
|
686
|
+
return toWriteResult(result)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async delete<table extends AnyTable>(
|
|
690
|
+
table: table,
|
|
691
|
+
value: PrimaryKeyInput<table>,
|
|
692
|
+
): Promise<boolean> {
|
|
693
|
+
let where = getPrimaryKeyWhere(table, value)
|
|
694
|
+
let result = await this.query(asQueryTableInput(table)).where(where).delete()
|
|
695
|
+
return toWriteResult(result).affectedRows > 0
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async deleteMany<table extends AnyTable>(
|
|
699
|
+
table: table,
|
|
700
|
+
options: DeleteManyOptions<table>,
|
|
701
|
+
): Promise<WriteResult> {
|
|
702
|
+
let query: QueryForTable<table> = this.query(asQueryTableInput(table)).where(options.where)
|
|
703
|
+
let orderBy = normalizeOrderByInput(options.orderBy)
|
|
704
|
+
|
|
705
|
+
for (let [column, direction] of orderBy) {
|
|
706
|
+
query = query.orderBy(column, direction)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (options.limit !== undefined) {
|
|
710
|
+
query = query.limit(options.limit)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (options.offset !== undefined) {
|
|
714
|
+
query = query.offset(options.offset)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
let result = await query.delete()
|
|
718
|
+
return toWriteResult(result)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async exec(statement: string | SqlStatement, values: unknown[] = []): Promise<AdapterResult> {
|
|
722
|
+
let sqlStatement = isSqlStatement(statement) ? statement : rawSql(statement, values)
|
|
723
|
+
|
|
724
|
+
return this[executeStatement]({
|
|
725
|
+
kind: 'raw',
|
|
726
|
+
sql: sqlStatement,
|
|
727
|
+
})
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async transaction<result>(
|
|
731
|
+
callback: (database: Database) => Promise<result>,
|
|
732
|
+
options?: TransactionOptions,
|
|
733
|
+
): Promise<result> {
|
|
734
|
+
if (!this.#token) {
|
|
735
|
+
let token = await this.#adapter.beginTransaction(options)
|
|
736
|
+
let tx = new DatabaseRuntime({
|
|
737
|
+
adapter: this.#adapter,
|
|
738
|
+
token,
|
|
739
|
+
now: this.#now,
|
|
740
|
+
savepointCounter: this.#savepointCounter,
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
try {
|
|
744
|
+
let result = await callback(tx)
|
|
745
|
+
await this.#adapter.commitTransaction(token)
|
|
746
|
+
return result
|
|
747
|
+
} catch (error) {
|
|
748
|
+
await this.#adapter.rollbackTransaction(token)
|
|
749
|
+
throw error
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (!this.#adapter.capabilities.savepoints) {
|
|
754
|
+
throw new DataTableQueryError('Nested transactions require adapter savepoint support')
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
let savepointName = 'sp_' + String(this.#savepointCounter.value)
|
|
758
|
+
this.#savepointCounter.value += 1
|
|
759
|
+
|
|
760
|
+
await this.#adapter.createSavepoint(this.#token, savepointName)
|
|
761
|
+
|
|
762
|
+
try {
|
|
763
|
+
let result = await callback(this)
|
|
764
|
+
await this.#adapter.releaseSavepoint(this.#token, savepointName)
|
|
765
|
+
return result
|
|
766
|
+
} catch (error) {
|
|
767
|
+
await this.#adapter.rollbackToSavepoint(this.#token, savepointName)
|
|
768
|
+
await this.#adapter.releaseSavepoint(this.#token, savepointName)
|
|
769
|
+
throw error
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async [executeStatement](statement: AdapterStatement): Promise<AdapterResult> {
|
|
774
|
+
try {
|
|
775
|
+
return await this.#adapter.execute({
|
|
776
|
+
statement,
|
|
777
|
+
transaction: this.#token,
|
|
778
|
+
})
|
|
779
|
+
} catch (error) {
|
|
780
|
+
throw new DataTableAdapterError('Adapter execution failed', {
|
|
781
|
+
cause: error,
|
|
782
|
+
metadata: {
|
|
783
|
+
dialect: this.#adapter.dialect,
|
|
784
|
+
statementKind: statement.kind,
|
|
785
|
+
},
|
|
786
|
+
})
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Creates a database runtime from an adapter.
|
|
793
|
+
* @param adapter Adapter implementation responsible for SQL execution.
|
|
794
|
+
* @param options Optional runtime options.
|
|
795
|
+
* @param options.now Clock function used for auto-managed timestamps.
|
|
796
|
+
* @returns A `Database` API instance.
|
|
797
|
+
*/
|
|
798
|
+
export function createDatabase(
|
|
799
|
+
adapter: DatabaseAdapter,
|
|
800
|
+
options?: { now?: () => unknown },
|
|
801
|
+
): Database {
|
|
802
|
+
let now = options?.now ?? defaultNow
|
|
803
|
+
|
|
804
|
+
return new DatabaseRuntime({
|
|
805
|
+
adapter,
|
|
806
|
+
token: undefined,
|
|
807
|
+
now,
|
|
808
|
+
savepointCounter: { value: 0 },
|
|
809
|
+
})
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Immutable query builder used by `db.query(table)`.
|
|
814
|
+
*/
|
|
815
|
+
export class QueryBuilder<
|
|
816
|
+
columnTypes extends Record<string, unknown>,
|
|
817
|
+
row extends Record<string, unknown>,
|
|
818
|
+
loaded extends Record<string, unknown> = {},
|
|
819
|
+
tableName extends string = string,
|
|
820
|
+
primaryKey extends readonly string[] = readonly string[],
|
|
821
|
+
> {
|
|
822
|
+
#database: DatabaseRuntime
|
|
823
|
+
#table: AnyTable
|
|
824
|
+
#state: QueryState
|
|
825
|
+
|
|
826
|
+
constructor(database: DatabaseRuntime, table: AnyTable, state: QueryState) {
|
|
827
|
+
this.#database = database
|
|
828
|
+
this.#table = table
|
|
829
|
+
this.#state = state
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Narrows selected columns, optionally with aliases.
|
|
834
|
+
*/
|
|
835
|
+
select<selection extends (keyof row & string)[]>(
|
|
836
|
+
...columns: selection
|
|
837
|
+
): QueryBuilder<columnTypes, Pick<row, selection[number]>, loaded, tableName, primaryKey>
|
|
838
|
+
select<selection extends Record<string, QueryColumnInput<columnTypes>>>(
|
|
839
|
+
selection: selection,
|
|
840
|
+
): QueryBuilder<
|
|
841
|
+
columnTypes,
|
|
842
|
+
SelectedAliasRow<columnTypes, selection>,
|
|
843
|
+
loaded,
|
|
844
|
+
tableName,
|
|
845
|
+
primaryKey
|
|
846
|
+
>
|
|
847
|
+
select(
|
|
848
|
+
...input: [Record<string, QueryColumnInput<columnTypes>>] | (keyof row & string)[]
|
|
849
|
+
): QueryBuilder<columnTypes, any, loaded, tableName, primaryKey> {
|
|
850
|
+
if (
|
|
851
|
+
input.length === 1 &&
|
|
852
|
+
typeof input[0] === 'object' &&
|
|
853
|
+
input[0] !== null &&
|
|
854
|
+
!Array.isArray(input[0])
|
|
855
|
+
) {
|
|
856
|
+
let selection = input[0] as Record<string, QueryColumnInput<columnTypes>>
|
|
857
|
+
let aliases = Object.keys(selection)
|
|
858
|
+
let select = aliases.map((alias) => ({
|
|
859
|
+
column: normalizeColumnInput(selection[alias]),
|
|
860
|
+
alias,
|
|
861
|
+
}))
|
|
862
|
+
|
|
863
|
+
return this.#clone({ select }) as QueryBuilder<
|
|
864
|
+
columnTypes,
|
|
865
|
+
any,
|
|
866
|
+
loaded,
|
|
867
|
+
tableName,
|
|
868
|
+
primaryKey
|
|
869
|
+
>
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
let columns = input as (keyof row & string)[]
|
|
873
|
+
|
|
874
|
+
return this.#clone({
|
|
875
|
+
select: columns.map((column) => ({ column, alias: column })),
|
|
876
|
+
}) as QueryBuilder<columnTypes, any, loaded, tableName, primaryKey>
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Toggles `distinct` selection.
|
|
881
|
+
* @param value When `true`, eliminates duplicate rows.
|
|
882
|
+
* @returns A cloned query builder with updated distinct state.
|
|
883
|
+
*/
|
|
884
|
+
distinct(value = true): QueryBuilder<columnTypes, row, loaded, tableName, primaryKey> {
|
|
885
|
+
return this.#clone({ distinct: value })
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Adds a where predicate.
|
|
890
|
+
* @param input Predicate expression or column-value shorthand.
|
|
891
|
+
* @returns A cloned query builder with the appended where predicate.
|
|
892
|
+
*/
|
|
893
|
+
where(
|
|
894
|
+
input: WhereInput<QueryColumns<columnTypes>>,
|
|
895
|
+
): QueryBuilder<columnTypes, row, loaded, tableName, primaryKey> {
|
|
896
|
+
let predicate = normalizeWhereInput(input)
|
|
897
|
+
let normalizedPredicate = normalizePredicateValues(
|
|
898
|
+
predicate,
|
|
899
|
+
createPredicateColumnResolver([this.#table, ...this.#state.joins.map((join) => join.table)]),
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
return this.#clone({
|
|
903
|
+
where: [...this.#state.where, normalizedPredicate],
|
|
904
|
+
})
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Adds a having predicate.
|
|
909
|
+
* @param input Predicate expression or aggregate filter shorthand.
|
|
910
|
+
* @returns A cloned query builder with the appended having predicate.
|
|
911
|
+
*/
|
|
912
|
+
having(
|
|
913
|
+
input: WhereInput<QueryColumns<columnTypes>>,
|
|
914
|
+
): QueryBuilder<columnTypes, row, loaded, tableName, primaryKey> {
|
|
915
|
+
let predicate = normalizeWhereInput(input)
|
|
916
|
+
let normalizedPredicate = normalizePredicateValues(
|
|
917
|
+
predicate,
|
|
918
|
+
createPredicateColumnResolver([this.#table, ...this.#state.joins.map((join) => join.table)]),
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
return this.#clone({
|
|
922
|
+
having: [...this.#state.having, normalizedPredicate],
|
|
923
|
+
})
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Adds a join clause.
|
|
928
|
+
* @param target Target table to join.
|
|
929
|
+
* @param on Join predicate.
|
|
930
|
+
* @param type Join type.
|
|
931
|
+
* @returns A query builder whose column map includes joined table columns.
|
|
932
|
+
*/
|
|
933
|
+
join<target extends AnyTable>(
|
|
934
|
+
target: target,
|
|
935
|
+
on: Predicate<QueryColumns<columnTypes> | QueryColumnName<target>>,
|
|
936
|
+
type: JoinType = 'inner',
|
|
937
|
+
): QueryBuilder<
|
|
938
|
+
MergeColumnTypeMaps<columnTypes, QueryColumnTypeMap<target>>,
|
|
939
|
+
row,
|
|
940
|
+
loaded,
|
|
941
|
+
tableName,
|
|
942
|
+
primaryKey
|
|
943
|
+
> {
|
|
944
|
+
let normalizedOn = normalizePredicateValues(
|
|
945
|
+
on,
|
|
946
|
+
createPredicateColumnResolver([
|
|
947
|
+
this.#table,
|
|
948
|
+
...this.#state.joins.map((join) => join.table),
|
|
949
|
+
target,
|
|
950
|
+
]),
|
|
951
|
+
) as Predicate<QueryColumns<columnTypes> | QueryColumnName<target>>
|
|
952
|
+
|
|
953
|
+
return new QueryBuilder(this.#database, this.#table, {
|
|
954
|
+
select: cloneSelection(this.#state.select),
|
|
955
|
+
distinct: this.#state.distinct,
|
|
956
|
+
joins: [...this.#state.joins, { type, table: target, on: normalizedOn }],
|
|
957
|
+
where: [...this.#state.where],
|
|
958
|
+
groupBy: [...this.#state.groupBy],
|
|
959
|
+
having: [...this.#state.having],
|
|
960
|
+
orderBy: [...this.#state.orderBy],
|
|
961
|
+
limit: this.#state.limit,
|
|
962
|
+
offset: this.#state.offset,
|
|
963
|
+
with: { ...this.#state.with },
|
|
964
|
+
}) as QueryBuilder<
|
|
965
|
+
MergeColumnTypeMaps<columnTypes, QueryColumnTypeMap<target>>,
|
|
966
|
+
row,
|
|
967
|
+
loaded,
|
|
968
|
+
tableName,
|
|
969
|
+
primaryKey
|
|
970
|
+
>
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Adds a left join clause.
|
|
975
|
+
* @param target Target table to join.
|
|
976
|
+
* @param on Join predicate.
|
|
977
|
+
* @returns A query builder whose column map includes joined table columns.
|
|
978
|
+
*/
|
|
979
|
+
leftJoin<target extends AnyTable>(
|
|
980
|
+
target: target,
|
|
981
|
+
on: Predicate<QueryColumns<columnTypes> | QueryColumnName<target>>,
|
|
982
|
+
): QueryBuilder<
|
|
983
|
+
MergeColumnTypeMaps<columnTypes, QueryColumnTypeMap<target>>,
|
|
984
|
+
row,
|
|
985
|
+
loaded,
|
|
986
|
+
tableName,
|
|
987
|
+
primaryKey
|
|
988
|
+
> {
|
|
989
|
+
return this.join(target, on, 'left')
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Adds a right join clause.
|
|
994
|
+
* @param target Target table to join.
|
|
995
|
+
* @param on Join predicate.
|
|
996
|
+
* @returns A query builder whose column map includes joined table columns.
|
|
997
|
+
*/
|
|
998
|
+
rightJoin<target extends AnyTable>(
|
|
999
|
+
target: target,
|
|
1000
|
+
on: Predicate<QueryColumns<columnTypes> | QueryColumnName<target>>,
|
|
1001
|
+
): QueryBuilder<
|
|
1002
|
+
MergeColumnTypeMaps<columnTypes, QueryColumnTypeMap<target>>,
|
|
1003
|
+
row,
|
|
1004
|
+
loaded,
|
|
1005
|
+
tableName,
|
|
1006
|
+
primaryKey
|
|
1007
|
+
> {
|
|
1008
|
+
return this.join(target, on, 'right')
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Appends an order-by clause.
|
|
1013
|
+
* @param column Column to sort by.
|
|
1014
|
+
* @param direction Sort direction.
|
|
1015
|
+
* @returns A cloned query builder with the appended order-by clause.
|
|
1016
|
+
*/
|
|
1017
|
+
orderBy(
|
|
1018
|
+
column: QueryColumnInput<columnTypes>,
|
|
1019
|
+
direction: OrderDirection = 'asc',
|
|
1020
|
+
): QueryBuilder<columnTypes, row, loaded, tableName, primaryKey> {
|
|
1021
|
+
return this.#clone({
|
|
1022
|
+
orderBy: [...this.#state.orderBy, { column: normalizeColumnInput(column), direction }],
|
|
1023
|
+
})
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Appends group-by columns.
|
|
1028
|
+
* @param columns Columns to include in the grouping set.
|
|
1029
|
+
* @returns A cloned query builder with appended group-by columns.
|
|
1030
|
+
*/
|
|
1031
|
+
groupBy(
|
|
1032
|
+
...columns: QueryColumnInput<columnTypes>[]
|
|
1033
|
+
): QueryBuilder<columnTypes, row, loaded, tableName, primaryKey> {
|
|
1034
|
+
return this.#clone({
|
|
1035
|
+
groupBy: [...this.#state.groupBy, ...columns.map((column) => normalizeColumnInput(column))],
|
|
1036
|
+
})
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Limits returned rows.
|
|
1041
|
+
* @param value Maximum number of rows to return.
|
|
1042
|
+
* @returns A cloned query builder with a row limit.
|
|
1043
|
+
*/
|
|
1044
|
+
limit(value: number): QueryBuilder<columnTypes, row, loaded, tableName, primaryKey> {
|
|
1045
|
+
return this.#clone({ limit: value })
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Skips returned rows.
|
|
1050
|
+
* @param value Number of rows to skip.
|
|
1051
|
+
* @returns A cloned query builder with a row offset.
|
|
1052
|
+
*/
|
|
1053
|
+
offset(value: number): QueryBuilder<columnTypes, row, loaded, tableName, primaryKey> {
|
|
1054
|
+
return this.#clone({ offset: value })
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Configures eager-loaded relations.
|
|
1059
|
+
* @param relations Relation map describing nested eager-load behavior.
|
|
1060
|
+
* @returns A cloned query builder with relation loading configuration.
|
|
1061
|
+
*/
|
|
1062
|
+
with<relations extends RelationMapForSourceName<tableName>>(
|
|
1063
|
+
relations: relations,
|
|
1064
|
+
): QueryBuilder<columnTypes, row, loaded & LoadedRelationMap<relations>, tableName, primaryKey> {
|
|
1065
|
+
return this.#clone({
|
|
1066
|
+
with: {
|
|
1067
|
+
...this.#state.with,
|
|
1068
|
+
...relations,
|
|
1069
|
+
},
|
|
1070
|
+
}) as QueryBuilder<
|
|
1071
|
+
columnTypes,
|
|
1072
|
+
row,
|
|
1073
|
+
loaded & LoadedRelationMap<relations>,
|
|
1074
|
+
tableName,
|
|
1075
|
+
primaryKey
|
|
1076
|
+
>
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* Executes the query and returns all rows.
|
|
1081
|
+
* @returns All matching rows with requested eager-loaded relations.
|
|
1082
|
+
*/
|
|
1083
|
+
async all(): Promise<Array<row & loaded>> {
|
|
1084
|
+
let statement = this.#toSelectStatement()
|
|
1085
|
+
let result = await this.#database[executeStatement](statement)
|
|
1086
|
+
let rows = normalizeRows(result.rows)
|
|
1087
|
+
|
|
1088
|
+
if (Object.keys(this.#state.with).length === 0) {
|
|
1089
|
+
return rows as Array<row & loaded>
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
let rowsWithRelations = await loadRelationsForRows(
|
|
1093
|
+
this.#database,
|
|
1094
|
+
this.#table,
|
|
1095
|
+
rows,
|
|
1096
|
+
this.#state.with,
|
|
1097
|
+
)
|
|
1098
|
+
return rowsWithRelations as Array<row & loaded>
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Executes the query and returns the first row.
|
|
1103
|
+
* @returns The first matching row, or `null` when no rows match.
|
|
1104
|
+
*/
|
|
1105
|
+
async first(): Promise<(row & loaded) | null> {
|
|
1106
|
+
let rows = await this.limit(1).all()
|
|
1107
|
+
return rows[0] ?? null
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Loads a single row by primary key.
|
|
1112
|
+
* @param value Primary-key value or composite-key object.
|
|
1113
|
+
* @returns The matching row, or `null` when no row exists.
|
|
1114
|
+
*/
|
|
1115
|
+
async find(value: PrimaryKeyInputForRow<row, primaryKey>): Promise<(row & loaded) | null> {
|
|
1116
|
+
let where = getPrimaryKeyObject(this.#table, value as any)
|
|
1117
|
+
return this.where(where as WhereInput<QueryColumns<columnTypes>>).first()
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Executes a count query.
|
|
1122
|
+
* @returns Number of rows that match the current query scope.
|
|
1123
|
+
*/
|
|
1124
|
+
async count(): Promise<number> {
|
|
1125
|
+
let statement: CountStatement<AnyTable> = {
|
|
1126
|
+
kind: 'count',
|
|
1127
|
+
table: this.#table,
|
|
1128
|
+
joins: [...this.#state.joins],
|
|
1129
|
+
where: [...this.#state.where],
|
|
1130
|
+
groupBy: [...this.#state.groupBy],
|
|
1131
|
+
having: [...this.#state.having],
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
let result = await this.#database[executeStatement](statement)
|
|
1135
|
+
|
|
1136
|
+
if (result.rows && result.rows[0] && typeof result.rows[0].count === 'number') {
|
|
1137
|
+
return result.rows[0].count as number
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
if (result.rows) {
|
|
1141
|
+
return result.rows.length
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
return 0
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Executes an existence query.
|
|
1149
|
+
* @returns `true` when at least one row matches the current query scope.
|
|
1150
|
+
*/
|
|
1151
|
+
async exists(): Promise<boolean> {
|
|
1152
|
+
let statement: ExistsStatement<AnyTable> = {
|
|
1153
|
+
kind: 'exists',
|
|
1154
|
+
table: this.#table,
|
|
1155
|
+
joins: [...this.#state.joins],
|
|
1156
|
+
where: [...this.#state.where],
|
|
1157
|
+
groupBy: [...this.#state.groupBy],
|
|
1158
|
+
having: [...this.#state.having],
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
let result = await this.#database[executeStatement](statement)
|
|
1162
|
+
|
|
1163
|
+
if (result.rows && result.rows[0] && typeof result.rows[0].exists === 'boolean') {
|
|
1164
|
+
return result.rows[0].exists as boolean
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (result.rows && result.rows[0] && typeof result.rows[0].count === 'number') {
|
|
1168
|
+
return Number(result.rows[0].count) > 0
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
return Boolean(result.rows && result.rows.length > 0)
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Inserts one row.
|
|
1176
|
+
* @param values Values to insert.
|
|
1177
|
+
* @param options Insert options.
|
|
1178
|
+
* @param options.returning Optional return selection for adapters that support returning.
|
|
1179
|
+
* @param options.touch When `true`, manages timestamp columns automatically.
|
|
1180
|
+
* @returns Insert metadata, and optionally the returned row.
|
|
1181
|
+
*/
|
|
1182
|
+
async insert(
|
|
1183
|
+
values: Partial<row>,
|
|
1184
|
+
options?: { returning?: ReturningInput<row>; touch?: boolean },
|
|
1185
|
+
): Promise<WriteResult | WriteRowResult<row>> {
|
|
1186
|
+
assertWriteState(this.#state, 'insert', {
|
|
1187
|
+
where: false,
|
|
1188
|
+
orderBy: false,
|
|
1189
|
+
limit: false,
|
|
1190
|
+
offset: false,
|
|
1191
|
+
})
|
|
1192
|
+
|
|
1193
|
+
let preparedValues = prepareInsertValues(
|
|
1194
|
+
this.#table,
|
|
1195
|
+
values,
|
|
1196
|
+
this.#database.now(),
|
|
1197
|
+
options?.touch ?? true,
|
|
1198
|
+
)
|
|
1199
|
+
let returning = options?.returning
|
|
1200
|
+
|
|
1201
|
+
assertReturningCapability(this.#database.adapter, 'insert', returning)
|
|
1202
|
+
|
|
1203
|
+
if (returning) {
|
|
1204
|
+
let statement: InsertStatement<AnyTable> = {
|
|
1205
|
+
kind: 'insert',
|
|
1206
|
+
table: this.#table,
|
|
1207
|
+
values: preparedValues,
|
|
1208
|
+
returning: normalizeReturningSelection(returning),
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
let result = await this.#database[executeStatement](statement)
|
|
1212
|
+
let row = (normalizeRows(result.rows)[0] ?? null) as row | null
|
|
1213
|
+
|
|
1214
|
+
return {
|
|
1215
|
+
affectedRows: result.affectedRows ?? 0,
|
|
1216
|
+
insertId: result.insertId,
|
|
1217
|
+
row,
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
let statement: InsertStatement<AnyTable> = {
|
|
1222
|
+
kind: 'insert',
|
|
1223
|
+
table: this.#table,
|
|
1224
|
+
values: preparedValues,
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
let result = await this.#database[executeStatement](statement)
|
|
1228
|
+
let metadata: WriteResult = {
|
|
1229
|
+
affectedRows: result.affectedRows ?? 0,
|
|
1230
|
+
insertId: result.insertId,
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
return metadata
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Inserts many rows.
|
|
1238
|
+
* @param values Values to insert.
|
|
1239
|
+
* @param options Insert options.
|
|
1240
|
+
* @param options.returning Optional return selection for adapters that support returning.
|
|
1241
|
+
* @param options.touch When `true`, manages timestamp columns automatically.
|
|
1242
|
+
* @returns Insert metadata, and optionally the returned rows.
|
|
1243
|
+
*/
|
|
1244
|
+
async insertMany(
|
|
1245
|
+
values: Partial<row>[],
|
|
1246
|
+
options?: { returning?: ReturningInput<row>; touch?: boolean },
|
|
1247
|
+
): Promise<WriteResult | WriteRowsResult<row>> {
|
|
1248
|
+
assertWriteState(this.#state, 'insertMany', {
|
|
1249
|
+
where: false,
|
|
1250
|
+
orderBy: false,
|
|
1251
|
+
limit: false,
|
|
1252
|
+
offset: false,
|
|
1253
|
+
})
|
|
1254
|
+
|
|
1255
|
+
let preparedValues = values.map((value) =>
|
|
1256
|
+
prepareInsertValues(this.#table, value, this.#database.now(), options?.touch ?? true),
|
|
1257
|
+
)
|
|
1258
|
+
|
|
1259
|
+
if (
|
|
1260
|
+
preparedValues.length > 0 &&
|
|
1261
|
+
preparedValues.every((preparedValue) => Object.keys(preparedValue).length === 0)
|
|
1262
|
+
) {
|
|
1263
|
+
throw new DataTableQueryError(
|
|
1264
|
+
'insertMany() requires at least one explicit value across the batch',
|
|
1265
|
+
)
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
let returning = options?.returning
|
|
1269
|
+
|
|
1270
|
+
assertReturningCapability(this.#database.adapter, 'insertMany', returning)
|
|
1271
|
+
|
|
1272
|
+
if (returning) {
|
|
1273
|
+
let statement: InsertManyStatement<AnyTable> = {
|
|
1274
|
+
kind: 'insertMany',
|
|
1275
|
+
table: this.#table,
|
|
1276
|
+
values: preparedValues,
|
|
1277
|
+
returning: normalizeReturningSelection(returning),
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
let result = await this.#database[executeStatement](statement)
|
|
1281
|
+
|
|
1282
|
+
return {
|
|
1283
|
+
affectedRows: result.affectedRows ?? 0,
|
|
1284
|
+
insertId: result.insertId,
|
|
1285
|
+
rows: normalizeRows(result.rows) as row[],
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
let statement: InsertManyStatement<AnyTable> = {
|
|
1290
|
+
kind: 'insertMany',
|
|
1291
|
+
table: this.#table,
|
|
1292
|
+
values: preparedValues,
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
let result = await this.#database[executeStatement](statement)
|
|
1296
|
+
let metadata: WriteResult = {
|
|
1297
|
+
affectedRows: result.affectedRows ?? 0,
|
|
1298
|
+
insertId: result.insertId,
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
return metadata
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Updates scoped rows.
|
|
1306
|
+
* @param changes Column changes to apply.
|
|
1307
|
+
* @param options Update options.
|
|
1308
|
+
* @param options.returning Optional return selection for adapters that support returning.
|
|
1309
|
+
* @param options.touch When `true`, updates timestamp columns automatically.
|
|
1310
|
+
* @returns Update metadata, and optionally the returned rows.
|
|
1311
|
+
*/
|
|
1312
|
+
async update(
|
|
1313
|
+
changes: Partial<row>,
|
|
1314
|
+
options?: { returning?: ReturningInput<row>; touch?: boolean },
|
|
1315
|
+
): Promise<WriteResult | WriteRowsResult<row>> {
|
|
1316
|
+
assertWriteState(this.#state, 'update', {
|
|
1317
|
+
where: true,
|
|
1318
|
+
orderBy: true,
|
|
1319
|
+
limit: true,
|
|
1320
|
+
offset: true,
|
|
1321
|
+
})
|
|
1322
|
+
|
|
1323
|
+
let preparedChanges = prepareUpdateValues(
|
|
1324
|
+
this.#table,
|
|
1325
|
+
changes,
|
|
1326
|
+
this.#database.now(),
|
|
1327
|
+
options?.touch ?? true,
|
|
1328
|
+
)
|
|
1329
|
+
let returning = options?.returning
|
|
1330
|
+
assertReturningCapability(this.#database.adapter, 'update', returning)
|
|
1331
|
+
|
|
1332
|
+
if (Object.keys(preparedChanges).length === 0) {
|
|
1333
|
+
throw new DataTableQueryError('update() requires at least one change')
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
if (hasScopedWriteModifiers(this.#state)) {
|
|
1337
|
+
let table = this.#table
|
|
1338
|
+
let queryState = this.#state
|
|
1339
|
+
|
|
1340
|
+
return this.#database.transaction(async (tx: Database) => {
|
|
1341
|
+
let primaryKeys = await loadPrimaryKeyRowsForScope(tx, table, queryState)
|
|
1342
|
+
let primaryKeyPredicate = buildPrimaryKeyPredicate(table, primaryKeys)
|
|
1343
|
+
|
|
1344
|
+
if (!primaryKeyPredicate) {
|
|
1345
|
+
if (!returning) {
|
|
1346
|
+
return {
|
|
1347
|
+
affectedRows: 0,
|
|
1348
|
+
insertId: undefined,
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
return {
|
|
1353
|
+
affectedRows: 0,
|
|
1354
|
+
insertId: undefined,
|
|
1355
|
+
rows: [],
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
return tx.query(table).where(primaryKeyPredicate).update(changes, options)
|
|
1360
|
+
})
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
let statement: UpdateStatement<AnyTable> = {
|
|
1364
|
+
kind: 'update',
|
|
1365
|
+
table: this.#table,
|
|
1366
|
+
changes: preparedChanges,
|
|
1367
|
+
where: [...this.#state.where],
|
|
1368
|
+
returning: returning ? normalizeReturningSelection(returning) : undefined,
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
let result = await this.#database[executeStatement](statement)
|
|
1372
|
+
|
|
1373
|
+
if (!returning) {
|
|
1374
|
+
return {
|
|
1375
|
+
affectedRows: result.affectedRows ?? 0,
|
|
1376
|
+
insertId: result.insertId,
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
return {
|
|
1381
|
+
affectedRows: result.affectedRows ?? 0,
|
|
1382
|
+
insertId: result.insertId,
|
|
1383
|
+
rows: normalizeRows(result.rows) as row[],
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Deletes scoped rows.
|
|
1389
|
+
* @param options Delete options.
|
|
1390
|
+
* @param options.returning Optional return selection for adapters that support returning.
|
|
1391
|
+
* @returns Delete metadata, and optionally the returned rows.
|
|
1392
|
+
*/
|
|
1393
|
+
async delete(options?: {
|
|
1394
|
+
returning?: ReturningInput<row>
|
|
1395
|
+
}): Promise<WriteResult | WriteRowsResult<row>> {
|
|
1396
|
+
assertWriteState(this.#state, 'delete', {
|
|
1397
|
+
where: true,
|
|
1398
|
+
orderBy: true,
|
|
1399
|
+
limit: true,
|
|
1400
|
+
offset: true,
|
|
1401
|
+
})
|
|
1402
|
+
|
|
1403
|
+
let returning = options?.returning
|
|
1404
|
+
assertReturningCapability(this.#database.adapter, 'delete', returning)
|
|
1405
|
+
|
|
1406
|
+
if (hasScopedWriteModifiers(this.#state)) {
|
|
1407
|
+
let table = this.#table
|
|
1408
|
+
let queryState = this.#state
|
|
1409
|
+
|
|
1410
|
+
return this.#database.transaction(async (tx: Database) => {
|
|
1411
|
+
let primaryKeys = await loadPrimaryKeyRowsForScope(tx, table, queryState)
|
|
1412
|
+
let primaryKeyPredicate = buildPrimaryKeyPredicate(table, primaryKeys)
|
|
1413
|
+
|
|
1414
|
+
if (!primaryKeyPredicate) {
|
|
1415
|
+
if (!returning) {
|
|
1416
|
+
return {
|
|
1417
|
+
affectedRows: 0,
|
|
1418
|
+
insertId: undefined,
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
return {
|
|
1423
|
+
affectedRows: 0,
|
|
1424
|
+
insertId: undefined,
|
|
1425
|
+
rows: [],
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
return tx.query(table).where(primaryKeyPredicate).delete(options)
|
|
1430
|
+
})
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
let statement: DeleteStatement<AnyTable> = {
|
|
1434
|
+
kind: 'delete',
|
|
1435
|
+
table: this.#table,
|
|
1436
|
+
where: [...this.#state.where],
|
|
1437
|
+
returning: returning ? normalizeReturningSelection(returning) : undefined,
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
let result = await this.#database[executeStatement](statement)
|
|
1441
|
+
|
|
1442
|
+
if (!returning) {
|
|
1443
|
+
return {
|
|
1444
|
+
affectedRows: result.affectedRows ?? 0,
|
|
1445
|
+
insertId: result.insertId,
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
return {
|
|
1450
|
+
affectedRows: result.affectedRows ?? 0,
|
|
1451
|
+
insertId: result.insertId,
|
|
1452
|
+
rows: normalizeRows(result.rows) as row[],
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* Performs an upsert operation.
|
|
1458
|
+
* @param values Values to insert.
|
|
1459
|
+
* @param options Upsert options.
|
|
1460
|
+
* @param options.returning Optional return selection for adapters that support returning.
|
|
1461
|
+
* @param options.touch When `true`, manages timestamp columns automatically.
|
|
1462
|
+
* @param options.conflictTarget Conflict target columns for adapters that require them.
|
|
1463
|
+
* @param options.update Optional update payload used when a conflict occurs.
|
|
1464
|
+
* @returns Upsert metadata, and optionally the returned row.
|
|
1465
|
+
*/
|
|
1466
|
+
async upsert(
|
|
1467
|
+
values: Partial<row>,
|
|
1468
|
+
options?: {
|
|
1469
|
+
returning?: ReturningInput<row>
|
|
1470
|
+
touch?: boolean
|
|
1471
|
+
conflictTarget?: (keyof row & string)[]
|
|
1472
|
+
update?: Partial<row>
|
|
1473
|
+
},
|
|
1474
|
+
): Promise<WriteResult | WriteRowResult<row>> {
|
|
1475
|
+
assertWriteState(this.#state, 'upsert', {
|
|
1476
|
+
where: false,
|
|
1477
|
+
orderBy: false,
|
|
1478
|
+
limit: false,
|
|
1479
|
+
offset: false,
|
|
1480
|
+
})
|
|
1481
|
+
|
|
1482
|
+
if (!this.#database.adapter.capabilities.upsert) {
|
|
1483
|
+
throw new DataTableQueryError('Adapter does not support upsert')
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
let preparedValues = prepareInsertValues(
|
|
1487
|
+
this.#table,
|
|
1488
|
+
values,
|
|
1489
|
+
this.#database.now(),
|
|
1490
|
+
options?.touch ?? true,
|
|
1491
|
+
)
|
|
1492
|
+
let updateChanges = options?.update
|
|
1493
|
+
? prepareUpdateValues(
|
|
1494
|
+
this.#table,
|
|
1495
|
+
options.update,
|
|
1496
|
+
this.#database.now(),
|
|
1497
|
+
options?.touch ?? true,
|
|
1498
|
+
)
|
|
1499
|
+
: undefined
|
|
1500
|
+
let returning = options?.returning
|
|
1501
|
+
assertReturningCapability(this.#database.adapter, 'upsert', returning)
|
|
1502
|
+
|
|
1503
|
+
if (returning) {
|
|
1504
|
+
let statement: UpsertStatement<AnyTable> = {
|
|
1505
|
+
kind: 'upsert',
|
|
1506
|
+
table: this.#table,
|
|
1507
|
+
values: preparedValues,
|
|
1508
|
+
conflictTarget: options?.conflictTarget ? [...options.conflictTarget] : undefined,
|
|
1509
|
+
update: updateChanges,
|
|
1510
|
+
returning: normalizeReturningSelection(returning),
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
let result = await this.#database[executeStatement](statement)
|
|
1514
|
+
let row = (normalizeRows(result.rows)[0] ?? null) as row | null
|
|
1515
|
+
|
|
1516
|
+
return {
|
|
1517
|
+
affectedRows: result.affectedRows ?? 0,
|
|
1518
|
+
insertId: result.insertId,
|
|
1519
|
+
row,
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
let statement: UpsertStatement<AnyTable> = {
|
|
1524
|
+
kind: 'upsert',
|
|
1525
|
+
table: this.#table,
|
|
1526
|
+
values: preparedValues,
|
|
1527
|
+
conflictTarget: options?.conflictTarget ? [...options.conflictTarget] : undefined,
|
|
1528
|
+
update: updateChanges,
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
let result = await this.#database[executeStatement](statement)
|
|
1532
|
+
let metadata: WriteResult = {
|
|
1533
|
+
affectedRows: result.affectedRows ?? 0,
|
|
1534
|
+
insertId: result.insertId,
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
return metadata
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
#toSelectStatement(): SelectStatement<AnyTable> {
|
|
1541
|
+
return {
|
|
1542
|
+
kind: 'select',
|
|
1543
|
+
table: this.#table,
|
|
1544
|
+
select: cloneSelection(this.#state.select),
|
|
1545
|
+
distinct: this.#state.distinct,
|
|
1546
|
+
joins: [...this.#state.joins],
|
|
1547
|
+
where: [...this.#state.where],
|
|
1548
|
+
groupBy: [...this.#state.groupBy],
|
|
1549
|
+
having: [...this.#state.having],
|
|
1550
|
+
orderBy: [...this.#state.orderBy],
|
|
1551
|
+
limit: this.#state.limit,
|
|
1552
|
+
offset: this.#state.offset,
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
#clone(
|
|
1557
|
+
patch: Partial<QueryState>,
|
|
1558
|
+
): QueryBuilder<columnTypes, row, loaded, tableName, primaryKey> {
|
|
1559
|
+
return new QueryBuilder(this.#database, this.#table, {
|
|
1560
|
+
select: patch.select ?? cloneSelection(this.#state.select),
|
|
1561
|
+
distinct: patch.distinct ?? this.#state.distinct,
|
|
1562
|
+
joins: patch.joins ? [...patch.joins] : [...this.#state.joins],
|
|
1563
|
+
where: patch.where ? [...patch.where] : [...this.#state.where],
|
|
1564
|
+
groupBy: patch.groupBy ? [...patch.groupBy] : [...this.#state.groupBy],
|
|
1565
|
+
having: patch.having ? [...patch.having] : [...this.#state.having],
|
|
1566
|
+
orderBy: patch.orderBy ? [...patch.orderBy] : [...this.#state.orderBy],
|
|
1567
|
+
limit: patch.limit === undefined ? this.#state.limit : patch.limit,
|
|
1568
|
+
offset: patch.offset === undefined ? this.#state.offset : patch.offset,
|
|
1569
|
+
with: patch.with ? { ...patch.with } : { ...this.#state.with },
|
|
1570
|
+
})
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
async function loadRelationsForRows(
|
|
1575
|
+
database: DatabaseRuntime,
|
|
1576
|
+
sourceTable: AnyTable,
|
|
1577
|
+
rows: Record<string, unknown>[],
|
|
1578
|
+
relationMap: Record<string, AnyRelation>,
|
|
1579
|
+
): Promise<Record<string, unknown>[]> {
|
|
1580
|
+
let output = rows.map((row) => ({ ...row }))
|
|
1581
|
+
|
|
1582
|
+
let relationNames = Object.keys(relationMap)
|
|
1583
|
+
|
|
1584
|
+
for (let relationName of relationNames) {
|
|
1585
|
+
let relation = relationMap[relationName]
|
|
1586
|
+
|
|
1587
|
+
if (relation.sourceTable !== sourceTable) {
|
|
1588
|
+
throw new DataTableQueryError(
|
|
1589
|
+
'Relation "' +
|
|
1590
|
+
relationName +
|
|
1591
|
+
'" is not defined for source table "' +
|
|
1592
|
+
getTableName(sourceTable) +
|
|
1593
|
+
'"',
|
|
1594
|
+
)
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
let values = await resolveRelationValues(database, output, relation)
|
|
1598
|
+
let index = 0
|
|
1599
|
+
|
|
1600
|
+
while (index < output.length) {
|
|
1601
|
+
output[index][relationName] = values[index]
|
|
1602
|
+
index += 1
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
return output
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
async function resolveRelationValues(
|
|
1610
|
+
database: DatabaseRuntime,
|
|
1611
|
+
sourceRows: Record<string, unknown>[],
|
|
1612
|
+
relation: AnyRelation,
|
|
1613
|
+
): Promise<unknown[]> {
|
|
1614
|
+
if (relation.relationKind === 'hasManyThrough') {
|
|
1615
|
+
return loadHasManyThroughValues(database, sourceRows, relation)
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
return loadDirectRelationValues(database, sourceRows, relation)
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
async function loadDirectRelationValues(
|
|
1622
|
+
database: DatabaseRuntime,
|
|
1623
|
+
sourceRows: Record<string, unknown>[],
|
|
1624
|
+
relation: AnyRelation,
|
|
1625
|
+
): Promise<unknown[]> {
|
|
1626
|
+
if (sourceRows.length === 0) {
|
|
1627
|
+
return []
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
let sourceTuples = uniqueTuples(sourceRows, relation.sourceKey)
|
|
1631
|
+
|
|
1632
|
+
if (sourceTuples.length === 0) {
|
|
1633
|
+
return sourceRows.map(() => (relation.cardinality === 'many' ? [] : null))
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
let query = database.query(relation.targetTable)
|
|
1637
|
+
let linkPredicate = buildLinkPredicate(relation.targetKey, sourceTuples)
|
|
1638
|
+
|
|
1639
|
+
if (linkPredicate) {
|
|
1640
|
+
query = query.where(linkPredicate as Predicate<QueryColumnName<typeof relation.targetTable>>)
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
query = applyRelationModifiers(query, relation, {
|
|
1644
|
+
includePagination: false,
|
|
1645
|
+
})
|
|
1646
|
+
|
|
1647
|
+
let relatedRows = (await query.all()) as unknown as Record<string, unknown>[]
|
|
1648
|
+
let grouped = groupRowsByTuple(relatedRows, relation.targetKey)
|
|
1649
|
+
|
|
1650
|
+
return sourceRows.map((sourceRow) => {
|
|
1651
|
+
let key = getCompositeKey(sourceRow, relation.sourceKey)
|
|
1652
|
+
let matches = grouped.get(key) ?? []
|
|
1653
|
+
let pagedMatches = applyPagination(matches, relation.modifiers.limit, relation.modifiers.offset)
|
|
1654
|
+
|
|
1655
|
+
if (relation.cardinality === 'many') {
|
|
1656
|
+
return pagedMatches
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
return pagedMatches[0] ?? null
|
|
1660
|
+
})
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
async function loadHasManyThroughValues(
|
|
1664
|
+
database: DatabaseRuntime,
|
|
1665
|
+
sourceRows: Record<string, unknown>[],
|
|
1666
|
+
relation: AnyRelation,
|
|
1667
|
+
): Promise<unknown[]> {
|
|
1668
|
+
if (!relation.through) {
|
|
1669
|
+
throw new DataTableQueryError('hasManyThrough relation is missing through metadata')
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (sourceRows.length === 0) {
|
|
1673
|
+
return []
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
let throughRelation = relation.through.relation
|
|
1677
|
+
let sourceTuples = uniqueTuples(sourceRows, throughRelation.sourceKey)
|
|
1678
|
+
|
|
1679
|
+
if (sourceTuples.length === 0) {
|
|
1680
|
+
return sourceRows.map(() => [])
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
let throughQuery = database.query(throughRelation.targetTable)
|
|
1684
|
+
let throughPredicate = buildLinkPredicate(throughRelation.targetKey, sourceTuples)
|
|
1685
|
+
|
|
1686
|
+
if (throughPredicate) {
|
|
1687
|
+
throughQuery = throughQuery.where(
|
|
1688
|
+
throughPredicate as Predicate<QueryColumnName<typeof throughRelation.targetTable>>,
|
|
1689
|
+
)
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
throughQuery = applyRelationModifiers(throughQuery, throughRelation, {
|
|
1693
|
+
includePagination: false,
|
|
1694
|
+
})
|
|
1695
|
+
|
|
1696
|
+
let throughRows = (await throughQuery.all()) as unknown as Record<string, unknown>[]
|
|
1697
|
+
|
|
1698
|
+
if (throughRows.length === 0) {
|
|
1699
|
+
return sourceRows.map(() => [])
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
let throughRowsBySource = groupRowsByTuple(throughRows, throughRelation.targetKey)
|
|
1703
|
+
let pagedThroughRowsBySource = new Map<string, Record<string, unknown>[]>()
|
|
1704
|
+
let pagedThroughRows: Record<string, unknown>[] = []
|
|
1705
|
+
|
|
1706
|
+
for (let sourceRow of sourceRows) {
|
|
1707
|
+
let sourceKey = getCompositeKey(sourceRow, throughRelation.sourceKey)
|
|
1708
|
+
let matchedThroughRows = throughRowsBySource.get(sourceKey) ?? []
|
|
1709
|
+
let pagedMatchedRows = applyPagination(
|
|
1710
|
+
matchedThroughRows,
|
|
1711
|
+
throughRelation.modifiers.limit,
|
|
1712
|
+
throughRelation.modifiers.offset,
|
|
1713
|
+
)
|
|
1714
|
+
|
|
1715
|
+
pagedThroughRowsBySource.set(sourceKey, pagedMatchedRows)
|
|
1716
|
+
pagedThroughRows.push(...pagedMatchedRows)
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
let throughTuples = uniqueTuples(pagedThroughRows, relation.through.throughSourceKey)
|
|
1720
|
+
|
|
1721
|
+
if (throughTuples.length === 0) {
|
|
1722
|
+
return sourceRows.map(() => [])
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
let targetQuery = database.query(relation.targetTable)
|
|
1726
|
+
let targetPredicate = buildLinkPredicate(relation.through.throughTargetKey, throughTuples)
|
|
1727
|
+
|
|
1728
|
+
if (targetPredicate) {
|
|
1729
|
+
targetQuery = targetQuery.where(
|
|
1730
|
+
targetPredicate as Predicate<QueryColumnName<typeof relation.targetTable>>,
|
|
1731
|
+
)
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
targetQuery = applyRelationModifiers(targetQuery, relation, {
|
|
1735
|
+
includePagination: false,
|
|
1736
|
+
})
|
|
1737
|
+
|
|
1738
|
+
let relatedRows = (await targetQuery.all()) as unknown as Record<string, unknown>[]
|
|
1739
|
+
let targetRowsByThrough = groupRowsByTuple(relatedRows, relation.through.throughTargetKey)
|
|
1740
|
+
|
|
1741
|
+
return sourceRows.map((sourceRow) => {
|
|
1742
|
+
let sourceKey = getCompositeKey(sourceRow, throughRelation.sourceKey)
|
|
1743
|
+
let matchedThroughRows = pagedThroughRowsBySource.get(sourceKey) ?? []
|
|
1744
|
+
let outputRows: Record<string, unknown>[] = []
|
|
1745
|
+
let seen = new Set<string>()
|
|
1746
|
+
|
|
1747
|
+
for (let throughRow of matchedThroughRows) {
|
|
1748
|
+
let throughKey = getCompositeKey(throughRow, relation.through!.throughSourceKey)
|
|
1749
|
+
let rowsForThrough = targetRowsByThrough.get(throughKey) ?? []
|
|
1750
|
+
|
|
1751
|
+
for (let row of rowsForThrough) {
|
|
1752
|
+
let rowIdentity = getCompositeKey(row, getTablePrimaryKey(relation.targetTable))
|
|
1753
|
+
|
|
1754
|
+
if (!seen.has(rowIdentity)) {
|
|
1755
|
+
seen.add(rowIdentity)
|
|
1756
|
+
outputRows.push(row)
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
return applyPagination(outputRows, relation.modifiers.limit, relation.modifiers.offset)
|
|
1762
|
+
})
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
function applyRelationModifiers<table extends AnyTable>(
|
|
1766
|
+
query: QueryForTable<table>,
|
|
1767
|
+
relation: Relation<any, table, any, any>,
|
|
1768
|
+
options: { includePagination: boolean },
|
|
1769
|
+
): QueryForTable<table, any> {
|
|
1770
|
+
let next = query
|
|
1771
|
+
|
|
1772
|
+
for (let predicate of relation.modifiers.where) {
|
|
1773
|
+
next = next.where(predicate)
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
for (let clause of relation.modifiers.orderBy) {
|
|
1777
|
+
next = next.orderBy(clause.column as QueryColumns<QueryColumnTypeMap<table>>, clause.direction)
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
if (options.includePagination && relation.modifiers.limit !== undefined) {
|
|
1781
|
+
next = next.limit(relation.modifiers.limit)
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
if (options.includePagination && relation.modifiers.offset !== undefined) {
|
|
1785
|
+
next = next.offset(relation.modifiers.offset)
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
if (Object.keys(relation.modifiers.with).length > 0) {
|
|
1789
|
+
next = next.with(relation.modifiers.with)
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
return next
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function applyPagination<row>(
|
|
1796
|
+
rows: row[],
|
|
1797
|
+
limit: number | undefined,
|
|
1798
|
+
offset: number | undefined,
|
|
1799
|
+
): row[] {
|
|
1800
|
+
let offsetRows = offset === undefined ? rows : rows.slice(offset)
|
|
1801
|
+
return limit === undefined ? offsetRows : offsetRows.slice(0, limit)
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
function normalizeRows(rows: AdapterResult['rows']): Record<string, unknown>[] {
|
|
1805
|
+
if (!rows) {
|
|
1806
|
+
return []
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
return rows.map((row) => ({ ...row }))
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function hasScopedWriteModifiers(state: QueryState): boolean {
|
|
1813
|
+
return state.orderBy.length > 0 || state.limit !== undefined || state.offset !== undefined
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
function asQueryTableInput<table extends AnyTable>(
|
|
1817
|
+
table: table,
|
|
1818
|
+
): QueryTableInput<TableName<table>, TableRow<table>, TablePrimaryKey<table>> {
|
|
1819
|
+
return table as unknown as QueryTableInput<
|
|
1820
|
+
TableName<table>,
|
|
1821
|
+
TableRow<table>,
|
|
1822
|
+
TablePrimaryKey<table>
|
|
1823
|
+
>
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
function getPrimaryKeyWhere<table extends AnyTable>(
|
|
1827
|
+
table: table,
|
|
1828
|
+
value: PrimaryKeyInput<table>,
|
|
1829
|
+
): SingleTableWhere<table> {
|
|
1830
|
+
return getPrimaryKeyObject(table, value as any) as SingleTableWhere<table>
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
function getPrimaryKeyWhereFromRow<table extends AnyTable>(
|
|
1834
|
+
table: table,
|
|
1835
|
+
row: Record<string, unknown>,
|
|
1836
|
+
): SingleTableWhere<table> {
|
|
1837
|
+
let where: Record<string, unknown> = {}
|
|
1838
|
+
|
|
1839
|
+
for (let key of getTablePrimaryKey(table) as string[]) {
|
|
1840
|
+
where[key] = row[key]
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
return where as SingleTableWhere<table>
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
function resolveCreateRowWhere<table extends AnyTable>(
|
|
1847
|
+
table: table,
|
|
1848
|
+
values: Partial<TableRow<table>>,
|
|
1849
|
+
insertId: unknown,
|
|
1850
|
+
): SingleTableWhere<table> {
|
|
1851
|
+
let primaryKey = getTablePrimaryKey(table) as string[]
|
|
1852
|
+
|
|
1853
|
+
if (primaryKey.length === 1) {
|
|
1854
|
+
let key = primaryKey[0]
|
|
1855
|
+
|
|
1856
|
+
if (Object.prototype.hasOwnProperty.call(values, key)) {
|
|
1857
|
+
return {
|
|
1858
|
+
[key]: (values as Record<string, unknown>)[key],
|
|
1859
|
+
} as SingleTableWhere<table>
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
if (insertId !== undefined) {
|
|
1863
|
+
return {
|
|
1864
|
+
[key]: insertId,
|
|
1865
|
+
} as SingleTableWhere<table>
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
let where: Record<string, unknown> = {}
|
|
1870
|
+
|
|
1871
|
+
for (let key of primaryKey) {
|
|
1872
|
+
if (!Object.prototype.hasOwnProperty.call(values, key)) {
|
|
1873
|
+
throw new DataTableQueryError(
|
|
1874
|
+
'create({ returnRow: true }) requires primary key values for table "' +
|
|
1875
|
+
getTableName(table) +
|
|
1876
|
+
'" when adapter does not support RETURNING',
|
|
1877
|
+
)
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
where[key] = (values as Record<string, unknown>)[key]
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
return where as SingleTableWhere<table>
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
function normalizeOrderByInput<table extends AnyTable>(
|
|
1887
|
+
input: OrderByInput<table> | undefined,
|
|
1888
|
+
): OrderByTuple<table>[] {
|
|
1889
|
+
if (!input) {
|
|
1890
|
+
return []
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
if (input.length === 0) {
|
|
1894
|
+
return []
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
if (Array.isArray(input[0])) {
|
|
1898
|
+
return input as OrderByTuple<table>[]
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
return [input as OrderByTuple<table>]
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function toWriteResult(result: WriteResult | WriteRowsResult<unknown>): WriteResult {
|
|
1905
|
+
return {
|
|
1906
|
+
affectedRows: result.affectedRows,
|
|
1907
|
+
insertId: result.insertId,
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
type WriteStatePolicy = {
|
|
1912
|
+
where: boolean
|
|
1913
|
+
orderBy: boolean
|
|
1914
|
+
limit: boolean
|
|
1915
|
+
offset: boolean
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
function assertWriteState(
|
|
1919
|
+
state: QueryState,
|
|
1920
|
+
operation: 'insert' | 'insertMany' | 'update' | 'delete' | 'upsert',
|
|
1921
|
+
policy: WriteStatePolicy,
|
|
1922
|
+
): void {
|
|
1923
|
+
let unsupported: string[] = []
|
|
1924
|
+
|
|
1925
|
+
if (state.select !== '*') {
|
|
1926
|
+
unsupported.push('select()')
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
if (state.distinct) {
|
|
1930
|
+
unsupported.push('distinct()')
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
if (state.joins.length > 0) {
|
|
1934
|
+
unsupported.push('join()')
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
if (state.groupBy.length > 0) {
|
|
1938
|
+
unsupported.push('groupBy()')
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
if (state.having.length > 0) {
|
|
1942
|
+
unsupported.push('having()')
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
if (Object.keys(state.with).length > 0) {
|
|
1946
|
+
unsupported.push('with()')
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
if (!policy.where && state.where.length > 0) {
|
|
1950
|
+
unsupported.push('where()')
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
if (!policy.orderBy && state.orderBy.length > 0) {
|
|
1954
|
+
unsupported.push('orderBy()')
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
if (!policy.limit && state.limit !== undefined) {
|
|
1958
|
+
unsupported.push('limit()')
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
if (!policy.offset && state.offset !== undefined) {
|
|
1962
|
+
unsupported.push('offset()')
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
if (unsupported.length > 0) {
|
|
1966
|
+
throw new DataTableQueryError(
|
|
1967
|
+
operation + '() does not support these query modifiers: ' + unsupported.join(', '),
|
|
1968
|
+
)
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
async function loadPrimaryKeyRowsForScope<table extends AnyTable>(
|
|
1973
|
+
database: Database,
|
|
1974
|
+
table: table,
|
|
1975
|
+
state: QueryState,
|
|
1976
|
+
): Promise<Record<string, unknown>[]> {
|
|
1977
|
+
let query: QueryForTable<table> = database.query<
|
|
1978
|
+
TableName<table>,
|
|
1979
|
+
TableRow<table>,
|
|
1980
|
+
TablePrimaryKey<table>
|
|
1981
|
+
>(table as unknown as QueryTableInput<TableName<table>, TableRow<table>, TablePrimaryKey<table>>)
|
|
1982
|
+
|
|
1983
|
+
for (let predicate of state.where) {
|
|
1984
|
+
query = query.where(predicate as Predicate<QueryColumnName<table>>)
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
for (let clause of state.orderBy) {
|
|
1988
|
+
query = query.orderBy(
|
|
1989
|
+
clause.column as QueryColumns<QueryColumnTypeMap<table>>,
|
|
1990
|
+
clause.direction,
|
|
1991
|
+
)
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
if (state.limit !== undefined) {
|
|
1995
|
+
query = query.limit(state.limit)
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
if (state.offset !== undefined) {
|
|
1999
|
+
query = query.offset(state.offset)
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
let rows = await query
|
|
2003
|
+
.select(...(getTablePrimaryKey(table) as (keyof TableRow<table> & string)[]))
|
|
2004
|
+
.all()
|
|
2005
|
+
let primaryKeys = getTablePrimaryKey(table) as string[]
|
|
2006
|
+
|
|
2007
|
+
return rows.map((row) => {
|
|
2008
|
+
let keyObject: Record<string, unknown> = {}
|
|
2009
|
+
|
|
2010
|
+
for (let key of rowKeys(row as Record<string, unknown>, primaryKeys)) {
|
|
2011
|
+
keyObject[key] = (row as Record<string, unknown>)[key]
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
return keyObject
|
|
2015
|
+
})
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
function createInitialQueryState(): QueryState {
|
|
2019
|
+
return {
|
|
2020
|
+
select: '*',
|
|
2021
|
+
distinct: false,
|
|
2022
|
+
joins: [],
|
|
2023
|
+
where: [],
|
|
2024
|
+
groupBy: [],
|
|
2025
|
+
having: [],
|
|
2026
|
+
orderBy: [],
|
|
2027
|
+
with: {},
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
function cloneSelection(selection: '*' | SelectColumn[]): '*' | SelectColumn[] {
|
|
2032
|
+
if (selection === '*') {
|
|
2033
|
+
return '*'
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
return selection.map((column) => ({ ...column }))
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
function defaultNow(): Date {
|
|
2040
|
+
return new Date()
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
function prepareInsertValues<table extends AnyTable>(
|
|
2044
|
+
table: table,
|
|
2045
|
+
values: Partial<TableRow<table>>,
|
|
2046
|
+
now: unknown,
|
|
2047
|
+
touch: boolean,
|
|
2048
|
+
): Record<string, unknown> {
|
|
2049
|
+
let output = validateWriteValues(table, values)
|
|
2050
|
+
let timestamps = getTableTimestamps(table)
|
|
2051
|
+
let columns = getTableColumns(table)
|
|
2052
|
+
|
|
2053
|
+
if (touch && timestamps) {
|
|
2054
|
+
let createdAt = timestamps.createdAt
|
|
2055
|
+
let updatedAt = timestamps.updatedAt
|
|
2056
|
+
|
|
2057
|
+
if (
|
|
2058
|
+
Object.prototype.hasOwnProperty.call(columns, createdAt) &&
|
|
2059
|
+
output[createdAt] === undefined
|
|
2060
|
+
) {
|
|
2061
|
+
output[createdAt] = now
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
if (
|
|
2065
|
+
Object.prototype.hasOwnProperty.call(columns, updatedAt) &&
|
|
2066
|
+
output[updatedAt] === undefined
|
|
2067
|
+
) {
|
|
2068
|
+
output[updatedAt] = now
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
return output
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
function prepareUpdateValues<table extends AnyTable>(
|
|
2076
|
+
table: table,
|
|
2077
|
+
values: Partial<TableRow<table>>,
|
|
2078
|
+
now: unknown,
|
|
2079
|
+
touch: boolean,
|
|
2080
|
+
): Record<string, unknown> {
|
|
2081
|
+
let output = validateWriteValues(table, values)
|
|
2082
|
+
let timestamps = getTableTimestamps(table)
|
|
2083
|
+
let columns = getTableColumns(table)
|
|
2084
|
+
|
|
2085
|
+
if (touch && timestamps) {
|
|
2086
|
+
let updatedAt = timestamps.updatedAt
|
|
2087
|
+
|
|
2088
|
+
if (
|
|
2089
|
+
Object.prototype.hasOwnProperty.call(columns, updatedAt) &&
|
|
2090
|
+
output[updatedAt] === undefined
|
|
2091
|
+
) {
|
|
2092
|
+
output[updatedAt] = now
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
return output
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
function validateWriteValues<table extends AnyTable>(
|
|
2100
|
+
table: table,
|
|
2101
|
+
values: Partial<TableRow<table>>,
|
|
2102
|
+
): Record<string, unknown> {
|
|
2103
|
+
let columns = getTableColumns(table)
|
|
2104
|
+
let tableName = getTableName(table)
|
|
2105
|
+
let result = validatePartialRow(table, values)
|
|
2106
|
+
|
|
2107
|
+
if ('issues' in result) {
|
|
2108
|
+
let firstIssue = result.issues[0]
|
|
2109
|
+
let issuePath = firstIssue?.path
|
|
2110
|
+
let firstPathSegment = issuePath && issuePath.length > 0 ? issuePath[0] : undefined
|
|
2111
|
+
let column = typeof firstPathSegment === 'string' ? firstPathSegment : undefined
|
|
2112
|
+
|
|
2113
|
+
if (column && !Object.prototype.hasOwnProperty.call(columns, column)) {
|
|
2114
|
+
throw new DataTableValidationError(
|
|
2115
|
+
'Unknown column "' + column + '" for table "' + tableName + '"',
|
|
2116
|
+
[],
|
|
2117
|
+
)
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
if (column) {
|
|
2121
|
+
throw new DataTableValidationError(
|
|
2122
|
+
'Invalid value for column "' + column + '" in table "' + tableName + '"',
|
|
2123
|
+
result.issues,
|
|
2124
|
+
{
|
|
2125
|
+
metadata: {
|
|
2126
|
+
table: tableName,
|
|
2127
|
+
column,
|
|
2128
|
+
},
|
|
2129
|
+
},
|
|
2130
|
+
)
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
throw new DataTableValidationError(
|
|
2134
|
+
'Invalid value for table "' + tableName + '"',
|
|
2135
|
+
result.issues,
|
|
2136
|
+
{
|
|
2137
|
+
metadata: {
|
|
2138
|
+
table: tableName,
|
|
2139
|
+
},
|
|
2140
|
+
},
|
|
2141
|
+
)
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
return result.value as Record<string, unknown>
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
type ResolvedPredicateColumn = {
|
|
2148
|
+
tableName: string
|
|
2149
|
+
columnName: string
|
|
2150
|
+
schema: unknown
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
function createPredicateColumnResolver(
|
|
2154
|
+
tables: AnyTable[],
|
|
2155
|
+
): (column: string) => ResolvedPredicateColumn {
|
|
2156
|
+
let qualifiedColumns = new Map<string, ResolvedPredicateColumn>()
|
|
2157
|
+
let unqualifiedColumns = new Map<string, ResolvedPredicateColumn>()
|
|
2158
|
+
let ambiguousColumns = new Set<string>()
|
|
2159
|
+
|
|
2160
|
+
for (let table of tables) {
|
|
2161
|
+
let tableColumns = getTableColumns(table)
|
|
2162
|
+
let tableName = getTableName(table)
|
|
2163
|
+
|
|
2164
|
+
for (let columnName in tableColumns) {
|
|
2165
|
+
if (!Object.prototype.hasOwnProperty.call(tableColumns, columnName)) {
|
|
2166
|
+
continue
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
let resolvedColumn: ResolvedPredicateColumn = {
|
|
2170
|
+
tableName,
|
|
2171
|
+
columnName,
|
|
2172
|
+
schema: tableColumns[columnName],
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
qualifiedColumns.set(tableName + '.' + columnName, resolvedColumn)
|
|
2176
|
+
|
|
2177
|
+
if (ambiguousColumns.has(columnName)) {
|
|
2178
|
+
continue
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
if (unqualifiedColumns.has(columnName)) {
|
|
2182
|
+
unqualifiedColumns.delete(columnName)
|
|
2183
|
+
ambiguousColumns.add(columnName)
|
|
2184
|
+
continue
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
unqualifiedColumns.set(columnName, resolvedColumn)
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
return function resolveColumn(column: string): ResolvedPredicateColumn {
|
|
2192
|
+
let qualified = qualifiedColumns.get(column)
|
|
2193
|
+
|
|
2194
|
+
if (qualified) {
|
|
2195
|
+
return qualified
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
if (column.includes('.')) {
|
|
2199
|
+
throw new DataTableQueryError('Unknown predicate column "' + column + '"')
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
if (ambiguousColumns.has(column)) {
|
|
2203
|
+
throw new DataTableQueryError(
|
|
2204
|
+
'Ambiguous predicate column "' + column + '". Use a qualified column name',
|
|
2205
|
+
)
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
let unqualified = unqualifiedColumns.get(column)
|
|
2209
|
+
|
|
2210
|
+
if (!unqualified) {
|
|
2211
|
+
throw new DataTableQueryError('Unknown predicate column "' + column + '"')
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
return unqualified
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
function normalizePredicateValues(
|
|
2219
|
+
predicate: Predicate,
|
|
2220
|
+
resolveColumn: (column: string) => ResolvedPredicateColumn,
|
|
2221
|
+
): Predicate {
|
|
2222
|
+
if (predicate.type === 'comparison') {
|
|
2223
|
+
let column = resolveColumn(predicate.column)
|
|
2224
|
+
|
|
2225
|
+
if (predicate.valueType === 'column') {
|
|
2226
|
+
resolveColumn(predicate.value)
|
|
2227
|
+
return predicate
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
if (
|
|
2231
|
+
(predicate.operator === 'eq' || predicate.operator === 'ne') &&
|
|
2232
|
+
(predicate.value === null || predicate.value === undefined)
|
|
2233
|
+
) {
|
|
2234
|
+
return predicate
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
if (predicate.operator === 'in' || predicate.operator === 'notIn') {
|
|
2238
|
+
if (!Array.isArray(predicate.value)) {
|
|
2239
|
+
throw new DataTableValidationError(
|
|
2240
|
+
'Invalid filter value for column "' +
|
|
2241
|
+
column.columnName +
|
|
2242
|
+
'" in table "' +
|
|
2243
|
+
column.tableName +
|
|
2244
|
+
'"',
|
|
2245
|
+
[{ message: 'Expected an array value for "' + predicate.operator + '" predicate' }],
|
|
2246
|
+
{
|
|
2247
|
+
metadata: {
|
|
2248
|
+
table: column.tableName,
|
|
2249
|
+
column: column.columnName,
|
|
2250
|
+
},
|
|
2251
|
+
},
|
|
2252
|
+
)
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
let parsedValues = predicate.value.map((value) => parsePredicateValue(column, value))
|
|
2256
|
+
|
|
2257
|
+
return {
|
|
2258
|
+
...predicate,
|
|
2259
|
+
value: parsedValues,
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
return {
|
|
2264
|
+
...predicate,
|
|
2265
|
+
value: parsePredicateValue(column, predicate.value),
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
if (predicate.type === 'between') {
|
|
2270
|
+
let column = resolveColumn(predicate.column)
|
|
2271
|
+
|
|
2272
|
+
return {
|
|
2273
|
+
...predicate,
|
|
2274
|
+
lower: parsePredicateValue(column, predicate.lower),
|
|
2275
|
+
upper: parsePredicateValue(column, predicate.upper),
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
if (predicate.type === 'null') {
|
|
2280
|
+
resolveColumn(predicate.column)
|
|
2281
|
+
return predicate
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
return {
|
|
2285
|
+
...predicate,
|
|
2286
|
+
predicates: predicate.predicates.map((child) => normalizePredicateValues(child, resolveColumn)),
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
function parsePredicateValue(column: ResolvedPredicateColumn, value: unknown): unknown {
|
|
2291
|
+
let result = parseSafe(column.schema as any, value) as
|
|
2292
|
+
| { success: true; value: unknown }
|
|
2293
|
+
| { success: false; issues: ReadonlyArray<unknown> }
|
|
2294
|
+
|
|
2295
|
+
if (!result.success) {
|
|
2296
|
+
throw new DataTableValidationError(
|
|
2297
|
+
'Invalid filter value for column "' +
|
|
2298
|
+
column.columnName +
|
|
2299
|
+
'" in table "' +
|
|
2300
|
+
column.tableName +
|
|
2301
|
+
'"',
|
|
2302
|
+
result.issues,
|
|
2303
|
+
{
|
|
2304
|
+
metadata: {
|
|
2305
|
+
table: column.tableName,
|
|
2306
|
+
column: column.columnName,
|
|
2307
|
+
},
|
|
2308
|
+
},
|
|
2309
|
+
)
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
return result.value
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
function uniqueTuples(rows: Record<string, unknown>[], columns: string[]): unknown[][] {
|
|
2316
|
+
let output: unknown[][] = []
|
|
2317
|
+
let seen = new Set<string>()
|
|
2318
|
+
|
|
2319
|
+
for (let row of rows) {
|
|
2320
|
+
let tuple = columns.map((column) => row[column])
|
|
2321
|
+
let key = tuple.map(stringifyForKey).join('::')
|
|
2322
|
+
|
|
2323
|
+
if (!seen.has(key)) {
|
|
2324
|
+
seen.add(key)
|
|
2325
|
+
output.push(tuple)
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
return output
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
function buildLinkPredicate(targetColumns: string[], tuples: unknown[][]): Predicate | undefined {
|
|
2333
|
+
if (tuples.length === 0) {
|
|
2334
|
+
return undefined
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
if (targetColumns.length === 1) {
|
|
2338
|
+
return inList(
|
|
2339
|
+
targetColumns[0],
|
|
2340
|
+
tuples.map((tuple) => tuple[0]),
|
|
2341
|
+
)
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
let tuplePredicates = tuples.map((tuple) => {
|
|
2345
|
+
let comparisons = targetColumns.map((column, index) => eq(column, tuple[index]))
|
|
2346
|
+
|
|
2347
|
+
return and(...comparisons)
|
|
2348
|
+
})
|
|
2349
|
+
|
|
2350
|
+
return or(...tuplePredicates)
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
function groupRowsByTuple(
|
|
2354
|
+
rows: Record<string, unknown>[],
|
|
2355
|
+
columns: string[],
|
|
2356
|
+
): Map<string, Record<string, unknown>[]> {
|
|
2357
|
+
let output = new Map<string, Record<string, unknown>[]>()
|
|
2358
|
+
|
|
2359
|
+
for (let row of rows) {
|
|
2360
|
+
let key = getCompositeKey(row, columns)
|
|
2361
|
+
let group = output.get(key)
|
|
2362
|
+
|
|
2363
|
+
if (group) {
|
|
2364
|
+
group.push(row)
|
|
2365
|
+
continue
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
output.set(key, [row])
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
return output
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
function stringifyForKey(value: unknown): string {
|
|
2375
|
+
if (value === null) {
|
|
2376
|
+
return 'null'
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
if (value === undefined) {
|
|
2380
|
+
return 'undefined'
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
if (value instanceof Date) {
|
|
2384
|
+
return 'date:' + value.toISOString()
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
if (typeof value === 'string') {
|
|
2388
|
+
return JSON.stringify(value)
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
2392
|
+
return String(value)
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
return JSON.stringify(value)
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
function normalizeReturningSelection<row extends Record<string, unknown>>(
|
|
2399
|
+
returning: ReturningInput<row>,
|
|
2400
|
+
): ReturningSelection {
|
|
2401
|
+
if (returning === '*') {
|
|
2402
|
+
return '*'
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
return [...returning]
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
function buildPrimaryKeyPredicate<table extends AnyTable>(
|
|
2409
|
+
table: table,
|
|
2410
|
+
keyObjects: Record<string, unknown>[],
|
|
2411
|
+
): Predicate<TableColumnName<table>> | undefined {
|
|
2412
|
+
let primaryKey = getTablePrimaryKey(table)
|
|
2413
|
+
|
|
2414
|
+
if (keyObjects.length === 0) {
|
|
2415
|
+
return undefined
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
if (primaryKey.length === 1) {
|
|
2419
|
+
let key = primaryKey[0] as TableColumnName<table>
|
|
2420
|
+
return inList(
|
|
2421
|
+
key,
|
|
2422
|
+
keyObjects.map((objectValue) => objectValue[key]),
|
|
2423
|
+
)
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
let predicates = keyObjects.map((objectValue) => {
|
|
2427
|
+
let comparisons = primaryKey.map((key) => {
|
|
2428
|
+
let typedKey = key as TableColumnName<table>
|
|
2429
|
+
return eq(typedKey, objectValue[typedKey])
|
|
2430
|
+
})
|
|
2431
|
+
|
|
2432
|
+
return and(...comparisons)
|
|
2433
|
+
})
|
|
2434
|
+
|
|
2435
|
+
return or(...predicates)
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
function rowKeys(row: Record<string, unknown>, keys: string[]): string[] {
|
|
2439
|
+
let output: string[] = []
|
|
2440
|
+
|
|
2441
|
+
for (let key of keys) {
|
|
2442
|
+
if (Object.prototype.hasOwnProperty.call(row, key)) {
|
|
2443
|
+
output.push(key)
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
return output
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
function assertReturningCapability<row extends Record<string, unknown>>(
|
|
2451
|
+
adapter: DatabaseAdapter,
|
|
2452
|
+
operation: 'insert' | 'insertMany' | 'update' | 'delete' | 'upsert',
|
|
2453
|
+
returning: ReturningInput<row> | undefined,
|
|
2454
|
+
): void {
|
|
2455
|
+
if (returning && !adapter.capabilities.returning) {
|
|
2456
|
+
throw new DataTableQueryError(operation + '() returning is not supported by this adapter')
|
|
2457
|
+
}
|
|
2458
|
+
}
|