@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.
- package/README.md +9 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/adapter.d.ts +100 -33
- package/dist/lib/adapter.d.ts.map +1 -1
- package/dist/lib/adapter.js +553 -17
- 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 +1 -7
- package/src/lib/adapter.ts +675 -69
- package/src/lib/sql-compiler.ts +66 -106
package/src/lib/adapter.ts
CHANGED
|
@@ -1,51 +1,27 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AdapterCapabilityOverrides,
|
|
3
|
-
|
|
4
|
-
|
|
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 {
|
|
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:
|
|
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:
|
|
54
|
+
#client: PostgresQueryable
|
|
70
55
|
#transactions = new Map<string, TransactionState>()
|
|
71
56
|
#transactionCounter = 0
|
|
72
57
|
|
|
73
|
-
constructor(client:
|
|
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
|
-
|
|
83
|
-
|
|
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.
|
|
93
|
+
rows: request.operation.returning ? [] : undefined,
|
|
88
94
|
}
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
let 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.
|
|
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.
|
|
103
|
-
insertId: normalizeInsertId(request.
|
|
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:
|
|
174
|
+
let transactionClient: PostgresPoolClient
|
|
110
175
|
|
|
111
|
-
if (this.#client
|
|
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
|
|
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
|
|
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
|
-
|
|
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):
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
290
|
-
|
|
417
|
+
kind: DataManipulationRequest['operation']['kind'],
|
|
418
|
+
operation: DataManipulationRequest['operation'],
|
|
291
419
|
rows: Record<string, unknown>[],
|
|
292
420
|
): unknown {
|
|
293
|
-
if (!
|
|
421
|
+
if (!isInsertOperationKind(kind) || !isInsertOperation(operation)) {
|
|
294
422
|
return undefined
|
|
295
423
|
}
|
|
296
424
|
|
|
297
|
-
let primaryKey = getTablePrimaryKey(
|
|
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
|
|
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
|
|
318
|
-
|
|
319
|
-
):
|
|
320
|
-
|
|
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
|
-
|
|
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
|
+
}
|