@remix-run/data-table-postgres 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.
@@ -1,51 +1,27 @@
1
1
  import type {
2
2
  AdapterCapabilityOverrides,
3
- AdapterExecuteRequest,
4
- AdapterResult,
3
+ DataMigrationRequest,
4
+ DataManipulationRequest,
5
+ DataMigrationResult,
6
+ DataMigrationOperation,
7
+ DataManipulationResult,
8
+ DataManipulationOperation,
5
9
  DatabaseAdapter,
10
+ ColumnDefinition,
11
+ SqlStatement,
12
+ TableRef,
6
13
  TransactionOptions,
7
14
  TransactionToken,
8
15
  } from '@remix-run/data-table'
9
16
  import { getTablePrimaryKey } from '@remix-run/data-table'
17
+ import {
18
+ isDataManipulationOperation as isDataManipulationOperationHelper,
19
+ quoteLiteral as quoteLiteralHelper,
20
+ quoteTableRef as quoteTableRefHelper,
21
+ } from '@remix-run/data-table/sql-helpers'
22
+ import type { Pool as PostgresPool, PoolClient as PostgresPoolClient } from 'pg'
10
23
 
11
- import { compilePostgresStatement } from './sql-compiler.ts'
12
-
13
- type Pretty<value> = {
14
- [key in keyof value]: value[key]
15
- } & {}
16
-
17
- /**
18
- * Result shape returned by postgres client `query()` calls.
19
- */
20
- export type PostgresQueryResult = {
21
- rows: unknown[]
22
- rowCount: number | null
23
- }
24
-
25
- /**
26
- * Minimal postgres client contract used by this adapter.
27
- */
28
- export type PostgresDatabaseClient = {
29
- query(text: string, values?: unknown[]): Promise<PostgresQueryResult>
30
- }
31
-
32
- /**
33
- * Postgres transaction client with optional connection release support.
34
- */
35
- export type PostgresTransactionClient = Pretty<
36
- PostgresDatabaseClient & {
37
- release?: () => void
38
- }
39
- >
40
-
41
- /**
42
- * Postgres pool-like client contract used by this adapter.
43
- */
44
- export type PostgresDatabasePool = Pretty<
45
- PostgresDatabaseClient & {
46
- connect?: () => Promise<PostgresTransactionClient>
47
- }
48
- >
24
+ import { compilePostgresOperation } from './sql-compiler.ts'
49
25
 
50
26
  /**
51
27
  * Postgres adapter configuration.
@@ -55,60 +31,149 @@ export type PostgresDatabaseAdapterOptions = {
55
31
  }
56
32
 
57
33
  type TransactionState = {
58
- client: PostgresTransactionClient
34
+ client: PostgresPoolClient
59
35
  releaseOnClose: boolean
60
36
  }
61
37
 
38
+ type PostgresQueryable = PostgresPool | PostgresPoolClient
39
+
62
40
  /**
63
41
  * `DatabaseAdapter` implementation for postgres-compatible clients.
64
42
  */
