@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.
@@ -1,103 +1,112 @@
1
1
  import type {
2
- AdapterCapabilityOverrides,
3
- AdapterExecuteRequest,
4
- AdapterResult,
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 { compileMysqlStatement } from './sql-compiler.ts'
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: MysqlDatabaseConnection
32
+ connection: MysqlTransactionConnection
59
33
  releaseOnClose: boolean
60
34
  }
61
35
 
62
- type MysqlQueryable = MysqlDatabasePool | MysqlDatabaseConnection
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, options?: MysqlDatabaseAdapterOptions) {
62
+ constructor(client: MysqlQueryable) {
76
63
  this.#client = client
77
64
  this.capabilities = {
78
- returning: options?.capabilities?.returning ?? false,
79
- savepoints: options?.capabilities?.savepoints ?? true,
80
- upsert: options?.capabilities?.upsert ?? true,
65
+ returning: false,
66
+ savepoints: true,
67
+ upsert: true,
68
+ transactionalDdl: false,
69
+ migrationLock: true,
81
70
  }
82
71
  }
83
72
 
84
- async execute(request: AdapterExecuteRequest): Promise<AdapterResult> {
85
- if (request.statement.kind === 'insertMany' && request.statement.values.length === 0) {
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.statement.returning ? [] : undefined,
97
+ rows: request.operation.returning ? [] : undefined,
90
98
  }
91
99
  }
92
100
 
93
- let statement = compileMysqlStatement(request.statement)
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.statement.kind === 'count' || request.statement.kind === 'exists') {
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.statement.kind, request.statement, header),
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: MysqlDatabaseConnection
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
- #resolveClient(token: TransactionToken | undefined): MysqlDatabaseConnection | MysqlDatabasePool {
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): MysqlDatabaseConnection {
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: MysqlQueryable,
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 MysqlDatabasePool {
233
- return typeof (client as MysqlDatabasePool).getConnection === 'function'
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 { affectedRows?: unknown; insertId?: unknown }
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: AdapterExecuteRequest['statement']['kind'],
288
- statement: AdapterExecuteRequest['statement'],
445
+ kind: DataManipulationRequest['operation']['kind'],
446
+ operation: DataManipulationRequest['operation'],
289
447
  header: MysqlQueryResultHeader,
290
448
  ): unknown {
291
- if (!isInsertStatementKind(kind) || !isInsertStatement(statement)) {
449
+ if (!isInsertOperationKind(kind) || !isInsertOperation(operation)) {
292
450
  return undefined
293
451
  }
294
452
 
295
- if (getTablePrimaryKey(statement.table).length !== 1) {
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 isInsertStatementKind(kind: AdapterExecuteRequest['statement']['kind']): boolean {
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 isInsertStatement(
311
- statement: AdapterExecuteRequest['statement'],
312
- ): statement is Extract<
313
- AdapterExecuteRequest['statement'],
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
- statement.kind === 'insert' || statement.kind === 'insertMany' || statement.kind === 'upsert'
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
+ }