Skip to content

Conversation

@kamiazya
Copy link
Owner

@kamiazya kamiazya commented Sep 27, 2025

Summary

This PR implements an automatic database migration system using SQLDelight, enabling seamless schema updates when the application version changes.

Key Features

🔧 Migration Infrastructure

  • DatabaseMigrationManager: Centralized migration orchestration with thread-safe execution
  • ApplicationVersion: Version and schema tracking system
  • Automatic Migration: Database schemas update automatically on application startup

🛡️ Safety Features

  • Thread-safe migration execution with
  • Proper transaction handling using SqlDriver API
  • Fail-fast when database version is newer than application
  • Graceful handling of existing databases without version

📚 Documentation & Testing

  • Comprehensive migration guide for developers
  • Unit tests for migration manager and version tracking
  • Migration placeholder files for future schema changes

Implementation Details

Architecture

The migration system follows Clean Architecture principles:

  • Migration logic in platform/infrastructure layer
  • Each bounded context manages its own database migrations
  • Version tracking via SQLite PRAGMA user_version

Files Changed

  • ✅ Platform infrastructure for migration management
  • ✅ Updated all SqlDelightDatabaseProviders (scope-management, event-store, device-synchronization)
  • ✅ Migration placeholder files (*.sqm) for future schema changes
  • ✅ Developer documentation and testing

Testing

The implementation includes:

  • Unit tests for DatabaseMigrationManager
  • Unit tests for ApplicationVersion tracking
  • Integration with existing repository tests

Benefits

  1. Zero Manual Migration: Users don't need to run migration scripts
  2. Version Safety: Prevents running older app versions on newer databases
  3. Future-Proof: Easy to add schema changes by creating migration files
  4. Clean Architecture: Follows project patterns and maintains separation of concerns

Migration Guide

Developers can add new migrations by:

  1. Creating a migration file (e.g., )
  2. Updating schema version in ApplicationVersion
  3. Testing the migration
  4. Committing with version bump

See for detailed instructions.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Automated, transactional database migrations with centralized app/schema versioning; in-memory databases now record target schema versions for test consistency.
    • Placeholder migration templates added for multiple contexts.
  • Bug Fixes / Reliability

    • Rollback on migration failure, fail-fast on downgrades, and support for per-version migration callbacks.
  • Breaking Changes

    • One database factory now returns a managed/auto-closeable wrapper (return type changed).
  • Documentation

    • Developer migration guide and guidance for enabling foreign keys in tests.
  • Tests

    • New tests covering migration flows, callbacks, rollbacks, and version compatibility.

- 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-bot
Copy link

changeset-bot bot commented Sep 27, 2025

🦋 Changeset detected

Latest commit: 3e7b849

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
scopes Minor

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 27, 2025

Note

Other AI code review bot(s) detected

CodeRabbit 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.

Walkthrough

Adds 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

Cohort / File(s) Summary
Platform: Migration manager
platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt
New DatabaseMigrationManager with synchronized, transactional migrate(driver, schema, targetVersion, callbacks), PRAGMA user_version handling, empty-db detection, migration callbacks, rollback on error, DatabaseMigrationException, and createDefault() factory.
Platform: Version registry
platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/version/ApplicationVersion.kt
New ApplicationVersion singleton: CURRENT_VERSION, SchemaVersions constants, VersionMapping/versionHistory, getCurrentMapping(), parseVersion/compareVersions, and isCompatible() utilities.
Providers: Event Store
contexts/event-store/infrastructure/src/main/kotlin/.../SqlDelightDatabaseProvider.kt
Replaces direct schema.create with DatabaseMigrationManager.migrate(...); in-memory path still creates fresh schema but sets PRAGMA user_version to EVENT_STORE.
Providers: Device Synchronization
contexts/device-synchronization/infrastructure/src/main/kotlin/.../SqlDelightDatabaseProvider.kt
Replaces direct schema.create with migration to ApplicationVersion.SchemaVersions.DEVICE_SYNCHRONIZATION; in-memory creates fresh schema and sets PRAGMA user_version.
Providers: Scope Management (API change)
contexts/scope-management/infrastructure/src/main/kotlin/.../SqlDelightDatabaseProvider.kt
Introduces ManagedDatabase wrapper (implements AutoCloseable), migrationManager usage; changes createDatabase / createInMemoryDatabase return types to ManagedDatabase; in-memory sets PRAGMA to scope target version.
Migrations: placeholders
contexts/event-store/.../migrations/1.sqm, contexts/device-synchronization/.../migrations/1.sqm, contexts/scope-management/.../migrations/1.sqm
Added commented template migration files (1→2) with example SQL and activation guidance; no executable SQL included.
Docs: Migration guide
docs/guides/development/database-migration-guide.md
New guide covering DatabaseMigrationManager usage, ApplicationVersion, migration file conventions, callbacks, testing, rollback, examples and best practices.
Docs: FK in-tests guidance
docs/issues/enable-foreign-keys-in-tests.md
New guidance on enabling PRAGMA foreign_keys for in-memory tests and recommended test updates to improve parity.
Tests: Migration manager
platform/infrastructure/src/test/kotlin/.../database/DatabaseMigrationManagerTest.kt
New Kotest suite validating fresh-create, upgrade, no-op, callbacks, rollback on failure, downgrade fail-fast, and PRAGMA user_version behavior using in-memory driver.
Tests: Version registry
platform/infrastructure/src/test/kotlin/.../version/ApplicationVersionTest.kt
New tests for CURRENT_VERSION, schema constants, parseVersion, compareVersions, getCurrentMapping, isCompatible, and versionHistory.
Meta: changeset
.changeset/database-migration-feature.md
Adds changelog describing the migration feature, components, migration files, docs, and tests.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Poem

