-
-
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?
Conversation
- Add DatabaseMigrationManager for automatic schema migrations - Add ApplicationVersion for version and schema tracking - Update all SqlDelightDatabaseProviders to use migration system - Add thread-safe migration with proper transaction handling - Fail fast when database version is newer than application - Add migration placeholder files for future schema changes - Add comprehensive migration guide documentation - Add tests for migration manager and version tracking This enables automatic database schema updates when the application version changes, eliminating manual migration steps for users. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 3e7b849 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds a migration subsystem: new DatabaseMigrationManager and ApplicationVersion, migration-aware SqlDelight providers for Event Store, Device Synchronization, and Scope Management (Scope Management now returns a ManagedDatabase wrapper), placeholder .sqm migration files, tests, and docs; in-memory databases set PRAGMA user_version. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant App as App Code
participant Provider as SqlDelightDatabaseProvider
participant Driver as SqlDriver
participant DMM as DatabaseMigrationManager
participant Schema as SqlDelight.Schema
App->>Provider: createDatabase(path)
Provider->>Driver: open connection
Provider->>DMM: migrate(driver, Schema, targetVersion)
alt Fresh DB (user_version = 0)
DMM->>Schema: schema.create(driver)
DMM->>Driver: PRAGMA user_version = targetVersion
else Upgrade (current < target)
DMM->>Schema: schema.migrate(driver, current, target)
DMM->>Driver: PRAGMA user_version = targetVersion
DMM->>DMM: run per-version callbacks
else Downgrade (current > target)
DMM-->>Provider: throw DatabaseMigrationException
end
Provider-->>App: return Database instance (ManagedDatabase where applicable)
note over Provider,App: In-memory: create fresh schema + set PRAGMA user_version
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @kamiazya, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a foundational automatic database migration system for SQLDelight databases. The primary goal is to simplify schema evolution by automating updates when the application version changes, thereby reducing manual intervention and potential errors. The system is designed with safety in mind, incorporating version checks to prevent running older application versions on newer databases, and includes a clear guide for developers to easily manage future schema changes. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Snapshot WarningsEnsure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice. Scanned FilesNone |
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.
Code Review
This pull request introduces a robust and well-designed database migration system using SQLDelight. The implementation is clean, follows good architectural principles, and includes comprehensive documentation and testing. The core DatabaseMigrationManager is thread-safe and handles various migration scenarios gracefully, including new databases, existing unversioned databases, and preventing downgrades. The addition of version tracking in ApplicationVersion is also a great feature for maintainability. My review includes a few suggestions to improve efficiency in the database providers and to correct some examples in the new migration guide.
| // Create the database schema | ||
| DeviceSyncDatabase.Schema.create(driver) | ||
| // Perform migration if needed | ||
| val migrationManager = DatabaseMigrationManager.createDefault() |
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.
A new instance of DatabaseMigrationManager is created on every call to createDatabase. Since SqlDelightDatabaseProvider is a singleton (object), it's more efficient to create a single, reusable instance of the manager as a private property of the object.
This avoids unnecessary object creation and improves performance slightly. You can apply this pattern to all SqlDelightDatabaseProvider files.
Example of the recommended structure:
object SqlDelightDatabaseProvider {
private val migrationManager = DatabaseMigrationManager.createDefault()
fun createDatabase(databasePath: String): DeviceSyncDatabase {
val driver: SqlDriver = JdbcSqliteDriver("jdbc:sqlite:$databasePath")
// Perform migration if needed
migrationManager.migrate(
driver = driver,
schema = DeviceSyncDatabase.Schema,
targetVersion = ApplicationVersion.SchemaVersions.DEVICE_SYNCHRONIZATION
)
// ...
}
// ...
}| // Create the database schema | ||
| EventStoreDatabase.Schema.create(driver) | ||
| // Perform migration if needed | ||
| val migrationManager = DatabaseMigrationManager.createDefault() |
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.
A new instance of DatabaseMigrationManager is created on every call to createDatabase. Since SqlDelightDatabaseProvider is a singleton (object), it's more efficient to create a single, reusable instance of the manager as a private property of the object.
This avoids unnecessary object creation and improves performance slightly.
Example of the recommended structure:
object SqlDelightDatabaseProvider {
private val migrationManager = DatabaseMigrationManager.createDefault()
fun createDatabase(databasePath: String): EventStoreDatabase {
val driver: SqlDriver = JdbcSqliteDriver("jdbc:sqlite:$databasePath")
// Perform migration if needed
migrationManager.migrate(
driver = driver,
schema = EventStoreDatabase.Schema,
targetVersion = ApplicationVersion.SchemaVersions.EVENT_STORE
)
// ...
}
// ...
}| // Create the database schema | ||
| ScopeManagementDatabase.Schema.create(driver) | ||
| // Perform migration if needed | ||
| val migrationManager = DatabaseMigrationManager.createDefault() |
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.
A new instance of DatabaseMigrationManager is created on every call to createDatabase. Since SqlDelightDatabaseProvider is a singleton (object), it's more efficient to create a single, reusable instance of the manager as a private property of the object.
This avoids unnecessary object creation and improves performance slightly.
Example of the recommended structure:
object SqlDelightDatabaseProvider {
private val migrationManager = DatabaseMigrationManager.createDefault()
fun createDatabase(databasePath: String): ManagedDatabase {
val managedDriver = ManagedSqlDriver.createWithDefaults(databasePath)
val driver = managedDriver.driver
// Perform migration if needed
migrationManager.migrate(
driver = driver,
schema = ScopeManagementDatabase.Schema,
targetVersion = ApplicationVersion.SchemaVersions.SCOPE_MANAGEMENT
)
return ManagedDatabase(ScopeManagementDatabase(driver), managedDriver)
}
// ...
}| driver.execute( | ||
| null, | ||
| "UPDATE scopes SET status = 'archived' WHERE updated_at < ?", | ||
| 1, | ||
| bindLong(0, thirtyDaysAgo) | ||
| ) |
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.
The code example for using a MigrationCallback with driver.execute appears to have an incorrect signature. The binder argument should be passed as a trailing lambda, not as a direct function call in the argument list. This will be confusing for developers and lead to code that doesn't compile.
| driver.execute( | |
| null, | |
| "UPDATE scopes SET status = 'archived' WHERE updated_at < ?", | |
| 1, | |
| bindLong(0, thirtyDaysAgo) | |
| ) | |
| driver.execute( | |
| identifier = null, | |
| sql = "UPDATE scopes SET status = 'archived' WHERE updated_at < ?", | |
| parameters = 1 | |
| ) { | |
| bindLong(0, thirtyDaysAgo) | |
| } |
|
|
||
| // Run migration | ||
| val manager = DatabaseMigrationManager.createDefault() | ||
| manager.migrate(driver, TestSchema, 2L) |
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.
The unit test example refers to TestSchema, which is not defined in the guide. This will likely confuse developers trying to implement a similar test. It should probably be schema to match the variable that would typically be defined in a test setup.
| manager.migrate(driver, TestSchema, 2L) | |
| manager.migrate(driver, schema, 2L) |
| mapper = { cursor -> | ||
| if (cursor.next()) { | ||
| cursor.getLong(0) == 0L | ||
| } else { | ||
| true | ||
| } |
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.
The else branch in the mapper lambda is unreachable because a COUNT(*) query in SQL always returns exactly one row. While this doesn't cause a bug, it can be simplified to make the code cleaner and show a clearer understanding of the SQL behavior.
mapper = { cursor ->
cursor.next()
cursor.getLong(0) == 0L
},- Fixed import paths for Logger and ConsoleLogger (use logging package) - Replaced SqlDriver.newTransaction() with raw SQL transactions (BEGIN/COMMIT/ROLLBACK) - Fixed executeQuery return type handling (.value accessor) - Updated error logging to use correct Logger API signature - Removed incorrect afterVersion lambda from schema.migrate call The compilation errors were caused by: 1. Wrong package imports for observability classes 2. SqlDriver doesn't have newTransaction() method - using raw SQL instead 3. executeQuery returns QueryResult which needs .value accessor 4. Logger.error() requires throwable parameter, not just message
🔧 Fixed Compilation ErrorsThe latest commit resolves all compilation errors in Issues Fixed:
Implementation Details:
Waiting for GitHub Actions to verify the build... 🚀 |
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.
Actionable comments posted: 13
🧹 Nitpick comments (4)
platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.kt (1)
128-160: Add a test: database version newer than app should fail fast.Cover the branch where currentVersion > targetVersion to lock the expected exception and message.
contexts/scope-management/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/scopemanagement/infrastructure/sqldelight/SqlDelightDatabaseProvider.kt (2)
36-49: Verify and mitigate breaking change in return type (ScopeManagementDatabase → ManagedDatabase).This is a public API change; downstream callers expecting ScopeManagementDatabase will break. Consider one of:
- Keep the function signature returning ScopeManagementDatabase and still return ManagedDatabase (it implements ScopeManagementDatabase) to preserve source/binary compatibility, and add a new createManagedDatabase() for explicit resource ownership; or
- Keep the new return type but add a deprecation period with alternative factory helpers at call sites.
61-71: Enable foreign key enforcement for safety.Set PRAGMA foreign_keys = ON after opening the driver (both on-disk and in-memory) to ensure referential integrity.
Apply:
ScopeManagementDatabase.Schema.create(driver) // Set the version for consistency driver.execute( identifier = null, sql = "PRAGMA user_version = ${ApplicationVersion.SchemaVersions.SCOPE_MANAGEMENT}", parameters = 0 ) + // Enforce FK constraints + driver.execute( + identifier = null, + sql = "PRAGMA foreign_keys = ON", + parameters = 0 + )platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt (1)
41-48: Pre-migration “no version but not empty” path risks masking unknown state. Require an explicit bootstrap version or callback.Blindly forcing version=1 may be wrong if the DB came from a newer build. Consider taking a required parameter (e.g., bootstrapVersionProvider) or fail fast unless the caller opts in.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
contexts/device-synchronization/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/devicesync/infrastructure/sqldelight/SqlDelightDatabaseProvider.kt(2 hunks)contexts/device-synchronization/infrastructure/src/main/sqldelight/migrations/1.sqm(1 hunks)contexts/event-store/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/eventstore/infrastructure/sqldelight/SqlDelightDatabaseProvider.kt(2 hunks)contexts/event-store/infrastructure/src/main/sqldelight/migrations/1.sqm(1 hunks)contexts/scope-management/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/scopemanagement/infrastructure/sqldelight/SqlDelightDatabaseProvider.kt(1 hunks)contexts/scope-management/infrastructure/src/main/sqldelight/migrations/1.sqm(1 hunks)docs/guides/development/database-migration-guide.md(1 hunks)platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt(1 hunks)platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/version/ApplicationVersion.kt(1 hunks)platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.kt(1 hunks)platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/version/ApplicationVersionTest.kt(1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{kt,kts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{kt,kts}: Adhere to ktlint formatting rules for all Kotlin source and script files; run ktlintFormat before committing
Code must pass Detekt static analysis; avoid suppressing rules without clear justification
Files:
platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/version/ApplicationVersionTest.ktplatform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.ktcontexts/scope-management/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/scopemanagement/infrastructure/sqldelight/SqlDelightDatabaseProvider.ktcontexts/event-store/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/eventstore/infrastructure/sqldelight/SqlDelightDatabaseProvider.ktcontexts/device-synchronization/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/devicesync/infrastructure/sqldelight/SqlDelightDatabaseProvider.ktplatform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.ktplatform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/version/ApplicationVersion.kt
**/src/**/kotlin/**/*.kt
📄 CodeRabbit inference engine (CLAUDE.md)
**/src/**/kotlin/**/*.kt: Use Kotlin’s error(), check(), and require() instead of throwing exceptions directly
Never use "unknown" or default fallbacks that could mask data corruption
Use Arrow’s Either for functional error handling
Fail fast for critical configuration issues (e.g., missing text model)
Files:
platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/version/ApplicationVersionTest.ktplatform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.ktcontexts/scope-management/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/scopemanagement/infrastructure/sqldelight/SqlDelightDatabaseProvider.ktcontexts/event-store/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/eventstore/infrastructure/sqldelight/SqlDelightDatabaseProvider.ktcontexts/device-synchronization/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/devicesync/infrastructure/sqldelight/SqlDelightDatabaseProvider.ktplatform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.ktplatform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/version/ApplicationVersion.kt
**/src/test/kotlin/**/*.kt
📄 CodeRabbit inference engine (CLAUDE.md)
**/src/test/kotlin/**/*.kt: Do not use mock services in tests
Make tests verbose to aid debugging
Use Kotest for testing (project-standard test framework)
Files:
platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/version/ApplicationVersionTest.ktplatform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.kt
docs/**
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Documentation under docs/ should follow the Diátaxis framework structure
Files:
docs/guides/development/database-migration-guide.md
docs/**/*.md
📄 CodeRabbit inference engine (CLAUDE.md)
Write documentation in English (project’s official language)
Files:
docs/guides/development/database-migration-guide.md
🪛 GitHub Actions: Test
platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt
[error] 5-5: Unresolved reference 'logger'. Compilation failed in Kotlin file. (Gradle task ':platform-infrastructure:compileKotlin')
[error] 6-6: Unresolved reference 'logger'. Compilation failed in Kotlin file. (Gradle task ':platform-infrastructure:compileKotlin')
[error] 19-19: Unresolved reference 'Logger'. Compilation failed in Kotlin file. (Gradle task ':platform-infrastructure:compileKotlin')
[error] 19-19: Unresolved reference 'ConsoleLogger'. Compilation failed in Kotlin file. (Gradle task ':platform-infrastructure:compileKotlin')
[error] 40-40: Inapplicable candidate(s): fun WHEN_CALL(vararg branches: K): K. Compilation failed in Kotlin file. (Gradle task ':platform-infrastructure:compileKotlin')
[error] 43-43: Unresolved reference 'warn'. Compilation failed in Kotlin file. (Gradle task ':platform-infrastructure:compileKotlin')
[error] 51-51: Unresolved reference 'info'. Compilation failed in Kotlin file. (Gradle task ':platform-infrastructure:compileKotlin')
[error] 52-52: Cannot infer type for type parameter 'R'. Unresolved references and type inference failures in Kotlin file. (Gradle task ':platform-infrastructure:compileKotlin')
[error] 56-56: Unresolved reference 'endTransaction'. Compilation failed in Kotlin file. (Gradle task ':platform-infrastructure:compileKotlin')
[error] 58-58: Unresolved reference 'endTransaction'. Compilation failed in Kotlin file. (Gradle task ':platform-infrastructure:compileKotlin')
[error] 64-64: Unresolved reference 'info'. Compilation failed in Kotlin file. (Gradle task ':platform-infrastructure:compileKotlin')
[error] 71-71: Unresolved reference 'error'. Compilation failed in Kotlin file. (Gradle task ':platform-infrastructure:compileKotlin')
[error] 75-75: Unresolved reference 'debug'. Compilation failed in Kotlin file. (Gradle task ':platform-infrastructure:compileKotlin')
[error] 84-84: Return type mismatch: expected 'Boolean', actual 'QueryResult'. (Gradle task ':platform-infrastructure:compileKotlin')
[error] 88-88: Return type mismatch: expected 'Boolean', actual 'QueryResult'. Condition type mismatch. (Gradle task ':platform-infrastructure:compileKotlin')
[error] 103-103: Return type mismatch: expected 'Long', actual 'QueryResult'. (Gradle task ':platform-infrastructure:compileKotlin')
🪛 markdownlint-cli2 (0.18.1)
docs/guides/development/database-migration-guide.md
21-21: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Cross-Platform Native Build (macos-13, darwin, x64)
- GitHub Check: Cross-Platform Native Build (windows-latest, win32, arm64, true)
- GitHub Check: Cross-Platform Native Build (windows-latest, win32, x64, true)
- GitHub Check: Analyze (java-kotlin)
| DeviceSyncDatabase.Schema.create(driver) | ||
|
|
||
| // Set the version for consistency | ||
| driver.execute( | ||
| identifier = null, | ||
| sql = "PRAGMA user_version = ${ApplicationVersion.SchemaVersions.DEVICE_SYNCHRONIZATION}", | ||
| parameters = 0 | ||
| ) | ||
|
|
||
| return DeviceSyncDatabase(driver) |
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
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| DeviceSyncDatabase.Schema.create(driver) | |
| // Set the version for consistency | |
| driver.execute( | |
| identifier = null, | |
| sql = "PRAGMA user_version = ${ApplicationVersion.SchemaVersions.DEVICE_SYNCHRONIZATION}", | |
| parameters = 0 | |
| ) | |
| return DeviceSyncDatabase(driver) | |
| // For in-memory databases, always create fresh schema | |
| DeviceSyncDatabase.Schema.create(driver) | |
| // Mirror production FK behaviour for parity with persisted databases | |
| driver.execute( | |
| identifier = null, | |
| sql = "PRAGMA foreign_keys=ON", | |
| parameters = 0 | |
| ) | |
| // Set the version for consistency | |
| driver.execute( | |
| identifier = null, | |
| sql = "PRAGMA user_version = ${ApplicationVersion.SchemaVersions.DEVICE_SYNCHRONIZATION}", | |
| parameters = 0 | |
| ) | |
| return DeviceSyncDatabase(driver) |
🤖 Prompt for AI Agents
In
contexts/device-synchronization/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/devicesync/infrastructure/sqldelight/SqlDelightDatabaseProvider.kt
around lines 45-54, the in-memory database path creates the schema and sets
user_version but does not enable foreign key enforcement, causing tests to skip
FK checks; add an execution of "PRAGMA foreign_keys = ON" on the driver (using
driver.execute with appropriate identifier/sql/parameters) before creating the
schema so the in-memory branch mirrors the on-disk branch’s FK behaviour.
| 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) |
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 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
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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) | |
| // 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 | |
| ) | |
| return EventStoreDatabase(driver) |
🤖 Prompt for AI Agents
In
contexts/event-store/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/eventstore/infrastructure/sqldelight/SqlDelightDatabaseProvider.kt
around lines 45 to 54, the in-memory DB is created without enabling SQLite
foreign key enforcement; add a driver.execute call to run "PRAGMA foreign_keys =
ON" (identifier = null, parameters = 0) so FK constraints are enforced in tests.
Place this execute immediately after obtaining the driver and before creating
the schema (or at least before returning the EventStoreDatabase) so the
in-memory DB behavior matches production.
| ``` | ||
| contexts/{context}/infrastructure/src/main/sqldelight/migrations/{version}.sqm | ||
| ``` |
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.
Specify language on fenced code block.
markdownlint flags this fence; add a language (e.g. text) so the guide passes lint.
-```
+```text
contexts/{context}/infrastructure/src/main/sqldelight/migrations/{version}.sqmBased on static analysis hints.
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
21-21: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
In docs/guides/development/database-migration-guide.md around lines 21 to 23,
the fenced code block lacks a language specifier causing markdownlint to flag
it; update the fence to include a language (for example "text") so it becomes
```text ... ``` to satisfy the linter and preserve formatting.
...otlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt
Outdated
Show resolved
Hide resolved
| class DatabaseMigrationManager( | ||
| private val logger: Logger = ConsoleLogger() | ||
| ) { |
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.
Provide an in-file no-op logger to retain logging hooks without external dependency.
Replaces missing dependency with a simple default to restore build.
Apply:
-class DatabaseMigrationManager(
- private val logger: Logger = ConsoleLogger()
-) {
+class DatabaseMigrationManager(
+ private val logger: Logger = NoopLogger
+) {Add at file bottom (before closing brace):
+ interface Logger {
+ fun info(message: String) {}
+ fun warn(message: String) {}
+ fun error(message: String, error: Throwable? = null) {}
+ fun debug(message: String) {}
+ }
+ private object NoopLogger : Logger📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| class DatabaseMigrationManager( | |
| private val logger: Logger = ConsoleLogger() | |
| ) { | |
| class DatabaseMigrationManager( | |
| private val logger: Logger = NoopLogger | |
| ) { | |
| // existing implementation … | |
| interface Logger { | |
| fun info(message: String) {} | |
| fun warn(message: String) {} | |
| fun error(message: String, error: Throwable? = null) {} | |
| fun debug(message: String) {} | |
| } | |
| private object NoopLogger : Logger | |
| } |
🧰 Tools
🪛 GitHub Actions: Test
[error] 19-19: Unresolved reference 'Logger'. Compilation failed in Kotlin file. (Gradle task ':platform-infrastructure:compileKotlin')
[error] 19-19: Unresolved reference 'ConsoleLogger'. Compilation failed in Kotlin file. (Gradle task ':platform-infrastructure:compileKotlin')
🤖 Prompt for AI Agents
In
platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt
around lines 18-20, the constructor uses ConsoleLogger() as a default but that
dependency is missing; add a simple in-file no-op ConsoleLogger implementation
before the file's closing brace that implements the Logger interface used here
and provides no-op (or minimal) implementations for required methods so the
class compiles without external dependencies; place the class at the bottom of
the file (before the final brace) and keep it package-private and minimal.
...otlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt
Outdated
Show resolved
Hide resolved
| fun parseVersion(version: String): Triple<Int, Int, Int> { | ||
| val parts = version.split(".") | ||
| return Triple( | ||
| parts.getOrNull(0)?.toIntOrNull() ?: 0, | ||
| parts.getOrNull(1)?.toIntOrNull() ?: 0, | ||
| parts.getOrNull(2)?.toIntOrNull() ?: 0 | ||
| ) |
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.
Fail fast on malformed semantic version segments
parseVersion currently coerces any non-numeric segment to 0, so a typo like "1.0.O" is silently treated as "1.0.0". That masks configuration/version corruption and lets migrations proceed under a wrong assumption, which clashes with our requirement to fail fast on invalid configuration. Please reject malformed segments instead of defaulting to zero. As per coding guidelines.
fun parseVersion(version: String): Triple<Int, Int, Int> {
- val parts = version.split(".")
- return Triple(
- parts.getOrNull(0)?.toIntOrNull() ?: 0,
- parts.getOrNull(1)?.toIntOrNull() ?: 0,
- parts.getOrNull(2)?.toIntOrNull() ?: 0
- )
+ val parts = version.split(".")
+
+ fun parsePart(index: Int): Int {
+ val value = parts.getOrNull(index) ?: return 0
+ require(value.toIntOrNull() != null) {
+ "Invalid semantic version segment '$value' in '$version'"
+ }
+ return value.toInt()
+ }
+
+ return Triple(
+ parsePart(0),
+ parsePart(1),
+ parsePart(2)
+ )
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun parseVersion(version: String): Triple<Int, Int, Int> { | |
| val parts = version.split(".") | |
| return Triple( | |
| parts.getOrNull(0)?.toIntOrNull() ?: 0, | |
| parts.getOrNull(1)?.toIntOrNull() ?: 0, | |
| parts.getOrNull(2)?.toIntOrNull() ?: 0 | |
| ) | |
| fun parseVersion(version: String): Triple<Int, Int, Int> { | |
| val parts = version.split(".") | |
| fun parsePart(index: Int): Int { | |
| val value = parts.getOrNull(index) ?: return 0 | |
| require(value.toIntOrNull() != null) { | |
| "Invalid semantic version segment '$value' in '$version'" | |
| } | |
| return value.toInt() | |
| } | |
| return Triple( | |
| parsePart(0), | |
| parsePart(1), | |
| parsePart(2) | |
| ) | |
| } |
🤖 Prompt for AI Agents
platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/version/ApplicationVersion.kt
around lines 90-96: parseVersion currently coerces non-numeric segments to 0;
change it to validate input and fail fast by requiring exactly three
dot-separated segments and that each segment is a valid integer. If the split
does not produce three parts or any part fails to parse to Int, throw an
IllegalArgumentException with a clear message including the offending version
string; otherwise return the Triple of parsed Ints.
...n/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.kt
Outdated
Show resolved
Hide resolved
| it("should handle invalid versions") { | ||
| ApplicationVersion.parseVersion("abc") shouldBe Triple(0, 0, 0) | ||
| ApplicationVersion.parseVersion("1.x.3") shouldBe Triple(1, 0, 3) | ||
| ApplicationVersion.parseVersion("1.2.x") shouldBe Triple(1, 2, 0) | ||
| } |
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.
Align test expectations with strict semantic version parsing
Once parseVersion rejects malformed segments, this test must stop asserting the current silent fallback to zeros. Please update it to expect an IllegalArgumentException (or the chosen failure mode) for bad inputs.
+import io.kotest.assertions.throwables.shouldThrow
@@
- it("should handle invalid versions") {
- ApplicationVersion.parseVersion("abc") shouldBe Triple(0, 0, 0)
- ApplicationVersion.parseVersion("1.x.3") shouldBe Triple(1, 0, 3)
- ApplicationVersion.parseVersion("1.2.x") shouldBe Triple(1, 2, 0)
- }
+ it("should reject invalid versions") {
+ listOf("abc", "1.x.3", "1.2.x").forEach { invalid ->
+ shouldThrow<IllegalArgumentException> {
+ ApplicationVersion.parseVersion(invalid)
+ }
+ }
+ }🤖 Prompt for AI Agents
In
platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/version/ApplicationVersionTest.kt
around lines 36 to 40, the test currently expects silent fallbacks for malformed
version strings; change the assertions to expect the chosen failure mode
(IllegalArgumentException) instead of Triple(0,0,0). Update the three assertions
to assert that ApplicationVersion.parseVersion("abc"), parseVersion("1.x.3") and
parseVersion("1.2.x") throw IllegalArgumentException (e.g., use
shouldThrow<IllegalArgumentException> or your test framework's equivalent).
| describe("versionHistory") { | ||
| it("should contain at least one version mapping") { | ||
| ApplicationVersion.versionHistory.size shouldBe 1 | ||
| ApplicationVersion.versionHistory.first().appVersion shouldBe "0.1.0" | ||
| } |
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.
Remove brittle assertion on versionHistory size
This test claims to verify “at least one version mapping” but hard-codes the list size to 1. As soon as we add the next release mapping the suite will fail. Please make the assertion reflect the stated intent.
+import io.kotest.matchers.ints.shouldBeGreaterThan
@@
- ApplicationVersion.versionHistory.size shouldBe 1
- ApplicationVersion.versionHistory.first().appVersion shouldBe "0.1.0"
+ ApplicationVersion.versionHistory.size shouldBeGreaterThan 0
+ ApplicationVersion.versionHistory.last().appVersion shouldBe ApplicationVersion.CURRENT_VERSION📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| describe("versionHistory") { | |
| it("should contain at least one version mapping") { | |
| ApplicationVersion.versionHistory.size shouldBe 1 | |
| ApplicationVersion.versionHistory.first().appVersion shouldBe "0.1.0" | |
| } | |
| // Add at the top of the test file alongside the other Kotest imports | |
| import io.kotest.matchers.ints.shouldBeGreaterThan | |
| describe("versionHistory") { | |
| it("should contain at least one version mapping") { | |
| - ApplicationVersion.versionHistory.size shouldBe 1 | |
| ApplicationVersion.versionHistory.size shouldBeGreaterThan 0 | |
| ApplicationVersion.versionHistory.last().appVersion shouldBe ApplicationVersion.CURRENT_VERSION | |
| } | |
| } |
🤖 Prompt for AI Agents
In
platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/version/ApplicationVersionTest.kt
around lines 84 to 88, the test asserts ApplicationVersion.versionHistory.size
shouldBe 1 which is brittle; change the assertion to reflect "at least one" by
asserting the collection is not empty (e.g., size should be greater than 0 or
use isNotEmpty()) and keep the existing check that the first mapping's
appVersion equals "0.1.0".
- Fix QueryResult compilation errors by wrapping mapper returns with QueryResult.Value - Add database-level locking using BEGIN IMMEDIATE for process-safe migrations - Improve exception handling with proper rollback error recovery - Refactor migration logic for better separation of concerns - Add changeset file for minor version bump 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
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.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
.changeset/database-migration-feature.md(1 hunks)platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{kt,kts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{kt,kts}: Adhere to ktlint formatting rules for all Kotlin source and script files; run ktlintFormat before committing
Code must pass Detekt static analysis; avoid suppressing rules without clear justification
Files:
platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt
**/src/**/kotlin/**/*.kt
📄 CodeRabbit inference engine (CLAUDE.md)
**/src/**/kotlin/**/*.kt: Use Kotlin’s error(), check(), and require() instead of throwing exceptions directly
Never use "unknown" or default fallbacks that could mask data corruption
Use Arrow’s Either for functional error handling
Fail fast for critical configuration issues (e.g., missing text model)
Files:
platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: Cross-Platform Native Build (windows-latest, win32, x64, true)
- GitHub Check: Cross-Platform Native Build (windows-latest, win32, arm64, true)
- GitHub Check: Cross-Platform Native Build (macos-14, darwin, arm64)
- GitHub Check: Cross-Platform Native Build (ubuntu-latest, linux, arm64)
- GitHub Check: Cross-Platform Native Build (macos-13, darwin, x64)
- GitHub Check: Analyze (java-kotlin)
- GitHub Check: Cross-Platform Native Build (ubuntu-latest, linux, x64)
Performance improvements based on AI review feedback: - Reuse DatabaseMigrationManager instances as private properties in SqlDelightDatabaseProvider objects - Eliminate unnecessary instance creation overhead in production usage Added comprehensive test coverage: - New test case for fail-fast scenario when database version is newer than application version - Verifies proper exception handling and state preservation during version conflicts This addresses the remaining CI compilation issues and incorporates valuable AI review suggestions for better performance and test coverage. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
…erTest - Update all executeQuery usage to handle QueryResult wrapper type correctly - Add .value accessor to extract actual values from QueryResult<T> - Fix cursor.next() calls to use .value for boolean result - Ensure consistent QueryResult handling across all test cases This resolves the CI compilation failures in the database migration tests and aligns with the QueryResult API changes in SQLDelight. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
…erTest - Fix SqlSchema mock type parameter to use wildcard instead of Any - Add explicit type parameters to executeQuery calls - Wrap mapper return values in QueryResult.Value() - Import QueryResult class for proper type handling - Resolves all compilation errors related to SQLDelight QueryResult API
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.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.kt(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{kt,kts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{kt,kts}: Adhere to ktlint formatting rules for all Kotlin source and script files; run ktlintFormat before committing
Code must pass Detekt static analysis; avoid suppressing rules without clear justification
Files:
platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.kt
**/src/**/kotlin/**/*.kt
📄 CodeRabbit inference engine (CLAUDE.md)
**/src/**/kotlin/**/*.kt: Use Kotlin’s error(), check(), and require() instead of throwing exceptions directly
Never use "unknown" or default fallbacks that could mask data corruption
Use Arrow’s Either for functional error handling
Fail fast for critical configuration issues (e.g., missing text model)
Files:
platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.kt
**/src/test/kotlin/**/*.kt
📄 CodeRabbit inference engine (CLAUDE.md)
**/src/test/kotlin/**/*.kt: Do not use mock services in tests
Make tests verbose to aid debugging
Use Kotest for testing (project-standard test framework)
Files:
platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.kt
🧬 Code graph analysis (1)
platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.kt (1)
platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/version/ApplicationVersionTest.kt (1)
describe(7-91)
🪛 GitHub Actions: Test
platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.kt
[error] 120-120: Return type mismatch: expected 'QueryResult', actual 'Unit'.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: Cross-Platform Native Build (macos-14, darwin, arm64)
- GitHub Check: Cross-Platform Native Build (windows-latest, win32, x64, true)
- GitHub Check: Cross-Platform Native Build (windows-latest, win32, arm64, true)
- GitHub Check: Cross-Platform Native Build (ubuntu-latest, linux, x64)
- GitHub Check: Cross-Platform Native Build (ubuntu-latest, linux, arm64)
- GitHub Check: Cross-Platform Native Build (macos-13, darwin, x64)
- GitHub Check: Analyze (java-kotlin)
...n/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.kt
Outdated
Show resolved
Hide resolved
- Replace mockk with anonymous object implementations in DatabaseMigrationManagerTest to fix NullPointerException issues with value class unwrapping - Fix documentation code example to return QueryResult.Value instead of raw boolean - Track foreign key enforcement for test databases as future work - All 132 tests now passing successfully Based on AI review feedback from PR #280
…ionManager - Replace all throw DatabaseMigrationException statements with Kotlin's error() function - Update test expectations to catch IllegalStateException instead of DatabaseMigrationException - Adjust error message expectations to include "Migration failed: " prefix - Fix Konsist architecture test violations requiring idiomatic Kotlin error handling - Replace MockK with anonymous objects to resolve value class compatibility issues - All 40 platform-infrastructure tests now pass (100% success rate) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
…ationManager - Added transaction state tracking with boolean flag to prevent rollback attempts on inactive transactions - Fixed "cannot rollback - no transaction is active" error in native builds - Replaced mockk usage with anonymous object implementations in DatabaseMigrationManagerTest - Updated test error message expectations to match actual error format - All DatabaseMigrationManagerTest tests now pass (8/8, 100% success rate) Resolves the SQLite transaction handling issue that was causing Linux x64 native build E2E test failures. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
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.
Actionable comments posted: 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt(1 hunks)platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.kt(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.kt
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{kt,kts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{kt,kts}: Adhere to ktlint formatting rules for all Kotlin source and script files; run ktlintFormat before committing
Code must pass Detekt static analysis; avoid suppressing rules without clear justification
Files:
platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt
**/src/**/kotlin/**/*.kt
📄 CodeRabbit inference engine (CLAUDE.md)
**/src/**/kotlin/**/*.kt: Use Kotlin’s error(), check(), and require() instead of throwing exceptions directly
Never use "unknown" or default fallbacks that could mask data corruption
Use Arrow’s Either for functional error handling
Fail fast for critical configuration issues (e.g., missing text model)
Files:
platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
- GitHub Check: Cross-Platform Native Build (macos-13, darwin, x64)
- GitHub Check: Cross-Platform Native Build (macos-14, darwin, arm64)
- GitHub Check: Cross-Platform Native Build (ubuntu-latest, linux, x64)
- GitHub Check: Cross-Platform Native Build (windows-latest, win32, arm64, true)
- GitHub Check: Cross-Platform Native Build (ubuntu-latest, linux, arm64)
- GitHub Check: Cross-Platform Native Build (windows-latest, win32, x64, true)
- GitHub Check: Analyze (java-kotlin)
- GitHub Check: Unit Tests
| // Use database-level locking to prevent concurrent migrations across processes | ||
| var transactionActive = false | ||
| try { | ||
| driver.execute(null, "BEGIN IMMEDIATE", 0) | ||
| transactionActive = true | ||
| performMigrationInternal(driver, schema, targetVersion, callbacks) | ||
| driver.execute(null, "COMMIT", 0) | ||
| transactionActive = false | ||
| } catch (e: Exception) { | ||
| if (transactionActive) { | ||
| try { | ||
| driver.execute(null, "ROLLBACK", 0) | ||
| } catch (rollbackException: Exception) { | ||
| // Log rollback failure but preserve original exception | ||
| logger.error("Failed to rollback migration transaction", throwable = rollbackException) | ||
| } | ||
| } | ||
| error("Migration failed: ${e.message}") | ||
| } |
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.
Preserve the original migration failure cause
The error("Migration failed: …") path throws a brand-new IllegalStateException, so every upstream caller loses the real stack trace and exception type from the driver/schema. That makes postmortems almost impossible. We already expose DatabaseMigrationException; please wrap the original throwable (or rethrow it if it’s already that type) and keep the cause attached when logging.
} catch (e: Exception) {
if (transactionActive) {
try {
driver.execute(null, "ROLLBACK", 0)
} catch (rollbackException: Exception) {
// Log rollback failure but preserve original exception
logger.error("Failed to rollback migration transaction", throwable = rollbackException)
}
}
- error("Migration failed: ${e.message}")
+ val wrapped = if (e is DatabaseMigrationException) e else DatabaseMigrationException("Migration failed", e)
+ logger.error(wrapped.message ?: "Migration failed", throwable = wrapped)
+ throw wrapped
}Apply the same preservation logic to the other catch blocks so every migration failure surfaces with its original cause.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Use database-level locking to prevent concurrent migrations across processes | |
| var transactionActive = false | |
| try { | |
| driver.execute(null, "BEGIN IMMEDIATE", 0) | |
| transactionActive = true | |
| performMigrationInternal(driver, schema, targetVersion, callbacks) | |
| driver.execute(null, "COMMIT", 0) | |
| transactionActive = false | |
| } catch (e: Exception) { | |
| if (transactionActive) { | |
| try { | |
| driver.execute(null, "ROLLBACK", 0) | |
| } catch (rollbackException: Exception) { | |
| // Log rollback failure but preserve original exception | |
| logger.error("Failed to rollback migration transaction", throwable = rollbackException) | |
| } | |
| } | |
| error("Migration failed: ${e.message}") | |
| } | |
| // Use database-level locking to prevent concurrent migrations across processes | |
| var transactionActive = false | |
| try { | |
| driver.execute(null, "BEGIN IMMEDIATE", 0) | |
| transactionActive = true | |
| performMigrationInternal(driver, schema, targetVersion, callbacks) | |
| driver.execute(null, "COMMIT", 0) | |
| transactionActive = false | |
| } catch (e: Exception) { | |
| if (transactionActive) { | |
| try { | |
| driver.execute(null, "ROLLBACK", 0) | |
| } catch (rollbackException: Exception) { | |
| // Log rollback failure but preserve original exception | |
| logger.error("Failed to rollback migration transaction", throwable = rollbackException) | |
| } | |
| } | |
| val wrapped = if (e is DatabaseMigrationException) e else DatabaseMigrationException("Migration failed", e) | |
| logger.error(wrapped.message ?: "Migration failed", throwable = wrapped) | |
| throw wrapped | |
| } |
🤖 Prompt for AI Agents
In
platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt
around lines 39 to 57, the catch block drops the original exception by calling
error("Migration failed: …") which creates a new exception and discards the
original stack trace; instead, wrap the original throwable in
DatabaseMigrationException (or rethrow the original if it is already a
DatabaseMigrationException) and throw that so the original cause and stack trace
are preserved, keeping the existing rollback logging for rollbackException;
apply the same pattern to the other catch blocks in this class so every
migration failure surfaces with its original cause.
| try { | ||
| schema.create(driver) | ||
| setVersion(driver, targetVersion) | ||
| } catch (e: Exception) { | ||
| error("Failed to create database schema: ${e.message}") | ||
| } |
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.
Retain the root cause when schema creation fails
This catch block also calls error, discarding the original exception before the outer handler can wrap it. Wrap the throwable in DatabaseMigrationException (or rethrow it unchanged if it already is one) so the failure that triggered the rollback is preserved with its stack trace.
try {
schema.create(driver)
setVersion(driver, targetVersion)
} catch (e: Exception) {
- error("Failed to create database schema: ${e.message}")
+ throw DatabaseMigrationException("Failed to create database schema", e)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| schema.create(driver) | |
| setVersion(driver, targetVersion) | |
| } catch (e: Exception) { | |
| error("Failed to create database schema: ${e.message}") | |
| } | |
| try { | |
| schema.create(driver) | |
| setVersion(driver, targetVersion) | |
| } catch (e: Exception) { | |
| throw DatabaseMigrationException("Failed to create database schema", e) | |
| } |
🤖 Prompt for AI Agents
In
platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt
around lines 83-88, the catch currently calls error(...) which discards the
original exception; replace this with rethrow logic that preserves the root
cause: if the caught exception is already a DatabaseMigrationException rethrow
it, otherwise throw a new DatabaseMigrationException with a clear message (e.g.,
"Failed to create database schema") and pass the caught exception as the cause
so the original stack trace is preserved.
| // Execute SQLDelight migrations | ||
| schema.migrate(driver, currentVersion, targetVersion) | ||
|
|
||
| // Note: The callbacks parameter with afterVersion lambda is not part of the standard migrate API | ||
| // If custom callbacks are needed at specific versions, they should be handled separately | ||
| for (version in (currentVersion + 1)..targetVersion) { | ||
| callbacks[version]?.let { callback -> | ||
| logger.debug("Executing custom migration callback for version $version") | ||
| callback.execute(driver) | ||
| } | ||
| } | ||
|
|
||
| // Update version | ||
| setVersion(driver, targetVersion) | ||
|
|
||
| logger.info("Migration completed successfully") | ||
| } catch (e: Exception) { | ||
| logger.error("Migration failed", throwable = e) | ||
| error("Failed to migrate database from $currentVersion to $targetVersion: ${e.message}") | ||
| } |
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.
Surface callback/migration errors with their causes
Here we log e but then rethrow via error, which again obscures the original throwable by replacing it with an IllegalStateException. Please wrap (or reuse) the original exception when failing the migration so callers see the true root cause.
} catch (e: Exception) {
logger.error("Migration failed", throwable = e)
- error("Failed to migrate database from $currentVersion to $targetVersion: ${e.message}")
+ throw if (e is DatabaseMigrationException) {
+ e
+ } else {
+ DatabaseMigrationException(
+ "Failed to migrate database from $currentVersion to $targetVersion",
+ e
+ )
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Execute SQLDelight migrations | |
| schema.migrate(driver, currentVersion, targetVersion) | |
| // Note: The callbacks parameter with afterVersion lambda is not part of the standard migrate API | |
| // If custom callbacks are needed at specific versions, they should be handled separately | |
| for (version in (currentVersion + 1)..targetVersion) { | |
| callbacks[version]?.let { callback -> | |
| logger.debug("Executing custom migration callback for version $version") | |
| callback.execute(driver) | |
| } | |
| } | |
| // Update version | |
| setVersion(driver, targetVersion) | |
| logger.info("Migration completed successfully") | |
| } catch (e: Exception) { | |
| logger.error("Migration failed", throwable = e) | |
| error("Failed to migrate database from $currentVersion to $targetVersion: ${e.message}") | |
| } | |
| } catch (e: Exception) { | |
| logger.error("Migration failed", throwable = e) | |
| throw if (e is DatabaseMigrationException) { | |
| e | |
| } else { | |
| DatabaseMigrationException( | |
| "Failed to migrate database from $currentVersion to $targetVersion", | |
| e | |
| ) | |
| } | |
| } |
🤖 Prompt for AI Agents
In
platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt
around lines 170 to 189, the catch block currently logs the original exception
but then calls Kotlin's error(...) which throws a new IllegalStateException and
loses the original throwable; change the final rethrow so the original exception
is preserved by either rethrowing the caught exception (throw e) or throwing a
new exception that wraps it as the cause (throw IllegalStateException("Failed to
migrate database from $currentVersion to $targetVersion", e)); ensure the log
remains and the thrown exception includes the original throwable as the cause.
Summary
This PR implements an automatic database migration system using SQLDelight, enabling seamless schema updates when the application version changes.
Key Features
🔧 Migration Infrastructure
🛡️ Safety Features
📚 Documentation & Testing
Implementation Details
Architecture
The migration system follows Clean Architecture principles:
Files Changed
Testing
The implementation includes:
Benefits
Migration Guide
Developers can add new migrations by:
See for detailed instructions.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes / Reliability
Breaking Changes
Documentation
Tests