feat(01-02): create Flyway migrator service and initial migration

- Add Migrator trait with migrate/status ZIO effects
- Create V1 migration for summer_users table
- Configure Flyway with summer_migrations table and classpath location
- Migrations run manually via CLI, not on startup
This commit is contained in:
Jakub Zych
2026-02-04 22:13:03 +01:00
parent 106e66413e
commit ddb1964d1a
2 changed files with 106 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
-- Summer CMS initial schema
-- Creates the base users table for future auth
CREATE TABLE summer_users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_summer_users_email ON summer_users(email);
-- Trigger for updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_summer_users_updated_at
BEFORE UPDATE ON summer_users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

View File

@@ -0,0 +1,80 @@
package db
import org.flywaydb.core.Flyway
import org.flywaydb.core.api.MigrationInfo
import zio.*
import javax.sql.DataSource
/** Migration status summary
*
* @param current
* The currently applied migration version (None if no migrations applied)
* @param pending
* Number of pending migrations waiting to be applied
*/
case class MigrationStatus(current: Option[String], pending: Int)
/** Flyway database migration service
*
* Wraps Flyway migrations in ZIO effects for controlled execution. Migrations are NOT auto-run on
* application startup - they must be explicitly invoked via CLI (Phase 5: CLI Scaffolding).
*
* Migration files are stored in: resources/db/migration/ Following Flyway naming convention:
* V{version}__{description}.sql
*
* Example: V1__create_summer_users.sql
*/
trait Migrator {
/** Run all pending migrations
*
* @return
* Number of migrations applied
*/
def migrate: Task[Int]
/** Get current migration status
*
* @return
* Status with current version and pending count
*/
def status: Task[MigrationStatus]
}
object Migrator {
/** Live implementation of Migrator backed by Flyway */
val live: ZLayer[DataSource, Nothing, Migrator] =
ZLayer.fromFunction { (ds: DataSource) =>
new Migrator {
private def flyway: Flyway =
Flyway.configure()
.dataSource(ds)
.locations("classpath:db/migration")
.table("summer_migrations")
.baselineOnMigrate(true)
.load()
override def migrate: Task[Int] =
ZIO.attempt {
flyway.migrate().migrationsExecuted
}
override def status: Task[MigrationStatus] =
ZIO.attempt {
val info = flyway.info()
val current = Option(info.current()).map(_.getVersion.getVersion)
val pending = info.pending().length
MigrationStatus(current, pending)
}
}
}
/** Accessor for migrate effect */
def migrate: ZIO[Migrator, Throwable, Int] =
ZIO.serviceWithZIO[Migrator](_.migrate)
/** Accessor for status effect */
def status: ZIO[Migrator, Throwable, MigrationStatus] =
ZIO.serviceWithZIO[Migrator](_.status)
}