@remix-run/data-table 0.1.0 → 0.2.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.
Files changed (111) hide show
  1. package/README.md +306 -55
  2. package/dist/index.d.ts +9 -5
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +4 -2
  5. package/dist/lib/adapter.d.ts +386 -16
  6. package/dist/lib/adapter.d.ts.map +1 -1
  7. package/dist/lib/column.d.ts +193 -0
  8. package/dist/lib/column.d.ts.map +1 -0
  9. package/dist/lib/column.js +302 -0
  10. package/dist/lib/database/execution-context.d.ts +10 -0
  11. package/dist/lib/database/execution-context.d.ts.map +1 -0
  12. package/dist/lib/database/execution-context.js +1 -0
  13. package/dist/lib/database/helpers.d.ts +26 -0
  14. package/dist/lib/database/helpers.d.ts.map +1 -0
  15. package/dist/lib/database/helpers.js +116 -0
  16. package/dist/lib/database/query-execution.d.ts +7 -0
  17. package/dist/lib/database/query-execution.d.ts.map +1 -0
  18. package/dist/lib/database/query-execution.js +401 -0
  19. package/dist/lib/database/relations.d.ts +4 -0
  20. package/dist/lib/database/relations.d.ts.map +1 -0
  21. package/dist/lib/database/relations.js +207 -0
  22. package/dist/lib/database/write-lifecycle.d.ts +13 -0
  23. package/dist/lib/database/write-lifecycle.d.ts.map +1 -0
  24. package/dist/lib/database/write-lifecycle.js +279 -0
  25. package/dist/lib/database.d.ts +141 -238
  26. package/dist/lib/database.d.ts.map +1 -1
  27. package/dist/lib/database.js +73 -1122
  28. package/dist/lib/errors.d.ts +9 -0
  29. package/dist/lib/errors.d.ts.map +1 -1
  30. package/dist/lib/errors.js +9 -0
  31. package/dist/lib/migrations/filename.d.ts +12 -0
  32. package/dist/lib/migrations/filename.d.ts.map +1 -0
  33. package/dist/lib/migrations/filename.js +20 -0
  34. package/dist/lib/migrations/helpers.d.ts +11 -0
  35. package/dist/lib/migrations/helpers.d.ts.map +1 -0
  36. package/dist/lib/migrations/helpers.js +77 -0
  37. package/dist/lib/migrations/journal-store.d.ts +15 -0
  38. package/dist/lib/migrations/journal-store.d.ts.map +1 -0
  39. package/dist/lib/migrations/journal-store.js +83 -0
  40. package/dist/lib/migrations/registry.d.ts +27 -0
  41. package/dist/lib/migrations/registry.d.ts.map +1 -0
  42. package/dist/lib/migrations/registry.js +51 -0
  43. package/dist/lib/migrations/runner.d.ts +20 -0
  44. package/dist/lib/migrations/runner.d.ts.map +1 -0
  45. package/dist/lib/migrations/runner.js +273 -0
  46. package/dist/lib/migrations/schema-api.d.ts +7 -0
  47. package/dist/lib/migrations/schema-api.d.ts.map +1 -0
  48. package/dist/lib/migrations/schema-api.js +326 -0
  49. package/dist/lib/migrations-node.d.ts +17 -0
  50. package/dist/lib/migrations-node.d.ts.map +1 -0
  51. package/dist/lib/migrations-node.js +65 -0
  52. package/dist/lib/migrations.d.ts +292 -0
  53. package/dist/lib/migrations.d.ts.map +1 -0
  54. package/dist/lib/migrations.js +38 -0
  55. package/dist/lib/operators.d.ts +3 -0
  56. package/dist/lib/operators.d.ts.map +1 -1
  57. package/dist/lib/query.d.ts +159 -0
  58. package/dist/lib/query.d.ts.map +1 -0
  59. package/dist/lib/query.js +401 -0
  60. package/dist/lib/references.d.ts +0 -1
  61. package/dist/lib/references.d.ts.map +1 -1
  62. package/dist/lib/sql-helpers.d.ts +50 -0
  63. package/dist/lib/sql-helpers.d.ts.map +1 -0
  64. package/dist/lib/sql-helpers.js +111 -0
  65. package/dist/lib/sql.d.ts +23 -6
  66. package/dist/lib/sql.d.ts.map +1 -1
  67. package/dist/lib/sql.js +19 -5
  68. package/dist/lib/table.d.ts +355 -40
  69. package/dist/lib/table.d.ts.map +1 -1
  70. package/dist/lib/table.js +113 -90
  71. package/dist/migrations/node.d.ts +2 -0
  72. package/dist/migrations/node.d.ts.map +1 -0
  73. package/dist/migrations/node.js +1 -0
  74. package/dist/migrations.d.ts +8 -0
  75. package/dist/migrations.d.ts.map +1 -0
  76. package/dist/migrations.js +5 -0
  77. package/dist/operators.d.ts +3 -0
  78. package/dist/operators.d.ts.map +1 -0
  79. package/dist/operators.js +1 -0
  80. package/dist/sql-helpers.d.ts +3 -0
  81. package/dist/sql-helpers.d.ts.map +1 -0
  82. package/dist/sql-helpers.js +1 -0
  83. package/package.json +23 -8
  84. package/src/index.ts +93 -10
  85. package/src/lib/adapter.ts +469 -25
  86. package/src/lib/column.ts +384 -0
  87. package/src/lib/database/execution-context.ts +15 -0
  88. package/src/lib/database/helpers.ts +216 -0
  89. package/src/lib/database/query-execution.ts +638 -0
  90. package/src/lib/database/relations.ts +332 -0
  91. package/src/lib/database/write-lifecycle.ts +487 -0
  92. package/src/lib/database.ts +246 -1848
  93. package/src/lib/errors.ts +10 -0
  94. package/src/lib/migrations/filename.ts +25 -0
  95. package/src/lib/migrations/helpers.ts +108 -0
  96. package/src/lib/migrations/journal-store.ts +122 -0
  97. package/src/lib/migrations/registry.ts +62 -0
  98. package/src/lib/migrations/runner.ts +374 -0
  99. package/src/lib/migrations/schema-api.ts +417 -0
  100. package/src/lib/migrations-node.ts +71 -0
  101. package/src/lib/migrations.ts +328 -0
  102. package/src/lib/operators.ts +3 -0
  103. package/src/lib/query.ts +958 -0
  104. package/src/lib/references.ts +0 -1
  105. package/src/lib/sql-helpers.ts +146 -0
  106. package/src/lib/sql.ts +23 -6
  107. package/src/lib/table.ts +484 -156
  108. package/src/migrations/node.ts +1 -0
  109. package/src/migrations.ts +26 -0
  110. package/src/operators.ts +18 -0
  111. package/src/sql-helpers.ts +9 -0
