@remix-run/data-table-mysql 0.4.0 → 0.5.1
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 +47 -24
- 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 +36 -0
- package/dist/lib/database.d.ts.map +1 -0
- package/dist/lib/database.js +32 -0
- package/dist/lib/{adapter.d.ts → driver.d.ts} +39 -39
- package/dist/lib/driver.d.ts.map +1 -0
- package/dist/lib/driver.js +581 -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 +6 -6
- package/package.json +12 -12
- package/src/index.ts +3 -1
- package/src/lib/database.ts +46 -0
- package/src/lib/{adapter.ts → driver.ts} +359 -55
- package/src/lib/sql-compiler.ts +8 -7
- package/dist/lib/adapter.d.ts.map +0 -1
- package/dist/lib/adapter.js +0 -344
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Database, type DatabaseOptions } from '@remix-run/data-table'
|
|
2
|
+
|
|
3
|
+
import { MysqlDatabaseDriver, type MysqlDatabaseInput } from './driver.ts'
|
|
4
|
+
|
|
5
|
+
/** Options for creating a MySQL database. */
|
|
6
|
+
export interface MysqlDatabaseOptions extends DatabaseOptions {
|
|
7
|
+
/** Character set assigned to the recreated database. */
|
|
8
|
+
characterSet?: string
|
|
9
|
+
/** Collation assigned to the recreated database. */
|
|
10
|
+
collation?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A {@link Database} backed by MySQL. */
|
|
14
|
+
export class MysqlDatabase extends Database<'mysql'> {
|
|
15
|
+
/**
|
|
16
|
+
* Creates a MySQL-backed database.
|
|
17
|
+
* @param input MySQL pool configuration, pool, connection, or URI.
|
|
18
|
+
* @param options Database runtime and recreation options.
|
|
19
|
+
*/
|
|
20
|
+
constructor(input: MysqlDatabaseInput, options: MysqlDatabaseOptions = {}) {
|
|
21
|
+
super(new MysqlDatabaseDriver(input, options), options)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates a MySQL-backed database.
|
|
27
|
+
*
|
|
28
|
+
* @param input MySQL pool configuration, pool, connection, or URI.
|
|
29
|
+
* @param options Database runtime and recreation options.
|
|
30
|
+
* @returns A MySQL database.
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { createMysqlDatabase } from 'remix/data-table/mysql'
|
|
34
|
+
*
|
|
35
|
+
* let db = createMysqlDatabase({
|
|
36
|
+
* uri: process.env.DATABASE_URL,
|
|
37
|
+
* multipleStatements: true,
|
|
38
|
+
* })
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function createMysqlDatabase(
|
|
42
|
+
input: MysqlDatabaseInput,
|
|
43
|
+
options: MysqlDatabaseOptions = {},
|
|
44
|
+
): MysqlDatabase {
|
|
45
|
+
return new MysqlDatabase(input, options)
|
|
46
|
+
}
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
DataManipulationOperation,
|
|
2
3
|
DataManipulationRequest,
|
|
3
4
|
DataManipulationResult,
|
|
4
|
-
|
|
5
|
-
DatabaseAdapter,
|
|
5
|
+
DatabaseDriver,
|
|
6
6
|
SqlStatement,
|
|
7
7
|
TableRef,
|
|
8
8
|
TransactionOptions,
|
|
9
9
|
TransactionToken,
|
|
10
10
|
} from '@remix-run/data-table'
|
|
11
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
12
|
+
|
|
11
13
|
import { getTablePrimaryKey } from '@remix-run/data-table'
|
|
14
|
+
import mysql from 'mysql2/promise'
|
|
12
15
|
import type {
|
|
13
16
|
Connection as MysqlConnection,
|
|
14
17
|
Pool as MysqlPool,
|
|
15
18
|
PoolConnection as MysqlPoolConnection,
|
|
19
|
+
PoolOptions as MysqlPoolOptions,
|
|
16
20
|
ResultSetHeader,
|
|
17
21
|
RowDataPacket,
|
|
18
22
|
} from 'mysql2/promise'
|
|
@@ -32,33 +36,62 @@ type MysqlQueryResultHeader = {
|
|
|
32
36
|
type MysqlTransactionConnection = MysqlConnection | MysqlPoolConnection
|
|
33
37
|
type MysqlQueryable = MysqlPool | MysqlTransactionConnection
|
|
34
38
|
|
|
39
|
+
export type MysqlDatabaseInput = string | MysqlPoolOptions | MysqlQueryable
|
|
40
|
+
|
|
41
|
+
const mysqlCapabilities = Object.freeze({
|
|
42
|
+
returning: false,
|
|
43
|
+
savepoints: true,
|
|
44
|
+
upsert: true,
|
|
45
|
+
transactionalDdl: false,
|
|
46
|
+
migrationLock: true,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
/** Database creation options used when wiping a config-backed MySQL driver. */
|
|
50
|
+
export interface MysqlDatabaseDriverOptions {
|
|
51
|
+
/** Character set assigned to the recreated database. */
|
|
52
|
+
characterSet?: string
|
|
53
|
+
/** Collation assigned to the recreated database. */
|
|
54
|
+
collation?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
35
57
|
/**
|
|
36
|
-
*
|
|
58
|
+
* MySQL database driver backed by a mysql-compatible client.
|
|
37
59
|
*/
|
|
38
|
-
export class
|
|
60
|
+
export class MysqlDatabaseDriver implements DatabaseDriver<'mysql'> {
|
|
39
61
|
/**
|
|
40
|
-
* The SQL dialect identifier reported by this
|
|
62
|
+
* The SQL dialect identifier reported by this database.
|
|
41
63
|
*/
|
|
42
|
-
dialect
|
|
64
|
+
get dialect(): 'mysql' {
|
|
65
|
+
return 'mysql'
|
|
66
|
+
}
|
|
43
67
|
|
|
44
68
|
/**
|
|
45
|
-
* Feature flags describing the
|
|
69
|
+
* Feature flags describing the MySQL behaviors supported by this database.
|
|
46
70
|
*/
|
|
47
|
-
capabilities
|
|
71
|
+
get capabilities() {
|
|
72
|
+
return mysqlCapabilities
|
|
73
|
+
}
|
|
48
74
|
|
|
75
|
+
#config?: string | MysqlPoolOptions
|
|
49
76
|
#client: MysqlQueryable
|
|
77
|
+
#characterSet?: string
|
|
78
|
+
#collation?: string
|
|
50
79
|
#transactions = new Map<string, TransactionState>()
|
|
51
80
|
#transactionCounter = 0
|
|
81
|
+
#migrationLockQueue = Promise.resolve()
|
|
82
|
+
#migrationLockStore = new AsyncLocalStorage<boolean>()
|
|
83
|
+
#poolClosed = false
|
|
52
84
|
|
|
53
|
-
constructor(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
transactionalDdl: false,
|
|
60
|
-
migrationLock: true,
|
|
85
|
+
constructor(config: MysqlDatabaseInput, options: MysqlDatabaseDriverOptions = {}) {
|
|
86
|
+
if (isMysqlQueryable(config)) {
|
|
87
|
+
this.#client = config
|
|
88
|
+
} else {
|
|
89
|
+
this.#config = config
|
|
90
|
+
this.#client = createMysqlPool(config)
|
|
61
91
|
}
|
|
92
|
+
|
|
93
|
+
this.#characterSet = options.characterSet
|
|
94
|
+
this.#collation = options.collation
|
|
62
95
|
}
|
|
63
96
|
|
|
64
97
|
/**
|
|
@@ -187,17 +220,24 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
|
|
|
187
220
|
connection = this.#client
|
|
188
221
|
}
|
|
189
222
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
223
|
+
try {
|
|
224
|
+
if (options?.isolationLevel) {
|
|
225
|
+
await connection.query('set transaction isolation level ' + options.isolationLevel)
|
|
226
|
+
}
|
|
193
227
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
228
|
+
if (options?.readOnly !== undefined) {
|
|
229
|
+
await connection.query(
|
|
230
|
+
options.readOnly ? 'set transaction read only' : 'set transaction read write',
|
|
231
|
+
)
|
|
232
|
+
}
|
|
199
233
|
|
|
200
|
-
|
|
234
|
+
await connection.beginTransaction()
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (releaseOnClose) {
|
|
237
|
+
destroyMysqlConnection(connection)
|
|
238
|
+
}
|
|
239
|
+
throw error
|
|
240
|
+
}
|
|
201
241
|
|
|
202
242
|
this.#transactionCounter += 1
|
|
203
243
|
let token = { id: 'tx_' + String(this.#transactionCounter) }
|
|
@@ -222,13 +262,21 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
|
|
|
222
262
|
throw new Error('Unknown transaction token: ' + token.id)
|
|
223
263
|
}
|
|
224
264
|
|
|
265
|
+
let failed = false
|
|
225
266
|
try {
|
|
226
267
|
await transaction.connection.commit()
|
|
268
|
+
} catch (error) {
|
|
269
|
+
failed = true
|
|
270
|
+
throw error
|
|
227
271
|
} finally {
|
|
228
272
|
this.#transactions.delete(token.id)
|
|
229
273
|
|
|
230
|
-
if (transaction.releaseOnClose
|
|
231
|
-
|
|
274
|
+
if (transaction.releaseOnClose) {
|
|
275
|
+
if (failed) {
|
|
276
|
+
destroyMysqlConnection(transaction.connection)
|
|
277
|
+
} else if (isMysqlPoolConnection(transaction.connection)) {
|
|
278
|
+
transaction.connection.release()
|
|
279
|
+
}
|
|
232
280
|
}
|
|
233
281
|
}
|
|
234
282
|
}
|
|
@@ -245,13 +293,21 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
|
|
|
245
293
|
throw new Error('Unknown transaction token: ' + token.id)
|
|
246
294
|
}
|
|
247
295
|
|
|
296
|
+
let failed = false
|
|
248
297
|
try {
|
|
249
298
|
await transaction.connection.rollback()
|
|
299
|
+
} catch (error) {
|
|
300
|
+
failed = true
|
|
301
|
+
throw error
|
|
250
302
|
} finally {
|
|
251
303
|
this.#transactions.delete(token.id)
|
|
252
304
|
|
|
253
|
-
if (transaction.releaseOnClose
|
|
254
|
-
|
|
305
|
+
if (transaction.releaseOnClose) {
|
|
306
|
+
if (failed) {
|
|
307
|
+
destroyMysqlConnection(transaction.connection)
|
|
308
|
+
} else if (isMysqlPoolConnection(transaction.connection)) {
|
|
309
|
+
transaction.connection.release()
|
|
310
|
+
}
|
|
255
311
|
}
|
|
256
312
|
}
|
|
257
313
|
}
|
|
@@ -290,19 +346,144 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
|
|
|
290
346
|
}
|
|
291
347
|
|
|
292
348
|
/**
|
|
293
|
-
*
|
|
294
|
-
* @returns A promise that resolves when the
|
|
349
|
+
* Destructively recreates the configured MySQL database.
|
|
350
|
+
* @returns A promise that resolves when the database is ready for use.
|
|
295
351
|
*/
|
|
296
|
-
async
|
|
297
|
-
|
|
352
|
+
async wipe(): Promise<void> {
|
|
353
|
+
let config = this.#configOrThrow('wipe')
|
|
354
|
+
this.#assertNoOpenTransactions('wipe')
|
|
355
|
+
let database = resolveMysqlDatabaseName(config)
|
|
356
|
+
await this.#closePool()
|
|
357
|
+
let connection: MysqlConnection | undefined
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
connection = await createMysqlConnection(toMysqlServerConfig(config))
|
|
361
|
+
await connection.query('drop database if exists ' + quoteIdentifier(database))
|
|
362
|
+
|
|
363
|
+
let sql = 'create database ' + quoteIdentifier(database)
|
|
364
|
+
|
|
365
|
+
if (this.#characterSet) {
|
|
366
|
+
sql += ' character set ' + quoteIdentifier(this.#characterSet)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (this.#collation) {
|
|
370
|
+
sql += ' collate ' + quoteIdentifier(this.#collation)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
await connection.query(sql)
|
|
374
|
+
} finally {
|
|
375
|
+
try {
|
|
376
|
+
await connection?.end()
|
|
377
|
+
} finally {
|
|
378
|
+
await this.#replacePool()
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Closes a pool created from configuration. Supplied connections and pools remain caller-owned. */
|
|
384
|
+
async close(): Promise<void> {
|
|
385
|
+
this.#assertNoOpenTransactions('close')
|
|
386
|
+
if (this.#config) {
|
|
387
|
+
await this.#closePool()
|
|
388
|
+
}
|
|
298
389
|
}
|
|
299
390
|
|
|
300
391
|
/**
|
|
301
|
-
*
|
|
302
|
-
*
|
|
392
|
+
* Runs migration work on the mysql connection that owns the named lock.
|
|
393
|
+
*
|
|
394
|
+
* Lock acquisition waits up to 60 seconds and throws when the lock cannot
|
|
395
|
+
* be acquired. Re-entering this method from inside `run` throws instead of
|
|
396
|
+
* deadlocking, and a failed run destroys the reserved connection instead of
|
|
397
|
+
* returning it to the pool.
|
|
398
|
+
* @param name Logical migration lock name.
|
|
399
|
+
* @param run Migration work to run with a connection-bound driver.
|
|
400
|
+
* @returns The callback result.
|
|
303
401
|
*/
|
|
304
|
-
async
|
|
305
|
-
|
|
402
|
+
async withMigrationLock<result>(
|
|
403
|
+
name: string,
|
|
404
|
+
run: (driver: DatabaseDriver<'mysql'>) => Promise<result>,
|
|
405
|
+
): Promise<result> {
|
|
406
|
+
if (this.#migrationLockStore.getStore()) {
|
|
407
|
+
throw new Error('MySQL migration lock is already held by this database')
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let waitForPreviousLock = this.#migrationLockQueue
|
|
411
|
+
let releaseQueue: () => void = () => undefined
|
|
412
|
+
this.#migrationLockQueue = new Promise((resolve) => {
|
|
413
|
+
releaseQueue = resolve
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
await waitForPreviousLock
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
let releaseOnClose = false
|
|
420
|
+
let connection: MysqlTransactionConnection
|
|
421
|
+
|
|
422
|
+
if (isMysqlPool(this.#client)) {
|
|
423
|
+
connection = await this.#client.getConnection()
|
|
424
|
+
releaseOnClose = true
|
|
425
|
+
} else {
|
|
426
|
+
connection = this.#client
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let driver = releaseOnClose ? new MysqlDatabaseDriver(connection) : this
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
let value = await this.#migrationLockStore.run(true, () =>
|
|
433
|
+
runWithMysqlMigrationLock(connection, name, driver, run),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
if (releaseOnClose && isMysqlPoolConnection(connection)) {
|
|
437
|
+
connection.release()
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return value
|
|
441
|
+
} catch (error) {
|
|
442
|
+
// A failed run can leave the reserved connection dirty (open
|
|
443
|
+
// transaction, still-held named lock), so destroy the connection
|
|
444
|
+
// instead of returning it to the pool.
|
|
445
|
+
if (releaseOnClose) {
|
|
446
|
+
destroyMysqlConnection(connection)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
throw error
|
|
450
|
+
}
|
|
451
|
+
} finally {
|
|
452
|
+
releaseQueue()
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async #closePool(): Promise<void> {
|
|
457
|
+
this.#transactions.clear()
|
|
458
|
+
// mysql2 pools error on end() when called twice, so ending must be
|
|
459
|
+
// tracked to keep close() idempotent.
|
|
460
|
+
if (isMysqlPool(this.#client) && !this.#poolClosed) {
|
|
461
|
+
this.#poolClosed = true
|
|
462
|
+
await this.#client.end()
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async #replacePool(): Promise<void> {
|
|
467
|
+
await this.#closePool().catch(() => undefined)
|
|
468
|
+
|
|
469
|
+
if (this.#config) {
|
|
470
|
+
this.#client = createMysqlPool(this.#config)
|
|
471
|
+
this.#poolClosed = false
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
#configOrThrow(method: string): string | MysqlPoolOptions {
|
|
476
|
+
if (!this.#config) {
|
|
477
|
+
throw new Error('MySQL database ' + method + '() requires config-based construction')
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return this.#config
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
#assertNoOpenTransactions(method: string): void {
|
|
484
|
+
if (this.#transactions.size > 0) {
|
|
485
|
+
throw new Error('MySQL database cannot ' + method + ' while transactions are open')
|
|
486
|
+
}
|
|
306
487
|
}
|
|
307
488
|
|
|
308
489
|
#resolveClient(token: TransactionToken | undefined): MysqlQueryable {
|
|
@@ -324,36 +505,159 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
|
|
|
324
505
|
}
|
|
325
506
|
}
|
|
326
507
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
* let adapter = createMysqlDatabaseAdapter(pool)
|
|
340
|
-
* let db = createDatabase(adapter)
|
|
341
|
-
* ```
|
|
342
|
-
*/
|
|
343
|
-
export function createMysqlDatabaseAdapter(client: MysqlQueryable): MysqlDatabaseAdapter {
|
|
344
|
-
return new MysqlDatabaseAdapter(client)
|
|
508
|
+
function isMysqlQueryable(value: unknown): value is MysqlQueryable {
|
|
509
|
+
return typeof value === 'object' && value !== null && 'query' in value
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function createMysqlPool(config: string | MysqlPoolOptions): MysqlPool {
|
|
513
|
+
return typeof config === 'string' ? mysql.createPool(config) : mysql.createPool(config)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function createMysqlConnection(config: string | MysqlPoolOptions): Promise<MysqlConnection> {
|
|
517
|
+
return typeof config === 'string'
|
|
518
|
+
? mysql.createConnection(config)
|
|
519
|
+
: mysql.createConnection(config)
|
|
345
520
|
}
|
|
346
521
|
|
|
347
522
|
function isMysqlPool(client: MysqlQueryable): client is MysqlPool {
|
|
348
523
|
return 'getConnection' in client && typeof client.getConnection === 'function'
|
|
349
524
|
}
|
|
350
525
|
|
|
526
|
+
function resolveMysqlDatabaseName(config: string | MysqlPoolOptions): string {
|
|
527
|
+
let database =
|
|
528
|
+
typeof config === 'string'
|
|
529
|
+
? resolveDatabaseNameFromUrl(config)
|
|
530
|
+
: (config.database ?? resolveDatabaseNameFromUrl(config.uri ?? ''))
|
|
531
|
+
|
|
532
|
+
if (!database) {
|
|
533
|
+
throw new Error('MySQL database config requires a database name')
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return database
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function resolveDatabaseNameFromUrl(value: string): string | undefined {
|
|
540
|
+
try {
|
|
541
|
+
let url = new URL(value)
|
|
542
|
+
let database = decodeURIComponent(url.pathname.replace(/^\//, ''))
|
|
543
|
+
return database || undefined
|
|
544
|
+
} catch {
|
|
545
|
+
return undefined
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function toMysqlServerConfig(config: string | MysqlPoolOptions): string | MysqlPoolOptions {
|
|
550
|
+
if (typeof config === 'string') {
|
|
551
|
+
try {
|
|
552
|
+
let url = new URL(config)
|
|
553
|
+
url.pathname = '/'
|
|
554
|
+
return url.toString()
|
|
555
|
+
} catch {
|
|
556
|
+
return config
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Pool-only options make mysql2's createConnection() log "Ignoring invalid
|
|
561
|
+
// configuration option" warnings, so strip them from the maintenance
|
|
562
|
+
// connection config.
|
|
563
|
+
let {
|
|
564
|
+
database: _database,
|
|
565
|
+
uri,
|
|
566
|
+
connectionLimit: _connectionLimit,
|
|
567
|
+
maxIdle: _maxIdle,
|
|
568
|
+
idleTimeout: _idleTimeout,
|
|
569
|
+
queueLimit: _queueLimit,
|
|
570
|
+
waitForConnections: _waitForConnections,
|
|
571
|
+
...serverConfig
|
|
572
|
+
} = config
|
|
573
|
+
|
|
574
|
+
if (uri === undefined) {
|
|
575
|
+
return serverConfig
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return { ...serverConfig, uri: removeDatabaseFromUrl(uri) }
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function removeDatabaseFromUrl(value: string): string {
|
|
582
|
+
try {
|
|
583
|
+
let url = new URL(value)
|
|
584
|
+
url.pathname = '/'
|
|
585
|
+
return url.toString()
|
|
586
|
+
} catch {
|
|
587
|
+
return value
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
351
591
|
function isMysqlPoolConnection(
|
|
352
592
|
connection: MysqlTransactionConnection,
|
|
353
593
|
): connection is MysqlPoolConnection {
|
|
354
594
|
return 'release' in connection && typeof connection.release === 'function'
|
|
355
595
|
}
|
|
356
596
|
|
|
597
|
+
function destroyMysqlConnection(connection: MysqlTransactionConnection): void {
|
|
598
|
+
let destroy = (connection as { destroy?: () => void }).destroy
|
|
599
|
+
|
|
600
|
+
if (typeof destroy === 'function') {
|
|
601
|
+
destroy.call(connection)
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
void connection.end().catch(() => undefined)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function runWithMysqlMigrationLock<result>(
|
|
609
|
+
connection: MysqlTransactionConnection,
|
|
610
|
+
name: string,
|
|
611
|
+
driver: MysqlDatabaseDriver,
|
|
612
|
+
run: (driver: DatabaseDriver<'mysql'>) => Promise<result>,
|
|
613
|
+
): Promise<result> {
|
|
614
|
+
// sha2(..., 256) yields 64 hex characters, exactly GET_LOCK's 64-character
|
|
615
|
+
// lock name limit, so any additional prefix must go inside the hash input.
|
|
616
|
+
let [lockRows] = await connection.query(
|
|
617
|
+
"select lock_name, get_lock(lock_name, 60) as `acquired` from (select sha2(concat(coalesce(database(), ''), ':', ?), 256) as lock_name) as migration_lock",
|
|
618
|
+
[name],
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
let lockRow = isRowsResult(lockRows) ? lockRows[0] : undefined
|
|
622
|
+
let lockName = lockRow?.lock_name
|
|
623
|
+
|
|
624
|
+
if (typeof lockName !== 'string' || !toBooleanExists(lockRow?.acquired)) {
|
|
625
|
+
throw new Error('MySQL migration lock could not be acquired')
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
let outcome: { status: 'success'; value: result } | { status: 'failure'; error: unknown }
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
outcome = { status: 'success', value: await run(driver) }
|
|
632
|
+
} catch (error) {
|
|
633
|
+
outcome = { status: 'failure', error }
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
let unlockFailed = false
|
|
637
|
+
let unlockError: unknown
|
|
638
|
+
|
|
639
|
+
try {
|
|
640
|
+
let [unlockRows] = await connection.query('select release_lock(?) as `released`', [lockName])
|
|
641
|
+
|
|
642
|
+
if (!isRowsResult(unlockRows) || !toBooleanExists(unlockRows[0]?.released)) {
|
|
643
|
+
throw new Error('MySQL migration lock was not held by the reserved connection')
|
|
644
|
+
}
|
|
645
|
+
} catch (error) {
|
|
646
|
+
unlockFailed = true
|
|
647
|
+
unlockError = error
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (outcome.status === 'failure') {
|
|
651
|
+
throw outcome.error
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (unlockFailed) {
|
|
655
|
+
throw unlockError
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return outcome.value
|
|
659
|
+
}
|
|
660
|
+
|
|
357
661
|
function isRowsResult(result: unknown): result is MysqlQueryRows {
|
|
358
662
|
return Array.isArray(result) && (result.length === 0 || !Array.isArray(result[0]))
|
|
359
663
|
}
|
package/src/lib/sql-compiler.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getTableName, getTablePrimaryKey } from '@remix-run/data-table'
|
|
2
|
-
import type {
|
|
2
|
+
import type { Predicate, SqlStatement } from '@remix-run/data-table'
|
|
3
|
+
import type { DataManipulationOperation } from '@remix-run/data-table'
|
|
3
4
|
import {
|
|
4
5
|
collectColumns as collectColumnsHelper,
|
|
5
6
|
normalizeJoinType as normalizeJoinTypeHelper,
|
|
@@ -43,8 +44,8 @@ export function compileMysqlOperation(operation: DataManipulationOperation): Sql
|
|
|
43
44
|
compileGroupByClause(operation.groupBy) +
|
|
44
45
|
compileHavingClause(operation.having, context) +
|
|
45
46
|
compileOrderByClause(operation.orderBy) +
|
|
46
|
-
compileLimitClause(operation.limit) +
|
|
47
|
-
compileOffsetClause(operation.offset),
|
|
47
|
+
compileLimitClause(operation.limit, context) +
|
|
48
|
+
compileOffsetClause(operation.offset, context),
|
|
48
49
|
values: context.values,
|
|
49
50
|
}
|
|
50
51
|
}
|
|
@@ -279,20 +280,20 @@ function compileOrderByClause(orderBy: { column: string; direction: 'asc' | 'des
|
|
|
279
280
|
)
|
|
280
281
|
}
|
|
281
282
|
|
|
282
|
-
function compileLimitClause(limit: number | undefined): string {
|
|
283
|
+
function compileLimitClause(limit: number | undefined, context: CompileContext): string {
|
|
283
284
|
if (limit === undefined) {
|
|
284
285
|
return ''
|
|
285
286
|
}
|
|
286
287
|
|
|
287
|
-
return ' limit ' +
|
|
288
|
+
return ' limit ' + pushValue(context, limit)
|
|
288
289
|
}
|
|
289
290
|
|
|
290
|
-
function compileOffsetClause(offset: number | undefined): string {
|
|
291
|
+
function compileOffsetClause(offset: number | undefined, context: CompileContext): string {
|
|
291
292
|
if (offset === undefined) {
|
|
292
293
|
return ''
|
|
293
294
|
}
|
|
294
295
|
|
|
295
|
-
return ' offset ' +
|
|
296
|
+
return ' offset ' + pushValue(context, offset)
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
function compilePredicate(predicate: Predicate, context: CompileContext): string {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/lib/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,uBAAuB,CAAA;AAE9B,OAAO,KAAK,EACV,UAAU,IAAI,eAAe,EAC7B,IAAI,IAAI,SAAS,EACjB,cAAc,IAAI,mBAAmB,EAGtC,MAAM,gBAAgB,CAAA;AAcvB,KAAK,0BAA0B,GAAG,eAAe,GAAG,mBAAmB,CAAA;AACvE,KAAK,cAAc,GAAG,SAAS,GAAG,0BAA0B,CAAA;AAE5D;;GAEG;AACH,qBAAa,oBAAqB,YAAW,eAAe;;IAC1D;;OAEG;IACH,OAAO,SAAU;IAEjB;;OAEG;IACH,YAAY;;;;;;MAAA;IAMZ,YAAY,MAAM,EAAE,cAAc,EASjC;IAED;;;;OAIG;IACH,UAAU,CAAC,SAAS,EAAE,yBAAyB,GAAG,YAAY,EAAE,CAG/D;IAED;;;;OAIG;IACG,OAAO,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA8B/E;IAED;;;;;;;;OAQG;IACG,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9E;IAED;;;;;OAKG;IACG,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,WAAW,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAchF;IAED;;;;;;OAMG;IACG,SAAS,CACb,KAAK,EAAE,QAAQ,EACf,MAAM,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,gBAAgB,GAC7B,OAAO,CAAC,OAAO,CAAC,CAclB;IAED;;;;OAIG;IACG,gBAAgB,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgC9E;IAED;;;;OAIG;IACG,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB9D;IAED;;;;OAIG;IACG,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBhE;IAED;;;;;OAKG;IACG,eAAe,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1E;IAED;;;;;OAKG;IACG,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9E;IAED;;;;;OAKG;IACG,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG3E;IAED;;;OAGG;IACG,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAE1C;IAED;;;OAGG;IACG,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAE1C;CAmBF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,cAAc,GAAG,oBAAoB,CAEvF"}
|