From c3cc01f68d90d28ba5c3998ae07d5788eda6420a Mon Sep 17 00:00:00 2001 From: Roman Langolf Date: Sun, 13 Sep 2026 12:54:35 +0700 Subject: [PATCH 1/2] update Scala to 3.9.0, use scala 3 features, remove scalafix --- .github/workflows/ci.yml | 4 +- .scalafix.conf | 26 --- .scalafmt.conf | 11 +- build.sbt | 20 +- .../src/main/scala/dumbo/cli/Arguments.scala | 2 +- .../src/main/scala/dumbo/cli/Dumbo.scala | 10 +- .../main/scala/dumbo/ConnectionConfig.scala | 13 +- .../shared/src/main/scala/dumbo/Dumbo.scala | 177 +++++++++--------- .../src/main/scala/dumbo/ResourceFile.scala | 49 +++-- .../dumbo/internal/ResourcesReader.scala | 2 +- .../src/main/scala/dumbo/logging/Logger.scala | 26 +-- .../example/src/main/scala/ExampleApp.scala | 2 +- .../example/src/main/scala/ExampleClean.scala | 2 +- .../src/main/scala/ExampleCustomSession.scala | 4 +- .../src/main/scala/ExampleLog4Cats.scala | 2 +- .../src/test/scala/DumboFlywaySpec.scala | 89 +++++---- .../jvm/src/test/scala/DumboJvmSpec.scala | 4 +- .../src/test/scala/DumboMigrationSpec.scala | 87 +++++---- .../src/test/scala/DumboResourcesSpec.scala | 24 +-- .../src/test/scala/ffstest/FFramework.scala | 16 +- project/plugins.sbt | 2 - 21 files changed, 275 insertions(+), 297 deletions(-) delete mode 100644 .scalafix.conf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9be129..fc410fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,9 +94,9 @@ jobs: - name: Check that workflows are up to date run: sbt githubWorkflowCheck - - name: Check scalafix/scalafmt lints + - name: Check scalafmt lints if: matrix.java == 'temurin@25' && (matrix.scala == '3') && matrix.project == 'rootJVM' && startsWith(matrix.os, 'ubuntu') - run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' 'Test/copyResources; scalafixAll --check; all scalafmtSbtCheck scalafmtCheckAll' + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' 'Test/copyResources; all scalafmtSbtCheck scalafmtCheckAll' - name: Check headers and formatting if: matrix.java == 'temurin@25' && matrix.os == 'ubuntu-latest' diff --git a/.scalafix.conf b/.scalafix.conf deleted file mode 100644 index fee5375..0000000 --- a/.scalafix.conf +++ /dev/null @@ -1,26 +0,0 @@ -rules = [ - DisableSyntax - LeakingImplicitClassVal - NoAutoTupling - NoValInForComprehension - OrganizeImports -] - -OrganizeImports { - removeUnused = true - targetDialect = Scala3 - expandRelative = true - groupedImports = Merge - groups = [ - "re:javax?\\.", - "scala.", - "*" - ] -} - -DisableSyntax.noThrows = true -DisableSyntax.noNulls = false -DisableSyntax.noReturns = true -DisableSyntax.noXml = true -DisableSyntax.noFinalize = true -DisableSyntax.noValPatterns = true diff --git a/.scalafmt.conf b/.scalafmt.conf index fe3ddca..7a47c48 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -13,7 +13,7 @@ danglingParentheses.preset = true optIn.annotationNewlines = true newlines.alwaysBeforeMultilineDef = false runner.dialect = scala3 -rewrite.rules = [RedundantBraces] +rewrite.rules = [RedundantBraces, Imports] indentOperator.exemptScope = aloneArgOrBody indentOperator.excludeRegex = "^(&&|\\|\\|)$" project.excludeFilters = [] @@ -23,4 +23,11 @@ rewriteTokens = { "⇒": "=>" "→": "->" "←": "<-" -} \ No newline at end of file +} + +rewrite.imports.sort = ascii +rewrite.imports.groups = [ + ["javax?\\..*"] + ["scala\\..*"] + [".*"] +] \ No newline at end of file diff --git a/build.sbt b/build.sbt index e55d7ff..6e16d85 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ import scala.scalanative.build.* -lazy val `scala-3` = "3.3.8" -lazy val `scala-3-latest` = "3.7.4" +lazy val `scala-3` = "3.9.0" +lazy val `scala-3-next` = "3.9.0" ThisBuild / tlBaseVersion := "0.11" ThisBuild / startYear := Some(2023) @@ -19,9 +19,6 @@ ThisBuild / versionScheme := Some("early-semver") ThisBuild / description := "Simple database migration tool for Scala + Postgres" ThisBuild / homepage := Some(url("https://github.com/rolang/dumbo")) -ThisBuild / semanticdbEnabled := true -ThisBuild / semanticdbVersion := scalafixSemanticdb.revision // use Scalafix compatible version - // githubWorkflow val defautOs = "ubuntu-latest" val linuxOsArm = "ubuntu-24.04-arm" @@ -42,7 +39,6 @@ ThisBuild / githubWorkflowBuildMatrixExclusions ++= Seq( ) ThisBuild / githubWorkflowJavaVersions := testJavaVersions ThisBuild / tlCiHeaderCheck := true -ThisBuild / tlCiScalafixCheck := false lazy val llvmVersion = "22" lazy val brewFormulas = Set("s2n", "utf8proc") @@ -74,8 +70,8 @@ ThisBuild / githubWorkflowBuildPreamble ++= List( ThisBuild / githubWorkflowBuild := { WorkflowStep.Sbt( - List("Test/copyResources; scalafixAll --check; all scalafmtSbtCheck scalafmtCheckAll"), - name = Some("Check scalafix/scalafmt lints"), + List("Test/copyResources; all scalafmtSbtCheck scalafmtCheckAll"), + name = Some("Check scalafmt lints"), cond = Some( s"matrix.java == '${defaultJavaVersion.render}' && (matrix.scala == '3') && matrix.project == 'rootJVM' && startsWith(matrix.os, 'ubuntu')" ), @@ -214,8 +210,8 @@ ThisBuild / githubWorkflowBuild += WorkflowStep.Run( cond = Some("matrix.project == 'rootJVM' && matrix.scala == '3'"), ) -addCommandAlias("fix", "; +Test/copyResources; +scalafixAll; +scalafmtAll; scalafmtSbt") -addCommandAlias("check", "; +Test/copyResources; +scalafixAll --check; +scalafmtCheckAll; scalafmtSbtCheck") +addCommandAlias("fix", "; +Test/copyResources; +scalafmtAll; scalafmtSbt") +addCommandAlias("check", "; +Test/copyResources; +scalafmtCheckAll; scalafmtSbtCheck") lazy val commonSettings = List( // Headers @@ -385,8 +381,8 @@ lazy val example = project .dependsOn(core.jvm) .settings(commonSettings) .settings( - scalaVersion := `scala-3-latest`, - crossScalaVersions := Seq(`scala-3-latest`), + scalaVersion := `scala-3-next`, + crossScalaVersions := Seq(`scala-3-next`), Compile / run / fork := true, Compile / headerCheck := Nil, scalacOptions -= "-Werror", diff --git a/modules/cli/shared/src/main/scala/dumbo/cli/Arguments.scala b/modules/cli/shared/src/main/scala/dumbo/cli/Arguments.scala index 3879397..f0bb531 100644 --- a/modules/cli/shared/src/main/scala/dumbo/cli/Arguments.scala +++ b/modules/cli/shared/src/main/scala/dumbo/cli/Arguments.scala @@ -9,9 +9,9 @@ import java.net.URI import scala.annotation.tailrec import scala.util.Try +import dumbo.ConnectionConfig import dumbo.Dumbo.defaults import fs2.io.file.Path -import dumbo.ConnectionConfig final case class Arguments( commands: List[Command], diff --git a/modules/cli/shared/src/main/scala/dumbo/cli/Dumbo.scala b/modules/cli/shared/src/main/scala/dumbo/cli/Dumbo.scala index 579247a..bb1a431 100644 --- a/modules/cli/shared/src/main/scala/dumbo/cli/Dumbo.scala +++ b/modules/cli/shared/src/main/scala/dumbo/cli/Dumbo.scala @@ -9,9 +9,9 @@ import cats.effect.std.Console import cats.effect.{ExitCode, IO, IOApp} import dumbo.BuildInfo import dumbo.Dumbo.defaults -import org.typelevel.otel4s.trace.Tracer.Implicits.noop -import org.typelevel.otel4s.metrics.Meter.Implicits.noop import dumbo.logging.Implicits.consolePrettyWithTimestamp +import org.typelevel.otel4s.metrics.Meter.Implicits.noop +import org.typelevel.otel4s.trace.Tracer.Implicits.noop object Dumbo extends IOApp { private def printHelp(cmd: Option[Command] = None) = { @@ -61,14 +61,14 @@ object Dumbo extends IOApp { def collectConfig[T](config: Config[T]): Option[Either[String, T]] = configs.collectFirst { case (c, v) if c == config => config.parse(v) } - for { + for uri <- collectConfig(Config.Url).toRight("Missing url").flatten _ <- Option(uri.getScheme()) match case None => Left(s"Missing scheme in $uri") case Some("postgresql") => Right(()) case Some(invalid) => Left(s"Unsupported scheme $invalid") host <- Option(uri.getHost()).toRight(s"Missing or invalid hostname in $uri") - port = { val p = uri.getPort(); if (p > -1) p else defaults.port } + port = { val p = uri.getPort(); if p > -1 then p else defaults.port } database <- Option(uri.getPath()).flatMap(_.split("/").drop(1).headOption).toRight(s"Missing database in $uri") user <- collectConfig(Config.User).toRight("Missing user").flatten password <- collectConfig(Config.Password) match @@ -98,7 +98,7 @@ object Dumbo extends IOApp { password = password, ssl = ssl, ) - } yield ( + yield ( dumbo.Dumbo .withFilesIn[IO](location) .apply( diff --git a/modules/core/shared/src/main/scala/dumbo/ConnectionConfig.scala b/modules/core/shared/src/main/scala/dumbo/ConnectionConfig.scala index ad16179..14d17d5 100644 --- a/modules/core/shared/src/main/scala/dumbo/ConnectionConfig.scala +++ b/modules/core/shared/src/main/scala/dumbo/ConnectionConfig.scala @@ -13,11 +13,8 @@ final case class ConnectionConfig( ssl: ConnectionConfig.SSL = ConnectionConfig.SSL.None, ) -object ConnectionConfig { - sealed trait SSL - object SSL { - case object None extends SSL // `SSL` which indicates that SSL is not to be used - case object Trusted extends SSL // `SSL` which trusts all certificates - case object System extends SSL // `SSL` from the system default `SSLContext` - } -} +object ConnectionConfig: + enum SSL: + case None // `SSL` which indicates that SSL is not to be used + case Trusted // `SSL` which trusts all certificates + case System // `SSL` from the system default `SSLContext` diff --git a/modules/core/shared/src/main/scala/dumbo/Dumbo.scala b/modules/core/shared/src/main/scala/dumbo/Dumbo.scala index 1c8f38e..525739c 100644 --- a/modules/core/shared/src/main/scala/dumbo/Dumbo.scala +++ b/modules/core/shared/src/main/scala/dumbo/Dumbo.scala @@ -39,7 +39,7 @@ final class DumboWithResourcesPartiallyApplied[F[_]](reader: ResourceReader[F]) schemaHistoryTable: String = Dumbo.defaults.schemaHistoryTable, validateOnMigrate: Boolean = Dumbo.defaults.validateOnMigrate, cleanDisabled: Boolean = Dumbo.defaults.cleanDisabled, - )(implicit + )(using S: Sync[F], T: Temporal[F], L: Logger[F], @@ -64,7 +64,7 @@ final class DumboWithResourcesPartiallyApplied[F[_]](reader: ResourceReader[F]) schemaHistoryTable: String = Dumbo.defaults.schemaHistoryTable, validateOnMigrate: Boolean = Dumbo.defaults.validateOnMigrate, cleanDisabled: Boolean = Dumbo.defaults.cleanDisabled, - )(implicit S: Sync[F], L: Logger[F]): Dumbo[F] = + )(using S: Sync[F], L: Logger[F]): Dumbo[F] = new Dumbo[F]( resReader = reader, sessionResource = sessionResource, @@ -82,9 +82,9 @@ final class DumboWithResourcesPartiallyApplied[F[_]](reader: ResourceReader[F]) schemaHistoryTable: String = Dumbo.defaults.schemaHistoryTable, validateOnMigrate: Boolean = Dumbo.defaults.validateOnMigrate, cleanDisabled: Boolean = Dumbo.defaults.cleanDisabled, - )(implicit A: Async[F], L: Logger[F], LIO: LiftIO[F], C: Console[F], TRC: Tracer[F], MTR: Meter[F]): Dumbo[F] = { - implicit val network: Network[F] = Network.forLiftIO[F] - val sessionResource = toSessionResource(connection, defaultSchema, schemas) + )(using A: Async[F], L: Logger[F], LIO: LiftIO[F], C: Console[F], TRC: Tracer[F], MTR: Meter[F]): Dumbo[F] = { + given Network[F] = Network.forLiftIO[F] + val sessionResource = toSessionResource(connection, defaultSchema, schemas) withMigrationStateLogAfterBySession(logMigrationStateAfter)( sessionResource, @@ -103,7 +103,7 @@ final class DumboWithResourcesPartiallyApplied[F[_]](reader: ResourceReader[F]) schemaHistoryTable: String = Dumbo.defaults.schemaHistoryTable, validateOnMigrate: Boolean = Dumbo.defaults.validateOnMigrate, cleanDisabled: Boolean = Dumbo.defaults.cleanDisabled, - )(implicit A: Async[F], L: Logger[F]): Dumbo[F] = + )(using A: Async[F], L: Logger[F]): Dumbo[F] = new Dumbo[F]( resReader = reader, sessionResource = sessionResource, @@ -136,18 +136,18 @@ final class DumboWithResourcesPartiallyApplied[F[_]](reader: ResourceReader[F]) ) ) .evalMap { case pid *: start *: changed *: state *: eventType *: event *: query *: _ => - for { + for now <- Clock[F].realTimeInstant startedAgo = now.getEpochSecond() - start.toEpochSecond() changedAgo = now.getEpochSecond() - changed.toEpochSecond() queryLogSize = 150 - queryLog = query.take(queryLogSize) + (if (query.size > queryLogSize) "..." else "") + queryLog = query.take(queryLogSize) + (if query.size > queryLogSize then "..." else "") _ <- L.logInfo( s"Awaiting query with pid: $pid started: ${startedAgo}s ago (state: $state / last changed: ${changedAgo}s ago, " + s"eventType: ${eventType.getOrElse("")}, event: ${event.getOrElse("")}):\n${queryLog}" ) - } yield () + yield () } .repeat .metered(logMigrationStateAfter) @@ -157,14 +157,14 @@ final class DumboWithResourcesPartiallyApplied[F[_]](reader: ResourceReader[F]) }, ) - def listMigrationFiles(implicit S: Sync[F]): F[ValidatedNec[DumboValidationException, List[ResourceFile]]] = + def listMigrationFiles(using S: Sync[F]): F[ValidatedNec[DumboValidationException, List[ResourceFile]]] = Dumbo.listMigrationFiles(reader) private def toSessionResource( connection: ConnectionConfig, defaultSchema: String, schemas: Set[String], - )(implicit T: Temporal[F], C: Console[F], TRC: Tracer[F], MTR: Meter[F], N: Network[F]) = { + )(using T: Temporal[F], C: Console[F], TRC: Tracer[F], MTR: Meter[F], N: Network[F]) = { val searchPath = Dumbo.toSearchPath(defaultSchema, schemas) val params = Session.DefaultConnectionParameters ++ Map("search_path" -> searchPath) @@ -185,7 +185,7 @@ final class DumboWithResourcesPartiallyApplied[F[_]](reader: ResourceReader[F]) } } -class Dumbo[F[_]: Sync: Logger]( +class Dumbo[F[_]: {Sync, Logger}]( private[dumbo] val resReader: ResourceReader[F], sessionResource: Resource[F, Session[F]], defaultSchema: String, @@ -209,11 +209,11 @@ class Dumbo[F[_]: Sync: Logger]( case _ => s"with repeatable migration \"${source.scriptDescription}\"" } - for { + for _ <- Logger[F].logInfo( s"""Migrating schema "$defaultSchema" $toVersion ${ - if (!source.executeInTransaction) " [non-transactional]" else "" + if !source.executeInTransaction then " [non-transactional]" else "" }""" ) @@ -221,7 +221,7 @@ class Dumbo[F[_]: Sync: Logger]( // for non transactional operations we need to split the content into single statements // When a simple Query message contains more than one SQL statement (separated by semicolons), those statements are executed as a single transaction. // https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-MULTI-STATEMENT - if (source.executeInTransaction) Vector(sql) else Statements.intoSingleStatements(sql) + if source.executeInTransaction then Vector(sql) else Statements.intoSingleStatements(sql) } (duration, _) <- Clock[F].timed { @@ -238,7 +238,7 @@ class Dumbo[F[_]: Sync: Logger]( .traverse_(session.executeDiscard(_)) } _ <- Logger[F].logInfo(s"Migration $toVersion completed in ${duration.toMillis}ms") - } yield HistoryEntry.New( + yield HistoryEntry.New( version = source.versionText, description = source.scriptDescription, `type` = "SQL", @@ -250,7 +250,7 @@ class Dumbo[F[_]: Sync: Logger]( } private def validationGuard(session: Session[F], resources: ResourceFiles) = - if (resources.nonEmpty) { + if resources.nonEmpty then { session .execute(dumboHistory.loadAllQuery) .map(history => validate(history, resources)) @@ -272,15 +272,15 @@ class Dumbo[F[_]: Sync: Logger]( resources match { case ResourceFiles(Nil, Nil) => none.pure[F] case ResourceFiles(versioned, repeatables) => - ((for { + ((for txn <- session.transaction _ <- progressMonitor - } yield txn).use { _ => - for { + yield txn).use { _ => + for _ <- - if (lockSupport.contains(LockSupport.TableLock)) + if lockSupport.contains(LockSupport.TableLock) then lockTable(session, defaultSchema, schemaHistoryTable).void - else if (lockSupport.contains(LockSupport.XactAdvisoryLock)) + else if lockSupport.contains(LockSupport.XactAdvisoryLock) then session.executeDiscard( sql"SELECT pg_advisory_xact_lock('#${advisoryLockKey(defaultSchema).toString}')".command ) @@ -297,7 +297,7 @@ class Dumbo[F[_]: Sync: Logger]( case _ => none.pure[F] } } - } yield res + yield res }).recoverWith { // https://www.cockroachlabs.com/docs/v25.2/transaction-retry-error-reference.html case SqlState.SerializationFailure(ex) if ex.message.toLowerCase.contains("restart transaction") => @@ -310,24 +310,23 @@ class Dumbo[F[_]: Sync: Logger]( versioned: List[ResourceFileVersioned], session: Session[F], fs: ResourceReader[F], - ): F[Option[(HistoryEntry, List[ResourceFileVersioned])]] = if (versioned.isEmpty) - none.pure[F] + ): F[Option[(HistoryEntry, List[ResourceFileVersioned])]] = if versioned.isEmpty then none.pure[F] else - for { + for latestInstalled <- session.option(dumboHistory.latestVersionedInstalled) latestInstalledV = latestInstalled.map(l => (l.resourceVersion, l.success)) result <- versioned.dropWhile { case (v, _) => latestInstalledV match { // drop versions applied successfully // retry the version which was not applied successfully - case Some((Some(lv), success)) => if (success) v <= lv else v < lv + case Some((Some(lv), success)) => if success then v <= lv else v < lv case _ => false } } match { case (_, x) :: xs => // acquire a new session for non-transactional operation val transactSession: Resource[F, Session[F]] = - if (x.executeInTransaction) Resource.pure(session) else sessionResource + if x.executeInTransaction then Resource.pure(session) else sessionResource transactSession.use { s => transact(x, fs, s) @@ -336,15 +335,15 @@ class Dumbo[F[_]: Sync: Logger]( } case _ => none.pure[F] } - } yield result + yield result private def processRepeatables( repeatables: List[ResourceFileRepeatable], session: Session[F], fs: ResourceReader[F], - ): F[Option[(HistoryEntry, List[ResourceFileRepeatable])]] = if (repeatables.isEmpty) none.pure[F] + ): F[Option[(HistoryEntry, List[ResourceFileRepeatable])]] = if repeatables.isEmpty then none.pure[F] else - for { + for latestRepeatables <- session.execute(dumboHistory.latestRepeatablesInstalled).map(_.toMap) res <- repeatables.filter { case (_, f) => latestRepeatables.get(f.scriptDescription) match { @@ -355,14 +354,14 @@ class Dumbo[F[_]: Sync: Logger]( case (_, x) :: xs => // acquire a new session for non-transactional operation val transactSession: Resource[F, Session[F]] = - if (x.executeInTransaction) Resource.pure(session) else sessionResource + if x.executeInTransaction then Resource.pure(session) else sessionResource transactSession.use { s => transact(x, fs, s).flatMap(updateHistory(None, s)).map((_, xs).some) } case Nil => none.pure[F] } - } yield res + yield res private def updateHistory(latestInstalled: Option[HistoryEntry], session: Session[F])(newEntry: HistoryEntry.New) = latestInstalled match { @@ -398,7 +397,7 @@ class Dumbo[F[_]: Sync: Logger]( case Nil => // validate the order val defaultIdx = spSchemas.indexOf(defaultSchema) - if (schemas.forall(s => spSchemas.indexOf(s) > defaultIdx)) { + if schemas.forall(s => spSchemas.indexOf(s) > defaultIdx) then { none[String].pure[F] } else { Logger[F] @@ -420,7 +419,7 @@ class Dumbo[F[_]: Sync: Logger]( // acquire a session and initialize the schemas and history table private[dumbo] def initSession = sessionResource.evalMap { session => - for { + for dbVersion <- session.unique(sql"SELECT version()".query(text)) _ <- Logger[F].logInfo(s"Starting migration on $dbVersion") lockSupport <- detectLockSupport(session) @@ -434,10 +433,10 @@ class Dumbo[F[_]: Sync: Logger]( } _ <- session.transaction.use { _ => - for { + for _ <- ( - if (lockSupport.contains(LockSupport.XactAdvisoryLock)) + if lockSupport.contains(LockSupport.XactAdvisoryLock) then session .executeDiscard( sql"SELECT pg_advisory_xact_lock('#${advisoryLockKey(defaultSchema).toString}')".command @@ -462,41 +461,43 @@ class Dumbo[F[_]: Sync: Logger]( _ <- session.execute(dumboHistory.createTableCommand).void.recover { case e: skunk.exception.PostgresErrorException if duplicateErrorCodes.contains(e.code) => () } - _ <- (if (schemaRes.nonEmpty) - session.execute(dumboHistory.insertSchemaEntry)(schemaRes.mkString("\"", "\",\"", "\"")).void - else ().pure[F]) - } yield () + _ <- + (if schemaRes.nonEmpty then + session.execute(dumboHistory.insertSchemaEntry)(schemaRes.mkString("\"", "\",\"", "\"")).void + else ().pure[F]) + yield () } - } yield (session, lockSupport) + yield (session, lockSupport) } private def migrateByInitializedSession( session: Session[F], lockSupport: Set[LockSupport], ): F[Dumbo.MigrationResult] = - for { - migrationResult <- for { - resources <- listMigrationFiles(resReader).flatMap { - case Valid(f) => f.pure[F] - case Invalid(errs) => - new DumboValidationException( - s"Error while reading migration files:\n${errs.toList.mkString("\n")}" - ).raiseError[F, List[ResourceFile]] - }.map(ResourceFiles.fromResources) - _ <- { - val inLocation = resReader.location.map(l => s" in $l").getOrElse("") - Logger[F].logInfo(s"Found ${resources.length} migration files$inLocation") - } - _ <- if (validateOnMigrate) validationGuard(session, resources) else ().pure[F] - migrationResult <- - Stream - .unfoldEval(resources)(migrateToNext(session, resReader, lockSupport)) - .compile - .toList - .map(Dumbo.MigrationResult(_)) - } yield migrationResult - - _ <- migrationResult.migrations.sorted(Ordering[HistoryEntry].reverse) match { + for + migrationResult <- + for + resources <- listMigrationFiles(resReader).flatMap { + case Valid(f) => f.pure[F] + case Invalid(errs) => + new DumboValidationException( + s"Error while reading migration files:\n${errs.toList.mkString("\n")}" + ).raiseError[F, List[ResourceFile]] + }.map(ResourceFiles.fromResources) + _ <- { + val inLocation = resReader.location.map(l => s" in $l").getOrElse("") + Logger[F].logInfo(s"Found ${resources.length} migration files$inLocation") + } + _ <- if validateOnMigrate then validationGuard(session, resources) else ().pure[F] + migrationResult <- + Stream + .unfoldEval(resources)(migrateToNext(session, resReader, lockSupport)) + .compile + .toList + .map(Dumbo.MigrationResult(_)) + yield migrationResult + + _ <- migrationResult.migrations.sorted(using Ordering[HistoryEntry].reverse) match { case Nil => Logger[F].logInfo(s"Schema ${defaultSchema} is up to date. No migration necessary") case history => val verLog = history.collectFirst { case HistoryEntry(_, Some(v), _, _, _, _, _, _, _, _) => v } @@ -511,7 +512,7 @@ class Dumbo[F[_]: Sync: Logger]( s"Successfully applied ${migrationResult.migrations.length} migrations$verLog $execDurationLog" ) } - } yield migrationResult + yield migrationResult private def validate( history: List[HistoryEntry], @@ -552,11 +553,11 @@ class Dumbo[F[_]: Sync: Logger]( } def runClean: F[Unit] = - if (cleanDisabled) + if cleanDisabled then Sync[F].raiseError(new DumboCleanException("Clean has been disabled. Set cleanDisabled to false to enable it.")) else sessionResource.use(cleanBySession) - private def cleanBySession(session: Session[F]): F[Unit] = for { + private def cleanBySession(session: Session[F]): F[Unit] = for _ <- Logger[F].logInfo(s"Cleaning schemas ${allSchemas.mkString(", ")}") // determine which schemas were created by Dumbo (recorded in schema history) // if history table doesn't exist, assume no schemas were created by Dumbo @@ -574,14 +575,15 @@ class Dumbo[F[_]: Sync: Logger]( } results <- allSchemas.traverse { schema => val createdByDumbo = schemasCreatedByDumbo.contains(schema) - for { + for _ <- Logger[F].logInfo( - s"""Cleaning schema "$schema"${if (createdByDumbo) " (will be dropped)" else ""}""" + s"""Cleaning schema "$schema"${if createdByDumbo then " (will be dropped)" else ""}""" ) - _ <- if (createdByDumbo) - session.execute(sql"DROP SCHEMA IF EXISTS #${quoteIdentifier(schema)} CASCADE".command).void - else cleanSchema(session, schema) - } yield (schema, createdByDumbo) + _ <- + if createdByDumbo then + session.execute(sql"DROP SCHEMA IF EXISTS #${quoteIdentifier(schema)} CASCADE".command).void + else cleanSchema(session, schema) + yield (schema, createdByDumbo) } _ <- { val dropped = results.collect { case (s, true) => s } @@ -592,7 +594,7 @@ class Dumbo[F[_]: Sync: Logger]( ).flatten Logger[F].logInfo(s"Successfully ${parts.mkString("; ")}") } - } yield () + yield () // Drops all objects in the given schema following Flyway's doClean order: // materialized views, views, tables, base types (with recreate), routines, @@ -603,7 +605,7 @@ class Dumbo[F[_]: Sync: Logger]( def dropAll(objType: String, names: List[String], cascade: Boolean = true): F[Unit] = names.traverse_ { name => - val cascadeSql = if (cascade) " CASCADE" else "" + val cascadeSql = if cascade then " CASCADE" else "" session .execute( sql"DROP #${objType} IF EXISTS #${quoteIdentifier(schema)}.#${quoteIdentifier(name)}#${cascadeSql}".command @@ -614,7 +616,7 @@ class Dumbo[F[_]: Sync: Logger]( def queryBaseTypes: F[List[(String, String)]] = session.execute(CatalogQueries.listBaseTypesQuery)(schema) - for { + for // 1. Materialized views matViews <- queryNames(CatalogQueries.listMaterializedViewsQuery) _ <- dropAll("MATERIALIZED VIEW", matViews) @@ -630,7 +632,7 @@ class Dumbo[F[_]: Sync: Logger]( // Recreate empty shell types only for Pseudo (P) and User-defined (U) categories, // matching Flyway's behavior to allow routine drops that reference these types _ <- types.traverse_ { case (typName, typCategory) => - if (typCategory == "P" || typCategory == "U") + if typCategory == "P" || typCategory == "U" then session .execute(sql"CREATE TYPE #${quoteIdentifier(schema)}.#${quoteIdentifier(typName)}".command) .void @@ -650,14 +652,14 @@ class Dumbo[F[_]: Sync: Logger]( // 9. Base types (final cleanup, no recreate) types2 <- queryBaseTypes _ <- dropAll("TYPE", types2.map(_._1)) - } yield () + yield () } private def cleanRoutines(session: Session[F], schema: String): F[Unit] = session.execute(CatalogQueries.listRoutinesQuery)(schema).flatMap { routines => routines.traverse_ { case (kind, signature) => val parenIdx = signature.indexOf('(') - val (funcName, argspart) = if (parenIdx >= 0) signature.splitAt(parenIdx) else (signature, "") + val (funcName, argspart) = if parenIdx >= 0 then signature.splitAt(parenIdx) else (signature, "") session .execute( sql"DROP #${kind} IF EXISTS #${quoteIdentifier(schema)}.#${quoteIdentifier(funcName)}#${argspart} CASCADE".command @@ -705,10 +707,8 @@ object Dumbo extends internal.DumboPlatform { String.format("%02d:%02d.%03d", pos / 60000, (pos / 1000) % 60, (pos % 1000)) + "s" } - private[dumbo] sealed trait LockSupport - private[dumbo] object LockSupport { - case object TableLock extends LockSupport - case object XactAdvisoryLock extends LockSupport + private[dumbo] enum LockSupport { + case TableLock, XactAdvisoryLock } private[dumbo] def detectLockSupport[F[_]: Sync](session: Session[F]): F[Set[LockSupport]] = @@ -785,9 +785,8 @@ object Dumbo extends internal.DumboPlatform { case Right(configs) => ResourceFileDescription.fromResourcePath(path) match { case Right(desc) => - for { - checksum <- checksum[F](path, fs) - } yield ResourceFile( + for checksum <- checksum[F](path, fs) + yield ResourceFile( description = desc, checksum = checksum, configs = configs, @@ -803,10 +802,10 @@ object Dumbo extends internal.DumboPlatform { // implementation of checksum from Flyway // https://github.com/flyway/flyway/blob/main/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ChecksumCalculator.java#L59 private[dumbo] def checksum[F[_]: Sync](p: ResourceFilePath, fs: ResourceReader[F]): F[Int] = - for { + for crc32 <- (new CRC32()).pure[F] _ <- fs.readUtf8Lines(p).map(_.foreach(line => crc32.update(line.getBytes(StandardCharsets.UTF_8)))) - } yield crc32.getValue().toInt + yield crc32.getValue().toInt // PostgreSQL identifier quoting: wraps in double quotes, escapes embedded quotes private[dumbo] def quoteIdentifier(id: String): String = diff --git a/modules/core/shared/src/main/scala/dumbo/ResourceFile.scala b/modules/core/shared/src/main/scala/dumbo/ResourceFile.scala index d1d3aec..13b8952 100644 --- a/modules/core/shared/src/main/scala/dumbo/ResourceFile.scala +++ b/modules/core/shared/src/main/scala/dumbo/ResourceFile.scala @@ -53,7 +53,9 @@ final case class ResourceFile( configs.collectFirst { case ResourceFileConfig.ExecuteInTransaction(v) => v }.getOrElse(true) } -sealed abstract class ResourceFileConfig(protected val key: String) { +enum ResourceFileConfig(protected val key: String) { + case ExecuteInTransaction(value: Boolean) extends ResourceFileConfig(ResourceFileConfig.txn) + override def hashCode(): Int = key.hashCode() override def equals(b: Any): Boolean = b.asInstanceOf[Matchable] match { @@ -63,8 +65,6 @@ sealed abstract class ResourceFileConfig(protected val key: String) { } object ResourceFileConfig { - final case class ExecuteInTransaction(value: Boolean) extends ResourceFileConfig(txn) - private val txn = "executeInTransaction" private def invalidBoolean(key: String, v: String) = @@ -137,9 +137,7 @@ object ResourceFileDescription { } } -sealed trait ResourceVersion extends Ordered[ResourceVersion] { - import ResourceVersion.* - +enum ResourceVersion extends Ordered[ResourceVersion] { def compare(that: ResourceVersion): Int = { @tailrec def cmprVersioned(a: List[Long], b: List[Long]): Int = @@ -164,26 +162,37 @@ sealed trait ResourceVersion extends Ordered[ResourceVersion] { case Repeatable(_) => None case Versioned(plain, _) => Some(plain) } -} -object ResourceVersion { - case class Repeatable(description: String) extends ResourceVersion - final case class Versioned(text: String, parts: NonEmptyList[Long]) extends ResourceVersion { - // strip trailing 0 - // 1.0 -> 1 - // 0.01.0.0 -> 0.1 - override def toString: String = + // strip trailing 0 + // 1.0 -> 1 + // 0.01.0.0 -> 0.1 + override def toString: String = this match { + case Repeatable(description) => s"Repeatable($description)" + case Versioned(_, parts) => parts.reverse.toList.dropWhile(_ <= 0).map(_.toString).reverse.mkString(".") + } - // 1.0 should yield same hash code as 1 or 1.0.0 etc. - override def hashCode: Int = parts.reverse.foldLeft("")(_ + _.toString).toInt + // 1.0 should yield same hash code as 1 or 1.0.0 etc. + override def hashCode: Int = this match { + case Repeatable(description) => description.hashCode + case Versioned(_, parts) => parts.reverse.foldLeft("")(_ + _.toString).toInt + } - override def equals(b: Any): Boolean = b.asInstanceOf[Matchable] match { - case s: Versioned => this.compare(s) == 0 - case _ => false - } + override def equals(b: Any): Boolean = b.asInstanceOf[Matchable] match { + case that: ResourceVersion => + (this, that) match { + case (Repeatable(descThis), Repeatable(descThat)) => descThis == descThat + case (thisV: Versioned, thatV: Versioned) => thisV.compare(thatV) == 0 + case _ => false + } + case _ => false } + case Repeatable(description: String) + case Versioned(text: String, parts: NonEmptyList[Long]) +} + +object ResourceVersion { object Versioned { def fromString(version: String): Either[String, Versioned] = Try(version.split('.').map(_.toLong)) match { diff --git a/modules/core/shared/src/main/scala/dumbo/internal/ResourcesReader.scala b/modules/core/shared/src/main/scala/dumbo/internal/ResourcesReader.scala index a7a77bc..f564456 100644 --- a/modules/core/shared/src/main/scala/dumbo/internal/ResourcesReader.scala +++ b/modules/core/shared/src/main/scala/dumbo/internal/ResourcesReader.scala @@ -33,7 +33,7 @@ private[dumbo] object ResourceReader { def fileFs[F[_]: Sync](sourceDir: Path): ResourceReader[F] = { val base = Path.of(new java.io.File("").toURI()) - @inline def absolutePath(p: Path) = if (p.isAbsolute) p else Path.of(base.toString(), p.toString()) + @inline def absolutePath(p: Path) = if p.isAbsolute then p else Path.of(base.toString(), p.toString()) new ResourceReader[F] { diff --git a/modules/core/shared/src/main/scala/dumbo/logging/Logger.scala b/modules/core/shared/src/main/scala/dumbo/logging/Logger.scala index 54228a0..7b1d165 100644 --- a/modules/core/shared/src/main/scala/dumbo/logging/Logger.scala +++ b/modules/core/shared/src/main/scala/dumbo/logging/Logger.scala @@ -21,7 +21,7 @@ trait Logger[F[_]] { } object Logger { - def apply[F[_]](implicit L: Logger[F]): Logger[F] = L + def apply[F[_]](using L: Logger[F]): Logger[F] = L def noop[F[_]: Applicative] = new Logger[F] { def apply(level: LogLevel, message: => String) = Applicative[F].unit @@ -34,7 +34,7 @@ object Logger { level: LogLevel, pretty: Boolean, ): F[Unit] = { - val formattedMsg = if (pretty) { + val formattedMsg = if pretty then { val lc = level match { case Info => Console.CYAN case Warn => Console.YELLOW @@ -80,7 +80,7 @@ object Logger { ) } - def fromConsoleWithTimestamp[F[_]: Clock: FlatMap]( + def fromConsoleWithTimestamp[F[_]: {Clock, FlatMap}]( console: CatsConsole[F], pretty: Boolean = true, ) = new Logger[F] { @@ -100,12 +100,12 @@ object Logger { } } -sealed trait LogLevel -object LogLevel { - case object Info extends LogLevel - case object Warn extends LogLevel +enum LogLevel { + case Info, Warn +} - implicit val show: Show[LogLevel] = new Show[LogLevel] { +object LogLevel { + given show: Show[LogLevel] = new Show[LogLevel] { override def show(t: LogLevel): String = t match { case Info => "info" case Warn => "warn" @@ -114,17 +114,17 @@ object LogLevel { } object Implicits { - implicit def console[F[_]: CatsConsole]: Logger[F] = + given console: [F[_]: CatsConsole] => Logger[F] = Logger.fromConsole(console = CatsConsole[F], pretty = false) - implicit def consolePretty[F[_]: CatsConsole]: Logger[F] = + given consolePretty: [F[_]: CatsConsole] => Logger[F] = Logger.fromConsole(console = CatsConsole[F], pretty = true) - implicit def consoleWithTimestamp[F[_]: CatsConsole: Clock: FlatMap]: Logger[F] = + given consoleWithTimestamp: [F[_]: {CatsConsole, Clock, FlatMap}] => Logger[F] = Logger.fromConsoleWithTimestamp(console = CatsConsole[F], pretty = false) - implicit def consolePrettyWithTimestamp[F[_]: CatsConsole: Clock: FlatMap]: Logger[F] = + given consolePrettyWithTimestamp: [F[_]: {CatsConsole, Clock, FlatMap}] => Logger[F] = Logger.fromConsoleWithTimestamp(console = CatsConsole[F], pretty = true) - implicit def noop[F[_]: Applicative]: Logger[F] = Logger.noop[F] + given noop: [F[_]: Applicative] => Logger[F] = Logger.noop[F] } diff --git a/modules/example/src/main/scala/ExampleApp.scala b/modules/example/src/main/scala/ExampleApp.scala index f899c34..47532c8 100644 --- a/modules/example/src/main/scala/ExampleApp.scala +++ b/modules/example/src/main/scala/ExampleApp.scala @@ -1,8 +1,8 @@ import cats.effect.{IO, IOApp} import dumbo.logging.Implicits.console import dumbo.{ConnectionConfig, Dumbo} -import org.typelevel.otel4s.trace.Tracer.Implicits.noop import org.typelevel.otel4s.metrics.Meter.Implicits.noop +import org.typelevel.otel4s.trace.Tracer.Implicits.noop object ExampleApp extends IOApp.Simple: def run = Dumbo diff --git a/modules/example/src/main/scala/ExampleClean.scala b/modules/example/src/main/scala/ExampleClean.scala index 4c2ec3a..df1a91e 100644 --- a/modules/example/src/main/scala/ExampleClean.scala +++ b/modules/example/src/main/scala/ExampleClean.scala @@ -1,8 +1,8 @@ import cats.effect.{IO, IOApp} import dumbo.logging.Implicits.console import dumbo.{ConnectionConfig, Dumbo} -import org.typelevel.otel4s.trace.Tracer.Implicits.noop import org.typelevel.otel4s.metrics.Meter.Implicits.noop +import org.typelevel.otel4s.trace.Tracer.Implicits.noop object ExampleClean extends IOApp.Simple: def run = Dumbo diff --git a/modules/example/src/main/scala/ExampleCustomSession.scala b/modules/example/src/main/scala/ExampleCustomSession.scala index 9205cb2..5e6a627 100644 --- a/modules/example/src/main/scala/ExampleCustomSession.scala +++ b/modules/example/src/main/scala/ExampleCustomSession.scala @@ -1,8 +1,8 @@ import cats.effect.{IO, IOApp} -import dumbo.logging.Implicits.console import dumbo.Dumbo -import org.typelevel.otel4s.trace.Tracer.Implicits.noop +import dumbo.logging.Implicits.console import org.typelevel.otel4s.metrics.Meter.Implicits.noop +import org.typelevel.otel4s.trace.Tracer.Implicits.noop import skunk.Session import skunk.Session.Credentials diff --git a/modules/example/src/main/scala/ExampleLog4Cats.scala b/modules/example/src/main/scala/ExampleLog4Cats.scala index bde5c84..59a7b26 100644 --- a/modules/example/src/main/scala/ExampleLog4Cats.scala +++ b/modules/example/src/main/scala/ExampleLog4Cats.scala @@ -1,8 +1,8 @@ import cats.effect.{IO, IOApp} import dumbo.{ConnectionConfig, Dumbo} import org.typelevel.log4cats.slf4j.Slf4jLogger -import org.typelevel.otel4s.trace.Tracer.Implicits.noop import org.typelevel.otel4s.metrics.Meter.Implicits.noop +import org.typelevel.otel4s.trace.Tracer.Implicits.noop object ExampleLog4Cats extends IOApp.Simple: def run = diff --git a/modules/tests-flyway/src/test/scala/DumboFlywaySpec.scala b/modules/tests-flyway/src/test/scala/DumboFlywaySpec.scala index 9c1af55..3b62fce 100644 --- a/modules/tests-flyway/src/test/scala/DumboFlywaySpec.scala +++ b/modules/tests-flyway/src/test/scala/DumboFlywaySpec.scala @@ -52,20 +52,20 @@ trait DumboFlywaySpec extends ffstest.FTest { dbTest("Same behaviour on changed checksum") { val schema = "schema_1" - for { + for res <- flywayMigrate(schema, Path("db/test_0")) _ = assert(res.migrationsExecuted == 2) flywayRes <- flywayMigrate(schema, Path("db/test_0_changed_checksum")).attempt _ = assert(flywayRes.left.exists(_.getMessage().contains("checksum mismatch"))) dumboRes <- dumboMigrate(schema, dumboWithResources("db/test_0_changed_checksum")).attempt _ = assert(dumboRes.left.exists(_.getMessage().contains("checksum mismatch"))) - } yield () + yield () } dbTest("Same behaviour on missing file") { val schema = "schema_1" - for { + for res <- flywayMigrate(schema, Path("db/test_0")) _ = assert(res.migrationsExecuted == 2) flywayRes <- flywayMigrate(schema, Path("db/test_0_missing_file")).attempt @@ -73,17 +73,17 @@ trait DumboFlywaySpec extends ffstest.FTest { dumboRes <- dumboMigrate(schema, dumboWithResources("db/test_0_missing_file")).attempt _ = assert(dumboRes.left.exists(_.isInstanceOf[dumbo.exception.DumboValidationException])) _ = assert(dumboRes.left.exists(_.getMessage().contains("Detected applied migration not resolved locally"))) - } yield () + yield () } dbTest("Same behaviour on failing migration") { val schema = "schema_1" - for { + for flywayRes <- flywayMigrate(schema, Path("db/test_failing_sql")).attempt _ = assert(flywayRes.isLeft) // Flyway does not provide more specific error message with CockroachDB in this case - _ = if (Set[Db](Db.Postgres(16), Db.Postgres(11)).contains(db)) { + _ = if Set[Db](Db.Postgres(16), Db.Postgres(11)).contains(db) then { assert(flywayRes.left.exists(_.getMessage().contains("relation \"test\" already exists"))) } historyFlyway <- loadHistory(schema).map(h => @@ -104,7 +104,7 @@ trait DumboFlywaySpec extends ffstest.FTest { ) historyDumbo <- loadHistory(schema) _ = assertEqualHistory(historyFlyway, historyDumbo) - } yield () + yield () } dbTest("Dumbo is compatible with Flyway history state") { @@ -113,7 +113,7 @@ trait DumboFlywaySpec extends ffstest.FTest { val withResourcesB = dumboWithResources("db/test_1_extended") val defaultSchema = "test_a" - for { + for flywayRes <- flywayMigrate(defaultSchema, path) _ = assert(flywayRes.success) _ = assertEquals(flywayRes.migrationsExecuted, 4) @@ -124,7 +124,7 @@ trait DumboFlywaySpec extends ffstest.FTest { _ = assertEquals(histA, histB) // history unchanged _ <- assertIO(dumboMigrate(defaultSchema, withResourcesB).map(_.migrationsExecuted), 1) _ <- assertIO(loadHistory(defaultSchema).map(_.length), histB.length + 1) // history extended - } yield () + yield () } dbTest("Flyway is compatible with Dumbo history state") { @@ -133,7 +133,7 @@ trait DumboFlywaySpec extends ffstest.FTest { val withResources = dumboWithResources("db/test_1") val defaultSchema = "test_a" - for { + for resDumbo <- dumboMigrate(defaultSchema, withResources) _ = assertEquals(resDumbo.migrationsExecuted, 4) histA <- loadHistory(defaultSchema) @@ -144,26 +144,26 @@ trait DumboFlywaySpec extends ffstest.FTest { _ = assertEquals(histA, histB) // history unchanged _ <- assertIO(flywayMigrate(defaultSchema, pathB).map(_.migrationsExecuted), 1) _ <- assertIO(loadHistory(defaultSchema).map(_.length), histB.length + 1) // history extended - } yield () + yield () } dbTest("Compatible with nested directories on reading from resources") { val schema = "schema_1" - for { + for _ <- flywayMigrate(schema, Path("db/nested")).map(r => assert(r.migrationsExecuted == 6)) historyFlyway <- loadHistory(schema) _ <- dropSchemas _ <- dumboMigrate(schema, dumboWithResources("db/nested")).map(r => assert(r.migrationsExecuted == 6)) historyDumbo <- loadHistory(schema) _ = assertEqualHistory(historyDumbo, historyFlyway) - } yield () + yield () } dbTest("Compatible with nested directories on reading from filesystem") { val schema = "schema_1" - for { + for _ <- flywayMigrate(schema, Path("db/nested")).map(r => assert(r.migrationsExecuted == 6)) historyFlyway <- loadHistory(schema) _ <- dropSchemas @@ -172,15 +172,15 @@ trait DumboFlywaySpec extends ffstest.FTest { ) historyDumbo <- loadHistory(schema) _ = assertEqualHistory(historyDumbo, historyFlyway) - } yield () + yield () } dbTest("Dumbo updates history entry of latest unsucessfully applied migration by Flyway") { // run on CockroachDb only just because it was the easiest way to reproduce a history record for an unsuccessfully applied migration with Flyway - if (db == Db.CockroachDb) { + if db == Db.CockroachDb then { val schema = "schema_1" - for { + for _ <- flywayMigrate(schema, Path("db/test_failing_sql")).attempt historyA <- loadHistory(schema) _ = assertEquals(historyA.last.success, false) @@ -189,7 +189,7 @@ trait DumboFlywaySpec extends ffstest.FTest { historyB <- loadHistory(schema) _ = assertEquals(historyB.length, 3) _ = assertEquals(historyB.last.success, true) // last entry was updated - } yield () + yield () } else IO.println(s"${AnsiColor.YELLOW}Skipped${AnsiColor.RESET}") } @@ -197,7 +197,7 @@ trait DumboFlywaySpec extends ffstest.FTest { val sD = "schema_dumbo" val sF = "schema_flyway" - for { + for _ <- assertIO(dumboMigrate(sD, dumboWithResources("db/test_repeatable")).map(_.migrations.length), 3) _ <- assertIO(flywayMigrate(sF, Path("db/test_repeatable")).map(_.migrationsExecuted), 3) _ <- loadHistory(sD).product(loadHistory(sF)).map(t => assertEqualSQLHistory(t._1, t._2)) @@ -211,7 +211,7 @@ trait DumboFlywaySpec extends ffstest.FTest { _ <- assertIO(dumboMigrate(sD, dumboWithResources("db/test_repeatable_modified")).map(_.migrations.length), 2) _ <- assertIO(flywayMigrate(sF, Path("db/test_repeatable_modified")).map(_.migrationsExecuted), 2) _ <- loadHistory(sD).product(loadHistory(sF)).map(t => assertEqualSQLHistory(t._1, t._2)) - } yield () + yield () } dbTest("Updates for different default schemas from Flyway to Dumbo") { @@ -220,7 +220,7 @@ trait DumboFlywaySpec extends ffstest.FTest { val schemaA = "test_a" val schemaB = "test_b" - for { + for resFlywayA <- flywayMigrate(schemaA, path) resFlywayB <- flywayMigrate(schemaB, path) _ = assertEquals(resFlywayA.migrationsExecuted, 4) @@ -229,7 +229,7 @@ trait DumboFlywaySpec extends ffstest.FTest { resDumboB <- dumboMigrate(schemaB, withResources) _ = assertEquals(resDumboA.migrationsExecuted, 0) _ = assertEquals(resDumboB.migrationsExecuted, 0) - } yield () + yield () } dbTest("Updates for different default schemas from Dumbo to Flyway") { @@ -238,7 +238,7 @@ trait DumboFlywaySpec extends ffstest.FTest { val schemaA = "test_a" val schemaB = "test_b" - for { + for resDumboA <- dumboMigrate(schemaA, withResources) resDumboB <- dumboMigrate(schemaB, withResources) _ = assertEquals(resDumboA.migrationsExecuted, 4) @@ -248,7 +248,7 @@ trait DumboFlywaySpec extends ffstest.FTest { resFlywayB <- flywayMigrate(schemaB, path) _ = assertEquals(resFlywayA.migrationsExecuted, 0) _ = assertEquals(resFlywayB.migrationsExecuted, 0) - } yield () + yield () } dbTest("Updates for multiple schemas with missing schema config") { @@ -256,7 +256,7 @@ trait DumboFlywaySpec extends ffstest.FTest { val withResources = dumboWithResources("db/test_three_schemas") val schemas = NonEmptyList.of("schema_1", "schema_2") - for { + for flywayRes <- flywayMigrate(schemas.head, path, schemas.tail).attempt _ = assert(flywayRes.isLeft) flywayHistory <- loadHistory(schemas.head).map(h => @@ -272,7 +272,7 @@ trait DumboFlywaySpec extends ffstest.FTest { _ = assert(dumboRes.isLeft) dumboHistory <- loadHistory(schemas.head) _ = assertEqualHistory(flywayHistory, dumboHistory) - } yield () + yield () } dbTest("Updates for multiple schemas") { @@ -280,7 +280,7 @@ trait DumboFlywaySpec extends ffstest.FTest { val withResources = dumboWithResources("db/test_three_schemas") val schemas = NonEmptyList.of("schema_1", "schema_2", "schema_3") - for { + for flywayRes <- flywayMigrate(schemas.head, path, schemas.tail) _ = assert(flywayRes.migrationsExecuted == 1) flywayHistory <- loadHistory(schemas.head) @@ -289,7 +289,7 @@ trait DumboFlywaySpec extends ffstest.FTest { _ = assert(dumboRes.migrationsExecuted == 1) dumboHistory <- loadHistory(schemas.head) _ = assertEqualHistory(flywayHistory, dumboHistory) - } yield () + yield () } dbTest("Same behaviour on non-transactional operations") { @@ -298,8 +298,8 @@ trait DumboFlywaySpec extends ffstest.FTest { val schema = "schema_1" // TODO: find a way to force Flyway to run the migration in a transaction on CockroachDb - if (db == Db.Postgres(11) || db == Db.Postgres(16)) - for { + if db == Db.Postgres(11) || db == Db.Postgres(16) then + for flywayRes <- flywayMigrate(schema, path).attempt flywayHistory <- loadHistory(schema) _ <- dropSchemas @@ -321,7 +321,7 @@ trait DumboFlywaySpec extends ffstest.FTest { } } _ = assertEqualHistory(flywayHistory, dumboHistory) - } yield () + yield () else IO.println( s"${AnsiColor.YELLOW}[$db] Skipping test 'Same behaviour on non-transactional operations' as Flyway can't run the statements in a transaction${AnsiColor.RESET}" @@ -331,7 +331,7 @@ trait DumboFlywaySpec extends ffstest.FTest { // Returns sorted list of (object_type, object_name) for all user objects in a schema def schemaObjects(schema: String): IO[List[(String, String)]] = session().use { s => - for { + for tables <- s.execute( sql"""SELECT 'TABLE', c.relname::text FROM pg_catalog.pg_class c @@ -366,7 +366,7 @@ trait DumboFlywaySpec extends ffstest.FTest { JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'S' AND n.nspname = ${text}""".query(text ~ text) )(schema) - } yield (tables ++ views ++ enums ++ funcs ++ seqs).sorted + yield (tables ++ views ++ enums ++ funcs ++ seqs).sorted } def schemaExists(schema: String): IO[Boolean] = @@ -381,7 +381,7 @@ trait DumboFlywaySpec extends ffstest.FTest { val path: Path = Path("db/test_1") val withResources = dumboWithResources("db/test_1") - for { + for // Flyway: migrate then clean _ <- flywayMigrate(schema, path) _ <- flywayClean(schema, path) @@ -395,7 +395,7 @@ trait DumboFlywaySpec extends ffstest.FTest { dumboExists <- schemaExists(schema) _ = assertEquals(dumboObjs, flywayObjs) _ = assertEquals(dumboExists, flywayExists) - } yield () + yield () } dbTest("Dumbo clean allows Flyway to re-migrate") { @@ -403,7 +403,7 @@ trait DumboFlywaySpec extends ffstest.FTest { val path: Path = Path("db/test_1") val withResources = dumboWithResources("db/test_1") - for { + for // Dumbo migrate, then Dumbo clean dumboRes <- dumboMigrate(schema, withResources) _ = assertEquals(dumboRes.migrationsExecuted, 4) @@ -411,7 +411,7 @@ trait DumboFlywaySpec extends ffstest.FTest { // Flyway should be able to migrate from scratch after Dumbo clean flywayRes <- flywayMigrate(schema, path) _ = assertEquals(flywayRes.migrationsExecuted, 4) - } yield () + yield () } dbTest("Flyway clean allows Dumbo to re-migrate") { @@ -419,7 +419,7 @@ trait DumboFlywaySpec extends ffstest.FTest { val path: Path = Path("db/test_1") val withResources = dumboWithResources("db/test_1") - for { + for // Flyway migrate, then Flyway clean flywayRes <- flywayMigrate(schema, path) _ = assertEquals(flywayRes.migrationsExecuted, 4) @@ -427,7 +427,7 @@ trait DumboFlywaySpec extends ffstest.FTest { // Dumbo should be able to migrate from scratch after Flyway clean dumboRes <- dumboMigrate(schema, withResources) _ = assertEquals(dumboRes.migrationsExecuted, 4) - } yield () + yield () } dbTest("Same behavior on copy") { @@ -435,20 +435,19 @@ trait DumboFlywaySpec extends ffstest.FTest { val withResources = dumboWithResources("db/test_copy_to") val schema = "schema_1" - for { + for flywayRes <- flywayMigrate(schema, path).attempt _ = assert(flywayRes.isLeft) _ <- dropSchemas dumboRes <- dumboMigrate(schema, withResources).attempt _ = assert(dumboRes.left.exists(_.isInstanceOf[skunk.exception.CopyNotSupportedException])) - } yield () + yield () } } -sealed trait Db -object Db { - case class Postgres(version: Int) extends Db - case object CockroachDb extends Db +enum Db { + case Postgres(version: Int) + case CockroachDb } class DumboFlywaySpecPostgresLatest extends DumboFlywaySpec { diff --git a/modules/tests/jvm/src/test/scala/DumboJvmSpec.scala b/modules/tests/jvm/src/test/scala/DumboJvmSpec.scala index c066383..2c6de9f 100644 --- a/modules/tests/jvm/src/test/scala/DumboJvmSpec.scala +++ b/modules/tests/jvm/src/test/scala/DumboJvmSpec.scala @@ -10,13 +10,13 @@ import cats.implicits.* class DumboJvmSpec extends ffstest.FTest { test("find resource in main") { - for { + for result <- Dumbo.withResourcesIn[IO]("main").listMigrationFiles _ = result match { case Valid(f) => assert(f.exists(_.path.fileName.toString == "V1__dummy.sql")) case Invalid(errs) => fail(errs.toList.mkString("\n")) } - } yield () + yield () } } diff --git a/modules/tests/shared/src/test/scala/DumboMigrationSpec.scala b/modules/tests/shared/src/test/scala/DumboMigrationSpec.scala index a852537..cbe1fb6 100644 --- a/modules/tests/shared/src/test/scala/DumboMigrationSpec.scala +++ b/modules/tests/shared/src/test/scala/DumboMigrationSpec.scala @@ -33,10 +33,10 @@ trait DumboMigrationSpec extends ffstest.FTest { val schema = someSchemaName val withResources = dumboWithResources("db/test_1") - for { + for lockSupport <- session().use(Dumbo.detectLockSupport(_)) // in case of missing XactAdvisoryLock support like under CockroachDB run initSession to initialize the schemas and history table beforehand - _ <- if (!lockSupport.contains(LockSupport.XactAdvisoryLock)) { + _ <- if !lockSupport.contains(LockSupport.XactAdvisoryLock) then { withResources .apply( connection = connectionConfig, @@ -51,14 +51,14 @@ trait DumboMigrationSpec extends ffstest.FTest { _ = assertEquals(ranks, List(1, 2, 3, 4)) history <- loadHistory(schema) _ = assert(history.length == 5) - } yield () + yield () } } dbTest("Validate checksum with validation enabled") { val schema = someSchemaName - for { + for _ <- dumboMigrate(schema, dumboWithResources("db/test_0")) res <- dumboMigrate(schema, dumboWithResources("db/test_0_changed_checksum"), validateOnMigrate = true).attempt _ = assert(res.isLeft) @@ -68,13 +68,13 @@ trait DumboMigrationSpec extends ffstest.FTest { case Invalid(errs) => assert(errs.toList.exists(_.getMessage().contains("checksum mismatch"))) case _ => fail("expected failure") } - } yield () + yield () } dbTest("Validate description with validation enabled") { val schema = someSchemaName - for { + for _ <- dumboMigrate(schema, dumboWithResources("db/test_0")) res <- dumboMigrate(schema, dumboWithResources("db/test_0_desc_changed"), validateOnMigrate = true).attempt _ = assert(res.isLeft) @@ -95,13 +95,13 @@ trait DumboMigrationSpec extends ffstest.FTest { }) case _ => fail("expected failure") } - } yield () + yield () } dbTest("Validate for missing files with validation enabled") { val schema = someSchemaName - for { + for _ <- dumboMigrate(schema, dumboWithResources("db/test_0")) res <- dumboMigrate(schema, dumboWithResources("db/test_0_missing_file"), validateOnMigrate = true).attempt _ = assert(res.isLeft) @@ -113,37 +113,37 @@ trait DumboMigrationSpec extends ffstest.FTest { assert(errs.toList.exists(_.getMessage().contains("Detected applied migration not resolved locally"))) case _ => fail("expected failure") } - } yield () + yield () } dbTest("Ignore missing files or missing checksum on validation disabled") { val schema = someSchemaName - for { + for _ <- dumboMigrate(schema, dumboWithResources("db/test_0")) resA <- dumboMigrate(schema, dumboWithResources("db/test_0_missing_file"), validateOnMigrate = false).attempt resB <- dumboMigrate(schema, dumboWithResources("db/test_0_changed_checksum"), validateOnMigrate = false).attempt resC <- dumboMigrate(schema, dumboWithResources("db/test_0_desc_changed"), validateOnMigrate = false).attempt _ = assert(resA.isRight && resB.isRight && resC.isRight) - } yield () + yield () } dbTest("Fail with CopyNotSupportedException") { val schema = someSchemaName - for { + for dumboResA <- dumboMigrate(schema, dumboWithResources("db/test_copy_from")).attempt _ = assert(dumboResA.left.exists(_.isInstanceOf[skunk.exception.CopyNotSupportedException])) dumboResB <- dumboMigrate(schema, dumboWithResources("db/test_copy_to")).attempt _ = assert(dumboResB.left.exists(_.isInstanceOf[skunk.exception.CopyNotSupportedException])) - } yield () + yield () } dbTest("Fail on non-transactional operations") { val withResources = dumboWithResources("db/test_non_transactional") val schema = someSchemaName - for { + for dumboRes <- dumboMigrate(schema, withResources).attempt _ = assert(dumboRes.isLeft) errLines = dumboRes.swap.toOption.get.getMessage().linesIterator @@ -155,14 +155,14 @@ trait DumboMigrationSpec extends ffstest.FTest { case Db.CockroachDb => assert(errLines.exists(_.matches(".*enum value is not yet public."))) } - } yield () + yield () } dbTest("Fail on non-transactional operations") { val withResources = dumboWithResources("db/test_non_transactional") val schema = someSchemaName - for { + for dumboRes <- dumboMigrate(schema, withResources).attempt _ = assert(dumboRes.isLeft) errLines = dumboRes.swap.toOption.get.getMessage().linesIterator @@ -174,19 +174,19 @@ trait DumboMigrationSpec extends ffstest.FTest { case Db.CockroachDb => assert(errLines.exists(_.matches(".*enum value is not yet public."))) } - } yield () + yield () } dbTest("schemas are included in the search path") { val withResources = dumboWithResources("db/test_search_path") val schemas = List("schema_1", "schema_2") - for { + for dumboRes <- dumboMigrate(schemas.head, withResources, schemas.tail).attempt _ = assert(dumboRes.isRight) history <- loadHistory(schemas.head) _ = assert(history.length != 2) - } yield () + yield () } dbTest("warn if schemas are not included in the search path for custom sessions") { @@ -206,9 +206,9 @@ trait DumboMigrationSpec extends ffstest.FTest { ) && hasWarning(l, m) def migrateBySession(params: Map[String, String] = Map.empty) = - dumboMigrateWithSession(schemas.head, withResources, session(params), schemas.tail)(testConsole).attempt + dumboMigrateWithSession(schemas.head, withResources, session(params), schemas.tail)(using testConsole).attempt - for { + for // warn about missing schemas in the search_path dumboRes <- migrateBySession() _ = assert(dumboRes.isRight) @@ -223,7 +223,7 @@ trait DumboMigrationSpec extends ffstest.FTest { dumboResC <- migrateBySession(Map("search_path" -> "schema_1,schema_2")) _ = assert(dumboResC.isRight) _ = assert(!testConsole.logs.get().exists(t => hasWarning(t._1, t._2))) - } yield () + yield () } dbTest("migrate by different schema using custom session") { @@ -231,12 +231,12 @@ trait DumboMigrationSpec extends ffstest.FTest { val schemaA = "test_a" val schemaB = "test_b" - for { + for resDumboA <- dumboMigrateWithSession(schemaA, withResources, session()) resDumboB <- dumboMigrateWithSession(schemaB, withResources, session()) _ = assertEquals(resDumboA.migrationsExecuted, 4) _ = assertEquals(resDumboB.migrationsExecuted, 4) - } yield () + yield () } dbTest("default schema is used when no schema is specified in migration sripts") { @@ -257,7 +257,7 @@ trait DumboMigrationSpec extends ffstest.FTest { assertEquals(schemas, List(schemaDefault)) } - for { + for _ <- dropSchemas // migrate by connection config _ <- dumboMigrate(schemaDefault, withResources, schemas) @@ -275,7 +275,7 @@ trait DumboMigrationSpec extends ffstest.FTest { schemas, ) _ <- assertDefaultSchemaHasTable - } yield () + yield () } } @@ -285,17 +285,17 @@ trait DumboMigrationSpec extends ffstest.FTest { dbTest("don't log on waiting for lock release if under provided duration") { val testLogger = new TestLogger() - for { - _ <- dumboMigrate("schema_1", withResources, logMigrationStateAfter = 5.second)(testLogger) + for + _ <- dumboMigrate("schema_1", withResources, logMigrationStateAfter = 5.second)(using testLogger) _ = assert(testLogger.logs.get().count(t => logMatch(t._1, t._2)) == 0) - } yield () + yield () } dbTest("log on waiting for lock release longer than provided duration") { val testLogger = new TestLogger() - for { - _ <- dumboMigrate("schema_1", withResources, logMigrationStateAfter = 800.millis)(testLogger) + for + _ <- dumboMigrate("schema_1", withResources, logMigrationStateAfter = 800.millis)(using testLogger) _ = db match { case Db.Postgres(_) => assert(testLogger.logs.get().count(t => logMatch(t._1, t._2)) >= 2) case Db.CockroachDb => @@ -303,14 +303,14 @@ trait DumboMigrationSpec extends ffstest.FTest { level == LogLevel.Warn && message.startsWith("Progress monitor is not supported") } == 1) } - } yield () + yield () } } dbTest("Clean drops all objects and allows re-migration") { val schema = someSchemaName - for { + for res1 <- dumboMigrate(schema, dumboWithResources("db/test_1")) _ = assert(res1.migrationsExecuted > 0) history <- loadHistory(schema) @@ -319,39 +319,39 @@ trait DumboMigrationSpec extends ffstest.FTest { // after clean, migrating again should re-apply all migrations res2 <- dumboMigrate(schema, dumboWithResources("db/test_1")) _ = assertEquals(res2.migrationsExecuted, res1.migrationsExecuted) - } yield () + yield () } dbTest("Clean on empty schema is idempotent") { val schema = someSchemaName - for { + for _ <- session().use(_.execute(sql"CREATE SCHEMA IF NOT EXISTS #${schema}".command)) _ <- dumboClean(schema, dumboWithResources("db/test_1")) // should be able to migrate after cleaning an empty schema res <- dumboMigrate(schema, dumboWithResources("db/test_1")) _ = assert(res.migrationsExecuted > 0) - } yield () + yield () } dbTest("Clean with multiple schemas") { val schema1 = someSchemaName val schema2 = someSchemaName - for { + for _ <- dumboMigrate(schema1, dumboWithResources("db/test_1"), schemas = List(schema1, schema2)) _ <- dumboClean(schema1, dumboWithResources("db/test_1"), schemas = List(schema1, schema2)) // both schemas should be recreated empty, migration should work again res <- dumboMigrate(schema1, dumboWithResources("db/test_1"), schemas = List(schema1, schema2)) _ = assert(res.migrationsExecuted > 0) - } yield () + yield () } dbTest("Clean fails when cleanDisabled is true") { val schema = someSchemaName val withResources = dumboWithResources("db/test_1") - for { + for result <- withResources .apply( connection = connectionConfig, @@ -361,14 +361,13 @@ trait DumboMigrationSpec extends ffstest.FTest { .attempt _ = assert(result.isLeft) _ = assert(result.left.exists(_.isInstanceOf[exception.DumboCleanException])) - } yield () + yield () } } -sealed trait Db -object Db { - case class Postgres(version: Int) extends Db - case object CockroachDb extends Db +enum Db { + case Postgres(version: Int) + case CockroachDb } class DumboSpecPostgresLatest extends DumboMigrationSpec { diff --git a/modules/tests/shared/src/test/scala/DumboResourcesSpec.scala b/modules/tests/shared/src/test/scala/DumboResourcesSpec.scala index 5092c68..478fbdd 100644 --- a/modules/tests/shared/src/test/scala/DumboResourcesSpec.scala +++ b/modules/tests/shared/src/test/scala/DumboResourcesSpec.scala @@ -12,7 +12,7 @@ import fs2.io.file.Path class DumboResourcesSpec extends ffstest.FTest { test("list migration files from resources") { - for { + for files <- dumboWithResources("db/test_1").listMigrationFiles _ = files match { case Valid(files) => @@ -26,11 +26,11 @@ class DumboResourcesSpec extends ffstest.FTest { ) case Invalid(errs) => fail(errs.toList.mkString("\n")) } - } yield () + yield () } test("list migration files from resources with subirectories") { - for { + for files <- dumboWithResources("db/nested").listMigrationFiles _ = files match { case Valid(files) => @@ -46,11 +46,11 @@ class DumboResourcesSpec extends ffstest.FTest { ) case Invalid(errs) => fail(errs.toList.mkString("\n")) } - } yield () + yield () } test("list migration files from relative path") { - for { + for files <- Dumbo.withFilesIn[IO](Path("modules/tests/shared/src/test/non_resource/db/test_1")).listMigrationFiles _ = files match { case Valid(files) => @@ -66,11 +66,11 @@ class DumboResourcesSpec extends ffstest.FTest { ) case Invalid(errs) => fail(errs.toList.mkString("\n")) } - } yield () + yield () } test("list migration files from absolute path") { - for { + for files <- Dumbo.withFilesIn[IO](Path("modules/tests/shared/src/test/non_resource/db/test_1").absolute).listMigrationFiles _ = files match { @@ -87,11 +87,11 @@ class DumboResourcesSpec extends ffstest.FTest { ) case Invalid(errs) => fail(errs.toList.mkString("\n")) } - } yield () + yield () } test("fail on files with same versions") { - for { + for result <- dumboWithResources("db/test_duplicate_versions").listMigrationFiles _ = result match { case Invalid(errs) => @@ -108,14 +108,14 @@ class DumboResourcesSpec extends ffstest.FTest { }) case _ => fail("expected failure") } - } yield () + yield () } test("handle non-existent directory without NPE") { - for { + for res <- Dumbo.withFilesIn[IO](Path("/non/existent/directory")).listMigrationFiles.attempt _ = assert(res.isLeft) _ = assert(res.left.exists(_.isInstanceOf[java.nio.file.NoSuchFileException])) - } yield () + yield () } } diff --git a/modules/tests/shared/src/test/scala/ffstest/FFramework.scala b/modules/tests/shared/src/test/scala/ffstest/FFramework.scala index ff303eb..7994109 100644 --- a/modules/tests/shared/src/test/scala/ffstest/FFramework.scala +++ b/modules/tests/shared/src/test/scala/ffstest/FFramework.scala @@ -24,8 +24,8 @@ import skunk.Session.Credentials import skunk.implicits.* trait FTest extends CatsEffectSuite with FTestPlatform { - implicit val noopMeter: Meter[IO] = Meter.noop[IO] - def postgresPort: Int = 5432 + given noopMeter: Meter[IO] = Meter.noop[IO] + def postgresPort: Int = 5432 def dbTest(name: String)(f: => IO[Unit]): Unit = test(name)(dropSchemas >> f) @@ -64,8 +64,8 @@ trait FTest extends CatsEffectSuite with FTestPlatform { schemaHistoryTable: String = "flyway_schema_history", validateOnMigrate: Boolean = true, logMigrationStateAfter: Duration = Duration.Inf, - )(implicit l: Logger[IO]): IO[Dumbo.MigrationResult] = - (if (logMigrationStateAfter.isFinite) { + )(using l: Logger[IO]): IO[Dumbo.MigrationResult] = + (if logMigrationStateAfter.isFinite then { withResources.withMigrationStateLogAfter(FiniteDuration(logMigrationStateAfter.toMillis, MILLISECONDS))( connection = connectionConfig, defaultSchema = defaultSchema, @@ -90,7 +90,7 @@ trait FTest extends CatsEffectSuite with FTestPlatform { schemas: List[String] = Nil, schemaHistoryTable: String = "flyway_schema_history", validateOnMigrate: Boolean = true, - )(implicit l: Logger[IO]): IO[Dumbo.MigrationResult] = + )(using l: Logger[IO]): IO[Dumbo.MigrationResult] = withResources .withSession( sessionResource = session, @@ -119,7 +119,7 @@ trait FTest extends CatsEffectSuite with FTestPlatform { withResources: DumboWithResourcesPartiallyApplied[IO], schemas: List[String] = Nil, schemaHistoryTable: String = "flyway_schema_history", - )(implicit l: Logger[IO]): IO[Unit] = + )(using l: Logger[IO]): IO[Unit] = withResources .apply( connection = connectionConfig, @@ -131,7 +131,7 @@ trait FTest extends CatsEffectSuite with FTestPlatform { .runClean def dropSchemas: IO[Unit] = session().use { s => - for { + for customSchemas <- s.execute( sql"""|SELECT schema_name::text @@ -142,7 +142,7 @@ trait FTest extends CatsEffectSuite with FTestPlatform { _ <- IO.println(s"Dropping schemas ${customSchemas.mkString(", ")}") c <- customSchemas.traverse(schema => s.execute(sql"DROP SCHEMA IF EXISTS #${schema} CASCADE".command)) _ <- IO.println(s"Schema drop result ${c.mkString(", ")}") - } yield () + yield () } } diff --git a/project/plugins.sbt b/project/plugins.sbt index d9345c4..573f473 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,8 +4,6 @@ addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.12") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.4.0") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.8") - lazy val sbtTlVersion = "0.8.7" addSbtPlugin("org.typelevel" % "sbt-typelevel-ci-release" % sbtTlVersion) From abf874a09fc3c59510d2855c49f03ec2707ad180 Mon Sep 17 00:00:00 2001 From: Roman Langolf Date: Sun, 13 Sep 2026 14:25:30 +0700 Subject: [PATCH 2/2] remove redundant braces --- build.sbt | 2 +- .../src/main/scala/dumbo/cli/Arguments.scala | 8 +- .../src/main/scala/dumbo/cli/Dumbo.scala | 21 +-- .../shared/src/main/scala/dumbo/Dumbo.scala | 145 +++++++----------- .../shared/src/main/scala/dumbo/History.scala | 20 +-- .../src/main/scala/dumbo/ResourceFile.scala | 83 ++++------ .../main/scala/dumbo/ResourceFilePath.scala | 8 +- .../scala/dumbo/internal/CatalogQueries.scala | 11 +- .../scala/dumbo/internal/DumboPlatform.scala | 6 +- .../dumbo/internal/ResourcesReader.scala | 61 ++++---- .../scala/dumbo/internal/Statements.scala | 11 +- .../src/main/scala/dumbo/logging/Logger.scala | 52 +++---- .../src/test/scala/DumboFlywaySpec.scala | 86 ++++------- .../jvm/src/test/scala/DumboJvmSpec.scala | 10 +- .../scala/ffstest/FFrameworkPlatform.scala | 3 +- .../src/test/scala/DumboMigrationSpec.scala | 119 ++++++-------- .../src/test/scala/DumboResourcesSpec.scala | 38 ++--- .../shared/src/test/scala/DumboUtilSpec.scala | 6 +- .../test/scala/ResourceFileConfigSpec.scala | 8 +- .../scala/ResourceFileDescriptionSpec.scala | 14 +- .../src/test/scala/StatementsParserSpec.scala | 7 +- .../src/test/scala/ffstest/FFramework.scala | 14 +- 22 files changed, 288 insertions(+), 445 deletions(-) diff --git a/build.sbt b/build.sbt index 6e16d85..768703d 100644 --- a/build.sbt +++ b/build.sbt @@ -224,7 +224,7 @@ lazy val commonSettings = List( |""".stripMargin ) ), - Compile / scalacOptions += "-source:future", + Compile / scalacOptions ++= (Seq("-source:future") ++ sys.env.get("CI").map(_ => Seq("-Werror")).getOrElse(Nil)), ) lazy val root = tlCrossRootProject diff --git a/modules/cli/shared/src/main/scala/dumbo/cli/Arguments.scala b/modules/cli/shared/src/main/scala/dumbo/cli/Arguments.scala index f0bb531..0697249 100644 --- a/modules/cli/shared/src/main/scala/dumbo/cli/Arguments.scala +++ b/modules/cli/shared/src/main/scala/dumbo/cli/Arguments.scala @@ -18,12 +18,12 @@ final case class Arguments( configs: List[(Config[?], String)], flags: List[Flag], unknown: List[String], -) { +): def withCommand(c: Command) = copy(commands = c :: commands) def withConfig(c: Config[?], v: String) = copy(configs = (c, v) :: configs) def withFlag(f: Flag) = copy(flags = f :: flags) def withUnknown(arg: String) = copy(unknown = arg :: unknown) -} +end Arguments object Arguments: val empty: Arguments = Arguments(Nil, Nil, Nil, Nil) @@ -63,6 +63,8 @@ object Arguments: walk(args = arguments.reverse, Arguments.empty) +end Arguments + enum Command(val keys: Set[String], val desc: String, val configs: List[Config[?]], val flags: List[Flag]): case Help extends Command(Set("help"), "Print this usage info and exit", Nil, Nil) case Migrate extends Command(Set("migrate"), "Migrates the database", Config.values.toList, Nil) @@ -165,6 +167,8 @@ enum Config[T](val key: String, val desc: String, val parse: String => Either[St }, ) +end Config + object Config: def helpMap(configs: List[Config[?]]) = configs.map(c => c.key -> c.desc).toMap val helpMapAll: Map[String, String] = helpMap(Config.values.toList) diff --git a/modules/cli/shared/src/main/scala/dumbo/cli/Dumbo.scala b/modules/cli/shared/src/main/scala/dumbo/cli/Dumbo.scala index bb1a431..b6f3510 100644 --- a/modules/cli/shared/src/main/scala/dumbo/cli/Dumbo.scala +++ b/modules/cli/shared/src/main/scala/dumbo/cli/Dumbo.scala @@ -13,16 +13,15 @@ import dumbo.logging.Implicits.consolePrettyWithTimestamp import org.typelevel.otel4s.metrics.Meter.Implicits.noop import org.typelevel.otel4s.trace.Tracer.Implicits.noop -object Dumbo extends IOApp { - private def printHelp(cmd: Option[Command] = None) = { +object Dumbo extends IOApp: + private def printHelp(cmd: Option[Command] = None) = val tab = " " - def helpMapStr(m: Map[String, String]) = { + def helpMapStr(m: Map[String, String]) = val colSize = m.keySet.maxByOption(_.length()).map(_.length()).getOrElse(0) + 3 m.map { case (k, v) => s"${k + Array.fill(colSize - k.length())(" ").mkString}$v" } .mkString(tab, s"\n$tab", "\n") - } val usageExample = cmd match case None => @@ -36,14 +35,13 @@ object Dumbo extends IOApp { case None => s"dumbo help [command]\nCommands\n${helpMapStr(Command.helpMap)}\n" case Some(_) => "" - val configsHelp = { + val configsHelp = val title = "Configuration parameters (Format: -key=value)" cmd match case None => Some(s"\n$title\n${helpMapStr(Config.helpMapAll)}") case Some(c) if c.configs.nonEmpty => Some(s"\n$title\n${helpMapStr(Config.helpMap(c.configs))}") case _ => None - } val help = s"""|Usage @@ -53,11 +51,10 @@ object Dumbo extends IOApp { |$commandsHelp${configsHelp.getOrElse("")}$usageExample""".stripMargin Console[IO].println(help) - } private[dumbo] def dumboFromConfigs( configs: List[(Config[?], String)] - ): Either[String, (dumbo.Dumbo[IO], dumbo.ConnectionConfig)] = { + ): Either[String, (dumbo.Dumbo[IO], dumbo.ConnectionConfig)] = def collectConfig[T](config: Config[T]): Option[Either[String, T]] = configs.collectFirst { case (c, v) if c == config => config.parse(v) } @@ -112,8 +109,6 @@ object Dumbo extends IOApp { connection, ) - } - private def runMigration(options: List[(Config[?], String)]): IO[ExitCode] = dumboFromConfigs(options) match case Left(value) => Console[IO].errorln(s"Invalid configuration: $value").as(ExitCode.Error) @@ -128,12 +123,11 @@ object Dumbo extends IOApp { dumboFromConfigs(options) match case Left(value) => Console[IO].errorln(s"Invalid configuration: $value").as(ExitCode.Error) case Right((d, _)) => - d.runValidationWithHistory.flatMap { + d.runValidationWithHistory.flatMap: case Valid(_) => Console[IO].println("Validation result: ok").as(ExitCode.Success) case Invalid(e) => val errs = e.toNonEmptyList.toList.map(_.getMessage()) Console[IO].errorln(s"Errors on validation: ${errs.mkString("\n", "\n", "")}").as(ExitCode.Success) - } def run(args: List[String]): IO[ExitCode] = val argsResult = Arguments.parse(args) @@ -161,4 +155,5 @@ object Dumbo extends IOApp { .as(ExitCode.Error) case unknowns => Console[IO].errorln(s"Invalid arguments: ${unknowns.mkString(", ")}").as(ExitCode.Error) -} + +end Dumbo diff --git a/modules/core/shared/src/main/scala/dumbo/Dumbo.scala b/modules/core/shared/src/main/scala/dumbo/Dumbo.scala index 525739c..ab950f3 100644 --- a/modules/core/shared/src/main/scala/dumbo/Dumbo.scala +++ b/modules/core/shared/src/main/scala/dumbo/Dumbo.scala @@ -31,7 +31,7 @@ import skunk.exception.PostgresErrorException import skunk.implicits.* import skunk.util.Origin -final class DumboWithResourcesPartiallyApplied[F[_]](reader: ResourceReader[F]) { +final class DumboWithResourcesPartiallyApplied[F[_]](reader: ResourceReader[F]): def apply( connection: ConnectionConfig, defaultSchema: String = Dumbo.defaults.defaultSchema, @@ -82,7 +82,7 @@ final class DumboWithResourcesPartiallyApplied[F[_]](reader: ResourceReader[F]) schemaHistoryTable: String = Dumbo.defaults.schemaHistoryTable, validateOnMigrate: Boolean = Dumbo.defaults.validateOnMigrate, cleanDisabled: Boolean = Dumbo.defaults.cleanDisabled, - )(using A: Async[F], L: Logger[F], LIO: LiftIO[F], C: Console[F], TRC: Tracer[F], MTR: Meter[F]): Dumbo[F] = { + )(using A: Async[F], L: Logger[F], LIO: LiftIO[F], C: Console[F], TRC: Tracer[F], MTR: Meter[F]): Dumbo[F] = given Network[F] = Network.forLiftIO[F] val sessionResource = toSessionResource(connection, defaultSchema, schemas) @@ -94,7 +94,6 @@ final class DumboWithResourcesPartiallyApplied[F[_]](reader: ResourceReader[F]) validateOnMigrate, cleanDisabled, ) - } def withMigrationStateLogAfterBySession(logMigrationStateAfter: FiniteDuration)( sessionResource: Resource[F, Session[F]], @@ -164,7 +163,7 @@ final class DumboWithResourcesPartiallyApplied[F[_]](reader: ResourceReader[F]) connection: ConnectionConfig, defaultSchema: String, schemas: Set[String], - )(using T: Temporal[F], C: Console[F], TRC: Tracer[F], MTR: Meter[F], N: Network[F]) = { + )(using T: Temporal[F], C: Console[F], TRC: Tracer[F], MTR: Meter[F], N: Network[F]) = val searchPath = Dumbo.toSearchPath(defaultSchema, schemas) val params = Session.DefaultConnectionParameters ++ Map("search_path" -> searchPath) @@ -182,8 +181,8 @@ final class DumboWithResourcesPartiallyApplied[F[_]](reader: ResourceReader[F]) }) .withConnectionParameters(params) .single - } -} + +end DumboWithResourcesPartiallyApplied class Dumbo[F[_]: {Sync, Logger}]( private[dumbo] val resReader: ResourceReader[F], @@ -194,7 +193,7 @@ class Dumbo[F[_]: {Sync, Logger}]( private[dumbo] val validateOnMigrate: Boolean, cleanDisabled: Boolean, progressMonitor: Resource[F, Unit] = Resource.unit[F], -) { +): import Dumbo.* private[dumbo] val allSchemas = combineSchemas(defaultSchema, schemas) @@ -203,11 +202,10 @@ class Dumbo[F[_]: {Sync, Logger}]( private def initSchemaCmd(schema: String) = sql"CREATE SCHEMA IF NOT EXISTS #${quoteIdentifier(schema)}".command - private def transact(source: ResourceFile, fs: ResourceReader[F], session: Session[F]): F[HistoryEntry.New] = { - val toVersion = source.versionText match { + private def transact(source: ResourceFile, fs: ResourceReader[F], session: Session[F]): F[HistoryEntry.New] = + val toVersion = source.versionText match case Some(v) => s"to version $v - \"${source.scriptDescription}\"" case _ => s"with repeatable migration \"${source.scriptDescription}\"" - } for _ <- @@ -224,7 +222,7 @@ class Dumbo[F[_]: {Sync, Logger}]( if source.executeInTransaction then Vector(sql) else Statements.intoSingleStatements(sql) } - (duration, _) <- Clock[F].timed { + (duration, _) <- Clock[F].timed: statements .map(statementSql => new Statement[Void] { @@ -236,7 +234,6 @@ class Dumbo[F[_]: {Sync, Logger}]( } ) .traverse_(session.executeDiscard(_)) - } _ <- Logger[F].logInfo(s"Migration $toVersion completed in ${duration.toMillis}ms") yield HistoryEntry.New( version = source.versionText, @@ -247,20 +244,18 @@ class Dumbo[F[_]: {Sync, Logger}]( executionTimeMs = duration.toMillis.toInt, success = true, ) - } private def validationGuard(session: Session[F], resources: ResourceFiles) = - if resources.nonEmpty then { + if resources.nonEmpty then session .execute(dumboHistory.loadAllQuery) .map(history => validate(history, resources)) - .flatMap { + .flatMap: case Valid(_) => ().pure[F] case Invalid(e) => new DumboValidationException(s"Error on validation:\n${e.toList.map(_.getMessage).mkString("\n")}") .raiseError[F, Unit] - } - } else ().pure[F] + else ().pure[F] private def migrateToNext( session: Session[F], @@ -269,7 +264,7 @@ class Dumbo[F[_]: {Sync, Logger}]( )( resources: ResourceFiles ): F[MigrateToNextResult] = - resources match { + resources match case ResourceFiles(Nil, Nil) => none.pure[F] case ResourceFiles(versioned, repeatables) => ((for @@ -298,13 +293,11 @@ class Dumbo[F[_]: {Sync, Logger}]( } } yield res - }).recoverWith { + }).recoverWith: // https://www.cockroachlabs.com/docs/v25.2/transaction-retry-error-reference.html case SqlState.SerializationFailure(ex) if ex.message.toLowerCase.contains("restart transaction") => Logger[F].logWarn(s"Retrying transaction on SerializationFailure: ${ex.message}") >> migrateToNext(session = session, fs = fs, lockSupport = lockSupport)(resources) - } - } private def processVersioned( versioned: List[ResourceFileVersioned], @@ -316,13 +309,12 @@ class Dumbo[F[_]: {Sync, Logger}]( latestInstalled <- session.option(dumboHistory.latestVersionedInstalled) latestInstalledV = latestInstalled.map(l => (l.resourceVersion, l.success)) result <- versioned.dropWhile { case (v, _) => - latestInstalledV match { + latestInstalledV match // drop versions applied successfully // retry the version which was not applied successfully case Some((Some(lv), success)) => if success then v <= lv else v < lv case _ => false - } - } match { + } match case (_, x) :: xs => // acquire a new session for non-transactional operation val transactSession: Resource[F, Session[F]] = @@ -334,7 +326,6 @@ class Dumbo[F[_]: {Sync, Logger}]( .map((_, xs).some) } case _ => none.pure[F] - } yield result private def processRepeatables( @@ -346,11 +337,10 @@ class Dumbo[F[_]: {Sync, Logger}]( for latestRepeatables <- session.execute(dumboHistory.latestRepeatablesInstalled).map(_.toMap) res <- repeatables.filter { case (_, f) => - latestRepeatables.get(f.scriptDescription) match { + latestRepeatables.get(f.scriptDescription) match case Some(checksum) => checksum != f.checksum case _ => true - } - } match { + } match case (_, x) :: xs => // acquire a new session for non-transactional operation val transactSession: Resource[F, Session[F]] = @@ -360,14 +350,12 @@ class Dumbo[F[_]: {Sync, Logger}]( transact(x, fs, s).flatMap(updateHistory(None, s)).map((_, xs).some) } case Nil => none.pure[F] - } yield res private def updateHistory(latestInstalled: Option[HistoryEntry], session: Session[F])(newEntry: HistoryEntry.New) = - latestInstalled match { + latestInstalled match case Some(value) if !value.success => session.unique(dumboHistory.updateSQLEntry)(newEntry -> value.installedRank) case _ => session.unique(dumboHistory.insertSQLEntry)(newEntry) - } // https://www.postgresql.org/docs/current/errcodes-appendix.html private val duplicateErrorCodes = Set( @@ -388,25 +376,23 @@ class Dumbo[F[_]: {Sync, Logger}]( } // search_path needs to include default schema before other schemas - private def verifySearchPath(sp: String): F[Option[String]] = { + private def verifySearchPath(sp: String): F[Option[String]] = val spSchemas = sp.split(",").map(_.trim).toVector val sps = spSchemas.mkString(", ") val expectedSearchPath = toSearchPath(defaultSchema, schemas) - allSchemas.diff(spSchemas) match { + allSchemas.diff(spSchemas) match case Nil => // validate the order val defaultIdx = spSchemas.indexOf(defaultSchema) - if schemas.forall(s => spSchemas.indexOf(s) > defaultIdx) then { - none[String].pure[F] - } else { + if schemas.forall(s => spSchemas.indexOf(s) > defaultIdx) then none[String].pure[F] + else Logger[F] .logWarn( s"""|Default schema '$defaultSchema' is not in the right position of the search path '$sps'. |The search_path will be set to '${expectedSearchPath}'. Consider adding it to session parameters instead.""".stripMargin ) .as(Some(expectedSearchPath)) - } case missing => Logger[F] .logWarn( @@ -414,8 +400,6 @@ class Dumbo[F[_]: {Sync, Logger}]( |The search_path will be set to '$expectedSearchPath'. Consider adding it to session parameters instead.""".stripMargin ) .as(Some(expectedSearchPath)) - } - } // acquire a session and initialize the schemas and history table private[dumbo] def initSession = sessionResource.evalMap { session => @@ -427,10 +411,11 @@ class Dumbo[F[_]: {Sync, Logger}]( // (autocommit_before_ddl = on). Turning it off makes DDL transactional, // which is required for our row-level locking to work correctly. // Only run if the parameter is supported to avoid PostgreSQL error logs. - _ <- session.unique(sql"SELECT current_setting('autocommit_before_ddl', true)".query(text.opt)).flatMap { - case Some(_) => session.executeDiscard(sql"SET autocommit_before_ddl = off".command).attempt.void - case _ => ().pure[F] - } + _ <- session + .unique(sql"SELECT current_setting('autocommit_before_ddl', true)".query(text.opt)) + .flatMap: + case Some(_) => session.executeDiscard(sql"SET autocommit_before_ddl = off".command).attempt.void + case _ => ().pure[F] _ <- session.transaction.use { _ => for @@ -444,11 +429,10 @@ class Dumbo[F[_]: {Sync, Logger}]( else ().pure[F] ) _ <- session.unique(sql"SHOW search_path".query(text)).flatMap { sp => - verifySearchPath(sp).flatMap { + verifySearchPath(sp).flatMap: case Some(searchPathUpdate) => session.execute(sql"SET search_path TO #${searchPathUpdate}".command).void case _ => ().pure[F] - } } schemaRes <- allSchemas.flatTraverse(schema => @@ -458,9 +442,11 @@ class Dumbo[F[_]: {Sync, Logger}]( case _ => Nil } ) - _ <- session.execute(dumboHistory.createTableCommand).void.recover { - case e: skunk.exception.PostgresErrorException if duplicateErrorCodes.contains(e.code) => () - } + _ <- session + .execute(dumboHistory.createTableCommand) + .void + .recover: + case e: skunk.exception.PostgresErrorException if duplicateErrorCodes.contains(e.code) => () _ <- (if schemaRes.nonEmpty then session.execute(dumboHistory.insertSchemaEntry)(schemaRes.mkString("\"", "\",\"", "\"")).void @@ -484,10 +470,9 @@ class Dumbo[F[_]: {Sync, Logger}]( s"Error while reading migration files:\n${errs.toList.mkString("\n")}" ).raiseError[F, List[ResourceFile]] }.map(ResourceFiles.fromResources) - _ <- { + _ <- val inLocation = resReader.location.map(l => s" in $l").getOrElse("") Logger[F].logInfo(s"Found ${resources.length} migration files$inLocation") - } _ <- if validateOnMigrate then validationGuard(session, resources) else ().pure[F] migrationResult <- Stream @@ -497,7 +482,7 @@ class Dumbo[F[_]: {Sync, Logger}]( .map(Dumbo.MigrationResult(_)) yield migrationResult - _ <- migrationResult.migrations.sorted(using Ordering[HistoryEntry].reverse) match { + _ <- migrationResult.migrations.sorted(using Ordering[HistoryEntry].reverse) match case Nil => Logger[F].logInfo(s"Schema ${defaultSchema} is up to date. No migration necessary") case history => val verLog = history.collectFirst { case HistoryEntry(_, Some(v), _, _, _, _, _, _, _, _) => v } @@ -511,20 +496,19 @@ class Dumbo[F[_]: {Sync, Logger}]( .logInfo( s"Successfully applied ${migrationResult.migrations.length} migrations$verLog $execDurationLog" ) - } yield migrationResult private def validate( history: List[HistoryEntry], resources: ResourceFiles, - ): ValidatedNec[DumboValidationException, Unit] = { + ): ValidatedNec[DumboValidationException, Unit] = val versionedMap: Map[String, ResourceFile] = resources.versioned.map { case (v, f) => (v.text, f) }.toMap val repeatablesScriptNames: Set[String] = resources.repeatable.map(_._2.path.value).toSet history .filter(_.`type` == "SQL") .traverse { h => - versionedMap.get(h.version.getOrElse("")) match { + versionedMap.get(h.version.getOrElse("")) match case None if !repeatablesScriptNames.exists(_.endsWith(h.script)) => new DumboValidationException(s"Detected applied migration not resolved locally ${h.script}") .invalidNec[Unit] @@ -547,10 +531,8 @@ class Dumbo[F[_]: {Sync, Logger}]( ).invalidNec[Unit] case _ => ().validNec[DumboValidationException] - } } .void - } def runClean: F[Unit] = if cleanDisabled then @@ -585,7 +567,7 @@ class Dumbo[F[_]: {Sync, Logger}]( else cleanSchema(session, schema) yield (schema, createdByDumbo) } - _ <- { + _ <- val dropped = results.collect { case (s, true) => s } val cleaned = results.collect { case (s, false) => s } val parts = List( @@ -593,13 +575,12 @@ class Dumbo[F[_]: {Sync, Logger}]( Option.when(cleaned.nonEmpty)(s"cleaned schemas ${cleaned.mkString(", ")}"), ).flatten Logger[F].logInfo(s"Successfully ${parts.mkString("; ")}") - } yield () // Drops all objects in the given schema following Flyway's doClean order: // materialized views, views, tables, base types (with recreate), routines, // enums, domains, sequences, base types (final cleanup) - private def cleanSchema(session: Session[F], schema: String): F[Unit] = { + private def cleanSchema(session: Session[F], schema: String): F[Unit] = def queryNames(q: Query[String, String]): F[List[String]] = session.execute(q)(schema) @@ -653,7 +634,6 @@ class Dumbo[F[_]: {Sync, Logger}]( types2 <- queryBaseTypes _ <- dropAll("TYPE", types2.map(_._1)) yield () - } private def cleanRoutines(session: Session[F], schema: String): F[Unit] = session.execute(CatalogQueries.listRoutinesQuery)(schema).flatMap { routines => @@ -669,30 +649,26 @@ class Dumbo[F[_]: {Sync, Logger}]( } def runValidationWithHistory: F[ValidatedNec[DumboValidationException, Unit]] = - listMigrationFiles(resReader).flatMap { + listMigrationFiles(resReader).flatMap: case Valid(resources) => sessionResource.use( _.execute(dumboHistory.loadAllQuery).map(history => validate(history, ResourceFiles.fromResources(resources))) ) case Invalid(c) => c.invalid.pure[F] - } -} -object Dumbo extends internal.DumboPlatform { +object Dumbo extends internal.DumboPlatform: private type MigrateToNextResult = Option[(HistoryEntry, ResourceFiles)] - object defaults { + object defaults: val defaultSchema: String = "public" val schemas: Set[String] = Set.empty[String] val schemaHistoryTable: String = "flyway_schema_history" val validateOnMigrate: Boolean = true val cleanDisabled: Boolean = true val port: Int = 5432 - } - final case class MigrationResult(migrations: List[HistoryEntry]) { + final case class MigrationResult(migrations: List[HistoryEntry]): val migrationsExecuted: Int = migrations.length - } def withResources[F[_]: Sync](resources: List[ResourceFilePath]): DumboWithResourcesPartiallyApplied[F] = new DumboWithResourcesPartiallyApplied[F](ResourceReader.embeddedResources(Sync[F].pure(resources))) @@ -702,29 +678,24 @@ object Dumbo extends internal.DumboPlatform { // input duration in milliseconds // output in format mm:ss.ms e.g. 00:00.000s - private[dumbo] def formatDuration(ms: Long): String = { + private[dumbo] def formatDuration(ms: Long): String = val pos = math.max(ms, 0L) String.format("%02d:%02d.%03d", pos / 60000, (pos / 1000) % 60, (pos % 1000)) + "s" - } - private[dumbo] enum LockSupport { + private[dumbo] enum LockSupport: case TableLock, XactAdvisoryLock - } private[dumbo] def detectLockSupport[F[_]: Sync](session: Session[F]): F[Set[LockSupport]] = - hasTableLockSupport(session).flatMap { + hasTableLockSupport(session).flatMap: case true => - hasXactAdvisoryLockSupport(session).map { + hasXactAdvisoryLockSupport(session).map: case true => Set(LockSupport.XactAdvisoryLock, LockSupport.TableLock) case false => Set(LockSupport.TableLock) - } case false => - hasXactAdvisoryLockSupport(session).map { + hasXactAdvisoryLockSupport(session).map: case true => Set(LockSupport.XactAdvisoryLock) case false => Set.empty - } - } private[dumbo] def hasTableLockSupport[F[_]: Sync](session: Session[F]) = session.transaction.use(_ => @@ -755,7 +726,7 @@ object Dumbo extends internal.DumboPlatform { val (errs, files) = (sf.collect { case Left(err) => err }, sf.collect { case Right(v) => v }) val duplicates = files.groupBy(_.version).filter(_._2.length > 1).toList - (duplicates, errs.map(new DumboValidationException(_))) match { + (duplicates, errs.map(new DumboValidationException(_))) match case (Nil, Nil) => files.sorted.validNec[DumboValidationException] case (Nil, x :: xs) => NonEmptyChain(x, xs*).invalid[List[ResourceFile]] case (diff, exceptions) => @@ -766,24 +737,22 @@ object Dumbo extends internal.DumboPlatform { ), exceptions* ).invalid[List[ResourceFile]] - } } private[dumbo] def readResourceFiles[F[_]: Sync]( fs: ResourceReader[F] ): F[List[Either[String, ResourceFile]]] = - fs.list.flatMap { + fs.list.flatMap: _.filter(f => f.value.endsWith(".sql")).traverse { path => val confPath = path.append(".conf") fs.exists(confPath) - .flatMap { + .flatMap: case true => fs.readUtf8Lines(confPath).map(ResourceFileConfig.fromLines) case false => Set.empty[ResourceFileConfig].asRight[String].pure[F] - } - .flatMap { + .flatMap: case Right(configs) => - ResourceFileDescription.fromResourcePath(path) match { + ResourceFileDescription.fromResourcePath(path) match case Right(desc) => for checksum <- checksum[F](path, fs) yield ResourceFile( @@ -792,12 +761,9 @@ object Dumbo extends internal.DumboPlatform { configs = configs, ).asRight[String] case Left(err) => err.asLeft[ResourceFile].pure[F] - } case Left(err) => err.asLeft[ResourceFile].pure[F] - } } - } // implementation of checksum from Flyway // https://github.com/flyway/flyway/blob/main/flyway-core/src/main/java/org/flywaydb/core/internal/resolver/ChecksumCalculator.java#L59 @@ -817,4 +783,5 @@ object Dumbo extends internal.DumboPlatform { private[dumbo] def toSearchPath(defaultSchema: String, schemas: Set[String]) = combineSchemas(defaultSchema, schemas).map(quoteIdentifier).mkString(", ") -} + +end Dumbo diff --git a/modules/core/shared/src/main/scala/dumbo/History.scala b/modules/core/shared/src/main/scala/dumbo/History.scala index 8b6ec76..9d83dd9 100644 --- a/modules/core/shared/src/main/scala/dumbo/History.scala +++ b/modules/core/shared/src/main/scala/dumbo/History.scala @@ -20,15 +20,14 @@ final case class HistoryEntry( installedOn: LocalDateTime, executionTimeMs: Int, success: Boolean, -) extends Ordered[HistoryEntry] { +) extends Ordered[HistoryEntry]: override def compare(that: HistoryEntry): Int = installedRank.compare(that.installedRank) def resourceVersion: Option[ResourceVersion.Versioned] = version.flatMap(ResourceVersion.Versioned.fromString(_).toOption) -} -object HistoryEntry { +object HistoryEntry: final case class New( version: Option[String], description: String, @@ -39,11 +38,10 @@ object HistoryEntry { success: Boolean, ) - object New { + object New: val codec: Codec[New] = (varchar(50).opt *: varchar(200) *: varchar(20) *: varchar(1000) *: int4.opt *: int4 *: bool) .to[New] - } val codec: Codec[HistoryEntry] = (int4 *: varchar(50).opt *: varchar(200) *: varchar(20) *: varchar(1000) *: int4.opt *: varchar( @@ -53,9 +51,8 @@ object HistoryEntry { val fieldNames = "installed_rank::INT4, version, description, type, script, checksum::INT4, installed_by, installed_on, execution_time::INT4, success" -} -class History(schema: String, table: String) { +class History(schema: String, table: String): private val quotedTableName = s"${Dumbo.quoteIdentifier(schema)}.${Dumbo.quoteIdentifier(table)}" val createTableCommand: Command[Void] = @@ -87,14 +84,13 @@ class History(schema: String, table: String) { ORDER BY description ASC, installed_rank DESC""" .query(varchar(200) ~ int4) - val insertSQLEntry: Query[HistoryEntry.New, HistoryEntry] = { + val insertSQLEntry: Query[HistoryEntry.New, HistoryEntry] = val nextRank = sql"(SELECT COALESCE(MAX(installed_rank), 0) + 1 FROM #${quotedTableName})" sql"""INSERT INTO #${quotedTableName} (installed_rank, version, description, type, script, checksum, execution_time, success, installed_on, installed_by) VALUES ($nextRank, ${HistoryEntry.New.codec}, CURRENT_TIMESTAMP, CURRENT_USER) RETURNING #${HistoryEntry.fieldNames}""".query(HistoryEntry.codec) - } val updateSQLEntry: Query[HistoryEntry.New *: Int *: EmptyTuple, HistoryEntry] = sql"""UPDATE #${quotedTableName} @@ -112,8 +108,8 @@ class History(schema: String, table: String) { VALUES (0, '<< Flyway Schema Creation >>', 'SCHEMA', ${varchar(1000)}, 0, true, CURRENT_TIMESTAMP, CURRENT_USER) ON CONFLICT DO NOTHING""".command -} -object History { +end History + +object History: def apply(schema: String, table: String) = new History(schema, table) -} diff --git a/modules/core/shared/src/main/scala/dumbo/ResourceFile.scala b/modules/core/shared/src/main/scala/dumbo/ResourceFile.scala index 13b8952..cded24e 100644 --- a/modules/core/shared/src/main/scala/dumbo/ResourceFile.scala +++ b/modules/core/shared/src/main/scala/dumbo/ResourceFile.scala @@ -13,27 +13,23 @@ import cats.implicits.* final case class ResourceFiles( versioned: List[ResourceFileVersioned], repeatable: List[ResourceFileRepeatable], -) { +): def length: Int = versioned.length + repeatable.length def nonEmpty: Boolean = versioned.nonEmpty || repeatable.nonEmpty -} -object ResourceFiles { - def fromResources(resources: List[ResourceFile]): ResourceFiles = { - val (versioned, repeatable) = resources.partitionMap { +object ResourceFiles: + def fromResources(resources: List[ResourceFile]): ResourceFiles = + val (versioned, repeatable) = resources.partitionMap: case f @ ResourceFile(ResourceFileDescription(v: ResourceVersion.Versioned, _, _), _, _) => Left((v, f)) case f @ ResourceFile(ResourceFileDescription(v: ResourceVersion.Repeatable, _, _), _, _) => Right((v, f)) - } ResourceFiles(versioned, repeatable) - } -} final case class ResourceFile( description: ResourceFileDescription, checksum: Int, configs: Set[ResourceFileConfig], -) extends Ordered[ResourceFile] { +) extends Ordered[ResourceFile]: def versionText: Option[String] = description.version.versionText def version: ResourceVersion = description.version def scriptDescription: String = description.description @@ -44,27 +40,23 @@ final case class ResourceFile( def compare(that: ResourceFile): Int = version.compare(that.version) - override def equals(b: Any): Boolean = b.asInstanceOf[Matchable] match { + override def equals(b: Any): Boolean = b.asInstanceOf[Matchable] match case s: ResourceFile => version.equals(s.version) case _ => false - } val executeInTransaction: Boolean = configs.collectFirst { case ResourceFileConfig.ExecuteInTransaction(v) => v }.getOrElse(true) -} -enum ResourceFileConfig(protected val key: String) { +enum ResourceFileConfig(protected val key: String): case ExecuteInTransaction(value: Boolean) extends ResourceFileConfig(ResourceFileConfig.txn) override def hashCode(): Int = key.hashCode() - override def equals(b: Any): Boolean = b.asInstanceOf[Matchable] match { + override def equals(b: Any): Boolean = b.asInstanceOf[Matchable] match case s: ResourceFileConfig => s.key == key case _ => false - } -} -object ResourceFileConfig { +object ResourceFileConfig: private val txn = "executeInTransaction" private def invalidBoolean(key: String, v: String) = @@ -85,36 +77,32 @@ object ResourceFileConfig { case Nil => Nil.asRight[String] }) .flatMap { l => - l.diff(l.distinct) match { + l.diff(l.distinct) match case x :: _ => Left(s"""Multiple configurations for "${x.key}"""") case Nil => Right(l.toSet) - } } -} final case class ResourceFileDescription( version: ResourceVersion, description: String, path: ResourceFilePath, -) extends Ordered[ResourceFileDescription] { +) extends Ordered[ResourceFileDescription]: def compare(that: ResourceFileDescription): Int = this.version.compare(that.version) override def hashCode: Int = this.version.hashCode - override def equals(b: Any): Boolean = b.asInstanceOf[Matchable] match { + override def equals(b: Any): Boolean = b.asInstanceOf[Matchable] match case s: ResourceFileDescription => this.version.equals(s.version) case _ => false - } def fileName: String = path.fileName -} -object ResourceFileDescription { - def fromResourcePath(p: ResourceFilePath): Either[String, ResourceFileDescription] = { +object ResourceFileDescription: + def fromResourcePath(p: ResourceFilePath): Either[String, ResourceFileDescription] = val versioned = "^V([^_]+)__(.+)\\.sql$".r val repeatable = "^R__(.+)\\.sql$".r - p.fileName.toString match { + p.fileName.toString match case versioned(version, name) => ResourceVersion.Versioned.fromString(version).map { v => ResourceFileDescription( @@ -133,69 +121,59 @@ object ResourceFileDescription { ) ) case other => Left(s"Invalid file name $other") - } - } -} -enum ResourceVersion extends Ordered[ResourceVersion] { - def compare(that: ResourceVersion): Int = { +enum ResourceVersion extends Ordered[ResourceVersion]: + def compare(that: ResourceVersion): Int = @tailrec def cmprVersioned(a: List[Long], b: List[Long]): Int = - (a, b) match { + (a, b) match case (xa :: xsa, xb :: xsb) if xa == xb => cmprVersioned(xsa, xsb) case (xa :: _, xb :: _) => xa.compare(xb) case (xa :: _, Nil) => xa.compare(0L) case (Nil, xb :: _) => xb.compare(0L) case (Nil, Nil) => 0 - } - (this, that) match { + (this, that) match case (Repeatable(_), Versioned(_, _)) => 1 case (Versioned(_, _), Repeatable(_)) => -1 case (Repeatable(descThis), Repeatable(descThat)) => descThis.compare(descThat) case (Versioned(_, thisParts), Versioned(_, thatParts)) => cmprVersioned(thisParts.toList, thatParts.toList) - } - } - def versionText: Option[String] = this match { + def versionText: Option[String] = this match case Repeatable(_) => None case Versioned(plain, _) => Some(plain) - } // strip trailing 0 // 1.0 -> 1 // 0.01.0.0 -> 0.1 - override def toString: String = this match { + override def toString: String = this match case Repeatable(description) => s"Repeatable($description)" case Versioned(_, parts) => parts.reverse.toList.dropWhile(_ <= 0).map(_.toString).reverse.mkString(".") - } // 1.0 should yield same hash code as 1 or 1.0.0 etc. - override def hashCode: Int = this match { + override def hashCode: Int = this match case Repeatable(description) => description.hashCode case Versioned(_, parts) => parts.reverse.foldLeft("")(_ + _.toString).toInt - } - override def equals(b: Any): Boolean = b.asInstanceOf[Matchable] match { + override def equals(b: Any): Boolean = b.asInstanceOf[Matchable] match case that: ResourceVersion => - (this, that) match { + (this, that) match case (Repeatable(descThis), Repeatable(descThat)) => descThis == descThat case (thisV: Versioned, thatV: Versioned) => thisV.compare(thatV) == 0 case _ => false - } case _ => false - } case Repeatable(description: String) case Versioned(text: String, parts: NonEmptyList[Long]) -} -object ResourceVersion { - object Versioned { +end ResourceVersion + +object ResourceVersion: + object Versioned: def fromString(version: String): Either[String, Versioned] = - Try(version.split('.').map(_.toLong)) match { + Try(version.split('.').map(_.toLong)) match case Success(Array(x, xs*)) => Right( Versioned( @@ -204,6 +182,3 @@ object ResourceVersion { ) ) case _ => Left(s"Invalid version $version") - } - } -} diff --git a/modules/core/shared/src/main/scala/dumbo/ResourceFilePath.scala b/modules/core/shared/src/main/scala/dumbo/ResourceFilePath.scala index 1183307..f59e28f 100644 --- a/modules/core/shared/src/main/scala/dumbo/ResourceFilePath.scala +++ b/modules/core/shared/src/main/scala/dumbo/ResourceFilePath.scala @@ -26,9 +26,8 @@ object ResourceFilePath: val jarFilePath = srcUriStr.slice(srcUriStr.lastIndexOf(":") + 1, srcUriStr.lastIndexOf("!")) val resources = Using.resource { - try { - java.util.zip.ZipFile(jarFilePath) - } catch { + try java.util.zip.ZipFile(jarFilePath) + catch case _: Throwable => // another attempt as a (hopefully temporary) workaround in case a path got URL encoded twice // e.g. a common SNAPSHOT version may contain a '+' sign and look like 'my-lib-0.0.0+123-456-SNAPSHOT' @@ -37,7 +36,6 @@ object ResourceFilePath: // not sure yet which side would need to fix that, for now this gets it working // helps only with a double encoded `+` sign which may be commonly used in snapshot versions java.util.zip.ZipFile(jarFilePath.replace("%252B", "%2B")) - } } { fs => fs .entries() @@ -74,3 +72,5 @@ object ResourceFilePath: inline def value: String = s inline def append(p: String): ResourceFilePath = s + p inline def fileName: String = Path.of(s).getFileName().toString() + +end ResourceFilePath diff --git a/modules/core/shared/src/main/scala/dumbo/internal/CatalogQueries.scala b/modules/core/shared/src/main/scala/dumbo/internal/CatalogQueries.scala index 2536a49..96d9e3d 100644 --- a/modules/core/shared/src/main/scala/dumbo/internal/CatalogQueries.scala +++ b/modules/core/shared/src/main/scala/dumbo/internal/CatalogQueries.scala @@ -4,13 +4,14 @@ package dumbo.internal -import skunk.* -import skunk.codec.all.* -import skunk.implicits.* +import skunk.Query +import skunk.codec.all.name +import skunk.codec.all.text +import skunk.implicits.sql // Queries for schema object discovery, mirroring Flyway's PostgreSQLSchema.doClean() // https://code.yawk.at/org.flywaydb/flyway-core/6.4.0/org/flywaydb/core/internal/database/postgresql/PostgreSQLSchema.java -private[dumbo] object CatalogQueries { +private[dumbo] object CatalogQueries: val listMaterializedViewsQuery: Query[String, String] = sql"""SELECT c.relname @@ -106,4 +107,4 @@ private[dumbo] object CatalogQueries { WHERE sequence_schema = ${text}""" .query(text) -} +end CatalogQueries diff --git a/modules/core/shared/src/main/scala/dumbo/internal/DumboPlatform.scala b/modules/core/shared/src/main/scala/dumbo/internal/DumboPlatform.scala index 6d8faae..08e45f5 100644 --- a/modules/core/shared/src/main/scala/dumbo/internal/DumboPlatform.scala +++ b/modules/core/shared/src/main/scala/dumbo/internal/DumboPlatform.scala @@ -7,8 +7,8 @@ package dumbo.internal import cats.effect.Sync import dumbo.{DumboWithResourcesPartiallyApplied, ResourceFilePath} -private[dumbo] trait DumboPlatform { - inline def withResourcesIn[F[_]: Sync](location: String): DumboWithResourcesPartiallyApplied[F] = { +private[dumbo] trait DumboPlatform: + inline def withResourcesIn[F[_]: Sync](location: String): DumboWithResourcesPartiallyApplied[F] = val resources = ResourceFilePath.fromResourcesDir(location) new DumboWithResourcesPartiallyApplied[F]( ResourceReader.embeddedResources( @@ -17,5 +17,3 @@ private[dumbo] trait DumboPlatform { locationRelative = Some(location), ) ) - } -} diff --git a/modules/core/shared/src/main/scala/dumbo/internal/ResourcesReader.scala b/modules/core/shared/src/main/scala/dumbo/internal/ResourcesReader.scala index f564456..d378871 100644 --- a/modules/core/shared/src/main/scala/dumbo/internal/ResourcesReader.scala +++ b/modules/core/shared/src/main/scala/dumbo/internal/ResourcesReader.scala @@ -15,7 +15,7 @@ import cats.effect.Sync import cats.implicits.* import dumbo.{ResourceFile, ResourceFilePath} -private[dumbo] trait ResourceReader[F[_]] { +private[dumbo] trait ResourceReader[F[_]]: def relativeResourcePath(resource: ResourceFile): String def location: Option[String] @@ -27,38 +27,37 @@ private[dumbo] trait ResourceReader[F[_]] { def readUtf8Lines(path: ResourceFilePath): F[List[String]] def exists(path: ResourceFilePath): F[Boolean] -} -private[dumbo] object ResourceReader { - def fileFs[F[_]: Sync](sourceDir: Path): ResourceReader[F] = { +private[dumbo] object ResourceReader: + def fileFs[F[_]: Sync](sourceDir: Path): ResourceReader[F] = val base = Path.of(new java.io.File("").toURI()) @inline def absolutePath(p: Path) = if p.isAbsolute then p else Path.of(base.toString(), p.toString()) - new ResourceReader[F] { + new ResourceReader[F]: override def relativeResourcePath(resource: ResourceFile): String = absolutePath(sourceDir).relativize(absolutePath(Path.of(resource.path.value))).toString override val location: Option[String] = Some(absolutePath(sourceDir).toString) - override def list: F[List[ResourceFilePath]] = { + override def list: F[List[ResourceFilePath]] = val dir = absolutePath(sourceDir) // checking whether the folder exists for consistent behaviour across JVM and Native platform // by default on the JVM a NoSuchFileException is thrown whereas on Native an empty result is returned - Sync[F].delay(Files.exists(dir)).flatMap { - case true => - Sync[F].delay( - Using.resource(Files.walk(dir))( - _.iterator().asScala - .filter(Files.isRegularFile(_)) - .map(p => ResourceFilePath(p.toString())) - .toList + Sync[F] + .delay(Files.exists(dir)) + .flatMap: + case true => + Sync[F].delay( + Using.resource(Files.walk(dir))( + _.iterator().asScala + .filter(Files.isRegularFile(_)) + .map(p => ResourceFilePath(p.toString())) + .toList + ) ) - ) - case false => - Sync[F].raiseError(new NoSuchFileException(s"Directory ${dir.toString()} was not found")) - } - } + case false => + Sync[F].raiseError(new NoSuchFileException(s"Directory ${dir.toString()} was not found")) def readUtf8Lines(path: ResourceFilePath): F[List[String]] = Sync[F].delay(Files.readAllLines(Path.of(path.value), StandardCharsets.UTF_8).asScala.toList) @@ -68,20 +67,17 @@ private[dumbo] object ResourceReader { override def exists(path: ResourceFilePath): F[Boolean] = Sync[F].delay(Files.exists(absolutePath(Path.of(path.value)))) - } - } def embeddedResources[F[_]: Sync]( readResources: F[List[ResourceFilePath]], locationInfo: Option[String] = None, locationRelative: Option[String] = None, ): ResourceReader[F] = - new ResourceReader[F] { + new ResourceReader[F]: override def relativeResourcePath(resource: ResourceFile): String = - locationRelative match { + locationRelative match case Some(l) => resource.path.value.stripPrefix(s"/$l/") case _ => resource.fileName - } override val location: Option[String] = locationInfo @@ -97,11 +93,12 @@ private[dumbo] object ResourceReader { Sync[F].delay(getClass().getResourceAsStream(path.value) != null) private def readResource[T](path: ResourceFilePath, f: BufferedSource => T): F[T] = - Sync[F].delay(Option(getClass().getResourceAsStream(path.value))).flatMap { - case Some(is) => - Sync[F].delay(Using.resource(Source.fromInputStream(is, StandardCharsets.UTF_8.toString()))(f)) - case None => - Sync[F].raiseError(new NoSuchFileException(s"Resource ${path.toString()} was not found")) - } - } -} + Sync[F] + .delay(Option(getClass().getResourceAsStream(path.value))) + .flatMap: + case Some(is) => + Sync[F].delay(Using.resource(Source.fromInputStream(is, StandardCharsets.UTF_8.toString()))(f)) + case None => + Sync[F].raiseError(new NoSuchFileException(s"Resource ${path.toString()} was not found")) + +end ResourceReader diff --git a/modules/core/shared/src/main/scala/dumbo/internal/Statements.scala b/modules/core/shared/src/main/scala/dumbo/internal/Statements.scala index d8a04f3..3c4bceb 100644 --- a/modules/core/shared/src/main/scala/dumbo/internal/Statements.scala +++ b/modules/core/shared/src/main/scala/dumbo/internal/Statements.scala @@ -6,11 +6,11 @@ package dumbo.internal import scala.annotation.tailrec -private[dumbo] object Statements { - def intoSingleStatements(sql: String): Vector[String] = { +private[dumbo] object Statements: + def intoSingleStatements(sql: String): Vector[String] = @tailrec def build(stmts: Vector[String], stmt: String, sc: Option[(String, Int)], next: Seq[Char]): Vector[String] = - (sc, next) match { + (sc, next) match // single quotes case (None, '\'' +: xs) => build(stmts, stmt + '\'', Some(("'", 0)), xs) case (Some(("'", _)), '\'' +: xs) => build(stmts, stmt + '\'', None, xs) @@ -39,8 +39,7 @@ private[dumbo] object Statements { case (None, ';' +: xs) => build(stmts :+ stmt, "", None, xs) case (s, c +: xs) => build(stmts, stmt + c, s, xs) case _ => stmts :+ stmt - } build(Vector.empty[String], "", None, sql.toCharArray().toIndexedSeq) - } -} + +end Statements diff --git a/modules/core/shared/src/main/scala/dumbo/logging/Logger.scala b/modules/core/shared/src/main/scala/dumbo/logging/Logger.scala index 7b1d165..fe3cb04 100644 --- a/modules/core/shared/src/main/scala/dumbo/logging/Logger.scala +++ b/modules/core/shared/src/main/scala/dumbo/logging/Logger.scala @@ -12,20 +12,18 @@ import cats.implicits.* import cats.{Applicative, FlatMap, Show} import dumbo.logging.LogLevel.{Info, Warn} -trait Logger[F[_]] { +trait Logger[F[_]]: def apply(level: LogLevel, message: => String): F[Unit] final def logInfo(message: => String) = apply(LogLevel.Info, message = message) final def logWarn(message: => String) = apply(LogLevel.Warn, message = message) -} -object Logger { +object Logger: def apply[F[_]](using L: Logger[F]): Logger[F] = L - def noop[F[_]: Applicative] = new Logger[F] { + def noop[F[_]: Applicative] = new Logger[F]: def apply(level: LogLevel, message: => String) = Applicative[F].unit - } private def consolePrintln[F[_]]( console: CatsConsole[F], @@ -33,40 +31,34 @@ object Logger { message: String, level: LogLevel, pretty: Boolean, - ): F[Unit] = { - val formattedMsg = if pretty then { - val lc = level match { + ): F[Unit] = + val formattedMsg = if pretty then + val lc = level match case Info => Console.CYAN case Warn => Console.YELLOW - } - val mc = level match { + val mc = level match case Info => Console.CYAN case Warn => Console.YELLOW - } - val tsStr = timestamp match { + val tsStr = timestamp match case None => "" case Some(ts) => s"${Console.BLUE}${ts.toString()}${Console.RESET} " - } s"$tsStr$lc${level.show}${Console.RESET} $mc$message${Console.RESET}" - } else { - val tsStr = timestamp match { + else + val tsStr = timestamp match case None => "" case Some(ts) => s"${ts.toString()} " - } s"$tsStr${level.show} $message" - } console.println(formattedMsg) - } def fromConsole[F[_]]( console: CatsConsole[F], pretty: Boolean = true, - ) = new Logger[F] { + ) = new Logger[F]: override def apply( ll: LogLevel, @@ -78,12 +70,11 @@ object Logger { level = ll, pretty = pretty, ) - } def fromConsoleWithTimestamp[F[_]: {Clock, FlatMap}]( console: CatsConsole[F], pretty: Boolean = true, - ) = new Logger[F] { + ) = new Logger[F]: override def apply( ll: LogLevel, message: => String, @@ -97,23 +88,19 @@ object Logger { pretty = pretty, ) } - } -} -enum LogLevel { +end Logger + +enum LogLevel: case Info, Warn -} -object LogLevel { - given show: Show[LogLevel] = new Show[LogLevel] { - override def show(t: LogLevel): String = t match { +object LogLevel: + given show: Show[LogLevel] = new Show[LogLevel]: + override def show(t: LogLevel): String = t match case Info => "info" case Warn => "warn" - } - } -} -object Implicits { +object Implicits: given console: [F[_]: CatsConsole] => Logger[F] = Logger.fromConsole(console = CatsConsole[F], pretty = false) @@ -127,4 +114,3 @@ object Implicits { Logger.fromConsoleWithTimestamp(console = CatsConsole[F], pretty = true) given noop: [F[_]: Applicative] => Logger[F] = Logger.noop[F] -} diff --git a/modules/tests-flyway/src/test/scala/DumboFlywaySpec.scala b/modules/tests-flyway/src/test/scala/DumboFlywaySpec.scala index 3b62fce..da3a3df 100644 --- a/modules/tests-flyway/src/test/scala/DumboFlywaySpec.scala +++ b/modules/tests-flyway/src/test/scala/DumboFlywaySpec.scala @@ -16,7 +16,7 @@ import org.flywaydb.core.api.output.{CleanResult, MigrateResult} import skunk.codec.all.* import skunk.implicits.* -trait DumboFlywaySpec extends ffstest.FTest { +trait DumboFlywaySpec extends ffstest.FTest: def db: Db private def flywayInstance(defaultSchema: String, sourcesPath: Path, schemas: List[String]) = @@ -39,17 +39,16 @@ trait DumboFlywaySpec extends ffstest.FTest { def flywayClean(defaultSchema: String, sourcesPath: Path, schemas: List[String] = Nil): IO[CleanResult] = IO(flywayInstance(defaultSchema, sourcesPath, schemas).clean()) - def assertEqualHistory(histA: List[HistoryEntry], histB: List[HistoryEntry]): Unit = { + def assertEqualHistory(histA: List[HistoryEntry], histB: List[HistoryEntry]): Unit = def toCompare(h: HistoryEntry) = (h.installedRank, h.version, h.script, h.checksum, h.`type`, h.installedBy, h.success) assertEquals(histA.map(toCompare), histB.map(toCompare)) - } def assertEqualSQLHistory(histA: List[HistoryEntry], histB: List[HistoryEntry]): Unit = assertEqualHistory(histA.filter(_.`type` != "SCHEMA"), histB.filter(_.`type` != "SCHEMA")) - dbTest("Same behaviour on changed checksum") { + dbTest("Same behaviour on changed checksum"): val schema = "schema_1" for @@ -60,9 +59,8 @@ trait DumboFlywaySpec extends ffstest.FTest { dumboRes <- dumboMigrate(schema, dumboWithResources("db/test_0_changed_checksum")).attempt _ = assert(dumboRes.left.exists(_.getMessage().contains("checksum mismatch"))) yield () - } - dbTest("Same behaviour on missing file") { + dbTest("Same behaviour on missing file"): val schema = "schema_1" for @@ -74,18 +72,16 @@ trait DumboFlywaySpec extends ffstest.FTest { _ = assert(dumboRes.left.exists(_.isInstanceOf[dumbo.exception.DumboValidationException])) _ = assert(dumboRes.left.exists(_.getMessage().contains("Detected applied migration not resolved locally"))) yield () - } - dbTest("Same behaviour on failing migration") { + dbTest("Same behaviour on failing migration"): val schema = "schema_1" for flywayRes <- flywayMigrate(schema, Path("db/test_failing_sql")).attempt _ = assert(flywayRes.isLeft) // Flyway does not provide more specific error message with CockroachDB in this case - _ = if Set[Db](Db.Postgres(16), Db.Postgres(11)).contains(db) then { - assert(flywayRes.left.exists(_.getMessage().contains("relation \"test\" already exists"))) - } + _ = if Set[Db](Db.Postgres(16), Db.Postgres(11)).contains(db) then + assert(flywayRes.left.exists(_.getMessage().contains("relation \"test\" already exists"))) historyFlyway <- loadHistory(schema).map(h => db match { case Db.Postgres(_) => h @@ -105,9 +101,8 @@ trait DumboFlywaySpec extends ffstest.FTest { historyDumbo <- loadHistory(schema) _ = assertEqualHistory(historyFlyway, historyDumbo) yield () - } - dbTest("Dumbo is compatible with Flyway history state") { + dbTest("Dumbo is compatible with Flyway history state"): val path: Path = Path("db/test_1") val withResources = dumboWithResources("db/test_1") val withResourcesB = dumboWithResources("db/test_1_extended") @@ -125,9 +120,8 @@ trait DumboFlywaySpec extends ffstest.FTest { _ <- assertIO(dumboMigrate(defaultSchema, withResourcesB).map(_.migrationsExecuted), 1) _ <- assertIO(loadHistory(defaultSchema).map(_.length), histB.length + 1) // history extended yield () - } - dbTest("Flyway is compatible with Dumbo history state") { + dbTest("Flyway is compatible with Dumbo history state"): val path = Path("db/test_1") val pathB = Path("db/test_1_extended") val withResources = dumboWithResources("db/test_1") @@ -145,9 +139,8 @@ trait DumboFlywaySpec extends ffstest.FTest { _ <- assertIO(flywayMigrate(defaultSchema, pathB).map(_.migrationsExecuted), 1) _ <- assertIO(loadHistory(defaultSchema).map(_.length), histB.length + 1) // history extended yield () - } - dbTest("Compatible with nested directories on reading from resources") { + dbTest("Compatible with nested directories on reading from resources"): val schema = "schema_1" for @@ -158,9 +151,8 @@ trait DumboFlywaySpec extends ffstest.FTest { historyDumbo <- loadHistory(schema) _ = assertEqualHistory(historyDumbo, historyFlyway) yield () - } - dbTest("Compatible with nested directories on reading from filesystem") { + dbTest("Compatible with nested directories on reading from filesystem"): val schema = "schema_1" for @@ -173,11 +165,10 @@ trait DumboFlywaySpec extends ffstest.FTest { historyDumbo <- loadHistory(schema) _ = assertEqualHistory(historyDumbo, historyFlyway) yield () - } - dbTest("Dumbo updates history entry of latest unsucessfully applied migration by Flyway") { + dbTest("Dumbo updates history entry of latest unsucessfully applied migration by Flyway"): // run on CockroachDb only just because it was the easiest way to reproduce a history record for an unsuccessfully applied migration with Flyway - if db == Db.CockroachDb then { + if db == Db.CockroachDb then val schema = "schema_1" for @@ -190,10 +181,9 @@ trait DumboFlywaySpec extends ffstest.FTest { _ = assertEquals(historyB.length, 3) _ = assertEquals(historyB.last.success, true) // last entry was updated yield () - } else IO.println(s"${AnsiColor.YELLOW}Skipped${AnsiColor.RESET}") - } + else IO.println(s"${AnsiColor.YELLOW}Skipped${AnsiColor.RESET}") - dbTest("Run repeatable migrations at the end and on changes in order by description") { + dbTest("Run repeatable migrations at the end and on changes in order by description"): val sD = "schema_dumbo" val sF = "schema_flyway" @@ -212,9 +202,8 @@ trait DumboFlywaySpec extends ffstest.FTest { _ <- assertIO(flywayMigrate(sF, Path("db/test_repeatable_modified")).map(_.migrationsExecuted), 2) _ <- loadHistory(sD).product(loadHistory(sF)).map(t => assertEqualSQLHistory(t._1, t._2)) yield () - } - dbTest("Updates for different default schemas from Flyway to Dumbo") { + dbTest("Updates for different default schemas from Flyway to Dumbo"): val path: Path = Path("db/test_1") val withResources = dumboWithResources("db/test_1") val schemaA = "test_a" @@ -230,9 +219,8 @@ trait DumboFlywaySpec extends ffstest.FTest { _ = assertEquals(resDumboA.migrationsExecuted, 0) _ = assertEquals(resDumboB.migrationsExecuted, 0) yield () - } - dbTest("Updates for different default schemas from Dumbo to Flyway") { + dbTest("Updates for different default schemas from Dumbo to Flyway"): val path: Path = Path("db/test_1") val withResources = dumboWithResources("db/test_1") val schemaA = "test_a" @@ -249,9 +237,8 @@ trait DumboFlywaySpec extends ffstest.FTest { _ = assertEquals(resFlywayA.migrationsExecuted, 0) _ = assertEquals(resFlywayB.migrationsExecuted, 0) yield () - } - dbTest("Updates for multiple schemas with missing schema config") { + dbTest("Updates for multiple schemas with missing schema config"): val path: Path = Path("db/test_three_schemas") val withResources = dumboWithResources("db/test_three_schemas") val schemas = NonEmptyList.of("schema_1", "schema_2") @@ -273,9 +260,8 @@ trait DumboFlywaySpec extends ffstest.FTest { dumboHistory <- loadHistory(schemas.head) _ = assertEqualHistory(flywayHistory, dumboHistory) yield () - } - dbTest("Updates for multiple schemas") { + dbTest("Updates for multiple schemas"): val path: Path = Path("db/test_three_schemas") val withResources = dumboWithResources("db/test_three_schemas") val schemas = NonEmptyList.of("schema_1", "schema_2", "schema_3") @@ -290,9 +276,8 @@ trait DumboFlywaySpec extends ffstest.FTest { dumboHistory <- loadHistory(schemas.head) _ = assertEqualHistory(flywayHistory, dumboHistory) yield () - } - dbTest("Same behaviour on non-transactional operations") { + dbTest("Same behaviour on non-transactional operations"): val path: Path = Path("db/test_non_transactional") val withResources = dumboWithResources("db/test_non_transactional") val schema = "schema_1" @@ -311,14 +296,13 @@ trait DumboFlywaySpec extends ffstest.FTest { flywayRes.swap.toOption.get, dumboRes.swap.toOption.get, ).map(_.getMessage().toLowerCase().linesIterator).foreach { lines => - db match { + db match case Db.Postgres(11) => assert(lines.exists(_.matches(".*alter type .* cannot run inside a transaction block.*"))) case Db.Postgres(_) => assert(lines.exists(_.matches(""".*unsafe use of new value ".*" of enum type.*"""))) case Db.CockroachDb => assert(lines.exists(_.matches(""".*enum value ".*" is not yet public.*"""))) - } } _ = assertEqualHistory(flywayHistory, dumboHistory) yield () @@ -326,7 +310,6 @@ trait DumboFlywaySpec extends ffstest.FTest { IO.println( s"${AnsiColor.YELLOW}[$db] Skipping test 'Same behaviour on non-transactional operations' as Flyway can't run the statements in a transaction${AnsiColor.RESET}" ) - } // Returns sorted list of (object_type, object_name) for all user objects in a schema def schemaObjects(schema: String): IO[List[(String, String)]] = @@ -376,7 +359,7 @@ trait DumboFlywaySpec extends ffstest.FTest { )(schema).map(_.nonEmpty) ) - dbTest("Dumbo clean leaves schema in same state as Flyway clean") { + dbTest("Dumbo clean leaves schema in same state as Flyway clean"): val schema = "schema_clean_test" val path: Path = Path("db/test_1") val withResources = dumboWithResources("db/test_1") @@ -396,9 +379,8 @@ trait DumboFlywaySpec extends ffstest.FTest { _ = assertEquals(dumboObjs, flywayObjs) _ = assertEquals(dumboExists, flywayExists) yield () - } - dbTest("Dumbo clean allows Flyway to re-migrate") { + dbTest("Dumbo clean allows Flyway to re-migrate"): val schema = "schema_clean_compat" val path: Path = Path("db/test_1") val withResources = dumboWithResources("db/test_1") @@ -412,9 +394,8 @@ trait DumboFlywaySpec extends ffstest.FTest { flywayRes <- flywayMigrate(schema, path) _ = assertEquals(flywayRes.migrationsExecuted, 4) yield () - } - dbTest("Flyway clean allows Dumbo to re-migrate") { + dbTest("Flyway clean allows Dumbo to re-migrate"): val schema = "schema_clean_compat2" val path: Path = Path("db/test_1") val withResources = dumboWithResources("db/test_1") @@ -428,9 +409,8 @@ trait DumboFlywaySpec extends ffstest.FTest { dumboRes <- dumboMigrate(schema, withResources) _ = assertEquals(dumboRes.migrationsExecuted, 4) yield () - } - dbTest("Same behavior on copy") { + dbTest("Same behavior on copy"): val path: Path = Path("db/test_copy_to") val withResources = dumboWithResources("db/test_copy_to") val schema = "schema_1" @@ -442,25 +422,21 @@ trait DumboFlywaySpec extends ffstest.FTest { dumboRes <- dumboMigrate(schema, withResources).attempt _ = assert(dumboRes.left.exists(_.isInstanceOf[skunk.exception.CopyNotSupportedException])) yield () - } -} -enum Db { +end DumboFlywaySpec + +enum Db: case Postgres(version: Int) case CockroachDb -} -class DumboFlywaySpecPostgresLatest extends DumboFlywaySpec { +class DumboFlywaySpecPostgresLatest extends DumboFlywaySpec: override val db: Db = Db.Postgres(17) override val postgresPort: Int = 5433 -} -class DumboFlywaySpecPostgres11 extends DumboFlywaySpec { +class DumboFlywaySpecPostgres11 extends DumboFlywaySpec: override val db: Db = Db.Postgres(11) override val postgresPort: Int = 5435 -} -class DumboFlywaySpecCockroachDb extends DumboFlywaySpec { +class DumboFlywaySpecCockroachDb extends DumboFlywaySpec: override val db: Db = Db.CockroachDb override val postgresPort: Int = 5437 -} diff --git a/modules/tests/jvm/src/test/scala/DumboJvmSpec.scala b/modules/tests/jvm/src/test/scala/DumboJvmSpec.scala index 2c6de9f..bda472e 100644 --- a/modules/tests/jvm/src/test/scala/DumboJvmSpec.scala +++ b/modules/tests/jvm/src/test/scala/DumboJvmSpec.scala @@ -7,16 +7,12 @@ import cats.data.Validated.{Invalid, Valid} import cats.effect.IO import cats.implicits.* -class DumboJvmSpec extends ffstest.FTest { +class DumboJvmSpec extends ffstest.FTest: - test("find resource in main") { + test("find resource in main"): for result <- Dumbo.withResourcesIn[IO]("main").listMigrationFiles - _ = result match { + _ = result match case Valid(f) => assert(f.exists(_.path.fileName.toString == "V1__dummy.sql")) case Invalid(errs) => fail(errs.toList.mkString("\n")) - } yield () - } - -} diff --git a/modules/tests/jvm/src/test/scala/ffstest/FFrameworkPlatform.scala b/modules/tests/jvm/src/test/scala/ffstest/FFrameworkPlatform.scala index ae0d852..1628b34 100644 --- a/modules/tests/jvm/src/test/scala/ffstest/FFrameworkPlatform.scala +++ b/modules/tests/jvm/src/test/scala/ffstest/FFrameworkPlatform.scala @@ -9,7 +9,6 @@ import dumbo.{Dumbo, DumboWithResourcesPartiallyApplied} import fs2.io.file.Path import munit.CatsEffectSuite -trait FTestPlatform extends CatsEffectSuite { +trait FTestPlatform extends CatsEffectSuite: inline def dumboWithResources(path: String): DumboWithResourcesPartiallyApplied[IO] = Dumbo.withResourcesIn(path) def dumboWithFiles(path: Path): DumboWithResourcesPartiallyApplied[IO] = Dumbo.withFilesIn(path) -} diff --git a/modules/tests/shared/src/test/scala/DumboMigrationSpec.scala b/modules/tests/shared/src/test/scala/DumboMigrationSpec.scala index cbe1fb6..bd27f87 100644 --- a/modules/tests/shared/src/test/scala/DumboMigrationSpec.scala +++ b/modules/tests/shared/src/test/scala/DumboMigrationSpec.scala @@ -18,17 +18,16 @@ import org.typelevel.otel4s.trace.Tracer.Implicits.noop import skunk.codec.all.* import skunk.implicits.* -trait DumboMigrationSpec extends ffstest.FTest { +trait DumboMigrationSpec extends ffstest.FTest: def db: Db - def assertEqualHistory(histA: List[HistoryEntry], histB: List[HistoryEntry]): Unit = { + def assertEqualHistory(histA: List[HistoryEntry], histB: List[HistoryEntry]): Unit = def toCompare(h: HistoryEntry) = (h.installedRank, h.version, h.script, h.checksum, h.`type`, h.installedBy, h.success) assertEquals(histA.map(toCompare), histB.map(toCompare)) - } - dbTest("Execute multiple migrations in parallel") { + dbTest("Execute multiple migrations in parallel"): (1 to 5).toList.traverse_ { _ => val schema = someSchemaName val withResources = dumboWithResources("db/test_1") @@ -36,16 +35,17 @@ trait DumboMigrationSpec extends ffstest.FTest { for lockSupport <- session().use(Dumbo.detectLockSupport(_)) // in case of missing XactAdvisoryLock support like under CockroachDB run initSession to initialize the schemas and history table beforehand - _ <- if !lockSupport.contains(LockSupport.XactAdvisoryLock) then { - withResources - .apply( - connection = connectionConfig, - defaultSchema = schema, - schemas = Set(schema), - ) - .initSession - .use_ - } else IO.unit + _ <- + if !lockSupport.contains(LockSupport.XactAdvisoryLock) then + withResources + .apply( + connection = connectionConfig, + defaultSchema = schema, + schemas = Set(schema), + ) + .initSession + .use_ + else IO.unit res <- (1 to 20).toList.parTraverse(_ => dumboMigrate(schema, withResources)) ranks = res.flatMap(_.migrations.map(_.installedRank)).sorted _ = assertEquals(ranks, List(1, 2, 3, 4)) @@ -53,9 +53,8 @@ trait DumboMigrationSpec extends ffstest.FTest { _ = assert(history.length == 5) yield () } - } - dbTest("Validate checksum with validation enabled") { + dbTest("Validate checksum with validation enabled"): val schema = someSchemaName for @@ -64,14 +63,12 @@ trait DumboMigrationSpec extends ffstest.FTest { _ = assert(res.isLeft) _ = assert(res.left.exists(_.getMessage().contains("checksum mismatch"))) vRes <- validateWithAppliedMigrations(schema, dumboWithResources("db/test_0_changed_checksum")) - _ = vRes match { + _ = vRes match case Invalid(errs) => assert(errs.toList.exists(_.getMessage().contains("checksum mismatch"))) case _ => fail("expected failure") - } yield () - } - dbTest("Validate description with validation enabled") { + dbTest("Validate description with validation enabled"): val schema = someSchemaName for @@ -85,7 +82,7 @@ trait DumboMigrationSpec extends ffstest.FTest { message.contains("test base") }) vRes <- validateWithAppliedMigrations(schema, dumboWithResources("db/test_0_desc_changed")) - _ = vRes match { + _ = vRes match case Invalid(errs) => assert(errs.exists { err => val message = err.getMessage() @@ -94,11 +91,9 @@ trait DumboMigrationSpec extends ffstest.FTest { message.contains("test base") }) case _ => fail("expected failure") - } yield () - } - dbTest("Validate for missing files with validation enabled") { + dbTest("Validate for missing files with validation enabled"): val schema = someSchemaName for @@ -108,15 +103,13 @@ trait DumboMigrationSpec extends ffstest.FTest { _ = assert(res.left.exists(_.isInstanceOf[dumbo.exception.DumboValidationException])) _ = assert(res.left.exists(_.getMessage().contains("Detected applied migration not resolved locally"))) vRes <- validateWithAppliedMigrations(schema, dumboWithResources("db/test_0_missing_file")) - _ = vRes match { + _ = vRes match case Invalid(errs) => assert(errs.toList.exists(_.getMessage().contains("Detected applied migration not resolved locally"))) case _ => fail("expected failure") - } yield () - } - dbTest("Ignore missing files or missing checksum on validation disabled") { + dbTest("Ignore missing files or missing checksum on validation disabled"): val schema = someSchemaName for @@ -126,9 +119,8 @@ trait DumboMigrationSpec extends ffstest.FTest { resC <- dumboMigrate(schema, dumboWithResources("db/test_0_desc_changed"), validateOnMigrate = false).attempt _ = assert(resA.isRight && resB.isRight && resC.isRight) yield () - } - dbTest("Fail with CopyNotSupportedException") { + dbTest("Fail with CopyNotSupportedException"): val schema = someSchemaName for @@ -137,9 +129,8 @@ trait DumboMigrationSpec extends ffstest.FTest { dumboResB <- dumboMigrate(schema, dumboWithResources("db/test_copy_to")).attempt _ = assert(dumboResB.left.exists(_.isInstanceOf[skunk.exception.CopyNotSupportedException])) yield () - } - dbTest("Fail on non-transactional operations") { + dbTest("Fail on non-transactional operations"): val withResources = dumboWithResources("db/test_non_transactional") val schema = someSchemaName @@ -147,18 +138,16 @@ trait DumboMigrationSpec extends ffstest.FTest { dumboRes <- dumboMigrate(schema, withResources).attempt _ = assert(dumboRes.isLeft) errLines = dumboRes.swap.toOption.get.getMessage().linesIterator - _ = db match { + _ = db match case Db.Postgres(11) => assert(errLines.exists(_.matches(".*ALTER TYPE .* cannot run inside a transaction block.*"))) case Db.Postgres(_) => assert(errLines.exists(_.matches(""".*Unsafe use of new value ".*" of enum type.*"""))) case Db.CockroachDb => assert(errLines.exists(_.matches(".*enum value is not yet public."))) - } yield () - } - dbTest("Fail on non-transactional operations") { + dbTest("Fail on non-transactional operations"): val withResources = dumboWithResources("db/test_non_transactional") val schema = someSchemaName @@ -166,18 +155,16 @@ trait DumboMigrationSpec extends ffstest.FTest { dumboRes <- dumboMigrate(schema, withResources).attempt _ = assert(dumboRes.isLeft) errLines = dumboRes.swap.toOption.get.getMessage().linesIterator - _ = db match { + _ = db match case Db.Postgres(11) => assert(errLines.exists(_.matches(".*ALTER TYPE .* cannot run inside a transaction block.*"))) case Db.Postgres(_) => assert(errLines.exists(_.matches(""".*Unsafe use of new value ".*" of enum type.*"""))) case Db.CockroachDb => assert(errLines.exists(_.matches(".*enum value is not yet public."))) - } yield () - } - dbTest("schemas are included in the search path") { + dbTest("schemas are included in the search path"): val withResources = dumboWithResources("db/test_search_path") val schemas = List("schema_1", "schema_2") @@ -187,9 +174,8 @@ trait DumboMigrationSpec extends ffstest.FTest { history <- loadHistory(schemas.head) _ = assert(history.length != 2) yield () - } - dbTest("warn if schemas are not included in the search path for custom sessions") { + dbTest("warn if schemas are not included in the search path for custom sessions"): val withResources = dumboWithResources("db/test_search_path") val schemas = List("schema_1", "schema_2") val testConsole = new TestLogger @@ -224,9 +210,8 @@ trait DumboMigrationSpec extends ffstest.FTest { _ = assert(dumboResC.isRight) _ = assert(!testConsole.logs.get().exists(t => hasWarning(t._1, t._2))) yield () - } - dbTest("migrate by different schema using custom session") { + dbTest("migrate by different schema using custom session"): val withResources = dumboWithResources("db/test_1") val schemaA = "test_a" val schemaB = "test_b" @@ -237,9 +222,8 @@ trait DumboMigrationSpec extends ffstest.FTest { _ = assertEquals(resDumboA.migrationsExecuted, 4) _ = assertEquals(resDumboB.migrationsExecuted, 4) yield () - } - dbTest("default schema is used when no schema is specified in migration sripts") { + dbTest("default schema is used when no schema is specified in migration sripts"): val withResources = dumboWithResources("db/test_default_schema") (1 to 5).toList.traverse_ { _ => @@ -277,37 +261,32 @@ trait DumboMigrationSpec extends ffstest.FTest { _ <- assertDefaultSchemaHasTable yield () } - } - { + locally: val withResources = dumboWithResources("db/test_long_running") def logMatch(l: LogLevel, s: String): Boolean = l == LogLevel.Info && s.startsWith("Awaiting query with pid") - dbTest("don't log on waiting for lock release if under provided duration") { + dbTest("don't log on waiting for lock release if under provided duration"): val testLogger = new TestLogger() for _ <- dumboMigrate("schema_1", withResources, logMigrationStateAfter = 5.second)(using testLogger) _ = assert(testLogger.logs.get().count(t => logMatch(t._1, t._2)) == 0) yield () - } - dbTest("log on waiting for lock release longer than provided duration") { + dbTest("log on waiting for lock release longer than provided duration"): val testLogger = new TestLogger() for _ <- dumboMigrate("schema_1", withResources, logMigrationStateAfter = 800.millis)(using testLogger) - _ = db match { + _ = db match case Db.Postgres(_) => assert(testLogger.logs.get().count(t => logMatch(t._1, t._2)) >= 2) case Db.CockroachDb => assert(testLogger.logs.get().count { case (level, message) => level == LogLevel.Warn && message.startsWith("Progress monitor is not supported") } == 1) - } yield () - } - } - dbTest("Clean drops all objects and allows re-migration") { + dbTest("Clean drops all objects and allows re-migration"): val schema = someSchemaName for @@ -320,9 +299,8 @@ trait DumboMigrationSpec extends ffstest.FTest { res2 <- dumboMigrate(schema, dumboWithResources("db/test_1")) _ = assertEquals(res2.migrationsExecuted, res1.migrationsExecuted) yield () - } - dbTest("Clean on empty schema is idempotent") { + dbTest("Clean on empty schema is idempotent"): val schema = someSchemaName for @@ -332,9 +310,8 @@ trait DumboMigrationSpec extends ffstest.FTest { res <- dumboMigrate(schema, dumboWithResources("db/test_1")) _ = assert(res.migrationsExecuted > 0) yield () - } - dbTest("Clean with multiple schemas") { + dbTest("Clean with multiple schemas"): val schema1 = someSchemaName val schema2 = someSchemaName @@ -345,9 +322,8 @@ trait DumboMigrationSpec extends ffstest.FTest { res <- dumboMigrate(schema1, dumboWithResources("db/test_1"), schemas = List(schema1, schema2)) _ = assert(res.migrationsExecuted > 0) yield () - } - dbTest("Clean fails when cleanDisabled is true") { + dbTest("Clean fails when cleanDisabled is true"): val schema = someSchemaName val withResources = dumboWithResources("db/test_1") @@ -362,32 +338,27 @@ trait DumboMigrationSpec extends ffstest.FTest { _ = assert(result.isLeft) _ = assert(result.left.exists(_.isInstanceOf[exception.DumboCleanException])) yield () - } -} -enum Db { +end DumboMigrationSpec + +enum Db: case Postgres(version: Int) case CockroachDb -} -class DumboSpecPostgresLatest extends DumboMigrationSpec { +class DumboSpecPostgresLatest extends DumboMigrationSpec: override val db: Db = Db.Postgres(17) override val postgresPort: Int = 5432 -} -class DumboSpecPostgres11 extends DumboMigrationSpec { +class DumboSpecPostgres11 extends DumboMigrationSpec: override val db: Db = Db.Postgres(11) override val postgresPort: Int = 5434 -} -object TestTags { +object TestTags: val CockroachDbTest: Tag = new Tag("CockroachDbTest") -} -class DumboSpecCockroachDb extends DumboMigrationSpec { +class DumboSpecCockroachDb extends DumboMigrationSpec: override val db: Db = Db.CockroachDb override val postgresPort: Int = 5436 override def dbTest(name: String)(f: => IO[Unit]): Unit = test(name.tag(TestTags.CockroachDbTest))(dropSchemas >> f) -} diff --git a/modules/tests/shared/src/test/scala/DumboResourcesSpec.scala b/modules/tests/shared/src/test/scala/DumboResourcesSpec.scala index 478fbdd..6a72259 100644 --- a/modules/tests/shared/src/test/scala/DumboResourcesSpec.scala +++ b/modules/tests/shared/src/test/scala/DumboResourcesSpec.scala @@ -10,11 +10,11 @@ import cats.effect.IO import cats.implicits.* import fs2.io.file.Path -class DumboResourcesSpec extends ffstest.FTest { - test("list migration files from resources") { +class DumboResourcesSpec extends ffstest.FTest: + test("list migration files from resources"): for files <- dumboWithResources("db/test_1").listMigrationFiles - _ = files match { + _ = files match case Valid(files) => assert( files.sorted.map(f => (f.version, f.path.fileName.toString)) == List( @@ -25,14 +25,12 @@ class DumboResourcesSpec extends ffstest.FTest { ) ) case Invalid(errs) => fail(errs.toList.mkString("\n")) - } yield () - } - test("list migration files from resources with subirectories") { + test("list migration files from resources with subirectories"): for files <- dumboWithResources("db/nested").listMigrationFiles - _ = files match { + _ = files match case Valid(files) => assert( files.sorted.map(f => (f.version, f.path.fileName.toString)) == List( @@ -45,14 +43,12 @@ class DumboResourcesSpec extends ffstest.FTest { ) ) case Invalid(errs) => fail(errs.toList.mkString("\n")) - } yield () - } - test("list migration files from relative path") { + test("list migration files from relative path"): for files <- Dumbo.withFilesIn[IO](Path("modules/tests/shared/src/test/non_resource/db/test_1")).listMigrationFiles - _ = files match { + _ = files match case Valid(files) => assert( files.sorted.map(f => (f.version, f.path.fileName.toString)) == List( @@ -65,15 +61,13 @@ class DumboResourcesSpec extends ffstest.FTest { ) ) case Invalid(errs) => fail(errs.toList.mkString("\n")) - } yield () - } - test("list migration files from absolute path") { + test("list migration files from absolute path"): for files <- Dumbo.withFilesIn[IO](Path("modules/tests/shared/src/test/non_resource/db/test_1").absolute).listMigrationFiles - _ = files match { + _ = files match case Valid(files) => assert( files.sorted.map(f => (f.version, f.path.fileName.toString)) == List( @@ -86,14 +80,12 @@ class DumboResourcesSpec extends ffstest.FTest { ) ) case Invalid(errs) => fail(errs.toList.mkString("\n")) - } yield () - } - test("fail on files with same versions") { + test("fail on files with same versions"): for result <- dumboWithResources("db/test_duplicate_versions").listMigrationFiles - _ = result match { + _ = result match case Invalid(errs) => assert(errs.toList.exists { err => val message = err.getMessage() @@ -107,15 +99,13 @@ class DumboResourcesSpec extends ffstest.FTest { message.contains("V0.1.0.0__test.sql") }) case _ => fail("expected failure") - } yield () - } - test("handle non-existent directory without NPE") { + test("handle non-existent directory without NPE"): for res <- Dumbo.withFilesIn[IO](Path("/non/existent/directory")).listMigrationFiles.attempt _ = assert(res.isLeft) _ = assert(res.left.exists(_.isInstanceOf[java.nio.file.NoSuchFileException])) yield () - } -} + +end DumboResourcesSpec diff --git a/modules/tests/shared/src/test/scala/DumboUtilSpec.scala b/modules/tests/shared/src/test/scala/DumboUtilSpec.scala index 980e3f3..34c64f2 100644 --- a/modules/tests/shared/src/test/scala/DumboUtilSpec.scala +++ b/modules/tests/shared/src/test/scala/DumboUtilSpec.scala @@ -4,8 +4,8 @@ package dumbo -class DumboUtilSpec extends ffstest.FTest { - test("format execution time") { +class DumboUtilSpec extends ffstest.FTest: + test("format execution time"): List( -100L -> "00:00.000s", -1000L -> "00:00.000s", @@ -23,5 +23,3 @@ class DumboUtilSpec extends ffstest.FTest { ).foreach { case (input, expected) => assertEquals(Dumbo.formatDuration(input), expected, s"input ${input}ms") } - } -} diff --git a/modules/tests/shared/src/test/scala/ResourceFileConfigSpec.scala b/modules/tests/shared/src/test/scala/ResourceFileConfigSpec.scala index 789ec32..df73e36 100644 --- a/modules/tests/shared/src/test/scala/ResourceFileConfigSpec.scala +++ b/modules/tests/shared/src/test/scala/ResourceFileConfigSpec.scala @@ -4,8 +4,8 @@ package dumbo -class ResourceFileConfigSpec extends ffstest.FTest { - test("parse config file") { +class ResourceFileConfigSpec extends ffstest.FTest: + test("parse config file"): assertEquals( ResourceFileConfig.fromLines(List("executeInTransaction=true")), Right[String, Set[ResourceFileConfig]](Set(ResourceFileConfig.ExecuteInTransaction(value = true))), @@ -50,5 +50,5 @@ class ResourceFileConfigSpec extends ffstest.FTest { .fromLines(List("executeInTransaction=abc")), Left("Invalid value for executeInTransaction (should be either true or false): abc"), ) - } -} + +end ResourceFileConfigSpec diff --git a/modules/tests/shared/src/test/scala/ResourceFileDescriptionSpec.scala b/modules/tests/shared/src/test/scala/ResourceFileDescriptionSpec.scala index 3632a49..6ed8eac 100644 --- a/modules/tests/shared/src/test/scala/ResourceFileDescriptionSpec.scala +++ b/modules/tests/shared/src/test/scala/ResourceFileDescriptionSpec.scala @@ -6,8 +6,8 @@ package dumbo import cats.data.NonEmptyList -class ResourceFileDescriptionSpec extends ffstest.FTest { - test("extract source file version from file path") { +class ResourceFileDescriptionSpec extends ffstest.FTest: + test("extract source file version from file path"): val fileNames = List( "R__c_view.sql", "R__a_view.sql", @@ -52,9 +52,8 @@ class ResourceFileDescriptionSpec extends ffstest.FTest { ) assertEquals(versions.sorted.map(_.version), expected) - } - test("distinct by version") { + test("distinct by version"): val fileNames = List( "V1__test.sql", "V01__test.sql", @@ -67,14 +66,13 @@ class ResourceFileDescriptionSpec extends ffstest.FTest { .map(s => ResourceFileDescription.fromResourcePath(ResourceFilePath(s))) .collect { case Right(r) => r } - val versionsDistinct = versions.distinct.collect { + val versionsDistinct = versions.distinct.collect: case ResourceFileDescription(v: ResourceVersion.Versioned, _, _) => v - } val versionsSet = versions.collect { case ResourceFileDescription(v: ResourceVersion.Versioned, _, _) => v }.toSet assertEquals(versions.length, 5) assertEquals(versionsDistinct.map(_.parts.head), List(1L)) assertEquals(versionsSet.map(_.parts.head), Set(1L)) - } -} + +end ResourceFileDescriptionSpec diff --git a/modules/tests/shared/src/test/scala/StatementsParserSpec.scala b/modules/tests/shared/src/test/scala/StatementsParserSpec.scala index c1ae570..e9718ee 100644 --- a/modules/tests/shared/src/test/scala/StatementsParserSpec.scala +++ b/modules/tests/shared/src/test/scala/StatementsParserSpec.scala @@ -4,9 +4,9 @@ package dumbo.internal -class StatementsSpec extends ffstest.FTest { +class StatementsSpec extends ffstest.FTest: - test("split into single statements") { + test("split into single statements"): val res = Statements.intoSingleStatements( """ |INSERT INTO test (template) VALUES ('T2'); @@ -133,6 +133,5 @@ class StatementsSpec extends ffstest.FTest { |;*/ |select 1""".stripMargin, ) - } -} +end StatementsSpec diff --git a/modules/tests/shared/src/test/scala/ffstest/FFramework.scala b/modules/tests/shared/src/test/scala/ffstest/FFramework.scala index 7994109..bacd0ed 100644 --- a/modules/tests/shared/src/test/scala/ffstest/FFramework.scala +++ b/modules/tests/shared/src/test/scala/ffstest/FFramework.scala @@ -23,17 +23,16 @@ import skunk.Session import skunk.Session.Credentials import skunk.implicits.* -trait FTest extends CatsEffectSuite with FTestPlatform { +trait FTest extends CatsEffectSuite with FTestPlatform: given noopMeter: Meter[IO] = Meter.noop[IO] def postgresPort: Int = 5432 def dbTest(name: String)(f: => IO[Unit]): Unit = test(name)(dropSchemas >> f) // note: schema name should not start with "pg_" or "crdb_" to avoid conflicts with reserved ones - def someSchemaName: String = { + def someSchemaName: String = val chars = "abcdefghijklmnopqrstuvwxyz" LazyList.continually(chars.charAt(Random.nextInt(chars.length))).take(15).mkString - } lazy val connectionConfig: ConnectionConfig = ConnectionConfig( host = "localhost", @@ -105,14 +104,13 @@ trait FTest extends CatsEffectSuite with FTestPlatform { defaultSchema: String, withResources: DumboWithResourcesPartiallyApplied[IO], schemas: List[String] = Nil, - ): IO[ValidatedNec[DumboValidationException, Unit]] = { + ): IO[ValidatedNec[DumboValidationException, Unit]] = import dumbo.logging.Implicits.consolePrettyWithTimestamp withResources( connection = connectionConfig, defaultSchema = defaultSchema, schemas = schemas.toSet, ).runValidationWithHistory - } def dumboClean( defaultSchema: String, @@ -144,9 +142,10 @@ trait FTest extends CatsEffectSuite with FTestPlatform { _ <- IO.println(s"Schema drop result ${c.mkString(", ")}") yield () } -} -class TestLogger extends Logger[IO] { +end FTest + +class TestLogger extends Logger[IO]: private val underlying = Logger.fromConsoleWithTimestamp(console = Console[IO], pretty = true) val logs: AtomicReference[Vector[(LogLevel, String)]] = new AtomicReference(Vector.empty) @@ -156,4 +155,3 @@ class TestLogger extends Logger[IO] { override def apply(level: LogLevel, message: => String): IO[Unit] = underlying.apply(level, message) *> IO { logs.getAndUpdate(_ :+ (level, message)) }.void -}