All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mchange.feedletter.db.Migratory.scala Maven / Gradle / Ivy

package com.mchange.feedletter.db

import zio.*
import java.time.Instant
import javax.sql.DataSource
import java.time.temporal.ChronoUnit
import com.mchange.feedletter.db.Migratory.lastHourDumpFileExists
import com.mchange.feedletter.BuildInfo.version
import com.mchange.sc.v1.log.*
import MLevel.*

object Migratory:
  private val DumpTimestampFormatter = java.time.format.DateTimeFormatter.ISO_INSTANT

  private val DumpFileNameRegex = """^feedletter-pg-dump\.(.+)\.sql$""".r

  private def dumpFileName( timestamp : String ) : String =
    "feedletter-pg-dump." + timestamp + ".sql"

  private def extractTimestampFromDumpFileName( dfn : String ) : Option[Instant] =
    DumpFileNameRegex.findFirstMatchIn(dfn)
      .map( m => m.group(1) )
      .map( DumpTimestampFormatter.parse )
      .map( Instant.from )

  def prepareDumpFileForInstant( dumpDir : os.Path, instant : Instant ) : Task[os.Path] = ZIO.attemptBlocking:
    if !os.exists( dumpDir ) then os.makeDir.all( dumpDir )
    val ts = DumpTimestampFormatter.format( instant )
    dumpDir / dumpFileName(ts)

  def lastHourDumpFileExists( dumpDir : os.Path ) : Task[Boolean] = ZIO.attemptBlocking:
    if os.exists( dumpDir ) then
      val instants =
        os.list( dumpDir )
          .map( _.last )
          .map( extractTimestampFromDumpFileName )
          .collect { case Some(instant) => instant }
      val now = Instant.now
      val anHourAgo = now.minus(1, ChronoUnit.HOURS)
      instants.exists( i => anHourAgo.compareTo(i) < 0 )
    else
      false
    
trait Migratory:
  private lazy given logger : MLogger = mlogger( this )

  def targetDbVersion : Int

  def fetchDumpDir(ds : DataSource) : Task[os.Path]
  def dump(ds : DataSource) : Task[os.Path]
  def dbVersionStatus(ds : DataSource) : Task[DbVersionStatus]
  def upMigrate(ds : DataSource, from : Option[Int]) : Task[Unit]

  def migrate(ds : DataSource) : Task[Unit] =
    def handleStatus( status : DbVersionStatus ) : Task[Unit] =
      TRACE.log( s"handleStatus( ${status} )" )
      status match
        case DbVersionStatus.Current(version) =>
          INFO.log( s"Schema up-to-date (current version: ${version})" )
          ZIO.succeed( () )
        case DbVersionStatus.OutOfDate( schemaVersion, requiredVersion ) =>
          assert( schemaVersion < requiredVersion, s"An out-of-date scheme should have schema version (${schemaVersion}) < required version (${requiredVersion})" )
          INFO.log( s"Up-migrating from schema version ${schemaVersion})" )
          upMigrate( ds, Some( schemaVersion ) ) *> migrate( ds )
        case DbVersionStatus.SchemaMetadataNotFound => // uninitialized db, we presume
          INFO.log( s"Initializing new schema")
          upMigrate( ds, None ) *> migrate( ds )
        case other =>
          throw new CannotUpMigrate( s"""${other}: ${other.errMessage.getOrElse("")}""" ) // we should never see 
    for
      status <- dbVersionStatus( ds )
      _      <- handleStatus( status )
    yield ()

  def cautiousMigrate( ds : DataSource ) : Task[Unit] =
    val safeToTry =
      for
        initialStatus <- dbVersionStatus(ds)
        dumpDir       <- fetchDumpDir(ds)
        dumped        <- lastHourDumpFileExists(dumpDir)
      yield
        initialStatus == DbVersionStatus.SchemaMetadataNotFound || dumped

    safeToTry.flatMap: safe =>
      if safe then
        migrate(ds)
      else
        ZIO.fail( new NoRecentDump("Please dump the database prior to migrating, no recent dump file found.") )





© 2015 - 2025 Weber Informatics LLC | Privacy Policy