@remix-run/data-table-postgres 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -17
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/database.d.ts +35 -0
- package/dist/lib/database.d.ts.map +1 -0
- package/dist/lib/database.js +31 -0
- package/dist/lib/{adapter.d.ts → driver.d.ts} +50 -44
- package/dist/lib/driver.d.ts.map +1 -0
- package/dist/lib/driver.js +577 -0
- package/dist/lib/sql-compiler.d.ts +2 -1
- package/dist/lib/sql-compiler.d.ts.map +1 -1
- package/dist/lib/sql-compiler.js +179 -13
- package/package.json +12 -12
- package/src/index.ts +3 -1
- package/src/lib/database.ts +45 -0
- package/src/lib/driver.ts +760 -0
- package/src/lib/sql-compiler.ts +235 -15
- package/dist/lib/adapter.d.ts.map +0 -1
- package/dist/lib/adapter.js +0 -735
- package/src/lib/adapter.ts +0 -925
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { getTablePrimaryKey } from '@remix-run/data-table';
|
|
3
|
+
import pg from 'pg';
|
|
4
|
+
import { compilePostgresOperation } from './sql-compiler.js';
|
|
5
|
+
const postgresCapabilities = Object.freeze({
|
|
6
|
+
returning: true,
|
|
7
|
+
savepoints: true,
|
|
8
|
+
upsert: true,
|
|
9
|
+
transactionalDdl: true,
|
|
10
|
+
migrationLock: true,
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* PostgreSQL database driver backed by a postgres-compatible client.
|
|
14
|
+
*/
|
|
15
|
+
export class PostgresDatabaseDriver {
|
|
16
|
+
/**
|
|
17
|
+
* The SQL dialect identifier reported by this database.
|
|
18
|
+
*/
|
|
19
|
+
get dialect() {
|
|
20
|
+
return 'postgres';
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Feature flags describing the PostgreSQL behaviors supported by this database.
|
|
24
|
+
*/
|
|
25
|
+
get capabilities() {
|
|
26
|
+
return postgresCapabilities;
|
|
27
|
+
}
|
|
28
|
+
#config;
|
|
29
|
+
#client;
|
|
30
|
+
#maintenanceDatabase;
|
|
31
|
+
#template;
|
|
32
|
+
#transactions = new Map();
|
|
33
|
+
#transactionCounter = 0;
|
|
34
|
+
#migrationLockQueue = Promise.resolve();
|
|
35
|
+
#migrationLockStore = new AsyncLocalStorage();
|
|
36
|
+
#poolClosed = false;
|
|
37
|
+
constructor(config, options = {}) {
|
|
38
|
+
if (isPostgresQueryable(config)) {
|
|
39
|
+
this.#client = config;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
this.#config = config;
|
|
43
|
+
this.#client = new pg.Pool(config);
|
|
44
|
+
}
|
|
45
|
+
this.#maintenanceDatabase = options.maintenanceDatabase ?? 'postgres';
|
|
46
|
+
this.#template = options.template ?? 'template0';
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Compiles a data-manipulation operation to postgres SQL statements.
|
|
50
|
+
* @param operation Operation to compile.
|
|
51
|
+
* @returns Compiled SQL statements.
|
|
52
|
+
*/
|
|
53
|
+
compileSql(operation) {
|
|
54
|
+
let compiled = compilePostgresOperation(operation);
|
|
55
|
+
return [{ text: compiled.text, values: compiled.values }];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Executes a postgres data-manipulation request.
|
|
59
|
+
* @param request Request to execute.
|
|
60
|
+
* @returns Execution result.
|
|
61
|
+
*/
|
|
62
|
+
async execute(request) {
|
|
63
|
+
if (request.operation.kind === 'insertMany' && request.operation.values.length === 0) {
|
|
64
|
+
return {
|
|
65
|
+
affectedRows: 0,
|
|
66
|
+
insertId: undefined,
|
|
67
|
+
rows: request.operation.returning ? [] : undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
let statement = compilePostgresOperation(request.operation);
|
|
71
|
+
let client = this.#resolveClient(request.transaction);
|
|
72
|
+
let result = await client.query(statement.text, statement.values);
|
|
73
|
+
let rows = normalizeRows(result.rows);
|
|
74
|
+
if (request.operation.kind === 'count' || request.operation.kind === 'exists') {
|
|
75
|
+
rows = normalizeCountRows(rows);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
rows,
|
|
79
|
+
affectedRows: normalizeAffectedRows(request.operation.kind, result.rowCount, rows),
|
|
80
|
+
insertId: normalizeInsertId(request.operation.kind, request.operation, rows),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Executes a multi-statement postgres SQL script.
|
|
85
|
+
*
|
|
86
|
+
* Postgres natively supports multi-statement scripts when `query` is called
|
|
87
|
+
* without a parameter array.
|
|
88
|
+
* @param sql SQL script to execute.
|
|
89
|
+
* @param transaction Optional transaction token.
|
|
90
|
+
* @returns A promise that resolves once execution completes.
|
|
91
|
+
*/
|
|
92
|
+
async executeScript(sql, transaction) {
|
|
93
|
+
let client = this.#resolveClient(transaction);
|
|
94
|
+
await client.query(sql);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Checks whether a table exists in postgres.
|
|
98
|
+
* @param table Table reference to inspect.
|
|
99
|
+
* @param transaction Optional transaction token.
|
|
100
|
+
* @returns `true` when the table exists.
|
|
101
|
+
*/
|
|
102
|
+
async hasTable(table, transaction) {
|
|
103
|
+
let relation = toPostgresRelationName(table);
|
|
104
|
+
let client = this.#resolveClient(transaction);
|
|
105
|
+
let result = await client.query('select to_regclass($1) is not null as "exists"', [relation]);
|
|
106
|
+
let row = result.rows[0];
|
|
107
|
+
return toBooleanExists(row?.exists);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Checks whether a column exists in postgres.
|
|
111
|
+
* @param table Table reference to inspect.
|
|
112
|
+
* @param column Column name to look up.
|
|
113
|
+
* @param transaction Optional transaction token.
|
|
114
|
+
* @returns `true` when the column exists.
|
|
115
|
+
*/
|
|
116
|
+
async hasColumn(table, column, transaction) {
|
|
117
|
+
let relation = toPostgresRelationName(table);
|
|
118
|
+
let client = this.#resolveClient(transaction);
|
|
119
|
+
let result = await client.query('select exists (select 1 from pg_attribute where attrelid = to_regclass($1) and attname = $2 and attnum > 0 and not attisdropped) as "exists"', [relation, column]);
|
|
120
|
+
let row = result.rows[0];
|
|
121
|
+
return toBooleanExists(row?.exists);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Starts a postgres transaction.
|
|
125
|
+
* @param options Transaction options.
|
|
126
|
+
* @returns Transaction token.
|
|
127
|
+
*/
|
|
128
|
+
async beginTransaction(options) {
|
|
129
|
+
let releaseOnClose = false;
|
|
130
|
+
let transactionClient;
|
|
131
|
+
if (isPostgresPool(this.#client)) {
|
|
132
|
+
transactionClient = await this.#client.connect();
|
|
133
|
+
releaseOnClose = true;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
transactionClient = this.#client;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
await transactionClient.query('begin');
|
|
140
|
+
if (options?.isolationLevel || options?.readOnly !== undefined) {
|
|
141
|
+
await transactionClient.query(buildSetTransactionStatement(options));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (releaseOnClose) {
|
|
146
|
+
destroyPostgresClient(transactionClient, error);
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
this.#transactionCounter += 1;
|
|
151
|
+
let token = { id: 'tx_' + String(this.#transactionCounter) };
|
|
152
|
+
this.#transactions.set(token.id, {
|
|
153
|
+
client: transactionClient,
|
|
154
|
+
releaseOnClose,
|
|
155
|
+
});
|
|
156
|
+
return token;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Commits an open postgres transaction.
|
|
160
|
+
* @param token Transaction token to commit.
|
|
161
|
+
* @returns A promise that resolves when the transaction is committed.
|
|
162
|
+
*/
|
|
163
|
+
async commitTransaction(token) {
|
|
164
|
+
let transaction = this.#transactions.get(token.id);
|
|
165
|
+
if (!transaction) {
|
|
166
|
+
throw new Error('Unknown transaction token: ' + token.id);
|
|
167
|
+
}
|
|
168
|
+
let failure;
|
|
169
|
+
try {
|
|
170
|
+
await transaction.client.query('commit');
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
failure = error;
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
this.#transactions.delete(token.id);
|
|
178
|
+
if (transaction.releaseOnClose) {
|
|
179
|
+
if (failure === undefined) {
|
|
180
|
+
releasePostgresClient(transaction.client);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
destroyPostgresClient(transaction.client, failure);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Rolls back an open postgres transaction.
|
|
190
|
+
* @param token Transaction token to roll back.
|
|
191
|
+
* @returns A promise that resolves when the transaction is rolled back.
|
|
192
|
+
*/
|
|
193
|
+
async rollbackTransaction(token) {
|
|
194
|
+
let transaction = this.#transactions.get(token.id);
|
|
195
|
+
if (!transaction) {
|
|
196
|
+
throw new Error('Unknown transaction token: ' + token.id);
|
|
197
|
+
}
|
|
198
|
+
let failure;
|
|
199
|
+
try {
|
|
200
|
+
await transaction.client.query('rollback');
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
failure = error;
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
this.#transactions.delete(token.id);
|
|
208
|
+
if (transaction.releaseOnClose) {
|
|
209
|
+
if (failure === undefined) {
|
|
210
|
+
releasePostgresClient(transaction.client);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
destroyPostgresClient(transaction.client, failure);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Creates a savepoint in an open postgres transaction.
|
|
220
|
+
* @param token Transaction token to use.
|
|
221
|
+
* @param name Savepoint name.
|
|
222
|
+
* @returns A promise that resolves when the savepoint is created.
|
|
223
|
+
*/
|
|
224
|
+
async createSavepoint(token, name) {
|
|
225
|
+
let client = this.#transactionClient(token);
|
|
226
|
+
await client.query('savepoint ' + quoteIdentifier(name));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Rolls back to a savepoint in an open postgres transaction.
|
|
230
|
+
* @param token Transaction token to use.
|
|
231
|
+
* @param name Savepoint name.
|
|
232
|
+
* @returns A promise that resolves when the rollback completes.
|
|
233
|
+
*/
|
|
234
|
+
async rollbackToSavepoint(token, name) {
|
|
235
|
+
let client = this.#transactionClient(token);
|
|
236
|
+
await client.query('rollback to savepoint ' + quoteIdentifier(name));
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Releases a savepoint in an open postgres transaction.
|
|
240
|
+
* @param token Transaction token to use.
|
|
241
|
+
* @param name Savepoint name.
|
|
242
|
+
* @returns A promise that resolves when the savepoint is released.
|
|
243
|
+
*/
|
|
244
|
+
async releaseSavepoint(token, name) {
|
|
245
|
+
let client = this.#transactionClient(token);
|
|
246
|
+
await client.query('release savepoint ' + quoteIdentifier(name));
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Destructively recreates the configured PostgreSQL database.
|
|
250
|
+
* @returns A promise that resolves when the database is ready for use.
|
|
251
|
+
*/
|
|
252
|
+
async wipe() {
|
|
253
|
+
let config = this.#configOrThrow('wipe');
|
|
254
|
+
this.#assertNoOpenTransactions('wipe');
|
|
255
|
+
let database = resolvePostgresDatabaseName(config);
|
|
256
|
+
// Resolve the maintenance config before closing the pool so a config
|
|
257
|
+
// error cannot leave the database without a usable pool.
|
|
258
|
+
let maintenanceConfig = this.#maintenanceConfig(database);
|
|
259
|
+
await this.#closePool();
|
|
260
|
+
let maintenance;
|
|
261
|
+
try {
|
|
262
|
+
maintenance = new pg.Client(maintenanceConfig);
|
|
263
|
+
await maintenance.connect();
|
|
264
|
+
await maintenance.query('select pg_terminate_backend(pid) from pg_stat_activity where datname = $1 and pid <> pg_backend_pid()', [database]);
|
|
265
|
+
await maintenance.query('drop database if exists ' + quoteIdentifier(database));
|
|
266
|
+
await maintenance.query('create database ' +
|
|
267
|
+
quoteIdentifier(database) +
|
|
268
|
+
' template ' +
|
|
269
|
+
quoteIdentifier(this.#template));
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
try {
|
|
273
|
+
await maintenance?.end();
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
await this.#replacePool();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/** Closes a pool created from configuration. Supplied clients and pools remain caller-owned. */
|
|
281
|
+
async close() {
|
|
282
|
+
this.#assertNoOpenTransactions('close');
|
|
283
|
+
if (this.#config) {
|
|
284
|
+
await this.#closePool();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Runs migration work on the postgres connection that owns the advisory lock.
|
|
289
|
+
*
|
|
290
|
+
* Lock acquisition waits up to 60 seconds and throws when the lock cannot
|
|
291
|
+
* be acquired. Re-entering this method from inside `run` throws instead of
|
|
292
|
+
* deadlocking, and a failed run destroys the reserved connection instead of
|
|
293
|
+
* returning it to the pool.
|
|
294
|
+
* @param name Logical migration lock name.
|
|
295
|
+
* @param run Migration work to run with a connection-bound driver.
|
|
296
|
+
* @returns The callback result.
|
|
297
|
+
*/
|
|
298
|
+
async withMigrationLock(name, run) {
|
|
299
|
+
if (this.#migrationLockStore.getStore()) {
|
|
300
|
+
throw new Error('Postgres migration lock is already held by this database');
|
|
301
|
+
}
|
|
302
|
+
let waitForPreviousLock = this.#migrationLockQueue;
|
|
303
|
+
let releaseQueue = () => undefined;
|
|
304
|
+
this.#migrationLockQueue = new Promise((resolve) => {
|
|
305
|
+
releaseQueue = resolve;
|
|
306
|
+
});
|
|
307
|
+
await waitForPreviousLock;
|
|
308
|
+
try {
|
|
309
|
+
let releaseOnClose = false;
|
|
310
|
+
let client;
|
|
311
|
+
if (isPostgresPool(this.#client)) {
|
|
312
|
+
client = await this.#client.connect();
|
|
313
|
+
releaseOnClose = true;
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
client = this.#client;
|
|
317
|
+
}
|
|
318
|
+
let driver = releaseOnClose ? new PostgresDatabaseDriver(client) : this;
|
|
319
|
+
try {
|
|
320
|
+
let value = await this.#migrationLockStore.run(true, () => runWithPostgresMigrationLock(client, name, driver, run));
|
|
321
|
+
if (releaseOnClose) {
|
|
322
|
+
releasePostgresClient(client);
|
|
323
|
+
}
|
|
324
|
+
return value;
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
// A failed run can leave the reserved session dirty (aborted
|
|
328
|
+
// transaction, still-held advisory lock), so destroy the connection
|
|
329
|
+
// instead of returning it to the pool.
|
|
330
|
+
if (releaseOnClose) {
|
|
331
|
+
destroyPostgresClient(client, error);
|
|
332
|
+
}
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
releaseQueue();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async #closePool() {
|
|
341
|
+
this.#transactions.clear();
|
|
342
|
+
// pg pools reject end() when called twice, so ending must be tracked to
|
|
343
|
+
// keep close() idempotent.
|
|
344
|
+
if (isPostgresPool(this.#client) && !this.#poolClosed) {
|
|
345
|
+
this.#poolClosed = true;
|
|
346
|
+
await this.#client.end();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
#configOrThrow(method) {
|
|
350
|
+
if (!this.#config) {
|
|
351
|
+
throw new Error('Postgres database ' + method + '() requires config-based construction');
|
|
352
|
+
}
|
|
353
|
+
return this.#config;
|
|
354
|
+
}
|
|
355
|
+
#assertNoOpenTransactions(method) {
|
|
356
|
+
if (this.#transactions.size > 0) {
|
|
357
|
+
throw new Error('Postgres database cannot ' + method + ' while transactions are open');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
#maintenanceConfig(targetDatabase) {
|
|
361
|
+
let maintenanceDatabase = this.#maintenanceDatabase;
|
|
362
|
+
if (maintenanceDatabase === targetDatabase) {
|
|
363
|
+
maintenanceDatabase = targetDatabase === 'postgres' ? 'template1' : 'postgres';
|
|
364
|
+
}
|
|
365
|
+
let config = this.#configOrThrow('maintenance');
|
|
366
|
+
let connectionString = replaceDatabaseInConnectionString(config?.connectionString, maintenanceDatabase);
|
|
367
|
+
return { ...config, connectionString, database: maintenanceDatabase };
|
|
368
|
+
}
|
|
369
|
+
async #replacePool() {
|
|
370
|
+
await this.#closePool().catch(() => undefined);
|
|
371
|
+
if (this.#config) {
|
|
372
|
+
this.#client = new pg.Pool(this.#config);
|
|
373
|
+
this.#poolClosed = false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
#resolveClient(token) {
|
|
377
|
+
if (!token) {
|
|
378
|
+
return this.#client;
|
|
379
|
+
}
|
|
380
|
+
return this.#transactionClient(token);
|
|
381
|
+
}
|
|
382
|
+
#transactionClient(token) {
|
|
383
|
+
let transaction = this.#transactions.get(token.id);
|
|
384
|
+
if (!transaction) {
|
|
385
|
+
throw new Error('Unknown transaction token: ' + token.id);
|
|
386
|
+
}
|
|
387
|
+
return transaction.client;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function isPostgresQueryable(value) {
|
|
391
|
+
return typeof value === 'object' && value !== null && 'query' in value;
|
|
392
|
+
}
|
|
393
|
+
function isPostgresPool(client) {
|
|
394
|
+
if (client instanceof pg.Client) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
return 'connect' in client && typeof client.connect === 'function' && !('release' in client);
|
|
398
|
+
}
|
|
399
|
+
function resolvePostgresDatabaseName(config) {
|
|
400
|
+
let database = resolveDatabaseNameFromConnectionString(config?.connectionString) ??
|
|
401
|
+
config?.database ??
|
|
402
|
+
process.env.PGDATABASE;
|
|
403
|
+
if (!database) {
|
|
404
|
+
throw new Error('Postgres database config requires a database name');
|
|
405
|
+
}
|
|
406
|
+
return database;
|
|
407
|
+
}
|
|
408
|
+
function replaceDatabaseInConnectionString(connectionString, database) {
|
|
409
|
+
if (!connectionString) {
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
let url;
|
|
413
|
+
try {
|
|
414
|
+
url = new URL(connectionString);
|
|
415
|
+
}
|
|
416
|
+
catch (cause) {
|
|
417
|
+
throw new Error('Postgres connection string must be a valid URL to resolve the maintenance database', { cause });
|
|
418
|
+
}
|
|
419
|
+
url.pathname = '/' + encodeURIComponent(database);
|
|
420
|
+
return url.toString();
|
|
421
|
+
}
|
|
422
|
+
function resolveDatabaseNameFromConnectionString(connectionString) {
|
|
423
|
+
if (!connectionString) {
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
let url = new URL(connectionString);
|
|
428
|
+
let database = decodeURIComponent(url.pathname.replace(/^\//, ''));
|
|
429
|
+
return database || undefined;
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function releasePostgresClient(client) {
|
|
436
|
+
let release = client.release;
|
|
437
|
+
release?.();
|
|
438
|
+
}
|
|
439
|
+
function destroyPostgresClient(client, error) {
|
|
440
|
+
let release = client.release;
|
|
441
|
+
if (typeof release === 'function') {
|
|
442
|
+
// A truthy argument tells pg to destroy the client instead of pooling it.
|
|
443
|
+
release.call(client, error instanceof Error ? error : true);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
void client.end().catch(() => undefined);
|
|
447
|
+
}
|
|
448
|
+
// Matches the 60 second wait bound used by the MySQL driver's get_lock().
|
|
449
|
+
const MIGRATION_LOCK_TIMEOUT_MS = 60_000;
|
|
450
|
+
async function runWithPostgresMigrationLock(client, name, driver, run) {
|
|
451
|
+
await client.query('set lock_timeout to ' + String(MIGRATION_LOCK_TIMEOUT_MS));
|
|
452
|
+
try {
|
|
453
|
+
await client.query('select pg_advisory_lock(hashtext($1))', [name]);
|
|
454
|
+
}
|
|
455
|
+
catch (cause) {
|
|
456
|
+
await client.query('set lock_timeout to default').catch(() => undefined);
|
|
457
|
+
throw new Error('Postgres migration lock could not be acquired', { cause });
|
|
458
|
+
}
|
|
459
|
+
await client.query('set lock_timeout to default');
|
|
460
|
+
let outcome;
|
|
461
|
+
try {
|
|
462
|
+
outcome = { status: 'success', value: await run(driver) };
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
outcome = { status: 'failure', error };
|
|
466
|
+
}
|
|
467
|
+
let unlockFailed = false;
|
|
468
|
+
let unlockError;
|
|
469
|
+
try {
|
|
470
|
+
let result = await client.query('select pg_advisory_unlock(hashtext($1)) as "released"', [name]);
|
|
471
|
+
let row = result.rows[0];
|
|
472
|
+
if (!toBooleanExists(row?.released)) {
|
|
473
|
+
throw new Error('Postgres migration lock was not held by the reserved connection');
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
unlockFailed = true;
|
|
478
|
+
unlockError = error;
|
|
479
|
+
}
|
|
480
|
+
if (outcome.status === 'failure') {
|
|
481
|
+
throw outcome.error;
|
|
482
|
+
}
|
|
483
|
+
if (unlockFailed) {
|
|
484
|
+
throw unlockError;
|
|
485
|
+
}
|
|
486
|
+
return outcome.value;
|
|
487
|
+
}
|
|
488
|
+
function buildSetTransactionStatement(options) {
|
|
489
|
+
let parts = ['set transaction'];
|
|
490
|
+
if (options.isolationLevel) {
|
|
491
|
+
parts.push('isolation level ' + options.isolationLevel);
|
|
492
|
+
}
|
|
493
|
+
if (options.readOnly !== undefined) {
|
|
494
|
+
parts.push(options.readOnly ? 'read only' : 'read write');
|
|
495
|
+
}
|
|
496
|
+
return parts.join(' ');
|
|
497
|
+
}
|
|
498
|
+
function normalizeRows(rows) {
|
|
499
|
+
return rows.map((row) => {
|
|
500
|
+
if (typeof row !== 'object' || row === null) {
|
|
501
|
+
return {};
|
|
502
|
+
}
|
|
503
|
+
return { ...row };
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
function normalizeCountRows(rows) {
|
|
507
|
+
return rows.map((row) => {
|
|
508
|
+
let count = row.count;
|
|
509
|
+
if (typeof count === 'string') {
|
|
510
|
+
let numeric = Number(count);
|
|
511
|
+
if (!Number.isNaN(numeric)) {
|
|
512
|
+
return {
|
|
513
|
+
...row,
|
|
514
|
+
count: numeric,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (typeof count === 'bigint') {
|
|
519
|
+
return {
|
|
520
|
+
...row,
|
|
521
|
+
count: Number(count),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
return row;
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
function normalizeAffectedRows(kind, rowCount, rows) {
|
|
528
|
+
if (kind === 'select' || kind === 'count' || kind === 'exists') {
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
if (rowCount !== null) {
|
|
532
|
+
return rowCount;
|
|
533
|
+
}
|
|
534
|
+
if (kind === 'raw') {
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
return rows.length;
|
|
538
|
+
}
|
|
539
|
+
function normalizeInsertId(kind, operation, rows) {
|
|
540
|
+
if (!isInsertOperationKind(kind) || !isInsertOperation(operation)) {
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
let primaryKey = getTablePrimaryKey(operation.table);
|
|
544
|
+
if (primaryKey.length !== 1) {
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
let key = primaryKey[0];
|
|
548
|
+
let row = rows[rows.length - 1];
|
|
549
|
+
return row ? row[key] : undefined;
|
|
550
|
+
}
|
|
551
|
+
function quoteIdentifier(value) {
|
|
552
|
+
return '"' + value.replace(/"/g, '""') + '"';
|
|
553
|
+
}
|
|
554
|
+
function toPostgresRelationName(table) {
|
|
555
|
+
if (table.schema) {
|
|
556
|
+
return quoteIdentifier(table.schema) + '.' + quoteIdentifier(table.name);
|
|
557
|
+
}
|
|
558
|
+
return quoteIdentifier(table.name);
|
|
559
|
+
}
|
|
560
|
+
function toBooleanExists(value) {
|
|
561
|
+
if (typeof value === 'boolean') {
|
|
562
|
+
return value;
|
|
563
|
+
}
|
|
564
|
+
if (typeof value === 'number') {
|
|
565
|
+
return value > 0;
|
|
566
|
+
}
|
|
567
|
+
if (typeof value === 'string') {
|
|
568
|
+
return value === 't' || value === 'true' || value === '1';
|
|
569
|
+
}
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
function isInsertOperationKind(kind) {
|
|
573
|
+
return kind === 'insert' || kind === 'insertMany' || kind === 'upsert';
|
|
574
|
+
}
|
|
575
|
+
function isInsertOperation(operation) {
|
|
576
|
+
return (operation.kind === 'insert' || operation.kind === 'insertMany' || operation.kind === 'upsert');
|
|
577
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SqlStatement } from '@remix-run/data-table';
|
|
2
|
+
import type { DataManipulationOperation } from '@remix-run/data-table';
|
|
2
3
|
export declare function compilePostgresOperation(operation: DataManipulationOperation): SqlStatement;
|
|
3
4
|
//# sourceMappingURL=sql-compiler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql-compiler.d.ts","sourceRoot":"","sources":["../../src/lib/sql-compiler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"sql-compiler.d.ts","sourceRoot":"","sources":["../../src/lib/sql-compiler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAa,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAA;AAetE,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,yBAAyB,GAAG,YAAY,CAqG3F"}
|