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