Fixed MySQL migration

This commit is contained in:
Thomas Hii 2024-07-20 15:37:25 +08:00
parent 7e42a7e317
commit 5ed443ae33
14 changed files with 113 additions and 22 deletions

View file

@ -11,7 +11,7 @@ This package contains the abstract classes for implementing database migration i
* Create tables based on ORM models * Create tables based on ORM models
* Drop tables based on ORM models * Drop tables based on ORM models
* Add new tables based ORM models * Add new tables based on ORM models
## Limitation ## Limitation

View file

@ -4,9 +4,10 @@
* Updated README * Updated README
* Updated examples * Updated examples
* Added test cases for `PostgreSQL` * Updated `PostgresMigrationRunner` error handling
* Added test cases for `MySQL` * Fixed `MySqlMigrationRunner` migration issues
* Added test cases for `MariaDB` * Added test cases for `PostgreSQL`, `MySQL` and `MariaDB`
* Added auto increment integer primary key suppport to MySQL and MariaDB
## 8.2.0 ## 8.2.0

View file

@ -50,6 +50,7 @@ class _RefreshCommand extends Command {
@override @override
String get name => 'refresh'; String get name => 'refresh';
@override @override
String get description => String get description =>
'Resets the database, and then re-runs all migrations.'; 'Resets the database, and then re-runs all migrations.';
@ -67,8 +68,9 @@ class _RollbackCommand extends Command {
@override @override
String get name => 'rollback'; String get name => 'rollback';
@override @override
String get description => 'Undoes the last batch of migrations.'; String get description => 'Undo the last batch of migrations.';
final MigrationRunner migrationRunner; final MigrationRunner migrationRunner;

View file

@ -40,7 +40,7 @@ class MariaDbMigrationRunner implements MigrationRunner {
await connection.query(''' await connection.query('''
CREATE TABLE IF NOT EXISTS migrations ( CREATE TABLE IF NOT EXISTS migrations (
id serial, id integer NOT NULL AUTO_INCREMENT,
batch integer, batch integer,
path varchar(255), path varchar(255),
PRIMARY KEY(id) PRIMARY KEY(id)

View file

@ -48,6 +48,12 @@ abstract class MariaDbGenerator {
buf.write(' UNIQUE'); buf.write(' UNIQUE');
} else if (column.indexType == IndexType.primaryKey) { } else if (column.indexType == IndexType.primaryKey) {
buf.write(' PRIMARY KEY'); buf.write(' PRIMARY KEY');
// For int based primary key, apply NOT NULL
// and AUTO_INCREMENT
if (column.type == ColumnType.int) {
buf.write(' NOT NULL AUTO_INCREMENT');
}
} }
for (var ref in column.externalReferences) { for (var ref in column.externalReferences) {
@ -105,7 +111,7 @@ class MariaDbTable extends Table {
if (indexBuf.isNotEmpty) { if (indexBuf.isNotEmpty) {
for (var i = 0; i < indexBuf.length; i++) { for (var i = 0; i < indexBuf.length; i++) {
buf.write(',\n${indexBuf[$1]}'); buf.write(',\n${indexBuf[i]}');
} }
} }
} }

View file

@ -42,15 +42,18 @@ class MySqlMigrationRunner implements MigrationRunner {
await connection.execute(''' await connection.execute('''
CREATE TABLE IF NOT EXISTS migrations ( CREATE TABLE IF NOT EXISTS migrations (
id serial, id integer NOT NULL AUTO_INCREMENT,
batch integer, batch integer,
path varchar(255), path varchar(500),
PRIMARY KEY(id) PRIMARY KEY(id)
); );
''').then((result) { ''').then((result) {
//print(result);
_log.info('Check and create "migrations" table'); _log.info('Check and create "migrations" table');
}).catchError((e) { }).catchError((e) {
//print(e);
_log.severe('Failed to create "migrations" table.'); _log.severe('Failed to create "migrations" table.');
throw e;
}); });
} }
@ -59,8 +62,9 @@ class MySqlMigrationRunner implements MigrationRunner {
await _init(); await _init();
var result = await connection.execute('SELECT path from migrations;'); var result = await connection.execute('SELECT path from migrations;');
var existing = <String>[]; var existing = <String>[];
if (result.rows.isNotEmpty) { for (var item in result.rows) {
existing = result.rows.first.assoc().values.cast<String>().toList(); var rec = item.assoc().values.first ?? "";
existing.add(rec.replaceAll("\\", "\\\\"));
} }
var toRun = <String>[]; var toRun = <String>[];
@ -113,8 +117,9 @@ class MySqlMigrationRunner implements MigrationRunner {
result = await connection result = await connection
.execute('SELECT path from migrations WHERE batch = $curBatch;'); .execute('SELECT path from migrations WHERE batch = $curBatch;');
var existing = <String>[]; var existing = <String>[];
if (result.rows.isNotEmpty) { for (var item in result.rows) {
existing = result.rows.first.assoc().values.cast<String>().toList(); var rec = item.assoc().values.first ?? "";
existing.add(rec.replaceAll("\\", "\\\\"));
} }
var toRun = <String>[]; var toRun = <String>[];
@ -143,11 +148,15 @@ class MySqlMigrationRunner implements MigrationRunner {
await _init(); await _init();
var result = await connection var result = await connection
.execute('SELECT path from migrations ORDER BY batch DESC;'); .execute('SELECT path from migrations ORDER BY batch DESC;');
// "mysql_client" driver will auto convert path containing "\\" to "\".
// So need to revert "\" back to "\\" for the migration logic to work
var existing = <String>[]; var existing = <String>[];
if (result.rows.isNotEmpty) { for (var item in result.rows) {
var firstRow = result.rows.first; var rec = item.assoc().values.first ?? "";
existing = firstRow.assoc().values.cast<String>().toList(); existing.add(rec.replaceAll("\\", "\\\\"));
} }
var toRun = existing.where(migrations.containsKey).toList(); var toRun = existing.where(migrations.containsKey).toList();
if (toRun.isNotEmpty) { if (toRun.isNotEmpty) {

View file

@ -22,11 +22,13 @@ class MySqlSchema extends Schema {
await connection.transactional((ctx) async { await connection.transactional((ctx) async {
var sql = compile(); var sql = compile();
var result = await ctx.execute(sql).catchError((e) { var result = await ctx.execute(sql).catchError((e) {
print(e);
_log.severe('Failed to run query: [ $sql ]', e); _log.severe('Failed to run query: [ $sql ]', e);
throw Exception(e); throw Exception(e);
}); });
affectedRows = result.affectedRows.toInt(); affectedRows = result.affectedRows.toInt();
}).catchError((e) { }).catchError((e) {
print(e);
_log.severe('Failed to run query in a transaction', e); _log.severe('Failed to run query in a transaction', e);
}); });

View file

@ -10,6 +10,7 @@ abstract class MySqlGenerator {
if (column.type == ColumnType.timeStamp) { if (column.type == ColumnType.timeStamp) {
str = ColumnType.dateTime.name; str = ColumnType.dateTime.name;
} }
if (column.type.hasLength) { if (column.type.hasLength) {
return '$str(${column.length})'; return '$str(${column.length})';
} else { } else {
@ -20,7 +21,11 @@ abstract class MySqlGenerator {
static String compileColumn(MigrationColumn column) { static String compileColumn(MigrationColumn column) {
var buf = StringBuffer(columnType(column)); var buf = StringBuffer(columnType(column));
if (column.isNullable == false) buf.write(' NOT NULL'); if (!column.isNullable) {
buf.write(' NOT NULL');
}
// Default value
if (column.defaultValue != null) { if (column.defaultValue != null) {
String s; String s;
var value = column.defaultValue; var value = column.defaultValue;
@ -47,6 +52,12 @@ abstract class MySqlGenerator {
buf.write(' UNIQUE'); buf.write(' UNIQUE');
} else if (column.indexType == IndexType.primaryKey) { } else if (column.indexType == IndexType.primaryKey) {
buf.write(' PRIMARY KEY'); buf.write(' PRIMARY KEY');
// For int based primary key, apply NOT NULL
// and AUTO_INCREMENT
if (column.type == ColumnType.int) {
buf.write(' NOT NULL AUTO_INCREMENT');
}
} }
for (var ref in column.externalReferences) { for (var ref in column.externalReferences) {
@ -104,9 +115,11 @@ class MysqlTable extends Table {
if (indexBuf.isNotEmpty) { if (indexBuf.isNotEmpty) {
for (var i = 0; i < indexBuf.length; i++) { for (var i = 0; i < indexBuf.length; i++) {
buf.write(',\n${indexBuf[$1]}'); buf.write(',\n${indexBuf[i]}');
} }
} }
print(buf);
} }
} }

View file

@ -45,7 +45,7 @@ class PostgresMigrationRunner implements MigrationRunner {
CREATE TABLE IF NOT EXISTS "migrations" ( CREATE TABLE IF NOT EXISTS "migrations" (
id serial, id serial,
batch integer, batch integer,
path varchar, path varchar(500),
PRIMARY KEY(id) PRIMARY KEY(id)
); );
''').then((result) { ''').then((result) {

View file

@ -5,7 +5,7 @@ import 'package:angel3_migration_runner/mariadb.dart';
import 'package:mysql1/mysql1.dart'; import 'package:mysql1/mysql1.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'models/todo.dart'; import 'models/mysql_todo.dart';
void main() async { void main() async {
late MySqlConnection conn; late MySqlConnection conn;

View file

@ -0,0 +1,53 @@
import 'package:angel3_migration/angel3_migration.dart';
import 'package:angel3_orm/angel3_orm.dart';
class UserMigration implements Migration {
@override
void up(Schema schema) {
schema.create('users', (table) {
table
..integer('id').primaryKey()
..varChar('username', length: 32).unique()
..varChar('password')
..boolean('account_confirmed').defaultsTo(false);
});
}
@override
void down(Schema schema) {
schema.drop('users');
}
}
class TodoMigration implements Migration {
@override
void up(Schema schema) {
schema.create('todos', (table) {
table
..integer('id').primaryKey()
..integer('user_id').references('users', 'id').onDeleteCascade()
..varChar('text')
..boolean('completed').defaultsTo(false);
});
}
@override
void down(Schema schema) {
schema.drop('todos');
}
}
class ItemMigration extends Migration {
@override
void up(Schema schema) {
schema.create('items', (table) {
table
..integer('id').primaryKey()
..varChar('name', length: 64)
..timeStamp('created_at').defaultsTo(currentTimestamp);
});
}
@override
void down(Schema schema) => schema.drop('items');
}

View file

@ -5,7 +5,7 @@ import 'package:angel3_migration_runner/mysql.dart';
import 'package:mysql_client/mysql_client.dart'; import 'package:mysql_client/mysql_client.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'models/todo.dart'; import 'models/mysql_todo.dart';
void main() async { void main() async {
late MySQLConnection conn; late MySQLConnection conn;
@ -47,6 +47,11 @@ void main() async {
print("Test migration up"); print("Test migration up");
await runner.up(); await runner.up();
}); });
test('reset', () async {
print("Test migration reset");
await runner.reset();
});
}); });
tearDown(() async { tearDown(() async {

View file

@ -5,7 +5,7 @@ import 'package:angel3_migration_runner/postgres.dart';
import 'package:postgres/postgres.dart'; import 'package:postgres/postgres.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'models/todo.dart'; import 'models/pg_todo.dart';
void main() async { void main() async {
late Connection conn; late Connection conn;