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