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:
26
summercms/resources/db/migration/V1__create_summer_users.sql
Normal file
26
summercms/resources/db/migration/V1__create_summer_users.sql
Normal 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();
|
||||
80
summercms/src/db/Migrator.scala
Normal file
80
summercms/src/db/Migrator.scala
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user