@remix-run/data-table-postgres 0.3.1 → 0.5.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.
@@ -0,0 +1,760 @@
1
+ import type {
2
+ DataManipulationOperation,
3
+ DataManipulationRequest,
4
+ DataManipulationResult,
5
+ DatabaseDriver,
6
+ SqlStatement,
7
+ TableRef,
8
+ TransactionOptions,
9
+ TransactionToken,
10
+ } from '@remix-run/data-table'
11
+ import { AsyncLocalStorage } from 'node:async_hooks'
12
+
13
+ import { getTablePrimaryKey } from '@remix-run/data-table'
14
+ import pg from 'pg'
15
+ import type {
16
+ Client as PostgresClient,
17
+ Pool as PostgresPool,
18
+ PoolClient as PostgresPoolClient,
19
+ } from 'pg'
20
+
21
+ import { compilePostgresOperation } from './sql-compiler.ts'
22
+
23
+ type TransactionState = {
24
+ client: PostgresClient | PostgresPoolClient
25
+ releaseOnClose: boolean
26
+ }
27
+
28
+ type PostgresPoolConfig = ConstructorParameters<typeof pg.Pool>[0]
29
+ type PostgresClientConfig = ConstructorParameters<typeof pg.Client>[0]
30
+
31
+ /** Database recreation options for a config-backed PostgreSQL driver. */
32
+ export interface PostgresDatabaseDriverOptions {
33
+ /** Database used while dropping and recreating the configured database (`postgres` by default). */
34
+ maintenanceDatabase?: string
35
+ /** Template used to recreate the configured database (`template0` by default). */
36
+ template?: string
37
+ }
38
+
39
+ type PostgresQueryable = PostgresClient | PostgresPool | PostgresPoolClient
40
+
41
+ const postgresCapabilities = Object.freeze({
42
+ returning: true,
43
+ savepoints: true,
44
+ upsert: true,
45
+ transactionalDdl: true,
46
+ migrationLock: true,
47
+ })
48
+
49
+ export type PostgresDatabaseInput = PostgresPoolConfig | PostgresQueryable
50
+
51
+ /**
52
+ * PostgreSQL database driver backed by a postgres-compatible client.
53
+ */
54
+ export class PostgresDatabaseDriver implements DatabaseDriver<'postgres'> {
55
+ /**
56
+ * The SQL dialect identifier reported by this database.
57
+ */
58
+ get dialect(): 'postgres' {
59
+ return 'postgres'
60
+ }
61
+
62
+ /**
63
+ * Feature flags describing the PostgreSQL behaviors supported by this database.
64
+ */
65
+ get capabilities() {
66
+ return postgresCapabilities
67
+ }
68
+
69
+ #config?: PostgresPoolConfig
70
+ #client: PostgresQueryable
71
+ #maintenanceDatabase: string
72
+ #template: string
73
+ #transactions = new Map<string, TransactionState>()
74
+ #transactionCounter = 0
75
+ #migrationLockQueue = Promise.resolve()
76
+ #migrationLockStore = new AsyncLocalStorage<boolean>()
77
+ #poolClosed = false
78
+
79
+ constructor(config: PostgresDatabaseInput, options: PostgresDatabaseDriverOptions = {}) {
80
+ if (isPostgresQueryable(config)) {
81
+ this.#client = config
82
+ } else {
83
+ this.#config = config
84
+ this.#client = new pg.Pool(config)
85
+ }
86
+
87
+ this.#maintenanceDatabase = options.maintenanceDatabase ?? 'postgres'
88
+ this.#template = options.template ?? 'template0'
89
+ }
90
+
91
+ /**
92
+ * Compiles a data-manipulation operation to postgres SQL statements.
93
+ * @param operation Operation to compile.
94
+ * @returns Compiled SQL statements.
95
+ */
96
+ compileSql(operation: DataManipulationOperation): SqlStatement[] {
97
+ let compiled = compilePostgresOperation(operation)
98
+ return [{ text: compiled.text, values: compiled.values }]
99
+ }
100
+
101
+ /**
102
+ * Executes a postgres data-manipulation request.
103
+ * @param request Request to execute.
104
+ * @returns Execution result.
105
+ */
106
+ async execute(request: DataManipulationRequest): Promise<DataManipulationResult> {
107
+ if (request.operation.kind === 'insertMany' && request.operation.values.length === 0) {
108
+ return {
109
+ affectedRows: 0,
110
+ insertId: undefined,
111
+ rows: request.operation.returning ? [] : undefined,
112
+ }
113
+ }
114
+
115
+ let statement = compilePostgresOperation(request.operation)
116
+ let client = this.#resolveClient(request.transaction)
117
+ let result = await client.query(statement.text, statement.values)
118
+ let rows = normalizeRows(result.rows)
119
+
120
+ if (request.operation.kind === 'count' || request.operation.kind === 'exists') {
121
+ rows = normalizeCountRows(rows)
122
+ }
123
+
124
+ return {
125
+ rows,
126
+ affectedRows: normalizeAffectedRows(request.operation.kind, result.rowCount, rows),
127
+ insertId: normalizeInsertId(request.operation.kind, request.operation, rows),
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Executes a multi-statement postgres SQL script.
133
+ *
134
+ * Postgres natively supports multi-statement scripts when `query` is called
135
+ * without a parameter array.
136
+ * @param sql SQL script to execute.
137
+ * @param transaction Optional transaction token.
138
+ * @returns A promise that resolves once execution completes.
139
+ */
140
+ async executeScript(sql: string, transaction?: TransactionToken): Promise<void> {
141
+ let client = this.#resolveClient(transaction)
142
+ await client.query(sql)
143
+ }
144
+
145
+ /**
146
+ * Checks whether a table exists in postgres.
147
+ * @param table Table reference to inspect.
148
+ * @param transaction Optional transaction token.
149
+ * @returns `true` when the table exists.
150
+ */
151
+ async hasTable(table: TableRef, transaction?: TransactionToken): Promise<boolean> {
152
+ let relation = toPostgresRelationName(table)
153
+ let client = this.#resolveClient(transaction)
154
+ let result = await client.query('select to_regclass($1) is not null as "exists"', [relation])
155
+ let row = result.rows[0] as Record<string, unknown> | undefined
156
+ return toBooleanExists(row?.exists)
157
+ }
158
+
159
+ /**
160
+ * Checks whether a column exists in postgres.
161
+ * @param table Table reference to inspect.
162
+ * @param column Column name to look up.
163
+ * @param transaction Optional transaction token.
164
+ * @returns `true` when the column exists.
165
+ */
166
+ async hasColumn(
167
+ table: TableRef,
168
+ column: string,
169
+ transaction?: TransactionToken,
170
+ ): Promise<boolean> {
171
+ let relation = toPostgresRelationName(table)
172
+ let client = this.#resolveClient(transaction)
173
+ let result = await client.query(
174
+ 'select exists (select 1 from pg_attribute where attrelid = to_regclass($1) and attname = $2 and attnum > 0 and not attisdropped) as "exists"',
175
+ [relation, column],
176
+ )
177
+ let row = result.rows[0] as Record<string, unknown> | undefined
178
+ return toBooleanExists(row?.exists)
179
+ }
180
+
181
+ /**
182
+ * Starts a postgres transaction.
183
+ * @param options Transaction options.
184
+ * @returns Transaction token.
185
+ */
186
+ async beginTransaction(options?: TransactionOptions): Promise<TransactionToken> {
187
+ let releaseOnClose = false
188
+ let transactionClient: PostgresClient | PostgresPoolClient
189
+
190
+ if (isPostgresPool(this.#client)) {
191
+ transactionClient = await this.#client.connect()
192
+ releaseOnClose = true
193
+ } else {
194
+ transactionClient = this.#client
195
+ }
196
+
197
+ try {
198
+ await transactionClient.query('begin')
199
+
200
+ if (options?.isolationLevel || options?.readOnly !== undefined) {
201
+ await transactionClient.query(buildSetTransactionStatement(options))
202
+ }
203
+ } catch (error) {
204
+ if (releaseOnClose) {
205
+ destroyPostgresClient(transactionClient, error)
206
+ }
207
+ throw error
208
+ }
209
+
210
+ this.#transactionCounter += 1
211
+ let token = { id: 'tx_' + String(this.#transactionCounter) }
212
+
213
+ this.#transactions.set(token.id, {
214
+ client: transactionClient,
215
+ releaseOnClose,
216
+ })
217
+
218
+ return token
219
+ }
220
+
221
+ /**
222
+ * Commits an open postgres transaction.
223
+ * @param token Transaction token to commit.
224
+ * @returns A promise that resolves when the transaction is committed.
225
+ */
226
+ async commitTransaction(token: TransactionToken): Promise<void> {
227
+ let transaction = this.#transactions.get(token.id)
228
+
229
+ if (!transaction) {
230
+ throw new Error('Unknown transaction token: ' + token.id)
231
+ }
232
+
233
+ let failure: unknown
234
+ try {
235
+ await transaction.client.query('commit')
236
+ } catch (error) {
237
+ failure = error
238
+ throw error
239
+ } finally {
240
+ this.#transactions.delete(token.id)
241
+
242
+ if (transaction.releaseOnClose) {
243
+ if (failure === undefined) {
244
+ releasePostgresClient(transaction.client)
245
+ } else {
246
+ destroyPostgresClient(transaction.client, failure)
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Rolls back an open postgres transaction.
254
+ * @param token Transaction token to roll back.
255
+ * @returns A promise that resolves when the transaction is rolled back.
256
+ */
257
+ async rollbackTransaction(token: TransactionToken): Promise<void> {
258
+ let transaction = this.#transactions.get(token.id)
259
+
260
+ if (!transaction) {
261
+ throw new Error('Unknown transaction token: ' + token.id)
262
+ }
263
+
264
+ let failure: unknown
265
+ try {
266
+ await transaction.client.query('rollback')
267
+ } catch (error) {
268
+ failure = error
269
+ throw error
270
+ } finally {
271
+ this.#transactions.delete(token.id)
272
+
273
+ if (transaction.releaseOnClose) {
274
+ if (failure === undefined) {
275
+ releasePostgresClient(transaction.client)
276
+ } else {
277
+ destroyPostgresClient(transaction.client, failure)
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Creates a savepoint in an open postgres transaction.
285
+ * @param token Transaction token to use.
286
+ * @param name Savepoint name.
287
+ * @returns A promise that resolves when the savepoint is created.
288
+ */
289
+ async createSavepoint(token: TransactionToken, name: string): Promise<void> {
290
+ let client = this.#transactionClient(token)
291
+ await client.query('savepoint ' + quoteIdentifier(name))
292
+ }
293
+
294
+ /**
295
+ * Rolls back to a savepoint in an open postgres transaction.
296
+ * @param token Transaction token to use.
297
+ * @param name Savepoint name.
298
+ * @returns A promise that resolves when the rollback completes.
299
+ */
300
+ async rollbackToSavepoint(token: TransactionToken, name: string): Promise<void> {
301
+ let client = this.#transactionClient(token)
302
+ await client.query('rollback to savepoint ' + quoteIdentifier(name))
303
+ }
304
+
305
+ /**
306
+ * Releases a savepoint in an open postgres transaction.
307
+ * @param token Transaction token to use.
308
+ * @param name Savepoint name.
309
+ * @returns A promise that resolves when the savepoint is released.
310
+ */
311
+ async releaseSavepoint(token: TransactionToken, name: string): Promise<void> {
312
+ let client = this.#transactionClient(token)
313
+ await client.query('release savepoint ' + quoteIdentifier(name))
314
+ }
315
+
316
+ /**
317
+ * Destructively recreates the configured PostgreSQL database.
318
+ * @returns A promise that resolves when the database is ready for use.
319
+ */
320
+ async wipe(): Promise<void> {
321
+ let config = this.#configOrThrow('wipe')
322
+ this.#assertNoOpenTransactions('wipe')
323
+ let database = resolvePostgresDatabaseName(config)
324
+ // Resolve the maintenance config before closing the pool so a config
325
+ // error cannot leave the database without a usable pool.
326
+ let maintenanceConfig = this.#maintenanceConfig(database)
327
+ await this.#closePool()
328
+ let maintenance: PostgresClient | undefined
329
+
330
+ try {
331
+ maintenance = new pg.Client(maintenanceConfig)
332
+ await maintenance.connect()
333
+ await maintenance.query(
334
+ 'select pg_terminate_backend(pid) from pg_stat_activity where datname = $1 and pid <> pg_backend_pid()',
335
+ [database],
336
+ )
337
+ await maintenance.query('drop database if exists ' + quoteIdentifier(database))
338
+ await maintenance.query(
339
+ 'create database ' +
340
+ quoteIdentifier(database) +
341
+ ' template ' +
342
+ quoteIdentifier(this.#template),
343
+ )
344
+ } finally {
345
+ try {
346
+ await maintenance?.end()
347
+ } finally {
348
+ await this.#replacePool()
349
+ }
350
+ }
351
+ }
352
+
353
+ /** Closes a pool created from configuration. Supplied clients and pools remain caller-owned. */
354
+ async close(): Promise<void> {
355
+ this.#assertNoOpenTransactions('close')
356
+ if (this.#config) {
357
+ await this.#closePool()
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Runs migration work on the postgres connection that owns the advisory lock.
363
+ *
364
+ * Lock acquisition waits up to 60 seconds and throws when the lock cannot
365
+ * be acquired. Re-entering this method from inside `run` throws instead of
366
+ * deadlocking, and a failed run destroys the reserved connection instead of
367
+ * returning it to the pool.
368
+ * @param name Logical migration lock name.
369
+ * @param run Migration work to run with a connection-bound driver.
370
+ * @returns The callback result.
371
+ */
372
+ async withMigrationLock<result>(
373
+ name: string,
374
+ run: (driver: DatabaseDriver<'postgres'>) => Promise<result>,
375
+ ): Promise<result> {
376
+ if (this.#migrationLockStore.getStore()) {
377
+ throw new Error('Postgres migration lock is already held by this database')
378
+ }
379
+
380
+ let waitForPreviousLock = this.#migrationLockQueue
381
+ let releaseQueue: () => void = () => undefined
382
+ this.#migrationLockQueue = new Promise((resolve) => {
383
+ releaseQueue = resolve
384
+ })
385
+
386
+ await waitForPreviousLock
387
+
388
+ try {
389
+ let releaseOnClose = false
390
+ let client: PostgresClient | PostgresPoolClient
391
+
392
+ if (isPostgresPool(this.#client)) {
393
+ client = await this.#client.connect()
394
+ releaseOnClose = true
395
+ } else {
396
+ client = this.#client
397
+ }
398
+
399
+ let driver = releaseOnClose ? new PostgresDatabaseDriver(client) : this
400
+
401
+ try {
402
+ let value = await this.#migrationLockStore.run(true, () =>
403
+ runWithPostgresMigrationLock(client, name, driver, run),
404
+ )
405
+
406
+ if (releaseOnClose) {
407
+ releasePostgresClient(client)
408
+ }
409
+
410
+ return value
411
+ } catch (error) {
412
+ // A failed run can leave the reserved session dirty (aborted
413
+ // transaction, still-held advisory lock), so destroy the connection
414
+ // instead of returning it to the pool.
415
+ if (releaseOnClose) {
416
+ destroyPostgresClient(client, error)
417
+ }
418
+
419
+ throw error
420
+ }
421
+ } finally {
422
+ releaseQueue()
423
+ }
424
+ }
425
+
426
+ async #closePool(): Promise<void> {
427
+ this.#transactions.clear()
428
+ // pg pools reject end() when called twice, so ending must be tracked to
429
+ // keep close() idempotent.
430
+ if (isPostgresPool(this.#client) && !this.#poolClosed) {
431
+ this.#poolClosed = true
432
+ await this.#client.end()
433
+ }
434
+ }
435
+
436
+ #configOrThrow(method: string): PostgresPoolConfig {
437
+ if (!this.#config) {
438
+ throw new Error('Postgres database ' + method + '() requires config-based construction')
439
+ }
440
+
441
+ return this.#config
442
+ }
443
+
444
+ #assertNoOpenTransactions(method: string): void {
445
+ if (this.#transactions.size > 0) {
446
+ throw new Error('Postgres database cannot ' + method + ' while transactions are open')
447
+ }
448
+ }
449
+
450
+ #maintenanceConfig(targetDatabase: string): PostgresClientConfig {
451
+ let maintenanceDatabase = this.#maintenanceDatabase
452
+
453
+ if (maintenanceDatabase === targetDatabase) {
454
+ maintenanceDatabase = targetDatabase === 'postgres' ? 'template1' : 'postgres'
455
+ }
456
+
457
+ let config = this.#configOrThrow('maintenance')
458
+ let connectionString = replaceDatabaseInConnectionString(
459
+ config?.connectionString,
460
+ maintenanceDatabase,
461
+ )
462
+
463
+ return { ...config, connectionString, database: maintenanceDatabase }
464
+ }
465
+
466
+ async #replacePool(): Promise<void> {
467
+ await this.#closePool().catch(() => undefined)
468
+ if (this.#config) {
469
+ this.#client = new pg.Pool(this.#config)
470
+ this.#poolClosed = false
471
+ }
472
+ }
473
+
474
+ #resolveClient(token: TransactionToken | undefined): PostgresQueryable {
475
+ if (!token) {
476
+ return this.#client
477
+ }
478
+
479
+ return this.#transactionClient(token)
480
+ }
481
+
482
+ #transactionClient(token: TransactionToken): PostgresClient | PostgresPoolClient {
483
+ let transaction = this.#transactions.get(token.id)
484
+
485
+ if (!transaction) {
486
+ throw new Error('Unknown transaction token: ' + token.id)
487
+ }
488
+
489
+ return transaction.client
490
+ }
491
+ }
492
+
493
+ function isPostgresQueryable(value: unknown): value is PostgresQueryable {
494
+ return typeof value === 'object' && value !== null && 'query' in value
495
+ }
496
+
497
+ function isPostgresPool(client: PostgresQueryable): client is PostgresPool {
498
+ if (client instanceof pg.Client) {
499
+ return false
500
+ }
501
+
502
+ return 'connect' in client && typeof client.connect === 'function' && !('release' in client)
503
+ }
504
+
505
+ function resolvePostgresDatabaseName(config: PostgresPoolConfig): string {
506
+ let database =
507
+ resolveDatabaseNameFromConnectionString(config?.connectionString) ??
508
+ config?.database ??
509
+ process.env.PGDATABASE
510
+
511
+ if (!database) {
512
+ throw new Error('Postgres database config requires a database name')
513
+ }
514
+
515
+ return database
516
+ }
517
+
518
+ function replaceDatabaseInConnectionString(
519
+ connectionString: string | undefined,
520
+ database: string,
521
+ ): string | undefined {
522
+ if (!connectionString) {
523
+ return undefined
524
+ }
525
+
526
+ let url: URL
527
+
528
+ try {
529
+ url = new URL(connectionString)
530
+ } catch (cause) {
531
+ throw new Error(
532
+ 'Postgres connection string must be a valid URL to resolve the maintenance database',
533
+ { cause },
534
+ )
535
+ }
536
+
537
+ url.pathname = '/' + encodeURIComponent(database)
538
+ return url.toString()
539
+ }
540
+
541
+ function resolveDatabaseNameFromConnectionString(
542
+ connectionString: string | undefined,
543
+ ): string | undefined {
544
+ if (!connectionString) {
545
+ return undefined
546
+ }
547
+
548
+ try {
549
+ let url = new URL(connectionString)
550
+ let database = decodeURIComponent(url.pathname.replace(/^\//, ''))
551
+ return database || undefined
552
+ } catch {
553
+ return undefined
554
+ }
555
+ }
556
+
557
+ function releasePostgresClient(client: PostgresClient | PostgresPoolClient): void {
558
+ let release = (client as { release?: () => void }).release
559
+ release?.()
560
+ }
561
+
562
+ function destroyPostgresClient(client: PostgresClient | PostgresPoolClient, error: unknown): void {
563
+ let release = (client as { release?: (destroy?: Error | boolean) => void }).release
564
+
565
+ if (typeof release === 'function') {
566
+ // A truthy argument tells pg to destroy the client instead of pooling it.
567
+ release.call(client, error instanceof Error ? error : true)
568
+ return
569
+ }
570
+
571
+ void (client as PostgresClient).end().catch(() => undefined)
572
+ }
573
+
574
+ // Matches the 60 second wait bound used by the MySQL driver's get_lock().
575
+ const MIGRATION_LOCK_TIMEOUT_MS = 60_000
576
+
577
+ async function runWithPostgresMigrationLock<result>(
578
+ client: PostgresClient | PostgresPoolClient,
579
+ name: string,
580
+ driver: PostgresDatabaseDriver,
581
+ run: (driver: DatabaseDriver<'postgres'>) => Promise<result>,
582
+ ): Promise<result> {
583
+ await client.query('set lock_timeout to ' + String(MIGRATION_LOCK_TIMEOUT_MS))
584
+
585
+ try {
586
+ await client.query('select pg_advisory_lock(hashtext($1))', [name])
587
+ } catch (cause) {
588
+ await client.query('set lock_timeout to default').catch(() => undefined)
589
+ throw new Error('Postgres migration lock could not be acquired', { cause })
590
+ }
591
+
592
+ await client.query('set lock_timeout to default')
593
+
594
+ let outcome: { status: 'success'; value: result } | { status: 'failure'; error: unknown }
595
+
596
+ try {
597
+ outcome = { status: 'success', value: await run(driver) }
598
+ } catch (error) {
599
+ outcome = { status: 'failure', error }
600
+ }
601
+
602
+ let unlockFailed = false
603
+ let unlockError: unknown
604
+
605
+ try {
606
+ let result = await client.query('select pg_advisory_unlock(hashtext($1)) as "released"', [name])
607
+ let row = result.rows[0] as Record<string, unknown> | undefined
608
+
609
+ if (!toBooleanExists(row?.released)) {
610
+ throw new Error('Postgres migration lock was not held by the reserved connection')
611
+ }
612
+ } catch (error) {
613
+ unlockFailed = true
614
+ unlockError = error
615
+ }
616
+
617
+ if (outcome.status === 'failure') {
618
+ throw outcome.error
619
+ }
620
+
621
+ if (unlockFailed) {
622
+ throw unlockError
623
+ }
624
+
625
+ return outcome.value
626
+ }
627
+
628
+ function buildSetTransactionStatement(options: TransactionOptions): string {
629
+ let parts = ['set transaction']
630
+
631
+ if (options.isolationLevel) {
632
+ parts.push('isolation level ' + options.isolationLevel)
633
+ }
634
+
635
+ if (options.readOnly !== undefined) {
636
+ parts.push(options.readOnly ? 'read only' : 'read write')
637
+ }
638
+
639
+ return parts.join(' ')
640
+ }
641
+
642
+ function normalizeRows(rows: unknown[]): Record<string, unknown>[] {
643
+ return rows.map((row) => {
644
+ if (typeof row !== 'object' || row === null) {
645
+ return {}
646
+ }
647
+
648
+ return { ...(row as Record<string, unknown>) }
649
+ })
650
+ }
651
+
652
+ function normalizeCountRows(rows: Record<string, unknown>[]): Record<string, unknown>[] {
653
+ return rows.map((row) => {
654
+ let count = row.count
655
+
656
+ if (typeof count === 'string') {
657
+ let numeric = Number(count)
658
+
659
+ if (!Number.isNaN(numeric)) {
660
+ return {
661
+ ...row,
662
+ count: numeric,
663
+ }
664
+ }
665
+ }
666
+
667
+ if (typeof count === 'bigint') {
668
+ return {
669
+ ...row,
670
+ count: Number(count),
671
+ }
672
+ }
673
+
674
+ return row
675
+ })
676
+ }
677
+
678
+ function normalizeAffectedRows(
679
+ kind: DataManipulationRequest['operation']['kind'],
680
+ rowCount: number | null,
681
+ rows: Record<string, unknown>[],
682
+ ): number | undefined {
683
+ if (kind === 'select' || kind === 'count' || kind === 'exists') {
684
+ return undefined
685
+ }
686
+
687
+ if (rowCount !== null) {
688
+ return rowCount
689
+ }
690
+
691
+ if (kind === 'raw') {
692
+ return undefined
693
+ }
694
+
695
+ return rows.length
696
+ }
697
+
698
+ function normalizeInsertId(
699
+ kind: DataManipulationRequest['operation']['kind'],
700
+ operation: DataManipulationRequest['operation'],
701
+ rows: Record<string, unknown>[],
702
+ ): unknown {
703
+ if (!isInsertOperationKind(kind) || !isInsertOperation(operation)) {
704
+ return undefined
705
+ }
706
+
707
+ let primaryKey = getTablePrimaryKey(operation.table)
708
+
709
+ if (primaryKey.length !== 1) {
710
+ return undefined
711
+ }
712
+
713
+ let key = primaryKey[0]
714
+ let row = rows[rows.length - 1]
715
+
716
+ return row ? row[key] : undefined
717
+ }
718
+
719
+ function quoteIdentifier(value: string): string {
720
+ return '"' + value.replace(/"/g, '""') + '"'
721
+ }
722
+
723
+ function toPostgresRelationName(table: TableRef): string {
724
+ if (table.schema) {
725
+ return quoteIdentifier(table.schema) + '.' + quoteIdentifier(table.name)
726
+ }
727
+
728
+ return quoteIdentifier(table.name)
729
+ }
730
+
731
+ function toBooleanExists(value: unknown): boolean {
732
+ if (typeof value === 'boolean') {
733
+ return value
734
+ }
735
+
736
+ if (typeof value === 'number') {
737
+ return value > 0
738
+ }
739
+
740
+ if (typeof value === 'string') {
741
+ return value === 't' || value === 'true' || value === '1'
742
+ }
743
+
744
+ return false
745
+ }
746
+
747
+ function isInsertOperationKind(kind: DataManipulationRequest['operation']['kind']): boolean {
748
+ return kind === 'insert' || kind === 'insertMany' || kind === 'upsert'
749
+ }
750
+
751
+ function isInsertOperation(
752
+ operation: DataManipulationRequest['operation'],
753
+ ): operation is Extract<
754
+ DataManipulationRequest['operation'],
755
+ { kind: 'insert' | 'insertMany' | 'upsert' }
756
+ > {
757
+ return (
758
+ operation.kind === 'insert' || operation.kind === 'insertMany' || operation.kind === 'upsert'
759
+ )
760
+ }