"I nibble at the schema, hop and chew,
PRAGMA stamps the version true.
Callbacks twitch like whiskers bright,
Migrations roll through day and night.
A rabbit's patch—small, neat, and new." 🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “feat: SQLDelight database migration system implementation” succinctly captures the primary change by highlighting the addition of a SQLDelight-based migration system, aligning directly with the pull request’s objectives and reflecting the main feature introduced.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/db-migration

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @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

  • Automatic Database Migration: Implemented a robust, automatic database migration system using SQLDelight, ensuring seamless schema updates on application startup.
  • Centralized Migration Management: Introduced DatabaseMigrationManager for orchestrating migrations with thread-safe execution and proper transaction handling.
  • Version Tracking: Developed ApplicationVersion to track application and database schema versions, including semantic version parsing and comparison, and fail-fast for newer database versions.
  • Context-Specific Integration: Integrated the migration system into SqlDelightDatabaseProvider for Device Synchronization, Event Store, and Scope Management contexts, including handling of existing unversioned databases.
  • Comprehensive Documentation: Added a detailed Database Migration Guide covering how to add new migrations, best practices, testing, and troubleshooting.
  • Extensive Testing: Included unit tests for both DatabaseMigrationManager and ApplicationVersion to ensure reliability and correctness of the migration logic.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions
Copy link
Contributor

github-actions bot commented Sep 27, 2025

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA 3e7b849.
Ensure 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 Files

None

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a 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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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)
    }
    // ...
}

Comment on lines +125 to +130
driver.execute(
null,
"UPDATE scopes SET status = 'archived' WHERE updated_at < ?",
1,
bindLong(0, thirtyDaysAgo)
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
manager.migrate(driver, TestSchema, 2L)
manager.migrate(driver, schema, 2L)

Comment on lines 87 to 92
mapper = { cursor ->
if (cursor.next()) {
cursor.getLong(0) == 0L
} else {
true
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
@kamiazya
Copy link
Owner Author

🔧 Fixed Compilation Errors

The latest commit resolves all compilation errors in DatabaseMigrationManager:

Issues Fixed:

  1. Import paths: Fixed Logger and ConsoleLogger imports (using correct logging package)
  2. Transaction API: Replaced non-existent SqlDriver.newTransaction() with raw SQL transactions (BEGIN/COMMIT/ROLLBACK)
  3. Return types: Fixed executeQuery return type handling by adding .value accessor
  4. Logger API: Updated error logging to use correct method signature with throwable parameter
  5. Migration callback: Removed incorrect afterVersion lambda from schema.migrate call

Implementation Details:

  • Using raw SQL transactions is the standard approach for SQLDelight when you need explicit transaction control
  • The executeQuery method returns QueryResult<T> which requires .value to access the actual result
  • Logger's error method signature is: error(message: String, context: Map<String, Any> = emptyMap(), throwable: Throwable? = null)

Waiting for GitHub Actions to verify the build... 🚀

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3a23a0b and 3c390cb.

📒 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.kt
  • platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt
  • contexts/scope-management/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/scopemanagement/infrastructure/sqldelight/SqlDelightDatabaseProvider.kt
  • contexts/event-store/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/eventstore/infrastructure/sqldelight/SqlDelightDatabaseProvider.kt
  • contexts/device-synchronization/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/devicesync/infrastructure/sqldelight/SqlDelightDatabaseProvider.kt
  • platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.kt
  • platform/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.kt
  • platform/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManager.kt
  • contexts/scope-management/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/scopemanagement/infrastructure/sqldelight/SqlDelightDatabaseProvider.kt
  • contexts/event-store/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/eventstore/infrastructure/sqldelight/SqlDelightDatabaseProvider.kt
  • contexts/device-synchronization/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/devicesync/infrastructure/sqldelight/SqlDelightDatabaseProvider.kt
  • platform/infrastructure/src/test/kotlin/io/github/kamiazya/scopes/platform/infrastructure/database/DatabaseMigrationManagerTest.kt
  • platform/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.kt
  • platform/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)

Comment on lines 45 to 54
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines 45 to 54
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +21 to +23
```
contexts/{context}/infrastructure/src/main/sqldelight/migrations/{version}.sqm
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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}.sqm

Based 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.

Comment on lines +18 to +20
class DatabaseMigrationManager(
private val logger: Logger = ConsoleLogger()
) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +90 to +96
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
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +36 to +40
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)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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).

Comment on lines +84 to +88
describe("versionHistory") {
it("should contain at least one version mapping") {
ApplicationVersion.versionHistory.size shouldBe 1
ApplicationVersion.versionHistory.first().appVersion shouldBe "0.1.0"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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>
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4dc2154 and 839505e.

📒 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)

kamiazya and others added 3 commits September 28, 2025 00:37
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
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 05fd15a and b0331b1.

📒 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)

kamiazya and others added 3 commits September 28, 2025 10:45
- 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>
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between d6683b2 and 3e7b849.

📒 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

Comment on lines +39 to +57
// 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}")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
// 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.

Comment on lines +83 to +88
try {
schema.create(driver)
setVersion(driver, targetVersion)
} catch (e: Exception) {
error("Failed to create database schema: ${e.message}")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +170 to +189
// 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}")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
// 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants