Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add repair migration cli command. #37

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:serverpod/protocol.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod/src/database/analyze.dart';
import 'package:serverpod/src/database/migrations/migrations.dart';
import 'package:serverpod/src/database/migrations/repair_migrations.dart';
import 'package:serverpod_shared/serverpod_shared.dart';

import '../../generated/protocol.dart' as internal;
Expand Down Expand Up @@ -144,6 +145,26 @@ class MigrationManager {
return versions.sublist(index + 1);
}

/// Applies the repair migration to the database.
Future<void> applyRepairMigration(Session session) async {
var repairMigration = RepairMigration.load(Directory.current);
if (repairMigration == null) {
return;
}

var appliedRepairMigration = await DatabaseMigrationVersion.db.findFirstRow(
session,
where: (t) =>
t.module.equals(MigrationConstants.repairMigrationModuleName));

if (appliedRepairMigration != null &&
appliedRepairMigration.version == repairMigration.versionName) {
return;
}

await session.dbNext.unsafeExecute(repairMigration.sqlMigration);
}

/// Migrates all modules to the latest version.
Future<void> migrateToLatest(Session session) async {
var migrations = <Migration>[];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:serverpod_shared/serverpod_shared.dart';

/// [RepairMigration] is used to repair a database back to a migration version.
class RepairMigration {
/// The version of the repair migration.
final String versionName;

/// The SQL to run to repair the database.
final String sqlMigration;

RepairMigration._({
required this.versionName,
required this.sqlMigration,
});

/// Loads the repair migration from the repair migration directory.
/// Returns null if no repair migration is found.
static RepairMigration? load(Directory projectRootDirectory) {
var repairMigrationFiles =
MigrationConstants.repairMigrationDirectory(projectRootDirectory)
.listSync()
.whereType<File>();
if (repairMigrationFiles.isEmpty) {
return null;
}

var migrationSqlFile = repairMigrationFiles.cast<File?>().firstWhere(
(element) => element != null
? path.basename(element.path).endsWith('.sql')
: false,
orElse: () => null,
);

if (migrationSqlFile == null) {
return null;
}

var version = path.basename(migrationSqlFile.path).split('.').first;
var sqlMigration = migrationSqlFile.readAsStringSync();

return RepairMigration._(
versionName: version,
sqlMigration: sqlMigration,
);
}
}
10 changes: 10 additions & 0 deletions packages/serverpod/lib/src/server/command_line_args.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ class CommandLineArgs {
/// If true, the server will apply database migrations on startup.
late final bool applyMigrations;

/// If true, the server will apply database repair migration on startup.
late final bool applyRepairMigration;

/// Parses the command line arguments passed to Serverpod and creates a
/// [CommandLineArgs] object.
CommandLineArgs(List<String> args) {
Expand Down Expand Up @@ -92,6 +95,11 @@ class CommandLineArgs {
'apply-migrations',
abbr: 'a',
defaultsTo: false,
)
..addFlag(
'apply-repair-migration',
abbr: 'A',
defaultsTo: false,
);
var results = argParser.parse(args);

Expand Down Expand Up @@ -120,6 +128,7 @@ class CommandLineArgs {
}

applyMigrations = results['apply-migrations'] ?? false;
applyRepairMigration = results['apply-repair-migration'] ?? false;
} catch (e) {
stdout.writeln(
'Failed to parse command line arguments. Using default values. $e',
Expand All @@ -129,6 +138,7 @@ class CommandLineArgs {
loggingMode = ServerpodLoggingMode.normal;
role = ServerpodRole.monolith;
applyMigrations = false;
applyRepairMigration = false;
}
}

Expand Down
13 changes: 11 additions & 2 deletions packages/serverpod/lib/src/server/serverpod.dart
Original file line number Diff line number Diff line change
Expand Up @@ -350,9 +350,16 @@ class Serverpod {
migrationManager = MigrationManager();
await migrationManager.initialize(session);

if (commandLineArgs.applyRepairMigration) {
logVerbose('Applying database repair migration');
await migrationManager.applyRepairMigration(session);
await migrationManager.initialize(session);
}

if (commandLineArgs.applyMigrations) {
logVerbose('Applying database migrations.');
await migrationManager.migrateToLatest(session);
await migrationManager.initialize(session);
}

logVerbose('Verifying database integrity.');
Expand Down Expand Up @@ -425,7 +432,8 @@ class Serverpod {
// no other maintenance tasks will be run.
if (commandLineArgs.role == ServerpodRole.monolith ||
(commandLineArgs.role == ServerpodRole.maintenance &&
!commandLineArgs.applyMigrations)) {
!(commandLineArgs.applyMigrations |
commandLineArgs.applyRepairMigration))) {
logVerbose('Starting maintenance tasks.');

// Start future calls
Expand All @@ -438,7 +446,8 @@ class Serverpod {
logVerbose('Serverpod start complete.');

if (commandLineArgs.role == ServerpodRole.maintenance &&
commandLineArgs.applyMigrations) {
(commandLineArgs.applyMigrations |
commandLineArgs.applyRepairMigration)) {
logVerbose('Finished applying database migrations.');
exit(0);
}
Expand Down
13 changes: 12 additions & 1 deletion packages/serverpod_shared/lib/src/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,19 @@ abstract class MigrationConstants {
/// Filename of the migration registry.
static const migrationRegistryFileName = 'migration_registry.json';

/// Module name in database under which repair migrations are stored.
static const repairMigrationModuleName = '_repair';

/// Directory where migrations are stored.
static Directory migrationsBaseDirectory(Directory serverRootDirectory) =>
Directory(path.join(
serverRootDirectory.path, 'generated', 'migration', 'migrations'));
_migrationDirectory(serverRootDirectory).path, 'migrations'));

/// Directory where repair migrations are stored.
static Directory repairMigrationDirectory(Directory serverRootDirectory) =>
Directory(
path.join(_migrationDirectory(serverRootDirectory).path, 'repair'));

static Directory _migrationDirectory(Directory serverRootDirectory) =>
Directory(path.join(serverRootDirectory.path, 'generated', 'migration'));
}
37 changes: 37 additions & 0 deletions packages/serverpod_shared/lib/src/migration_exceptions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,40 @@ class MigrationVersionLoadException implements Exception {
required this.exception,
});
}

/// Exception thrown when trying to load the live database definition.
class MigrationLiveDatabaseDefinitionException implements Exception {
/// The exception that was thrown.
final String exception;

/// Creates a new [MigrationLiveDatabaseDefinitionException].
MigrationLiveDatabaseDefinitionException({
required this.exception,
});
}

/// Exception thrown when writing a migration fails.
class MigrationRepairWriteException implements Exception {
/// The exception that was thrown.
final String exception;

/// Creates a new [MigrationRepairWriteException].
MigrationRepairWriteException({
required this.exception,
});
}

/// Exception thrown when a migration target is not found.
class MigrationRepairTargetNotFoundException implements Exception {
/// The versions that were found.
final List<String> versionsFound;

/// The name of the target that was not found.
final String targetName;

/// Creates a new [MigrationRepairTargetNotFoundException].
MigrationRepairTargetNotFoundException({
required this.versionsFound,
required this.targetName,
});
}
Loading
Loading