@@ -0,0 +1,638 @@
1
+ import type {
2
+ CountOperation,
3
+ DataManipulationResult,
4
+ DeleteOperation,
5
+ ExistsOperation,
6
+ InsertManyOperation,
7
+ InsertOperation,
8
+ SelectColumn,
9
+ SelectOperation,
10
+ UpdateOperation,
11
+ UpsertOperation,
12
+ } from '../adapter.ts'
13
+ import { DataTableQueryError } from '../errors.ts'
14
+ import type { ReturningInput, WriteResult, WriteRowResult, WriteRowsResult } from '../database.ts'
15
+ import { normalizeWhereInput } from '../operators.ts'
16
+ import type { AnyQuery, QueryExecutionResult, QueryState } from '../query.ts'
17
+ import { cloneQueryState, querySnapshot } from '../query.ts'
18
+ import type { AnyTable } from '../table.ts'
19
+ import { getPrimaryKeyObject, getTableName } from '../table.ts'
20
+
21
+ import { executeOperation, type QueryExecutionContext } from './execution-context.ts'
22
+ import {
23
+ buildPrimaryKeyPredicate,
24
+ hasScopedWriteModifiers,
25
+ loadPrimaryKeyRowsForScope,
26
+ } from './helpers.ts'
27
+ import { loadRelationsForRows } from './relations.ts'
28
+ import {
29
+ applyAfterReadHooksToLoadedRows,
30
+ applyAfterReadHooksToRows,
31
+ assertReturningCapability,
32
+ normalizeReturningSelection,
33
+ prepareInsertValues,
34
+ prepareUpdateValues,
35
+ runAfterDeleteHook,
36
+ runAfterWriteHook,
37
+ runBeforeDeleteHook,
38
+ } from './write-lifecycle.ts'
39
+
40
+ export async function executeQuery<input extends AnyQuery>(
41
+ database: QueryExecutionContext,
42
+ input: input,
43
+ ): Promise<QueryExecutionResult<input>> {
44
+ let snapshot = input[querySnapshot]()
45
+
46
+ switch (snapshot.plan.kind) {
47
+ case 'all':
48
+ return (await executeAll(
49
+ database,
50
+ snapshot.table,
51
+ snapshot.state,
52
+ )) as QueryExecutionResult<input>
53
+ case 'first':
54
+ return (await executeFirst(
55
+ database,
56
+ snapshot.table,
57
+ snapshot.state,
58
+ )) as QueryExecutionResult<input>
59
+ case 'find':
60
+ return (await executeFind(
61
+ database,
62
+ snapshot.table,
63
+ snapshot.state,
64
+ snapshot.plan.value,
65
+ )) as QueryExecutionResult<input>
66
+ case 'count':
67
+ return (await executeCount(
68
+ database,
69
+ snapshot.table,
70
+ snapshot.state,
71
+ )) as QueryExecutionResult<input>
72
+ case 'exists':
73
+ return (await executeExists(
74
+ database,
75
+ snapshot.table,
76
+ snapshot.state,
77
+ )) as QueryExecutionResult<input>
78
+ case 'insert':
79
+ return (await executeInsert(
80
+ database,
81
+ snapshot.table,
82
+ snapshot.plan.values as Record<string, unknown>,
83
+ snapshot.plan.options,
84
+ )) as QueryExecutionResult<input>
85
+ case 'insertMany':
86
+ return (await executeInsertMany(
87
+ database,
88
+ snapshot.table,
89
+ snapshot.plan.values as Record<string, unknown>[],
90
+ snapshot.plan.options,
91
+ )) as QueryExecutionResult<input>
92
+ case 'update':
93
+ return (await executeUpdate(
94
+ database,
95
+ snapshot.table,
96
+ snapshot.state,
97
+ snapshot.plan.changes as Record<string, unknown>,
98
+ snapshot.plan.options,
99
+ )) as QueryExecutionResult<input>
100
+ case 'delete':
101
+ return (await executeDelete(
102
+ database,
103
+ snapshot.table,
104
+ snapshot.state,
105
+ snapshot.plan.options,
106
+ )) as QueryExecutionResult<input>
107
+ case 'upsert':
108
+ return (await executeUpsert(
109
+ database,
110
+ snapshot.table,
111
+ snapshot.plan.values as Record<string, unknown>,
112
+ snapshot.plan.options,
113
+ )) as QueryExecutionResult<input>
114
+ default:
115
+ throw new DataTableQueryError('Unknown query execution mode')
116
+ }
117
+ }
118
+
119
+ export async function loadRowsWithRelationsForQuery(
120
+ database: QueryExecutionContext,
121
+ input: AnyQuery,
122
+ ): Promise<Record<string, unknown>[]> {
123
+ let snapshot = input[querySnapshot]()
124
+ return loadRowsWithRelationsForState(database, snapshot.table, snapshot.state)
125
+ }
126
+
127
+ export async function loadRowsWithRelationsForState(
128
+ database: QueryExecutionContext,
129
+ table: AnyTable,
130
+ state: QueryState,
131
+ ): Promise<Record<string, unknown>[]> {
132
+ let operation = createSelectOperation(table, state)
133
+ let result = await database[executeOperation](operation)
134
+ let rows = normalizeRows(result.rows)
135
+
136
+ if (Object.keys(state.with).length === 0) {
137
+ return rows
138
+ }
139
+
140
+ return loadRelationsForRows(database, table, rows, state.with)
141
+ }
142
+
143
+ async function executeAll(
144
+ database: QueryExecutionContext,
145
+ table: AnyTable,
146
+ state: QueryState,
147
+ ): Promise<Record<string, unknown>[]> {
148
+ let rows = await loadRowsWithRelationsForState(database, table, state)
149
+ return applyAfterReadHooksToLoadedRows(table, rows, state.with)
150
+ }
151
+
152
+ async function executeFirst(
153
+ database: QueryExecutionContext,
154
+ table: AnyTable,
155
+ state: QueryState,
156
+ ): Promise<Record<string, unknown> | null> {
157
+ let rows = await executeAll(database, table, {
158
+ ...cloneQueryState(state),
159
+ limit: 1,
160
+ })
161
+
162
+ return rows[0] ?? null
163
+ }
164
+
165
+ async function executeFind(
166
+ database: QueryExecutionContext,
167
+ table: AnyTable,
168
+ state: QueryState,
169
+ value: unknown,
170
+ ): Promise<Record<string, unknown> | null> {
171
+ let scopedState = cloneQueryState(state)
172
+ scopedState.where.push(
173
+ normalizeWhereInput(getPrimaryKeyObject(table, value as never) as Record<string, unknown>),
174
+ )
175
+
176
+ return executeFirst(database, table, scopedState)
177
+ }
178
+
179
+ async function executeCount(
180
+ database: QueryExecutionContext,
181
+ table: AnyTable,
182
+ state: QueryState,
183
+ ): Promise<number> {
184
+ let operation: CountOperation<AnyTable> = {
185
+ kind: 'count',
186
+ table,
187
+ joins: [...state.joins],
188
+ where: [...state.where],
189
+ groupBy: [...state.groupBy],
190
+ having: [...state.having],
191
+ }
192
+
193
+ let result = await database[executeOperation](operation)
194
+
195
+ if (result.rows && result.rows[0] && typeof result.rows[0].count === 'number') {
196
+ return result.rows[0].count as number
197
+ }
198
+
199
+ if (result.rows) {
200
+ return result.rows.length
201
+ }
202
+
203
+ return 0
204
+ }
205
+
206
+ async function executeExists(
207
+ database: QueryExecutionContext,
208
+ table: AnyTable,
209
+ state: QueryState,
210
+ ): Promise<boolean> {
211
+ let operation: ExistsOperation<AnyTable> = {
212
+ kind: 'exists',
213
+ table,
214
+ joins: [...state.joins],
215
+ where: [...state.where],
216
+ groupBy: [...state.groupBy],
217
+ having: [...state.having],
218
+ }
219
+
220
+ let result = await database[executeOperation](operation)
221
+
222
+ if (result.rows && result.rows[0] && typeof result.rows[0].exists === 'boolean') {
223
+ return result.rows[0].exists as boolean
224
+ }
225
+
226
+ if (result.rows && result.rows[0] && typeof result.rows[0].count === 'number') {
227
+ return Number(result.rows[0].count) > 0
228
+ }
229
+
230
+ return Boolean(result.rows && result.rows.length > 0)
231
+ }
232
+
233
+ async function executeInsert(
234
+ database: QueryExecutionContext,
235
+ table: AnyTable,
236
+ values: Record<string, unknown>,
237
+ options?: { returning?: ReturningInput<Record<string, unknown>>; touch?: boolean },
238
+ ): Promise<WriteResult | WriteRowResult<Record<string, unknown>>> {
239
+ let preparedValues = prepareInsertValues(
240
+ table,
241
+ values as never,
242
+ database.now(),
243
+ options?.touch ?? true,
244
+ )
245
+ let returning = options?.returning
246
+
247
+ assertReturningCapability(database.adapter, 'insert', returning)
248
+
249
+ if (returning) {
250
+ let operation: InsertOperation<AnyTable> = {
251
+ kind: 'insert',
252
+ table,
253
+ values: preparedValues,
254
+ returning: normalizeReturningSelection(returning),
255
+ }
256
+
257
+ let result = await database[executeOperation](operation)
258
+ let row = applyAfterReadHooksToRows(table, normalizeRows(result.rows))[0] ?? null
259
+ let affectedRows = result.affectedRows ?? 0
260
+ runAfterWriteHook(table, {
261
+ operation: 'create',
262
+ tableName: getTableName(table),
263
+ values: [preparedValues],
264
+ affectedRows,
265
+ insertId: result.insertId,
266
+ })
267
+
268
+ return {
269
+ affectedRows,
270
+ insertId: result.insertId,
271
+ row,
272
+ }
273
+ }
274
+
275
+ let operation: InsertOperation<AnyTable> = {
276
+ kind: 'insert',
277
+ table,
278
+ values: preparedValues,
279
+ }
280
+
281
+ let result = await database[executeOperation](operation)
282
+ let affectedRows = result.affectedRows ?? 0
283
+ runAfterWriteHook(table, {
284
+ operation: 'create',
285
+ tableName: getTableName(table),
286
+ values: [preparedValues],
287
+ affectedRows,
288
+ insertId: result.insertId,
289
+ })
290
+
291
+ return {
292
+ affectedRows,
293
+ insertId: result.insertId,
294
+ }
295
+ }
296
+
297
+ async function executeInsertMany(
298
+ database: QueryExecutionContext,
299
+ table: AnyTable,
300
+ values: Record<string, unknown>[],
301
+ options?: { returning?: ReturningInput<Record<string, unknown>>; touch?: boolean },
302
+ ): Promise<WriteResult | WriteRowsResult<Record<string, unknown>>> {
303
+ let preparedValues = values.map((value) =>
304
+ prepareInsertValues(table, value as never, database.now(), options?.touch ?? true),
305
+ )
306
+
307
+ if (
308
+ preparedValues.length > 0 &&
309
+ preparedValues.every((preparedValue) => Object.keys(preparedValue).length === 0)
310
+ ) {
311
+ throw new DataTableQueryError(
312
+ 'insertMany() requires at least one explicit value across the batch',
313
+ )
314
+ }
315
+
316
+ let returning = options?.returning
317
+ assertReturningCapability(database.adapter, 'insertMany', returning)
318
+
319
+ if (returning) {
320
+ let operation: InsertManyOperation<AnyTable> = {
321
+ kind: 'insertMany',
322
+ table,
323
+ values: preparedValues,
324
+ returning: normalizeReturningSelection(returning),
325
+ }
326
+
327
+ let result = await database[executeOperation](operation)
328
+ let affectedRows = result.affectedRows ?? 0
329
+ runAfterWriteHook(table, {
330
+ operation: 'create',
331
+ tableName: getTableName(table),
332
+ values: preparedValues,
333
+ affectedRows,
334
+ insertId: result.insertId,
335
+ })
336
+
337
+ return {
338
+ affectedRows,
339
+ insertId: result.insertId,
340
+ rows: applyAfterReadHooksToRows(table, normalizeRows(result.rows)),
341
+ }
342
+ }
343
+
344
+ let operation: InsertManyOperation<AnyTable> = {
345
+ kind: 'insertMany',
346
+ table,
347
+ values: preparedValues,
348
+ }
349
+
350
+ let result = await database[executeOperation](operation)
351
+ let affectedRows = result.affectedRows ?? 0
352
+ runAfterWriteHook(table, {
353
+ operation: 'create',
354
+ tableName: getTableName(table),
355
+ values: preparedValues,
356
+ affectedRows,
357
+ insertId: result.insertId,
358
+ })
359
+
360
+ return {
361
+ affectedRows,
362
+ insertId: result.insertId,
363
+ }
364
+ }
365
+
366
+ async function executeUpdate(
367
+ database: QueryExecutionContext,
368
+ table: AnyTable,
369
+ state: QueryState,
370
+ changes: Record<string, unknown>,
371
+ options?: { returning?: ReturningInput<Record<string, unknown>>; touch?: boolean },
372
+ ): Promise<WriteResult | WriteRowsResult<Record<string, unknown>>> {
373
+ let returning = options?.returning
374
+ assertReturningCapability(database.adapter, 'update', returning)
375
+ let preparedChanges = prepareUpdateValues(
376
+ table,
377
+ changes as never,
378
+ database.now(),
379
+ options?.touch ?? true,
380
+ )
381
+
382
+ if (Object.keys(preparedChanges).length === 0) {
383
+ throw new DataTableQueryError('update() requires at least one change')
384
+ }
385
+
386
+ let result: DataManipulationResult
387
+
388
+ if (hasScopedWriteModifiers(state)) {
389
+ result = await database.transaction(async (tx) => {
390
+ let primaryKeys = await loadPrimaryKeyRowsForScope(tx, table, state)
391
+ let primaryKeyPredicate = buildPrimaryKeyPredicate(table, primaryKeys)
392
+
393
+ if (!primaryKeyPredicate) {
394
+ return {
395
+ affectedRows: 0,
396
+ insertId: undefined,
397
+ rows: returning ? [] : undefined,
398
+ }
399
+ }
400
+
401
+ return tx[executeOperation]({
402
+ kind: 'update',
403
+ table,
404
+ changes: preparedChanges,
405
+ where: [primaryKeyPredicate],
406
+ returning: returning ? normalizeReturningSelection(returning) : undefined,
407
+ })
408
+ })
409
+ } else {
410
+ let operation: UpdateOperation<AnyTable> = {
411
+ kind: 'update',
412
+ table,
413
+ changes: preparedChanges,
414
+ where: [...state.where],
415
+ returning: returning ? normalizeReturningSelection(returning) : undefined,
416
+ }
417
+
418
+ result = await database[executeOperation](operation)
419
+ }
420
+
421
+ let affectedRows = result.affectedRows ?? 0
422
+ runAfterWriteHook(table, {
423
+ operation: 'update',
424
+ tableName: getTableName(table),
425
+ values: [preparedChanges],
426
+ affectedRows,
427
+ insertId: result.insertId,
428
+ })
429
+
430
+ if (!returning) {
431
+ return {
432
+ affectedRows,
433
+ insertId: result.insertId,
434
+ }
435
+ }
436
+
437
+ return {
438
+ affectedRows,
439
+ insertId: result.insertId,
440
+ rows: applyAfterReadHooksToRows(table, normalizeRows(result.rows)),
441
+ }
442
+ }
443
+
444
+ async function executeDelete(
445
+ database: QueryExecutionContext,
446
+ table: AnyTable,
447
+ state: QueryState,
448
+ options?: { returning?: ReturningInput<Record<string, unknown>> },
449
+ ): Promise<WriteResult | WriteRowsResult<Record<string, unknown>>> {
450
+ let returning = options?.returning
451
+ assertReturningCapability(database.adapter, 'delete', returning)
452
+ let tableName = getTableName(table)
453
+ let deleteContext = {
454
+ tableName,
455
+ where: [...state.where],
456
+ orderBy: [...state.orderBy],
457
+ limit: state.limit,
458
+ offset: state.offset,
459
+ }
460
+
461
+ runBeforeDeleteHook(table, deleteContext)
462
+ let result: DataManipulationResult
463
+
464
+ if (hasScopedWriteModifiers(state)) {
465
+ result = await database.transaction(async (tx) => {
466
+ let primaryKeys = await loadPrimaryKeyRowsForScope(tx, table, state)
467
+ let primaryKeyPredicate = buildPrimaryKeyPredicate(table, primaryKeys)
468
+
469
+ if (!primaryKeyPredicate) {
470
+ return {
471
+ affectedRows: 0,
472
+ insertId: undefined,
473
+ rows: returning ? [] : undefined,
474
+ }
475
+ }
476
+
477
+ return tx[executeOperation]({
478
+ kind: 'delete',
479
+ table,
480
+ where: [primaryKeyPredicate],
481
+ returning: returning ? normalizeReturningSelection(returning) : undefined,
482
+ })
483
+ })
484
+ } else {
485
+ let operation: DeleteOperation<AnyTable> = {
486
+ kind: 'delete',
487
+ table,
488
+ where: [...state.where],
489
+ returning: returning ? normalizeReturningSelection(returning) : undefined,
490
+ }
491
+
492
+ result = await database[executeOperation](operation)
493
+ }
494
+
495
+ let affectedRows = result.affectedRows ?? 0
496
+ runAfterDeleteHook(table, {
497
+ tableName,
498
+ where: deleteContext.where,
499
+ orderBy: deleteContext.orderBy,
500
+ limit: deleteContext.limit,
501
+ offset: deleteContext.offset,
502
+ affectedRows,
503
+ })
504
+
505
+ if (!returning) {
506
+ return {
507
+ affectedRows,
508
+ insertId: result.insertId,
509
+ }
510
+ }
511
+
512
+ return {
513
+ affectedRows,
514
+ insertId: result.insertId,
515
+ rows: applyAfterReadHooksToRows(table, normalizeRows(result.rows)),
516
+ }
517
+ }
518
+
519
+ async function executeUpsert(
520
+ database: QueryExecutionContext,
521
+ table: AnyTable,
522
+ values: Record<string, unknown>,
523
+ options?: {
524
+ returning?: ReturningInput<Record<string, unknown>>
525
+ touch?: boolean
526
+ conflictTarget?: string[]
527
+ update?: Record<string, unknown>
528
+ },
529
+ ): Promise<WriteResult | WriteRowResult<Record<string, unknown>>> {
530
+ if (!database.adapter.capabilities.upsert) {
531
+ throw new DataTableQueryError('Adapter does not support upsert')
532
+ }
533
+
534
+ let preparedValues = prepareInsertValues(
535
+ table,
536
+ values as never,
537
+ database.now(),
538
+ options?.touch ?? true,
539
+ )
540
+ let updateChanges = options?.update
541
+ ? prepareUpdateValues(
542
+ table,
543
+ options.update as never,
544
+ database.now(),
545
+ options?.touch ?? true,
546
+ 'create',
547
+ )
548
+ : undefined
549
+ let returning = options?.returning
550
+ assertReturningCapability(database.adapter, 'upsert', returning)
551
+
552
+ if (returning) {
553
+ let operation: UpsertOperation<AnyTable> = {
554
+ kind: 'upsert',
555
+ table,
556
+ values: preparedValues,
557
+ conflictTarget: options?.conflictTarget ? [...options.conflictTarget] : undefined,
558
+ update: updateChanges,
559
+ returning: normalizeReturningSelection(returning),
560
+ }
561
+
562
+ let result = await database[executeOperation](operation)
563
+ let row = applyAfterReadHooksToRows(table, normalizeRows(result.rows))[0] ?? null
564
+ let affectedRows = result.affectedRows ?? 0
565
+ let preparedWriteValues = updateChanges ? [preparedValues, updateChanges] : [preparedValues]
566
+ runAfterWriteHook(table, {
567
+ operation: 'create',
568
+ tableName: getTableName(table),
569
+ values: preparedWriteValues,
570
+ affectedRows,
571
+ insertId: result.insertId,
572
+ })
573
+
574
+ return {
575
+ affectedRows,
576
+ insertId: result.insertId,
577
+ row,
578
+ }
579
+ }
580
+
581
+ let operation: UpsertOperation<AnyTable> = {
582
+ kind: 'upsert',
583
+ table,
584
+ values: preparedValues,
585
+ conflictTarget: options?.conflictTarget ? [...options.conflictTarget] : undefined,
586
+ update: updateChanges,
587
+ }
588
+
589
+ let result = await database[executeOperation](operation)
590
+ let affectedRows = result.affectedRows ?? 0
591
+ let preparedWriteValues = updateChanges ? [preparedValues, updateChanges] : [preparedValues]
592
+ runAfterWriteHook(table, {
593
+ operation: 'create',
594
+ tableName: getTableName(table),
595
+ values: preparedWriteValues,
596
+ affectedRows,
597
+ insertId: result.insertId,
598
+ })
599
+
600
+ return {
601
+ affectedRows,
602
+ insertId: result.insertId,
603
+ }
604
+ }
605
+
606
+ function createSelectOperation(table: AnyTable, state: QueryState): SelectOperation<AnyTable> {
607
+ let clonedState = cloneQueryState(state)
608
+
609
+ return {
610
+ kind: 'select',
611
+ table,
612
+ select: cloneSelection(clonedState.select),
613
+ distinct: clonedState.distinct,
614
+ joins: clonedState.joins,
615
+ where: clonedState.where,
616
+ groupBy: clonedState.groupBy,
617
+ having: clonedState.having,
618
+ orderBy: clonedState.orderBy,
619
+ limit: clonedState.limit,
620
+ offset: clonedState.offset,
621
+ }
622
+ }
623
+
624
+ function cloneSelection(selection: '*' | SelectColumn[]): '*' | SelectColumn[] {
625
+ if (selection === '*') {
626
+ return '*'
627
+ }
628
+
629
+ return selection.map((column) => ({ ...column }))
630
+ }
631
+
632
+ function normalizeRows(rows: DataManipulationResult['rows']): Record<string, unknown>[] {
633
+ if (!rows) {
634
+ return []
635
+ }
636
+
637
+ return rows.map((row) => ({ ...row }))
638
+ }