Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

read resources recursively #102

Merged
merged 2 commits into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions modules/core/jvm/src/main/scala-2/dumbo/ResourceFilePath.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ final case class ResourceFilePath(value: String) extends AnyVal {
}

object ResourceFilePath {
@scala.annotation.tailrec
private def listRec(dirs: List[File], files: List[File]): List[File] =
dirs match {
case x :: xs =>
val (d, f) = x.listFiles().toList.partition(_.isDirectory())
listRec(d ::: xs, f ::: files)
case Nil => files
}

private[dumbo] def fromResourcesDir[F[_]: Sync](location: String): (String, F[List[ResourceFilePath]]) =
Try(getClass().getClassLoader().getResources(location).asScala.toList) match {
case Failure(err) => ("", Sync[F].raiseError(err))
Expand All @@ -33,10 +42,9 @@ object ResourceFilePath {
Sync[F].delay {
val base = Paths.get(url.toURI())
val resources =
new File(base.toString())
.list()
.map(fileName => ResourceFilePath(s"/$location/$fileName"))
.toList
listRec(List(new File(base.toString())), Nil).map(f =>
ResourceFilePath(s"/$location/${base.relativize(Paths.get(f.getAbsolutePath()))}")
)
resources
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ private[dumbo] trait DumboPlatform {
val (locationInfo, resources) = ResourceFilePath.fromResourcesDir(location)

new DumboWithResourcesPartiallyApplied[F](
ResourceReader.embeddedResources(resources, Some(locationInfo))
ResourceReader.embeddedResources(
readResources = resources,
locationInfo = Some(locationInfo),
locationRelative = Some(location),
)
)
}
}
13 changes: 11 additions & 2 deletions modules/core/shared/src/main/scala-3/dumbo/ResourceFilePath.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,18 @@ object ResourceFilePath:

Expr(resources)
else
@scala.annotation.tailrec
def listRec(dirs: List[File], files: List[File]): List[File] =
dirs match
case x :: xs =>
val (d, f) = x.listFiles().toList.partition(_.isDirectory())
listRec(d ::: xs, f ::: files)
case Nil => files

val base = Paths.get(head.toURI())
val resources =
new File(base.toString()).list().map(fileName => s"/$location/$fileName").toList
val resources = listRec(List(File(base.toString())), Nil).map(f =>
s"/$location/${base.relativize(Paths.get(f.getAbsolutePath()))}"
)
Expr(resources)
case Nil => report.errorAndAbort(s"resource ${location} was not found")
case multiple =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import dumbo.{DumboWithResourcesPartiallyApplied, ResourceFilePath}
private[dumbo] trait DumboPlatform {
inline def withResourcesIn[F[_]: Sync](location: String): DumboWithResourcesPartiallyApplied[F] = {
val resources = ResourceFilePath.fromResourcesDir(location)
new DumboWithResourcesPartiallyApplied[F](ResourceReader.embeddedResources(Sync[F].pure(resources)))
new DumboWithResourcesPartiallyApplied[F](
ResourceReader.embeddedResources(
readResources = Sync[F].pure(resources),
locationInfo = Some(location),
locationRelative = Some(location),
)
)
}
}
12 changes: 9 additions & 3 deletions modules/core/shared/src/main/scala/dumbo/Dumbo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,19 @@ class Dumbo[F[_]: Sync: Console](
version = source.versionText,
description = source.scriptDescription,
`type` = "SQL",
script = source.path.fileName,
script = historyScriptPath(source),
checksum = Some(source.checksum),
executionTimeMs = duration.toMillis.toInt,
success = true,
)
}

private def historyScriptPath(resource: ResourceFile) =
resReader.locationRel match {
case Some(loc) => resource.path.value.stripPrefix(s"/$loc/")
case _ => resource.fileName
}

private def validationGuard(session: Session[F], resources: ResourceFiles) =
if (resources.nonEmpty) {
session
Expand Down Expand Up @@ -446,13 +452,13 @@ class Dumbo[F[_]: Sync: Console](
resources: ResourceFiles,
): ValidatedNec[DumboValidationException, Unit] = {
val versionedMap: Map[String, ResourceFile] = resources.versioned.map { case (v, f) => (v.text, f) }.toMap
val repeatablesFileNames: Set[String] = resources.repeatable.map(_._2.fileName).toSet
val repeatablesScriptNames: Set[String] = resources.repeatable.map(_._2.path.value).toSet

history
.filter(_.`type` == "SQL")
.traverse { h =>
versionedMap.get(h.version.getOrElse("")) match {
case None if !repeatablesFileNames.contains(h.script) =>
case None if !repeatablesScriptNames.exists(_.endsWith(h.script)) =>
new DumboValidationException(s"Detected applied migration not resolved locally ${h.script}")
.invalidNec[Unit]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@

package dumbo.internal

import java.io.File

import cats.effect.Sync
import cats.implicits.*
import dumbo.ResourceFilePath
import fs2.io.file.{Files as Fs2Files, Flags, Path}
import fs2.{Stream, text}

private[dumbo] trait ResourceReader[F[_]] {
// relative location info e.g. "db/migration"
def locationRel: Option[String]

// relative or absolute location info e.g. "file:/project/resources/db/migration"
def location: Option[String]

def list: fs2.Stream[F, ResourceFilePath]
Expand All @@ -28,12 +34,22 @@ private[dumbo] object ResourceReader {

@inline def absolutePath(p: Path) = if (p.isAbsolute) p else base / p

@scala.annotation.tailrec
def listRec(dirs: List[File], files: List[File]): List[File] =
dirs match {
case x :: xs =>
val (d, f) = x.listFiles().toList.partition(_.isDirectory())
listRec(d ::: xs, f ::: files)
case Nil => files
}

new ResourceReader[F] {
override val location: Option[String] = Some(absolutePath(sourceDir).toString)
override val locationRel: Option[String] = Some(sourceDir.toString)
override val location: Option[String] = Some(absolutePath(sourceDir).toString)
override def list: Stream[F, ResourceFilePath] =
Fs2Files[F]
.list(absolutePath(sourceDir))
.map(p => ResourceFilePath(p.toString))
Stream.emits(
listRec(List(new File(absolutePath(sourceDir).toString)), Nil).map(f => ResourceFilePath(f.getPath()))
)

override def readUtf8Lines(path: ResourceFilePath): Stream[F, String] =
Fs2Files[F].readUtf8Lines(absolutePath(Path(path.value)))
Expand All @@ -48,8 +64,11 @@ private[dumbo] object ResourceReader {
def embeddedResources[F[_]: Sync](
readResources: F[List[ResourceFilePath]],
locationInfo: Option[String] = None,
locationRelative: Option[String] = None,
): ResourceReader[F] =
new ResourceReader[F] {
override val locationRel: Option[String] = locationRelative

override val location: Option[String] = locationInfo

override def list: Stream[F, ResourceFilePath] = Stream.evals(readResources)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE TABLE test_c();
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE TABLE test_d();
6 changes: 4 additions & 2 deletions modules/test-lib/src/main/scala/TestLib.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import dumbo.ResourceFilePath

@main def run(args: String*) =
val resources = ResourceFilePath.fromResourcesDir("sample_lib")
val expected = List(
val resources = ResourceFilePath.fromResourcesDir("sample_lib").map(_.value).toSet
val expected = Set(
"/sample_lib/sub_dir/sub_sub_dir/V4__test_d.sql",
"/sample_lib/sub_dir/V3__test_c.sql",
"/sample_lib/V1__test.sql",
"/sample_lib/V2__test_b.sql",
)
Expand Down
13 changes: 13 additions & 0 deletions modules/tests-flyway/src/test/scala/DumboFlywaySpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,19 @@ trait DumboFlywaySpec extends ffstest.FTest {
} yield ()
}

dbTest("Compatible with nested directories") {
val schema = "schema_1"

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 ()
}

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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE TABLE test (id SERIAL PRIMARY KEY);
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE TABLE test_a();
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE TABLE test_b();
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE TABLE test_f();
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE TABLE test_d();
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE TABLE test_e();
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE TABLE test_c();
34 changes: 32 additions & 2 deletions modules/tests/shared/src/test/scala/DumboResourcesSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,39 @@ class DumboResourcesSpec extends ffstest.FTest {
} yield ()
}

test("list migration files from resources with subirectories") {
for {
files <- dumboWithResources("db/nested").listMigrationFiles
_ = files match {
case Valid(files) =>
assert(
files.sorted.map(f => (f.version, f.path.fileName.toString)) == List(
(ResourceVersion.Versioned("1", NonEmptyList.of(1)), "V1__test.sql"),
(ResourceVersion.Versioned("2", NonEmptyList.of(2)), "V2__test.sql"),
(ResourceVersion.Versioned("3", NonEmptyList.of(3)), "V3__test.sql"),
(ResourceVersion.Versioned("4", NonEmptyList.of(4)), "V4__test.sql"),
(ResourceVersion.Repeatable("a"), "R__a.sql"),
(ResourceVersion.Repeatable("b"), "R__b.sql"),
)
)
case Invalid(errs) => fail(errs.toList.mkString("\n"))
}
} yield ()
}

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 {
case Valid(files) =>
assert(
files.sorted.map(f => (f.version, f.path.fileName.toString)) == List(
(ResourceVersion.Versioned("1", NonEmptyList.of(1)), "V1__non_resource.sql")
(ResourceVersion.Versioned("1", NonEmptyList.of(1)), "V1__non_resource.sql"),
(ResourceVersion.Versioned("2", NonEmptyList.of(2)), "V2__non_resource.sql"),
(ResourceVersion.Versioned("3", NonEmptyList.of(3)), "V3__non_resource.sql"),
(ResourceVersion.Versioned("4", NonEmptyList.of(4)), "V4__non_resource.sql"),
(ResourceVersion.Repeatable("a"), "R__a.sql"),
(ResourceVersion.Repeatable("b"), "R__b.sql"),
)
)
case Invalid(errs) => fail(errs.toList.mkString("\n"))
Expand All @@ -52,7 +77,12 @@ class DumboResourcesSpec extends ffstest.FTest {
case Valid(files) =>
assert(
files.sorted.map(f => (f.version, f.path.fileName.toString)) == List(
(ResourceVersion.Versioned("1", NonEmptyList.of(1)), "V1__non_resource.sql")
(ResourceVersion.Versioned("1", NonEmptyList.of(1)), "V1__non_resource.sql"),
(ResourceVersion.Versioned("2", NonEmptyList.of(2)), "V2__non_resource.sql"),
(ResourceVersion.Versioned("3", NonEmptyList.of(3)), "V3__non_resource.sql"),
(ResourceVersion.Versioned("4", NonEmptyList.of(4)), "V4__non_resource.sql"),
(ResourceVersion.Repeatable("a"), "R__a.sql"),
(ResourceVersion.Repeatable("b"), "R__b.sql"),
)
)
case Invalid(errs) => fail(errs.toList.mkString("\n"))
Expand Down
Loading