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