Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.github.kamiazya.scopes.apps.cli.di.scopemanagement

import io.github.kamiazya.scopes.platform.application.port.TransactionManager
import io.github.kamiazya.scopes.platform.infrastructure.database.migration.MigrationAwareDatabaseProvider
import io.github.kamiazya.scopes.platform.infrastructure.database.migration.MigrationConfig
import io.github.kamiazya.scopes.platform.infrastructure.transaction.SqlDelightTransactionManager
import io.github.kamiazya.scopes.scopemanagement.db.ScopeManagementDatabase
import io.github.kamiazya.scopes.scopemanagement.domain.repository.ActiveContextRepository
Expand All @@ -17,13 +19,13 @@ import io.github.kamiazya.scopes.scopemanagement.infrastructure.adapters.ErrorMa
import io.github.kamiazya.scopes.scopemanagement.infrastructure.alias.generation.DefaultAliasGenerationService
import io.github.kamiazya.scopes.scopemanagement.infrastructure.alias.generation.providers.DefaultWordProvider
import io.github.kamiazya.scopes.scopemanagement.infrastructure.alias.generation.strategies.HaikunatorStrategy
import io.github.kamiazya.scopes.scopemanagement.infrastructure.migration.ScopeManagementMigrationProvider
import io.github.kamiazya.scopes.scopemanagement.infrastructure.repository.SqlDelightActiveContextRepository
import io.github.kamiazya.scopes.scopemanagement.infrastructure.repository.SqlDelightAspectDefinitionRepository
import io.github.kamiazya.scopes.scopemanagement.infrastructure.repository.SqlDelightContextViewRepository
import io.github.kamiazya.scopes.scopemanagement.infrastructure.repository.SqlDelightScopeAliasRepository
import io.github.kamiazya.scopes.scopemanagement.infrastructure.repository.SqlDelightScopeRepository
import io.github.kamiazya.scopes.scopemanagement.infrastructure.service.AspectQueryFilterValidator
import io.github.kamiazya.scopes.scopemanagement.infrastructure.sqldelight.SqlDelightDatabaseProvider
import org.koin.core.qualifier.named
import org.koin.dsl.module

Expand All @@ -38,15 +40,59 @@ import org.koin.dsl.module
* - Default services following Zero-Configuration principle
*/
val scopeManagementInfrastructureModule = module {
// SQLDelight Database
// Migration configuration
single<MigrationConfig>(named("scopeManagementMigrationConfig")) {
MigrationConfig(
maxRetries = 3,
)
}

// Migration provider
single(named("scopeManagementMigrationProvider")) {
{ ScopeManagementMigrationProvider(logger = get()).getMigrations() }
}

// Migration-aware database provider
single<MigrationAwareDatabaseProvider<ScopeManagementDatabase>>(named("scopeManagementDatabaseProvider")) {
val migrationProvider: () -> List<io.github.kamiazya.scopes.platform.infrastructure.database.migration.Migration> =
get(named("scopeManagementMigrationProvider"))
val config: MigrationConfig = get(named("scopeManagementMigrationConfig"))
val logger: io.github.kamiazya.scopes.platform.observability.logging.Logger = get()

MigrationAwareDatabaseProvider(
migrationProvider = migrationProvider,
config = config,
logger = logger,
databaseFactory = { driver ->
// Schema is created by migrations; avoid double-creation here
ScopeManagementDatabase(driver)
},
)
}

// SQLDelight Database using the migration-aware provider
single<ScopeManagementDatabase>(named("scopeManagement")) {
val databasePath: String = get<String>(named("databasePath"))
val provider: MigrationAwareDatabaseProvider<ScopeManagementDatabase> = get(named("scopeManagementDatabaseProvider"))

val dbPath = if (databasePath == ":memory:") {
":memory:"
} else {
"$databasePath/scope-management.db"
}
SqlDelightDatabaseProvider.createDatabase(dbPath)

// Create database with migrations applied
kotlinx.coroutines.runBlocking {
provider.createDatabase(dbPath).fold(
ifLeft = { err ->
// Fail-fast using Kotlin's error() function
error("Failed to create database: ${err.message}")
},
ifRight = { database ->
database
},
)
}
Comment on lines +85 to +95
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The use of runBlocking here to initialize the database within a Koin module is concerning. The pull request description mentions, "Removed runBlocking from DI modules in favor of lazy initialization," but this change seems to re-introduce it. While this is for a CLI application where blocking the main thread during startup might be less critical than in a GUI application, it's still an anti-pattern that can lead to performance problems and make the startup process rigid. It would be better to stick to asynchronous initialization, perhaps by having the provider return a Deferred<Database> or by making the creation suspendable and handling it at the application's entry point within a coroutine scope.

}

// Repository implementations - mix of SQLDelight and legacy SQLite
Expand Down Expand Up @@ -128,4 +174,10 @@ val scopeManagementInfrastructureModule = module {
logger = get(),
)
}

