-
-
Notifications
You must be signed in to change notification settings - Fork 3
feat: SQLDelight database migration system implementation #280
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
base: main
Are you sure you want to change the base?
Changes from all commits
3c390cb
4dc2154
839505e
ccd57de
05fd15a
b0331b1
8426cee
d6683b2
3e7b849
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| --- | ||
| "scopes": minor | ||
| --- | ||
|
|
||
| Add automatic database migration system for SQLDelight databases | ||
|
|
||
| - Implement DatabaseMigrationManager with thread-safe migration execution | ||
| - Add version tracking using SQLite PRAGMA user_version | ||
| - Support for custom migration callbacks at specific versions | ||
| - Automatic schema creation for fresh databases | ||
| - Fail-fast behavior when database version is newer than application | ||
| - Database-level locking to prevent concurrent migrations across processes | ||
| - Proper resource management and exception handling | ||
| - Migration files (.sqm) for all bounded contexts (scope-management, event-store, device-synchronization) | ||
| - Comprehensive documentation and examples in database-migration-guide.md |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| -- Migration from version 1 to version 2 | ||
| -- This is a placeholder migration file for the Device Synchronization context | ||
| -- | ||
| -- Example migrations for device sync (uncomment when needed): | ||
| -- | ||
| -- Add new column for sync status | ||
| -- ALTER TABLE devices ADD COLUMN last_error TEXT; | ||
| -- | ||
| -- Add table for sync conflicts | ||
| -- CREATE TABLE IF NOT EXISTS sync_conflicts ( | ||
| -- id TEXT PRIMARY KEY NOT NULL, | ||
| -- device_id TEXT NOT NULL, | ||
| -- conflict_data TEXT NOT NULL, | ||
| -- created_at INTEGER NOT NULL, | ||
| -- resolved_at INTEGER, | ||
| -- FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE | ||
| -- ); | ||
| -- | ||
| -- Note: This file is currently a placeholder. | ||
| -- When you need to make schema changes in the future: | ||
| -- 1. Uncomment and modify the SQL statements above | ||
| -- 2. Update ApplicationVersion.SchemaVersions.DEVICE_SYNCHRONIZATION to 2 | ||
| -- 3. Add a new VersionMapping in ApplicationVersion.versionHistory |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,20 +3,31 @@ package io.github.kamiazya.scopes.eventstore.infrastructure.sqldelight | |||||||||||||||||||||||||||||||||||||||||||||||||
| import app.cash.sqldelight.db.SqlDriver | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import io.github.kamiazya.scopes.eventstore.db.EventStoreDatabase | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import io.github.kamiazya.scopes.platform.infrastructure.database.DatabaseMigrationManager | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import io.github.kamiazya.scopes.platform.infrastructure.version.ApplicationVersion | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Provides SQLDelight database instances for Event Store. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * This provider creates databases with migration support. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| object SqlDelightDatabaseProvider { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private val migrationManager = DatabaseMigrationManager.createDefault() | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Creates a new EventStoreDatabase instance. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Automatically handles schema creation and migration based on version differences. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fun createDatabase(databasePath: String): EventStoreDatabase { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| val driver: SqlDriver = JdbcSqliteDriver("jdbc:sqlite:$databasePath") | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Create the database schema | ||||||||||||||||||||||||||||||||||||||||||||||||||
| EventStoreDatabase.Schema.create(driver) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Perform migration if needed | ||||||||||||||||||||||||||||||||||||||||||||||||||
| migrationManager.migrate( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| driver = driver, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| schema = EventStoreDatabase.Schema, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| targetVersion = ApplicationVersion.SchemaVersions.EVENT_STORE | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Enable foreign keys | ||||||||||||||||||||||||||||||||||||||||||||||||||
| driver.execute(null, "PRAGMA foreign_keys=ON", 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -26,10 +37,21 @@ object SqlDelightDatabaseProvider { | |||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Creates an in-memory database for testing. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Always creates a fresh schema without migration. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fun createInMemoryDatabase(): EventStoreDatabase { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| val driver: SqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // For in-memory databases, always create fresh schema | ||||||||||||||||||||||||||||||||||||||||||||||||||
| EventStoreDatabase.Schema.create(driver) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Set the version for consistency | ||||||||||||||||||||||||||||||||||||||||||||||||||
| driver.execute( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| identifier = null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| sql = "PRAGMA user_version = ${ApplicationVersion.SchemaVersions.EVENT_STORE}", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| parameters = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return EventStoreDatabase(driver) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
46
to
55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enable foreign keys for the in-memory event-store database too. Like the device-sync provider, tests using this in-memory DB won't enforce FK constraints, diverging from production. Enable them to keep parity. // For in-memory databases, always create fresh schema
EventStoreDatabase.Schema.create(driver)
+ // Match production FK enforcement
+ driver.execute(null, "PRAGMA foreign_keys=ON", 0)
+
// Set the version for consistency
driver.execute(
identifier = null,
sql = "PRAGMA user_version = ${ApplicationVersion.SchemaVersions.EVENT_STORE}",
parameters = 0
)π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| -- Migration from version 1 to version 2 | ||
| -- This is a placeholder migration file for the Event Store context | ||
| -- | ||
| -- Example migrations for event store (uncomment when needed): | ||
| -- | ||
| -- Add new column for event metadata | ||
| -- ALTER TABLE events ADD COLUMN correlation_id TEXT; | ||
| -- | ||
| -- Add index for correlation tracking | ||
| -- CREATE INDEX IF NOT EXISTS idx_events_correlation ON events(correlation_id); | ||
| -- | ||
| -- Note: This file is currently a placeholder. | ||
| -- When you need to make schema changes in the future: | ||
| -- 1. Uncomment and modify the SQL statements above | ||
| -- 2. Update ApplicationVersion.SchemaVersions.EVENT_STORE to 2 | ||
| -- 3. Add a new VersionMapping in ApplicationVersion.versionHistory |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,48 +1,74 @@ | ||
| package io.github.kamiazya.scopes.scopemanagement.infrastructure.sqldelight | ||
|
|
||
| import io.github.kamiazya.scopes.platform.infrastructure.database.DatabaseMigrationManager | ||
| import io.github.kamiazya.scopes.platform.infrastructure.database.ManagedSqlDriver | ||
| import io.github.kamiazya.scopes.platform.infrastructure.version.ApplicationVersion | ||
| import io.github.kamiazya.scopes.scopemanagement.db.ScopeManagementDatabase | ||
|
|
||
| /** | ||
| * Provides SQLDelight database instances for Scope Management. | ||
| * | ||
| * This provider creates databases with automatic resource management. | ||
| * This provider creates databases with automatic resource management and migration support. | ||
| * The returned ManagedDatabase wrapper ensures proper cleanup on close. | ||
| */ | ||
| object SqlDelightDatabaseProvider { | ||
|
|
||
| private val migrationManager = DatabaseMigrationManager.createDefault() | ||
|
|
||
| /** | ||
| * Wrapper that combines database and its managed driver for proper cleanup. | ||
| * Implements AutoCloseable to ensure resources are properly released. | ||
| */ | ||
| class ManagedDatabase(private val database: ScopeManagementDatabase, private val managedDriver: AutoCloseable) : | ||
| ScopeManagementDatabase by database, | ||
| AutoCloseable { | ||
| class ManagedDatabase( | ||
| private val database: ScopeManagementDatabase, | ||
| private val managedDriver: AutoCloseable | ||
| ) : ScopeManagementDatabase by database, AutoCloseable { | ||
| override fun close() { | ||
| // Close the driver to release file handles and WAL locks | ||
| managedDriver.close() | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Creates a new ScopeManagementDatabase instance with automatic resource management. | ||
| * Automatically handles schema creation and migration based on version differences. | ||
| * | ||
| * @return ManagedDatabase that must be closed when no longer needed | ||
| */ | ||
| fun createDatabase(databasePath: String): ScopeManagementDatabase { | ||
| fun createDatabase(databasePath: String): ManagedDatabase { | ||
| val managedDriver = ManagedSqlDriver.createWithDefaults(databasePath) | ||
| val driver = managedDriver.driver | ||
|
|
||
| // Create the database schema | ||
| ScopeManagementDatabase.Schema.create(driver) | ||
| // Perform migration if needed | ||
| migrationManager.migrate( | ||
| driver = driver, | ||
| schema = ScopeManagementDatabase.Schema, | ||
| targetVersion = ApplicationVersion.SchemaVersions.SCOPE_MANAGEMENT | ||
| ) | ||
|
|
||
| return ManagedDatabase(ScopeManagementDatabase(driver), managedDriver) | ||
| } | ||
|
|
||
| /** | ||
| * Creates an in-memory database for testing. | ||
| * Always creates a fresh schema without migration. | ||
| * | ||
| * @return ManagedDatabase that must be closed when no longer needed | ||
| */ | ||
| fun createInMemoryDatabase(): ScopeManagementDatabase { | ||
| fun createInMemoryDatabase(): ManagedDatabase { | ||
| val managedDriver = ManagedSqlDriver(":memory:") | ||
| val driver = managedDriver.driver | ||
|
|
||
| // For in-memory databases, always create fresh schema | ||
| ScopeManagementDatabase.Schema.create(driver) | ||
|
|
||
| // Set the version for consistency | ||
| driver.execute( | ||
| identifier = null, | ||
| sql = "PRAGMA user_version = ${ApplicationVersion.SchemaVersions.SCOPE_MANAGEMENT}", | ||
| parameters = 0 | ||
| ) | ||
|
|
||
| return ManagedDatabase(ScopeManagementDatabase(driver), managedDriver) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| -- Migration from version 1 to version 2 | ||
| -- This is an example migration file that will be used when upgrading from schema version 1 to 2 | ||
| -- | ||
| -- Example migrations (uncomment and modify as needed for actual schema changes): | ||
| -- | ||
| -- Add a new column to the scopes table | ||
| -- ALTER TABLE scopes ADD COLUMN status TEXT DEFAULT 'active'; | ||
| -- | ||
| -- Create a new index | ||
| -- CREATE INDEX IF NOT EXISTS idx_scopes_status ON scopes(status); | ||
| -- | ||
| -- Add a new table | ||
| -- CREATE TABLE IF NOT EXISTS scope_tags ( | ||
| -- scope_id TEXT NOT NULL, | ||
| -- tag TEXT NOT NULL, | ||
| -- created_at INTEGER NOT NULL, | ||
| -- PRIMARY KEY (scope_id, tag), | ||
| -- FOREIGN KEY (scope_id) REFERENCES scopes(id) ON DELETE CASCADE | ||
| -- ); | ||
| -- | ||
| -- Note: This file is currently a placeholder. | ||
| -- When you need to make schema changes in the future: | ||
| -- 1. Uncomment and modify the SQL statements above | ||
| -- 2. Update ApplicationVersion.SchemaVersions.SCOPE_MANAGEMENT to 2 | ||
| -- 3. Add a new VersionMapping in ApplicationVersion.versionHistory |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enable foreign keys in in-memory database provider.
Line 31 enables foreign keys for on-disk databases, but the in-memory path skips it. That leaves test setups without FK enforcement, letting violations slip through locally while production fails. Please mirror the production behaviour.
// For in-memory databases, always create fresh schema DeviceSyncDatabase.Schema.create(driver) + // Mirror production FK behaviour for parity with persisted databases + driver.execute(null, "PRAGMA foreign_keys=ON", 0) + // Set the version for consistency driver.execute( identifier = null, sql = "PRAGMA user_version = ${ApplicationVersion.SchemaVersions.DEVICE_SYNCHRONIZATION}", parameters = 0 )π Committable suggestion
π€ Prompt for AI Agents