
Lightweight migration library manages version-based updates, supporting rollback and recovery. Ideal for configuration changes, file migrations, database updates, with observer monitoring and customizable recovery strategies.
Icarion is a lightweight, extensible migration library designed to handle version-based update migrations for your application. It supports both rollback and recovery mechanisms for fine-grained control over migrations, making it ideal for settings and configuration changes, file migrations, even database updates and more.
Written for Kotlin Multiplatform you can run it on Android, iOS and any JVM based system: Ktor, Spring, Desktop, you name it...Android based projects were the main culprit behind Icarion idea as many times devs would just perform SharedPreferences or FirebaseConfig data updates in the Application onCreate() based on current BuildConfig version value without any long term organization of migrations.
This library is here to help alleviate some of the pain of rolling out your own system of migrations, no matter where you run it.
Inspired by Icarus myth, which is often interpreted as a cautionary tale about ego, self-sabotage, and the consequences of ignoring wise counsel. Icarus, in Greek mythology, son of the inventor Daedalus who perished by flying too near the Sun with waxen wings.
Add Icarion to your project as a dependency. If you use Gradle, add the following:
repositories {
mavenCentral()
}
dependencies {
implementation("xyz.amplituhedron:icarion:1.1.0")
}You can use any versioning system that implements Comparable. Icarion comes with two versioning schemes out of the box for easier integration:
data class IntVersion(val value: Int) and data class SemanticVersion(val major: Int, val minor: Int, val patch: Int)
val v1 = IntVersion(1)
val v3 = IntVersion(3)
val v1_1_1 = SemanticVersion(1, 1, 1)
val v2_3_0 = SemanticVersion.fromVersion("2.3.0")Enums can be used as they implement comparable by default via their natural ordering:
enum class YourAppNamedVersion {
ACACIA, // Lowest
BIRCH,
CEDAR,
DOUGLAS_FIR,
OAK,
PINE,
SEQUOIA; // Highest
}
val v1 = YourAppNamedVersion.ACACIA
val v3 = YourAppNamedVersion.CEDARTo define a migration, you need to implement the AppUpdateMigration interface. This interface requires you to define:
targetVersion: The version this migration updates to.migrate: The logic to apply the migration.rollback: The logic to revert the migration in case of failure.class SampleMigration : AppUpdateMigration<IntVersion> {
override val targetVersion = IntVersion(2)
override suspend fun migrate() {
println("Migrating to version $targetVersion")
// Add your migration logic here
}
override suspend fun rollback() {
println("Rolling back version $targetVersion")
// Add your rollback logic here
}
}class FeatureUpgradeMigrationV110 : AppUpdateMigration<SemanticVersion> {
override val targetVersion = SemanticVersion(1, 1, 0)
override suspend fun migrate() {
println("Upgrading feature to version $targetVersion")
// Add feature-specific migration logic here
// Intentionally failed migrations should throw an exception here, for ex. throw RuntimeException("Can not migrate all data to external storage...")
}
override suspend fun rollback() {
println("Reverting feature upgrade for version $targetVersion")
// Add feature-specific rollback logic here
}
}Define as many migrations as needed for your application. Each migration should handle only the changes required for its specific version.
Once you've implemented your migrations, register them with an instance of IcarionMigrator. This ensures the migrator knows which migrations are available for execution.
// Register multiple migrations at once
val migrator = IcarionMigrator<IntVersion>().apply {
registerMigration(FeatureUpgradeMigrationV1())
registerMigration(FeatureUpgradeMigrationV2())
registerMigration(FeatureUpgradeMigrationV3())
registerMigration(FeatureUpgradeMigrationV4())
registerMigration(FeatureUpgradeMigrationV5())
}
// Register individual migrations
migrator.registerMigration(SampleMigration())• You cannot register migrations while a migration process is running. An IllegalStateException will be thrown if you attempt to do so.
• Each migration must target a unique version. If you register two migrations with the same targetVersion, an IllegalArgumentException will be thrown.
Registering migrations correctly ensures that the migrator can execute the necessary upgrades in the right order.
To execute migrations, invoke the migrateTo method, specifying current and the target version. The migrator will ensure that all migrations between the current and target versions are executed sequentially.
val currentVersion = IntVersion("1")
val targetVersion = IntVersion("5")
val result = migrator.migrateTo(
from = currentVersion,
to = targetVersion
)In this example he migrator runs all migrations from version 1 up to and including 5.
Migration Result
The IcarionMigrationsResult class encapsulates the outcome of executed migrations, providing a detailed report of the migration process.
Result Types
IcarionFailureRecoveryHint.Skip
IcarionFailureRecoveryHint.Rollback
To provide insights into the migration process, IcarionMigrator supports an observer mechanism. By implementing the IcarionMigrationObserver interface, you can monitor the progress of each migration, handle failures, and decide the recovery strategy.
The IcarionMigrationObserver interface includes the following methods:
onMigrationStart(version: VERSION): Invoked when a migration targeting the specified version begins.onMigrationSuccess(version: VERSION): Invoked when a migration targeting the specified version completes successfully.onMigrationFailure(version: VERSION, exception: Exception): Invoked when a migration targeting the specified version fails. You can return an appropriate IcarionFailureRecoveryHint to determine the recovery strategy: Skip, Rollback, or Abort.Observer can be set via migrator.migrationObserver
The migrator supports three strategies to handle migration failures, configurable via IcarionFailureRecoveryHint:
These strategies can be set as a default via migrator.defaultFailureRecoveryHint or they can be determined on individual migration level.
The default value is IcarionFailureRecoveryHint.Abort
If no Migration Observer is set, the defaultFailureRecoveryHint is used.
With the Migration Observer, each migration must return how its failure should be addressed.
When using the Rollback strategy, the migrator will:
rollback function for all successfully executed migrations, in reverse order.Note: If rollback fails for any migration in the chain, the process is stopped and IcarionMigrationsResult.Failure is returned with info on which migrations have been completed and which have been rolled backed.
With this Result information you can then decide how to handle the failed rollback process.
The migrator provides detailed logging at every step of the process. You can integrate your preferred logging framework (e.g., SLF4J, Android Logcat, etc...) to monitor progress, failures, and skipped migrations.
Logging is done via a small and simple Logger Facade to not force any dependencies via Icarion.
IcarionLoggerAdapter.init(createLoggerFacade())
private fun createLoggerFacade() = object : IcarionLogger {
private val logger = LoggerFactory.getLogger("IcarionLogger")
override fun d(message: String) {
logger.debug(message)
}
override fun i(message: String) {
logger.info(message)
}
override fun e(t: Throwable, message: String) {
logger.error(message, t)
}
override fun e(t: Throwable) {
logger.error(t)
}
}
Take a look at working samples in the following folders ktor-sample, android-sample, desktop-sample (TODO).
The IcarionMigrator simplifies version migrations by handling:
You’re now ready to run migrations in your app!
Contributions are welcome! Please fork this repository and submit a pull request.
Icarion is a lightweight, extensible migration library designed to handle version-based update migrations for your application. It supports both rollback and recovery mechanisms for fine-grained control over migrations, making it ideal for settings and configuration changes, file migrations, even database updates and more.
Written for Kotlin Multiplatform you can run it on Android, iOS and any JVM based system: Ktor, Spring, Desktop, you name it...Android based projects were the main culprit behind Icarion idea as many times devs would just perform SharedPreferences or FirebaseConfig data updates in the Application onCreate() based on current BuildConfig version value without any long term organization of migrations.
This library is here to help alleviate some of the pain of rolling out your own system of migrations, no matter where you run it.
Inspired by Icarus myth, which is often interpreted as a cautionary tale about ego, self-sabotage, and the consequences of ignoring wise counsel. Icarus, in Greek mythology, son of the inventor Daedalus who perished by flying too near the Sun with waxen wings.
Add Icarion to your project as a dependency. If you use Gradle, add the following:
repositories {
mavenCentral()
}
dependencies {
implementation("xyz.amplituhedron:icarion:1.1.0")
}You can use any versioning system that implements Comparable. Icarion comes with two versioning schemes out of the box for easier integration:
data class IntVersion(val value: Int) and data class SemanticVersion(val major: Int, val minor: Int, val patch: Int)
val v1 = IntVersion(1)
val v3 = IntVersion(3)
val v1_1_1 = SemanticVersion(1, 1, 1)
val v2_3_0 = SemanticVersion.fromVersion("2.3.0")Enums can be used as they implement comparable by default via their natural ordering:
enum class YourAppNamedVersion {
ACACIA, // Lowest
BIRCH,
CEDAR,
DOUGLAS_FIR,
OAK,
PINE,
SEQUOIA; // Highest
}
val v1 = YourAppNamedVersion.ACACIA
val v3 = YourAppNamedVersion.CEDARTo define a migration, you need to implement the AppUpdateMigration interface. This interface requires you to define:
targetVersion: The version this migration updates to.migrate: The logic to apply the migration.rollback: The logic to revert the migration in case of failure.class SampleMigration : AppUpdateMigration<IntVersion> {
override val targetVersion = IntVersion(2)
override suspend fun migrate() {
println("Migrating to version $targetVersion")
// Add your migration logic here
}
override suspend fun rollback() {
println("Rolling back version $targetVersion")
// Add your rollback logic here
}
}class FeatureUpgradeMigrationV110 : AppUpdateMigration<SemanticVersion> {
override val targetVersion = SemanticVersion(1, 1, 0)
override suspend fun migrate() {
println("Upgrading feature to version $targetVersion")
// Add feature-specific migration logic here
// Intentionally failed migrations should throw an exception here, for ex. throw RuntimeException("Can not migrate all data to external storage...")
}
override suspend fun rollback() {
println("Reverting feature upgrade for version $targetVersion")
// Add feature-specific rollback logic here
}
}Define as many migrations as needed for your application. Each migration should handle only the changes required for its specific version.
Once you've implemented your migrations, register them with an instance of IcarionMigrator. This ensures the migrator knows which migrations are available for execution.
// Register multiple migrations at once
val migrator = IcarionMigrator<IntVersion>().apply {
registerMigration(FeatureUpgradeMigrationV1())
registerMigration(FeatureUpgradeMigrationV2())
registerMigration(FeatureUpgradeMigrationV3())
registerMigration(FeatureUpgradeMigrationV4())
registerMigration(FeatureUpgradeMigrationV5())
}
// Register individual migrations
migrator.registerMigration(SampleMigration())• You cannot register migrations while a migration process is running. An IllegalStateException will be thrown if you attempt to do so.
• Each migration must target a unique version. If you register two migrations with the same targetVersion, an IllegalArgumentException will be thrown.
Registering migrations correctly ensures that the migrator can execute the necessary upgrades in the right order.
To execute migrations, invoke the migrateTo method, specifying current and the target version. The migrator will ensure that all migrations between the current and target versions are executed sequentially.
val currentVersion = IntVersion("1")
val targetVersion = IntVersion("5")
val result = migrator.migrateTo(
from = currentVersion,
to = targetVersion
)In this example he migrator runs all migrations from version 1 up to and including 5.
Migration Result
The IcarionMigrationsResult class encapsulates the outcome of executed migrations, providing a detailed report of the migration process.
Result Types
IcarionFailureRecoveryHint.Skip
IcarionFailureRecoveryHint.Rollback
To provide insights into the migration process, IcarionMigrator supports an observer mechanism. By implementing the IcarionMigrationObserver interface, you can monitor the progress of each migration, handle failures, and decide the recovery strategy.
The IcarionMigrationObserver interface includes the following methods:
onMigrationStart(version: VERSION): Invoked when a migration targeting the specified version begins.onMigrationSuccess(version: VERSION): Invoked when a migration targeting the specified version completes successfully.onMigrationFailure(version: VERSION, exception: Exception): Invoked when a migration targeting the specified version fails. You can return an appropriate IcarionFailureRecoveryHint to determine the recovery strategy: Skip, Rollback, or Abort.Observer can be set via migrator.migrationObserver
The migrator supports three strategies to handle migration failures, configurable via IcarionFailureRecoveryHint:
These strategies can be set as a default via migrator.defaultFailureRecoveryHint or they can be determined on individual migration level.
The default value is IcarionFailureRecoveryHint.Abort
If no Migration Observer is set, the defaultFailureRecoveryHint is used.
With the Migration Observer, each migration must return how its failure should be addressed.
When using the Rollback strategy, the migrator will:
rollback function for all successfully executed migrations, in reverse order.Note: If rollback fails for any migration in the chain, the process is stopped and IcarionMigrationsResult.Failure is returned with info on which migrations have been completed and which have been rolled backed.
With this Result information you can then decide how to handle the failed rollback process.
The migrator provides detailed logging at every step of the process. You can integrate your preferred logging framework (e.g., SLF4J, Android Logcat, etc...) to monitor progress, failures, and skipped migrations.
Logging is done via a small and simple Logger Facade to not force any dependencies via Icarion.
IcarionLoggerAdapter.init(createLoggerFacade())
private fun createLoggerFacade() = object : IcarionLogger {
private val logger = LoggerFactory.getLogger("IcarionLogger")
override fun d(message: String) {
logger.debug(message)
}
override fun i(message: String) {
logger.info(message)
}
override fun e(t: Throwable, message: String) {
logger.error(message, t)
}
override fun e(t: Throwable) {
logger.error(t)
}
}
Take a look at working samples in the following folders ktor-sample, android-sample, desktop-sample (TODO).
The IcarionMigrator simplifies version migrations by handling:
You’re now ready to run migrations in your app!
Contributions are welcome! Please fork this repository and submit a pull request.