// Note: Schema version repository and migration executor are no longer needed here
// They are handled internally by MigrationAwareDatabaseProvider

// Note: Migrations are now automatically run when creating the ManagedSqlDriver
// No need for separate MigrationManager or DatabaseMigrationBootstrapper
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
"pattern" : "logback-.*\\.xml$"
}, {
"pattern" : "META-INF/services/.*"
}, {
"pattern" : "migrations/.*\\.sql$"
}, {
"pattern" : "migrations/.*/.*\\.sql$"
} ]
},
"bundles" : [ ]
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.kamiazya.scopes.devicesync.infrastructure.migration

import io.github.kamiazya.scopes.platform.infrastructure.database.migration.Migration
import io.github.kamiazya.scopes.platform.infrastructure.database.migration.scanner.ResourceMigrationDiscovery
import io.github.kamiazya.scopes.platform.observability.logging.Logger

/**
* Provides migrations for the Device Synchronization bounded context.
*/
class DeviceSyncMigrationProvider(private val logger: Logger) {
private val discovery = ResourceMigrationDiscovery(
resourcePath = "migrations/device-sync",
classLoader = this::class.java.classLoader,
logger = logger,
)

fun getMigrations(): List<Migration> = discovery.discoverMigrations()
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,50 @@
package io.github.kamiazya.scopes.devicesync.infrastructure.sqldelight

import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
import io.github.kamiazya.scopes.devicesync.db.DeviceSyncDatabase
import io.github.kamiazya.scopes.devicesync.infrastructure.migration.DeviceSyncMigrationProvider
import io.github.kamiazya.scopes.platform.infrastructure.database.ManagedSqlDriver
import io.github.kamiazya.scopes.platform.infrastructure.database.migration.applyMigrations
import io.github.kamiazya.scopes.platform.observability.logging.ConsoleLogger
import kotlinx.coroutines.runBlocking

/**
* Provides SQLDelight database instances for Device Synchronization.
* Provides SQLDelight database instances for Device Synchronization with automatic migrations.
*/
object SqlDelightDatabaseProvider {

/**
* Creates a new DeviceSyncDatabase instance.
*/
fun createDatabase(databasePath: String): DeviceSyncDatabase {
val driver: SqlDriver = JdbcSqliteDriver("jdbc:sqlite:$databasePath")

// Create the database schema
DeviceSyncDatabase.Schema.create(driver)
class ManagedDatabase(private val database: DeviceSyncDatabase, private val managedDriver: AutoCloseable) :
DeviceSyncDatabase by database,
AutoCloseable {
override fun close() = managedDriver.close()
}

// Enable foreign keys
driver.execute(null, "PRAGMA foreign_keys=ON", 0)
fun createDatabase(databasePath: String): DeviceSyncDatabase {
val managedDriver = ManagedSqlDriver.createWithDefaults(databasePath)
val driver = managedDriver.driver

return DeviceSyncDatabase(driver)
val logger = ConsoleLogger("DeviceSyncDB")
val migrations = DeviceSyncMigrationProvider(logger).getMigrations()
runBlocking {
driver.applyMigrations(migrations, logger).fold(
ifLeft = { err -> error("Migration failed: ${err.message}") },
ifRight = { },
)
}
return ManagedDatabase(DeviceSyncDatabase(driver), managedDriver)
}
Comment on lines +21 to 34
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

Leaked lifecycle: returning DeviceSyncDatabase hides close()

createDatabase returns DeviceSyncDatabase, so callers can’t invoke close() on the ManagedDatabase wrapper, risking driver leaks.

Two options:

  • Preferred: change return type to ManagedDatabase.
-    fun createDatabase(databasePath: String): DeviceSyncDatabase {
+    fun createDatabase(databasePath: String): ManagedDatabase {
...
-        return ManagedDatabase(DeviceSyncDatabase(driver), managedDriver)
+        return ManagedDatabase(DeviceSyncDatabase(driver), managedDriver)
     }
  • Backward‑compatible: add a new createManagedDatabase(databasePath) returning ManagedDatabase and keep the old method (deprecated) delegating to it.
    I can provide the overload if you want to preserve the existing API for now.
🤖 Prompt for AI Agents
In
contexts/device-synchronization/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/devicesync/infrastructure/sqldelight/SqlDelightDatabaseProvider.kt
around lines 21–34, the method returns DeviceSyncDatabase which hides the
ManagedDatabase.close() lifecycle and risks leaking the driver; change the API
to return ManagedDatabase instead (preferred) by updating the function signature
to return ManagedDatabase and return the existing
ManagedDatabase(DeviceSyncDatabase(driver), managedDriver) so callers can call
close(), or if you must preserve backward compatibility add a new
createManagedDatabase(databasePath: String): ManagedDatabase implementing the
same body and make the old createDatabase(databasePath: String):
DeviceSyncDatabase deprecated and delegate to createManagedDatabase(). Ensure
callers are updated to use the new managed return type (or the deprecated
wrapper) and keep the migration/runBlocking behavior unchanged.


/**
* Creates an in-memory database for testing.
*/
fun createInMemoryDatabase(): DeviceSyncDatabase {
val driver: SqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
DeviceSyncDatabase.Schema.create(driver)
return DeviceSyncDatabase(driver)
val managedDriver = ManagedSqlDriver(":memory:")
val driver = managedDriver.driver

val logger = ConsoleLogger("DeviceSyncDB-InMemory")
val migrations = DeviceSyncMigrationProvider(logger).getMigrations()
runBlocking {
driver.applyMigrations(migrations, logger).fold(
ifLeft = { err -> error("Migration failed: ${err.message}") },
ifRight = { },
)
}
return ManagedDatabase(DeviceSyncDatabase(driver), managedDriver)
}
}
Comment on lines +21 to 50
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

There is significant code duplication between createDatabase and createInMemoryDatabase. The logic for creating the driver, getting migrations, and applying them via runBlocking is nearly identical. This could be extracted into a private helper function to improve maintainability and reduce redundancy. The helper could take the database path or driver as a parameter.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- Initial schema for Device Synchronization

CREATE TABLE IF NOT EXISTS devices (
device_id TEXT PRIMARY KEY NOT NULL,
last_sync_at INTEGER,
last_successful_push INTEGER,
last_successful_pull INTEGER,
sync_status TEXT NOT NULL DEFAULT 'NEVER_SYNCED',
pending_changes INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_devices_sync_status ON devices(sync_status);
CREATE INDEX IF NOT EXISTS idx_devices_updated_at ON devices(updated_at);

CREATE TABLE IF NOT EXISTS vector_clocks (
device_id TEXT NOT NULL,
component_device TEXT NOT NULL,
timestamp INTEGER NOT NULL,
PRIMARY KEY (device_id, component_device)
);
Comment on lines +17 to +22
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

Add FK to devices for referential integrity

vector_clocks.device_id should reference devices(device_id).

Apply this diff:

 CREATE TABLE IF NOT EXISTS vector_clocks (
     device_id TEXT NOT NULL,
     component_device TEXT NOT NULL,
     timestamp INTEGER NOT NULL,
-    PRIMARY KEY (device_id, component_device)
+    PRIMARY KEY (device_id, component_device),
+    FOREIGN KEY (device_id) REFERENCES devices(device_id) ON DELETE CASCADE
 );
🤖 Prompt for AI Agents
In
contexts/device-synchronization/infrastructure/src/main/resources/migrations/device-sync/V1__Initial_device_sync_schema.sql
around lines 17 to 22, the vector_clocks table lacks a foreign key to devices;
modify the CREATE TABLE to add a foreign key constraint on device_id referencing
devices(device_id) (e.g. add ", FOREIGN KEY (device_id) REFERENCES
devices(device_id)" inside the table definition) so that device_id enforces
referential integrity; ensure syntax is valid for the target DB and that the
devices table exists before this migration.


CREATE INDEX IF NOT EXISTS idx_vector_clocks_device_id ON vector_clocks(device_id);

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.github.kamiazya.scopes.eventstore.infrastructure.migration

import io.github.kamiazya.scopes.platform.infrastructure.database.migration.Migration
import io.github.kamiazya.scopes.platform.infrastructure.database.migration.scanner.ResourceMigrationDiscovery
import io.github.kamiazya.scopes.platform.observability.logging.Logger

/**
* Provides migrations for the Event Store bounded context.
*
* Migrations are discovered from classpath resources under migrations/event-store.
*/
class EventStoreMigrationProvider(private val logger: Logger) {
private val discovery = ResourceMigrationDiscovery(
resourcePath = "migrations/event-store",
classLoader = this::class.java.classLoader,
logger = logger,
)

fun getMigrations(): List<Migration> = discovery.discoverMigrations()
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,50 @@
package io.github.kamiazya.scopes.eventstore.infrastructure.sqldelight

import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
import io.github.kamiazya.scopes.eventstore.db.EventStoreDatabase
import io.github.kamiazya.scopes.eventstore.infrastructure.migration.EventStoreMigrationProvider
import io.github.kamiazya.scopes.platform.infrastructure.database.ManagedSqlDriver
import io.github.kamiazya.scopes.platform.infrastructure.database.migration.applyMigrations
import io.github.kamiazya.scopes.platform.observability.logging.ConsoleLogger
import kotlinx.coroutines.runBlocking

/**
* Provides SQLDelight database instances for Event Store.
* Provides SQLDelight database instances for Event Store with automatic migrations.
*/
object SqlDelightDatabaseProvider {

/**
* Creates a new EventStoreDatabase instance.
*/
fun createDatabase(databasePath: String): EventStoreDatabase {
val driver: SqlDriver = JdbcSqliteDriver("jdbc:sqlite:$databasePath")

// Create the database schema
EventStoreDatabase.Schema.create(driver)
class ManagedDatabase(private val database: EventStoreDatabase, private val managedDriver: AutoCloseable) :
EventStoreDatabase by database,
AutoCloseable {
override fun close() = managedDriver.close()
}

// Enable foreign keys
driver.execute(null, "PRAGMA foreign_keys=ON", 0)
fun createDatabase(databasePath: String): EventStoreDatabase {
val managedDriver = ManagedSqlDriver.createWithDefaults(databasePath)
val driver = managedDriver.driver

return EventStoreDatabase(driver)
val logger = ConsoleLogger("EventStoreDB")
val migrations = EventStoreMigrationProvider(logger).getMigrations()
runBlocking {
driver.applyMigrations(migrations, logger).fold(
ifLeft = { err -> error("Migration failed: ${err.message}") },
ifRight = { },
)
}
return ManagedDatabase(EventStoreDatabase(driver), managedDriver)
}
Comment on lines +21 to 34
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

Leaked lifecycle: returning EventStoreDatabase hides close()

Same as device‑sync: callers can’t close the driver if return type is EventStoreDatabase.

Preferred change:

-    fun createDatabase(databasePath: String): EventStoreDatabase {
+    fun createDatabase(databasePath: String): ManagedDatabase {
...
-        return ManagedDatabase(EventStoreDatabase(driver), managedDriver)
+        return ManagedDatabase(EventStoreDatabase(driver), managedDriver)
     }

Alternatively, add createManagedDatabase(...) and deprecate the old method to maintain API compatibility.

🤖 Prompt for AI Agents
In
contexts/event-store/infrastructure/src/main/kotlin/io/github/kamiazya/scopes/eventstore/infrastructure/sqldelight/SqlDelightDatabaseProvider.kt
around lines 21 to 34, the current createDatabase(databasePath: String):
EventStoreDatabase leaks the driver's lifecycle because callers cannot close the
underlying driver; change the API to return the managed wrapper (e.g.,
ManagedDatabase or a new ManagedEventStoreDatabase) so ownership/close is
explicit and update all call sites to use the managed type, or alternatively add
a new createManagedDatabase(databasePath: String): ManagedDatabase (or similar)
that returns the wrapper and mark the old createDatabase as deprecated
forwarding to the new method to preserve compatibility; ensure migrations and
returned object are the managed instance and update imports/tests accordingly.


/**
* Creates an in-memory database for testing.
*/
fun createInMemoryDatabase(): EventStoreDatabase {
val driver: SqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
EventStoreDatabase.Schema.create(driver)
return EventStoreDatabase(driver)
val managedDriver = ManagedSqlDriver(":memory:")
val driver = managedDriver.driver

val logger = ConsoleLogger("EventStoreDB-InMemory")
val migrations = EventStoreMigrationProvider(logger).getMigrations()
runBlocking {
driver.applyMigrations(migrations, logger).fold(
ifLeft = { err -> error("Migration failed: ${err.message}") },
ifRight = { },
)
}
return ManagedDatabase(EventStoreDatabase(driver), managedDriver)
}
}
Comment on lines +21 to 50
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Similar to other database providers in this PR, there's a lot of duplicated code between the createDatabase and createInMemoryDatabase functions. The process of creating a driver, fetching migrations, and applying them is repeated. To improve maintainability, this common logic should be extracted into a private function.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- Initial schema for Event Store

CREATE TABLE IF NOT EXISTS events (
sequence_number INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT NOT NULL UNIQUE,
aggregate_id TEXT NOT NULL,
aggregate_version INTEGER NOT NULL,
event_type TEXT NOT NULL,
event_data TEXT NOT NULL,
occurred_at INTEGER NOT NULL,
stored_at INTEGER NOT NULL
);
Comment on lines +3 to +12
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

Enforce uniqueness of (aggregate_id, aggregate_version)

Event stores must prevent duplicate versions per aggregate.

Apply this diff to add a table-level constraint:

 CREATE TABLE IF NOT EXISTS events (
     sequence_number INTEGER PRIMARY KEY AUTOINCREMENT,
     event_id TEXT NOT NULL UNIQUE,
     aggregate_id TEXT NOT NULL,
     aggregate_version INTEGER NOT NULL,
     event_type TEXT NOT NULL,
     event_data TEXT NOT NULL,
     occurred_at INTEGER NOT NULL,
-    stored_at INTEGER NOT NULL
+    stored_at INTEGER NOT NULL,
+    UNIQUE (aggregate_id, aggregate_version)
 );

And update the index below (or remove it to avoid duplication).

📝 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
CREATE TABLE IF NOT EXISTS events (
sequence_number INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT NOT NULL UNIQUE,
aggregate_id TEXT NOT NULL,
aggregate_version INTEGER NOT NULL,
event_type TEXT NOT NULL,
event_data TEXT NOT NULL,
occurred_at INTEGER NOT NULL,
stored_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS events (
sequence_number INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT NOT NULL UNIQUE,
aggregate_id TEXT NOT NULL,
aggregate_version INTEGER NOT NULL,
event_type TEXT NOT NULL,
event_data TEXT NOT NULL,
occurred_at INTEGER NOT NULL,
stored_at INTEGER NOT NULL,
UNIQUE (aggregate_id, aggregate_version)
);
🤖 Prompt for AI Agents
In
contexts/event-store/infrastructure/src/main/resources/migrations/event-store/V1__Initial_event_store_schema.sql
around lines 3 to 12, the events table allows duplicate (aggregate_id,
aggregate_version) pairs; add a table-level UNIQUE constraint on (aggregate_id,
aggregate_version) to prevent duplicate versions per aggregate, and then either
remove the separate index that duplicates this uniqueness or update that index
to be unique as well so you don't have redundant non-unique indexes.


CREATE INDEX IF NOT EXISTS idx_events_aggregate_id_version ON events(aggregate_id, aggregate_version);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Make the aggregate/version index UNIQUE or drop it

The non-unique index conflicts with the integrity requirement above.

Apply one of:

  • Make it unique:
-CREATE INDEX IF NOT EXISTS idx_events_aggregate_id_version ON events(aggregate_id, aggregate_version);
+CREATE UNIQUE INDEX IF NOT EXISTS idx_events_aggregate_id_version ON events(aggregate_id, aggregate_version);
  • Or drop this index if you keep the table-level UNIQUE; SQLite will auto-create a unique index for the constraint.
📝 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
CREATE INDEX IF NOT EXISTS idx_events_aggregate_id_version ON events(aggregate_id, aggregate_version);
CREATE UNIQUE INDEX IF NOT EXISTS idx_events_aggregate_id_version
ON events(aggregate_id, aggregate_version);
🤖 Prompt for AI Agents
In
contexts/event-store/infrastructure/src/main/resources/migrations/event-store/V1__Initial_event_store_schema.sql
around line 14, the non-unique index on (aggregate_id, aggregate_version)
conflicts with the table-level UNIQUE constraint; either convert the index to a
UNIQUE INDEX (CREATE UNIQUE INDEX IF NOT EXISTS idx_events_aggregate_id_version
ON events(aggregate_id, aggregate_version)) or remove the explicit index
entirely if the table already declares a UNIQUE constraint (SQLite will
auto-create the unique index for that constraint). Ensure the migration reflects
one of these two options consistently so you don’t have a duplicate/non-unique
index conflicting with the integrity constraint.

CREATE INDEX IF NOT EXISTS idx_events_stored_at ON events(stored_at);
CREATE INDEX IF NOT EXISTS idx_events_occurred_at ON events(occurred_at);
CREATE INDEX IF NOT EXISTS idx_events_event_type_sequence_number ON events(event_type, sequence_number);
CREATE INDEX IF NOT EXISTS idx_events_event_type_occurred_at ON events(event_type, occurred_at);

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.github.kamiazya.scopes.scopemanagement.infrastructure.migration

import io.github.kamiazya.scopes.platform.infrastructure.database.migration.Migration
import io.github.kamiazya.scopes.platform.infrastructure.database.migration.scanner.ResourceMigrationDiscovery
import io.github.kamiazya.scopes.platform.observability.logging.Logger

/**
* Provides migrations for the Scope Management bounded context.
*
* Migrations are discovered from the classpath under the migrations directory.
* They follow the naming convention: V{version}__{description}.sql
*/
class ScopeManagementMigrationProvider(private val logger: Logger) {
private val discovery = ResourceMigrationDiscovery(
resourcePath = "migrations/scope-management",
classLoader = this::class.java.classLoader,
logger = logger,
)

/**
* Gets all migrations for the scope management context.
*/
fun getMigrations(): List<Migration> = discovery.discoverMigrations()
}
Loading
Loading