65
43
  export class PostgresDatabaseAdapter implements DatabaseAdapter {
44
+ /**
45
+ * The SQL dialect identifier reported by this adapter.
46
+ */
66
47
  dialect = 'postgres'
48
+
49
+ /**
50
+ * Feature flags describing the postgres behaviors supported by this adapter.
51
+ */
67
52
  capabilities
68
53
 
69
- #client: PostgresDatabasePool
54
+ #client: PostgresQueryable
70
55
  #transactions = new Map<string, TransactionState>()
71
56
  #transactionCounter = 0
72
57
 
73
- constructor(client: PostgresDatabasePool, options?: PostgresDatabaseAdapterOptions) {
58
+ constructor(client: PostgresQueryable, options?: PostgresDatabaseAdapterOptions) {
74
59
  this.#client = client
75
60
  this.capabilities = {
76
61
  returning: options?.capabilities?.returning ?? true,
77
62
  savepoints: options?.capabilities?.savepoints ?? true,
78
63
  upsert: options?.capabilities?.upsert ?? true,
64
+ transactionalDdl: options?.capabilities?.transactionalDdl ?? true,
65
+ migrationLock: options?.capabilities?.migrationLock ?? true,
79
66
  }
80
67
  }
81
68
 
82
- async execute(request: AdapterExecuteRequest): Promise<AdapterResult> {
83
- if (request.statement.kind === 'insertMany' && request.statement.values.length === 0) {
69
+ /**
70
+ * Compiles a data or migration operation to postgres SQL statements.
71
+ * @param operation Operation to compile.
72
+ * @returns Compiled SQL statements.
73
+ */
74
+ compileSql(operation: DataManipulationOperation | DataMigrationOperation): SqlStatement[] {
75
+ if (isDataManipulationOperation(operation)) {
76
+ let compiled = compilePostgresOperation(operation)
77
+ return [{ text: compiled.text, values: compiled.values }]
78
+ }
79
+
80
+ return compilePostgresMigrationOperations(operation)
81
+ }
82
+
83
+ /**
84
+ * Executes a postgres data-manipulation request.
85
+ * @param request Request to execute.
86
+ * @returns Execution result.
87
+ */
88
+ async execute(request: DataManipulationRequest): Promise<DataManipulationResult> {
89
+ if (request.operation.kind === 'insertMany' && request.operation.values.length === 0) {
84
90
  return {
85
91
  affectedRows: 0,
86
92
  insertId: undefined,
87
- rows: request.statement.returning ? [] : undefined,
93
+ rows: request.operation.returning ? [] : undefined,
88
94
  }
89
95
  }
90
96
 
91
- let statement = compilePostgresStatement(request.statement)
97
+ let statement = compilePostgresOperation(request.operation)
92
98
  let client = this.#resolveClient(request.transaction)
93
99
  let result = await client.query(statement.text, statement.values)
94
100
  let rows = normalizeRows(result.rows)
95
101
 
96
- if (request.statement.kind === 'count' || request.statement.kind === 'exists') {
102
+ if (request.operation.kind === 'count' || request.operation.kind === 'exists') {
97
103
  rows = normalizeCountRows(rows)
98
104
  }
99
105
 
100
106
  return {
101
107
  rows,
102
- affectedRows: normalizeAffectedRows(request.statement.kind, result.rowCount, rows),
103
- insertId: normalizeInsertId(request.statement.kind, request.statement, rows),
108
+ affectedRows: normalizeAffectedRows(request.operation.kind, result.rowCount, rows),
109
+ insertId: normalizeInsertId(request.operation.kind, request.operation, rows),
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Executes postgres migration operations.
115
+ * @param request Migration request to execute.
116
+ * @returns Migration result.
117
+ */
118
+ async migrate(request: DataMigrationRequest): Promise<DataMigrationResult> {
119
+ let statements = this.compileSql(request.operation)
120
+ let client = this.#resolveClient(request.transaction)
121
+
122
+ for (let statement of statements) {
123
+ await client.query(statement.text, statement.values)
124
+ }
125
+
126
+ return {
127
+ affectedOperations: statements.length,
104
128
  }
105
129
  }
106
130
 
131
+ /**
132
+ * Checks whether a table exists in postgres.
133
+ * @param table Table reference to inspect.
134
+ * @param transaction Optional transaction token.
135
+ * @returns `true` when the table exists.
136
+ */
137
+ async hasTable(table: TableRef, transaction?: TransactionToken): Promise<boolean> {
138
+ let relation = toPostgresRelationName(table)
139
+ let client = this.#resolveClient(transaction)
140
+ let result = await client.query('select to_regclass($1) is not null as "exists"', [relation])
141
+ let row = result.rows[0] as Record<string, unknown> | undefined
142
+ return toBooleanExists(row?.exists)
143
+ }
144
+
145
+ /**
146
+ * Checks whether a column exists in postgres.
147
+ * @param table Table reference to inspect.
148
+ * @param column Column name to look up.
149
+ * @param transaction Optional transaction token.
150
+ * @returns `true` when the column exists.
151
+ */
152
+ async hasColumn(
153
+ table: TableRef,
154
+ column: string,
155
+ transaction?: TransactionToken,
156
+ ): Promise<boolean> {
157
+ let relation = toPostgresRelationName(table)
158
+ let client = this.#resolveClient(transaction)
159
+ let result = await client.query(
160
+ 'select exists (select 1 from pg_attribute where attrelid = to_regclass($1) and attname = $2 and attnum > 0 and not attisdropped) as "exists"',
161
+ [relation, column],
162
+ )
163
+ let row = result.rows[0] as Record<string, unknown> | undefined
164
+ return toBooleanExists(row?.exists)
165
+ }
166
+
167
+ /**
168
+ * Starts a postgres transaction.
169
+ * @param options Transaction options.
170
+ * @returns Transaction token.
171
+ */
107
172
  async beginTransaction(options?: TransactionOptions): Promise<TransactionToken> {
108
173
  let releaseOnClose = false
109
- let transactionClient: PostgresTransactionClient
174
+ let transactionClient: PostgresPoolClient
110
175
 
111
- if (this.#client.connect) {
176
+ if (isPostgresPool(this.#client)) {
112
177
  transactionClient = await this.#client.connect()
113
178
  releaseOnClose = true
114
179
  } else {
@@ -132,6 +197,11 @@ export class PostgresDatabaseAdapter implements DatabaseAdapter {
132
197
  return token
133
198
  }
134
199
 
200
+ /**
201
+ * Commits an open postgres transaction.
202
+ * @param token Transaction token to commit.
203
+ * @returns A promise that resolves when the transaction is committed.
204
+ */
135
205
  async commitTransaction(token: TransactionToken): Promise<void> {
136
206
  let transaction = this.#transactions.get(token.id)
137
207
 
@@ -145,11 +215,16 @@ export class PostgresDatabaseAdapter implements DatabaseAdapter {
145
215
  this.#transactions.delete(token.id)
146
216
 
147
217
  if (transaction.releaseOnClose) {
148
- transaction.client.release?.()
218
+ releasePostgresClient(transaction.client)
149
219
  }
150
220
  }
151
221
  }
152
222
 
223
+ /**
224
+ * Rolls back an open postgres transaction.
225
+ * @param token Transaction token to roll back.
226
+ * @returns A promise that resolves when the transaction is rolled back.
227
+ */
153
228
  async rollbackTransaction(token: TransactionToken): Promise<void> {
154
229
  let transaction = this.#transactions.get(token.id)
155
230
 
@@ -163,27 +238,61 @@ export class PostgresDatabaseAdapter implements DatabaseAdapter {
163
238
  this.#transactions.delete(token.id)
164
239
 
165
240
  if (transaction.releaseOnClose) {
166
- transaction.client.release?.()
241
+ releasePostgresClient(transaction.client)
167
242
  }
168
243
  }
169
244
  }
170
245
 
246
+ /**
247
+ * Creates a savepoint in an open postgres transaction.
248
+ * @param token Transaction token to use.
249
+ * @param name Savepoint name.
250
+ * @returns A promise that resolves when the savepoint is created.
251
+ */
171
252
  async createSavepoint(token: TransactionToken, name: string): Promise<void> {
172
253
  let client = this.#transactionClient(token)
173
254
  await client.query('savepoint ' + quoteIdentifier(name))
174
255
  }
175
256
 
257
+ /**
258
+ * Rolls back to a savepoint in an open postgres transaction.
259
+ * @param token Transaction token to use.
260
+ * @param name Savepoint name.
261
+ * @returns A promise that resolves when the rollback completes.
262
+ */
176
263
  async rollbackToSavepoint(token: TransactionToken, name: string): Promise<void> {
177
264
  let client = this.#transactionClient(token)
178
265
  await client.query('rollback to savepoint ' + quoteIdentifier(name))
179
266
  }
180
267
 
268
+ /**
269
+ * Releases a savepoint in an open postgres transaction.
270
+ * @param token Transaction token to use.
271
+ * @param name Savepoint name.
272
+ * @returns A promise that resolves when the savepoint is released.
273
+ */
181
274
  async releaseSavepoint(token: TransactionToken, name: string): Promise<void> {
182
275
  let client = this.#transactionClient(token)
183
276
  await client.query('release savepoint ' + quoteIdentifier(name))
184
277
  }
185
278
 
186
- #resolveClient(token: TransactionToken | undefined): PostgresDatabaseClient {
279
+ /**
280
+ * Acquires the postgres migration lock.
281
+ * @returns A promise that resolves when the lock is acquired.
282
+ */
283
+ async acquireMigrationLock(): Promise<void> {
284
+ await this.#client.query('select pg_advisory_lock(hashtext($1))', ['data_table_migrations'])
285
+ }
286
+
287
+ /**
288
+ * Releases the postgres migration lock.
289
+ * @returns A promise that resolves when the lock is released.
290
+ */
291
+ async releaseMigrationLock(): Promise<void> {
292
+ await this.#client.query('select pg_advisory_unlock(hashtext($1))', ['data_table_migrations'])
293
+ }
294
+
295
+ #resolveClient(token: TransactionToken | undefined): PostgresQueryable {
187
296
  if (!token) {
188
297
  return this.#client
189
298
  }
@@ -191,7 +300,7 @@ export class PostgresDatabaseAdapter implements DatabaseAdapter {
191
300
  return this.#transactionClient(token)
192
301
  }
193
302
 
194
- #transactionClient(token: TransactionToken): PostgresTransactionClient {
303
+ #transactionClient(token: TransactionToken): PostgresPoolClient {
195
304
  let transaction = this.#transactions.get(token.id)
196
305
 
197
306
  if (!transaction) {
@@ -204,17 +313,36 @@ export class PostgresDatabaseAdapter implements DatabaseAdapter {
204
313
 
205
314
  /**
206
315
  * Creates a postgres `DatabaseAdapter`.
207
- * @param client Postgres pool or client.
316
+ * @param client `pg` pool or pool client.
208
317
  * @param options Optional adapter capability overrides.
209
318
  * @returns A configured postgres adapter.
319
+ * @example
320
+ * ```ts
321
+ * import { Pool } from 'pg'
322
+ * import { createDatabase } from 'remix/data-table'
323
+ * import { createPostgresDatabaseAdapter } from 'remix/data-table-postgres'
324
+ *
325
+ * let pool = new Pool({ connectionString: process.env.DATABASE_URL })
326
+ * let adapter = createPostgresDatabaseAdapter(pool)
327
+ * let db = createDatabase(adapter)
328
+ * ```
210
329
  */
211
330
  export function createPostgresDatabaseAdapter(
212
- client: PostgresDatabasePool,
331
+ client: PostgresQueryable,
213
332
  options?: PostgresDatabaseAdapterOptions,
214
333
  ): PostgresDatabaseAdapter {
215
334
  return new PostgresDatabaseAdapter(client, options)
216
335
  }
217
336
 
337
+ function isPostgresPool(client: PostgresQueryable): client is PostgresPool {
338
+ return 'connect' in client && typeof client.connect === 'function'
339
+ }
340
+
341
+ function releasePostgresClient(client: PostgresPoolClient): void {
342
+ let release = (client as { release?: () => void }).release
343
+ release?.()
344
+ }
345
+
218
346
  function buildSetTransactionStatement(options: TransactionOptions): string {
219
347
  let parts = ['set transaction']
220
348
 
@@ -266,7 +394,7 @@ function normalizeCountRows(rows: Record<string, unknown>[]): Record<string, unk
266
394
  }
267
395
 
268
396
  function normalizeAffectedRows(
269
- kind: AdapterExecuteRequest['statement']['kind'],
397
+ kind: DataManipulationRequest['operation']['kind'],
270
398
  rowCount: number | null,
271
399
  rows: Record<string, unknown>[],
272
400
  ): number | undefined {
@@ -286,15 +414,15 @@ function normalizeAffectedRows(
286
414
  }
287
415
 
288
416
  function normalizeInsertId(
289
- kind: AdapterExecuteRequest['statement']['kind'],
290
- statement: AdapterExecuteRequest['statement'],
417
+ kind: DataManipulationRequest['operation']['kind'],
418
+ operation: DataManipulationRequest['operation'],
291
419
  rows: Record<string, unknown>[],
292
420
  ): unknown {
293
- if (!isInsertStatementKind(kind) || !isInsertStatement(statement)) {
421
+ if (!isInsertOperationKind(kind) || !isInsertOperation(operation)) {
294
422
  return undefined
295
423
  }
296
424
 
297
- let primaryKey = getTablePrimaryKey(statement.table)
425
+ let primaryKey = getTablePrimaryKey(operation.table)
298
426
 
299
427
  if (primaryKey.length !== 1) {
300
428
  return undefined
@@ -310,17 +438,495 @@ function quoteIdentifier(value: string): string {
310
438
  return '"' + value.replace(/"/g, '""') + '"'
311
439
  }
312
440
 
313
- function isInsertStatementKind(kind: AdapterExecuteRequest['statement']['kind']): boolean {
441
+ function toPostgresRelationName(table: TableRef): string {
442
+ if (table.schema) {
443
+ return quoteIdentifier(table.schema) + '.' + quoteIdentifier(table.name)
444
+ }
445
+
446
+ return quoteIdentifier(table.name)
447
+ }
448
+
449
+ function toBooleanExists(value: unknown): boolean {
450
+ if (typeof value === 'boolean') {
451
+ return value
452
+ }
453
+
454
+ if (typeof value === 'number') {
455
+ return value > 0
456
+ }
457
+
458
+ if (typeof value === 'string') {
459
+ return value === 't' || value === 'true' || value === '1'
460
+ }
461
+
462
+ return false
463
+ }
464
+
465
+ function isInsertOperationKind(kind: DataManipulationRequest['operation']['kind']): boolean {
314
466
  return kind === 'insert' || kind === 'insertMany' || kind === 'upsert'
315
467
  }
316
468
 
317
- function isInsertStatement(
318
- statement: AdapterExecuteRequest['statement'],
319
- ): statement is Extract<
320
- AdapterExecuteRequest['statement'],
469
+ function isInsertOperation(
470
+ operation: DataManipulationRequest['operation'],
471
+ ): operation is Extract<
472
+ DataManipulationRequest['operation'],
321
473
  { kind: 'insert' | 'insertMany' | 'upsert' }
322
474
  > {
323
475
  return (
324
- statement.kind === 'insert' || statement.kind === 'insertMany' || statement.kind === 'upsert'
476
+ operation.kind === 'insert' || operation.kind === 'insertMany' || operation.kind === 'upsert'
325
477
  )
326
478
  }
479
+
480
+ function isDataManipulationOperation(
481
+ operation: DataManipulationOperation | DataMigrationOperation,
482
+ ): operation is DataManipulationOperation {
483
+ return isDataManipulationOperationHelper(operation)
484
+ }
485
+
486
+ function compilePostgresMigrationOperations(operation: DataMigrationOperation): SqlStatement[] {
487
+ if (operation.kind === 'raw') {
488
+ return [{ text: operation.sql.text, values: [...operation.sql.values] }]
489
+ }
490
+
491
+ if (operation.kind === 'createTable') {
492
+ let columns = Object.keys(operation.columns).map(
493
+ (columnName) =>
494
+ quoteIdentifier(columnName) + ' ' + compilePostgresColumn(operation.columns[columnName]),
495
+ )
496
+ let tableConstraints: string[] = []
497
+
498
+ if (operation.primaryKey) {
499
+ tableConstraints.push(
500
+ 'constraint ' +
501
+ quoteIdentifier(operation.primaryKey.name) +
502
+ ' primary key (' +
503
+ operation.primaryKey.columns.map((column) => quoteIdentifier(column)).join(', ') +
504
+ ')',
505
+ )
506
+ }
507
+
508
+ for (let unique of operation.uniques ?? []) {
509
+ tableConstraints.push(
510
+ 'constraint ' +
511
+ quoteIdentifier(unique.name) +
512
+ ' ' +
513
+ 'unique (' +
514
+ unique.columns.map((column) => quoteIdentifier(column)).join(', ') +
515
+ ')',
516
+ )
517
+ }
518
+
519
+ for (let check of operation.checks ?? []) {
520
+ tableConstraints.push(
521
+ 'constraint ' + quoteIdentifier(check.name) + ' ' + 'check (' + check.expression + ')',
522
+ )
523
+ }
524
+
525
+ for (let foreignKey of operation.foreignKeys ?? []) {
526
+ let clause =
527
+ 'constraint ' +
528
+ quoteIdentifier(foreignKey.name) +
529
+ ' ' +
530
+ 'foreign key (' +
531
+ foreignKey.columns.map((column) => quoteIdentifier(column)).join(', ') +
532
+ ') references ' +
533
+ quoteTableRef(foreignKey.references.table) +
534
+ ' (' +
535
+ foreignKey.references.columns.map((column) => quoteIdentifier(column)).join(', ') +
536
+ ')'
537
+
538
+ if (foreignKey.onDelete) {
539
+ clause += ' on delete ' + foreignKey.onDelete
540
+ }
541
+
542
+ if (foreignKey.onUpdate) {
543
+ clause += ' on update ' + foreignKey.onUpdate
544
+ }
545
+
546
+ tableConstraints.push(clause)
547
+ }
548
+
549
+ let sql =
550
+ 'create table ' +
551
+ (operation.ifNotExists ? 'if not exists ' : '') +
552
+ quoteTableRef(operation.table) +
553
+ ' (' +
554
+ [...columns, ...tableConstraints].join(', ') +
555
+ ')'
556
+ let statements: SqlStatement[] = [{ text: sql, values: [] }]
557
+
558
+ if (operation.comment) {
559
+ statements.push({
560
+ text:
561
+ 'comment on table ' +
562
+ quoteTableRef(operation.table) +
563
+ ' is ' +
564
+ quoteLiteral(operation.comment),
565
+ values: [],
566
+ })
567
+ }
568
+
569
+ return statements
570
+ }
571
+
572
+ if (operation.kind === 'alterTable') {
573
+ let sqlStatements: SqlStatement[] = []
574
+
575
+ for (let change of operation.changes) {
576
+ let sql = 'alter table ' + quoteTableRef(operation.table) + ' '
577
+
578
+ if (change.kind === 'addColumn') {
579
+ sql +=
580
+ 'add column ' +
581
+ quoteIdentifier(change.column) +
582
+ ' ' +
583
+ compilePostgresColumn(change.definition)
584
+ } else if (change.kind === 'changeColumn') {
585
+ let typeSql = compilePostgresColumnType(change.definition)
586
+ sql += 'alter column ' + quoteIdentifier(change.column) + ' type ' + typeSql
587
+ } else if (change.kind === 'renameColumn') {
588
+ sql += 'rename column ' + quoteIdentifier(change.from) + ' to ' + quoteIdentifier(change.to)
589
+ } else if (change.kind === 'dropColumn') {
590
+ sql +=
591
+ 'drop column ' + (change.ifExists ? 'if exists ' : '') + quoteIdentifier(change.column)
592
+ } else if (change.kind === 'addPrimaryKey') {
593
+ sql +=
594
+ 'add ' +
595
+ 'constraint ' +
596
+ quoteIdentifier(change.constraint.name) +
597
+ ' ' +
598
+ 'primary key (' +
599
+ change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +
600
+ ')'
601
+ } else if (change.kind === 'dropPrimaryKey') {
602
+ sql += 'drop constraint ' + quoteIdentifier(change.name)
603
+ } else if (change.kind === 'addUnique') {
604
+ sql +=
605
+ 'add ' +
606
+ 'constraint ' +
607
+ quoteIdentifier(change.constraint.name) +
608
+ ' ' +
609
+ 'unique (' +
610
+ change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +
611
+ ')'
612
+ } else if (change.kind === 'dropUnique') {
613
+ sql += 'drop constraint ' + quoteIdentifier(change.name)
614
+ } else if (change.kind === 'addForeignKey') {
615
+ sql +=
616
+ 'add ' +
617
+ 'constraint ' +
618
+ quoteIdentifier(change.constraint.name) +
619
+ ' ' +
620
+ 'foreign key (' +
621
+ change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +
622
+ ') references ' +
623
+ quoteTableRef(change.constraint.references.table) +
624
+ ' (' +
625
+ change.constraint.references.columns.map((column) => quoteIdentifier(column)).join(', ') +
626
+ ')'
627
+ } else if (change.kind === 'dropForeignKey') {
628
+ sql += 'drop constraint ' + quoteIdentifier(change.name)
629
+ } else if (change.kind === 'addCheck') {
630
+ sql +=
631
+ 'add ' +
632
+ 'constraint ' +
633
+ quoteIdentifier(change.constraint.name) +
634
+ ' ' +
635
+ 'check (' +
636
+ change.constraint.expression +
637
+ ')'
638
+ } else if (change.kind === 'dropCheck') {
639
+ sql += 'drop constraint ' + quoteIdentifier(change.name)
640
+ } else if (change.kind === 'setTableComment') {
641
+ sqlStatements.push({
642
+ text:
643
+ 'comment on table ' +
644
+ quoteTableRef(operation.table) +
645
+ ' is ' +
646
+ quoteLiteral(change.comment),
647
+ values: [],
648
+ })
649
+ continue
650
+ } else {
651
+ continue
652
+ }
653
+
654
+ sqlStatements.push({ text: sql, values: [] })
655
+ }
656
+
657
+ return sqlStatements
658
+ }
659
+
660
+ if (operation.kind === 'renameTable') {
661
+ return [
662
+ {
663
+ text:
664
+ 'alter table ' +
665
+ quoteTableRef(operation.from) +
666
+ ' rename to ' +
667
+ quoteIdentifier(operation.to.name),
668
+ values: [],
669
+ },
670
+ ]
671
+ }
672
+
673
+ if (operation.kind === 'dropTable') {
674
+ return [
675
+ {
676
+ text:
677
+ 'drop table ' +
678
+ (operation.ifExists ? 'if exists ' : '') +
679
+ quoteTableRef(operation.table) +
680
+ (operation.cascade ? ' cascade' : ''),
681
+ values: [],
682
+ },
683
+ ]
684
+ }
685
+
686
+ if (operation.kind === 'createIndex') {
687
+ return [
688
+ {
689
+ text:
690
+ 'create ' +
691
+ (operation.index.unique ? 'unique ' : '') +
692
+ 'index ' +
693
+ (operation.ifNotExists ? 'if not exists ' : '') +
694
+ quoteIdentifier(operation.index.name) +
695
+ ' on ' +
696
+ quoteTableRef(operation.index.table) +
697
+ (operation.index.using ? ' using ' + operation.index.using : '') +
698
+ ' (' +
699
+ operation.index.columns.map((column) => quoteIdentifier(column)).join(', ') +
700
+ ')' +
701
+ (operation.index.where ? ' where ' + operation.index.where : ''),
702
+ values: [],
703
+ },
704
+ ]
705
+ }
706
+
707
+ if (operation.kind === 'dropIndex') {
708
+ return [
709
+ {
710
+ text:
711
+ 'drop index ' +
712
+ (operation.ifExists ? 'if exists ' : '') +
713
+ quoteIdentifier(operation.name),
714
+ values: [],
715
+ },
716
+ ]
717
+ }
718
+
719
+ if (operation.kind === 'renameIndex') {
720
+ return [
721
+ {
722
+ text:
723
+ 'alter index ' +
724
+ quoteIdentifier(operation.from) +
725
+ ' rename to ' +
726
+ quoteIdentifier(operation.to),
727
+ values: [],
728
+ },
729
+ ]
730
+ }
731
+
732
+ if (operation.kind === 'addForeignKey') {
733
+ return [
734
+ {
735
+ text:
736
+ 'alter table ' +
737
+ quoteTableRef(operation.table) +
738
+ ' add ' +
739
+ 'constraint ' +
740
+ quoteIdentifier(operation.constraint.name) +
741
+ ' ' +
742
+ 'foreign key (' +
743
+ operation.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +
744
+ ') references ' +
745
+ quoteTableRef(operation.constraint.references.table) +
746
+ ' (' +
747
+ operation.constraint.references.columns
748
+ .map((column) => quoteIdentifier(column))
749
+ .join(', ') +
750
+ ')' +
751
+ (operation.constraint.onDelete ? ' on delete ' + operation.constraint.onDelete : '') +
752
+ (operation.constraint.onUpdate ? ' on update ' + operation.constraint.onUpdate : ''),
753
+ values: [],
754
+ },
755
+ ]
756
+ }
757
+
758
+ if (operation.kind === 'dropForeignKey') {
759
+ return [
760
+ {
761
+ text:
762
+ 'alter table ' +
763
+ quoteTableRef(operation.table) +
764
+ ' drop constraint ' +
765
+ quoteIdentifier(operation.name),
766
+ values: [],
767
+ },
768
+ ]
769
+ }
770
+
771
+ if (operation.kind === 'addCheck') {
772
+ return [
773
+ {
774
+ text:
775
+ 'alter table ' +
776
+ quoteTableRef(operation.table) +
777
+ ' add ' +
778
+ 'constraint ' +
779
+ quoteIdentifier(operation.constraint.name) +
780
+ ' ' +
781
+ 'check (' +
782
+ operation.constraint.expression +
783
+ ')',
784
+ values: [],
785
+ },
786
+ ]
787
+ }
788
+
789
+ if (operation.kind === 'dropCheck') {
790
+ return [
791
+ {
792
+ text:
793
+ 'alter table ' +
794
+ quoteTableRef(operation.table) +
795
+ ' drop constraint ' +
796
+ quoteIdentifier(operation.name),
797
+ values: [],
798
+ },
799
+ ]
800
+ }
801
+
802
+ throw new Error('Unsupported data migration operation kind')
803
+ }
804
+
805
+ function compilePostgresColumn(definition: ColumnDefinition): string {
806
+ let parts = [compilePostgresColumnType(definition)]
807
+
808
+ if (definition.nullable === false) {
809
+ parts.push('not null')
810
+ }
811
+
812
+ if (definition.default) {
813
+ if (definition.default.kind === 'now') {
814
+ parts.push('default now()')
815
+ } else if (definition.default.kind === 'sql') {
816
+ parts.push('default ' + definition.default.expression)
817
+ } else {
818
+ parts.push('default ' + quoteLiteral(definition.default.value))
819
+ }
820
+ }
821
+
822
+ if (definition.primaryKey) {
823
+ parts.push('primary key')
824
+ }
825
+
826
+ if (definition.unique) {
827
+ parts.push('unique')
828
+ }
829
+
830
+ if (definition.computed) {
831
+ if (!definition.computed.stored) {
832
+ throw new Error('Postgres only supports stored computed/generated columns')
833
+ }
834
+
835
+ parts.push('generated always as (' + definition.computed.expression + ') stored')
836
+ }
837
+
838
+ if (definition.references) {
839
+ let clause =
840
+ 'references ' +
841
+ quoteTableRef(definition.references.table) +
842
+ ' (' +
843
+ definition.references.columns.map((column) => quoteIdentifier(column)).join(', ') +
844
+ ')'
845
+
846
+ if (definition.references.onDelete) {
847
+ clause += ' on delete ' + definition.references.onDelete
848
+ }
849
+
850
+ if (definition.references.onUpdate) {
851
+ clause += ' on update ' + definition.references.onUpdate
852
+ }
853
+
854
+ parts.push(clause)
855
+ }
856
+
857
+ if (definition.checks && definition.checks.length > 0) {
858
+ for (let check of definition.checks) {
859
+ parts.push('check (' + check.expression + ')')
860
+ }
861
+ }
862
+
863
+ return parts.join(' ')
864
+ }
865
+
866
+ function compilePostgresColumnType(definition: ColumnDefinition): string {
867
+ if (definition.type === 'varchar') {
868
+ return 'varchar(' + String(definition.length ?? 255) + ')'
869
+ }
870
+
871
+ if (definition.type === 'text') {
872
+ return 'text'
873
+ }
874
+
875
+ if (definition.type === 'integer') {
876
+ return 'integer'
877
+ }
878
+
879
+ if (definition.type === 'bigint') {
880
+ return 'bigint'
881
+ }
882
+
883
+ if (definition.type === 'decimal') {
884
+ if (definition.precision !== undefined && definition.scale !== undefined) {
885
+ return 'decimal(' + String(definition.precision) + ', ' + String(definition.scale) + ')'
886
+ }
887
+
888
+ return 'decimal'
889
+ }
890
+
891
+ if (definition.type === 'boolean') {
892
+ return 'boolean'
893
+ }
894
+
895
+ if (definition.type === 'uuid') {
896
+ return 'uuid'
897
+ }
898
+
899
+ if (definition.type === 'date') {
900
+ return 'date'
901
+ }
902
+
903
+ if (definition.type === 'time') {
904
+ return definition.withTimezone ? 'time with time zone' : 'time without time zone'
905
+ }
906
+
907
+ if (definition.type === 'timestamp') {
908
+ return definition.withTimezone ? 'timestamp with time zone' : 'timestamp without time zone'
909
+ }
910
+
911
+ if (definition.type === 'json') {
912
+ return 'jsonb'
913
+ }
914
+
915
+ if (definition.type === 'binary') {
916
+ return 'bytea'
917
+ }
918
+
919
+ if (definition.type === 'enum') {
920
+ return 'text'
921
+ }
922
+
923
+ return 'text'
924
+ }
925
+
926
+ function quoteTableRef(table: TableRef): string {
927
+ return quoteTableRefHelper(table, quoteIdentifier)
928
+ }
929
+
930
+ function quoteLiteral(value: unknown): string {
931
+ return quoteLiteralHelper(value)
932
+ }