first messy db test working
This commit is contained in:
parent
8d0567195a
commit
f10f79ed95
7 changed files with 110 additions and 6 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -2,3 +2,7 @@ target/
|
||||||
project/target/
|
project/target/
|
||||||
project/project/
|
project/project/
|
||||||
.version
|
.version
|
||||||
|
|
||||||
|
# the test directory is used when running tests as an ephemeral dumping ground
|
||||||
|
test/*
|
||||||
|
!test/README.md
|
||||||
|
|
|
||||||
|
|
@ -55,12 +55,17 @@ val Deps = new {
|
||||||
}
|
}
|
||||||
|
|
||||||
val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.2.1"
|
val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.2.1"
|
||||||
|
|
||||||
|
val Slf4j = new {
|
||||||
|
val Nop: ModuleID = "org.slf4j" % "slf4j-nop" % "2.0.17"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy val testSettings = Seq(
|
lazy val testSettings = Seq(
|
||||||
libraryDependencies ++= Seq(
|
libraryDependencies ++= Seq(
|
||||||
Deps.MUnit % Test,
|
Deps.MUnit % Test,
|
||||||
Deps.Gs.Datagen % Test
|
Deps.Gs.Datagen % Test,
|
||||||
|
Deps.Slf4j.Nop % Test
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,26 @@ package gs.smolban.db.doobie
|
||||||
|
|
||||||
import doobie.enumerated.SqlState
|
import doobie.enumerated.SqlState
|
||||||
|
|
||||||
|
/** Used to support semantic SQL states (for error handling) across database
|
||||||
|
* implementations. Smolban supports both SQLite and PostgreSQL.
|
||||||
|
*/
|
||||||
sealed trait CuratedSqlStates:
|
sealed trait CuratedSqlStates:
|
||||||
def uniqueViolation: SqlState
|
def uniqueViolation: SqlState
|
||||||
|
|
||||||
object CuratedSqlStates:
|
object CuratedSqlStates:
|
||||||
|
|
||||||
|
lazy val postgres: CuratedSqlStates = new PostgreSQL
|
||||||
|
lazy val sqlite: CuratedSqlStates = new Sqlite
|
||||||
|
|
||||||
|
/** Should be injected when Smolban is run using PostgreSQL.
|
||||||
|
*/
|
||||||
final class PostgreSQL extends CuratedSqlStates:
|
final class PostgreSQL extends CuratedSqlStates:
|
||||||
|
|
||||||
override val uniqueViolation: SqlState =
|
override val uniqueViolation: SqlState =
|
||||||
doobie.postgres.sqlstate.class23.UNIQUE_VIOLATION
|
doobie.postgres.sqlstate.class23.UNIQUE_VIOLATION
|
||||||
|
|
||||||
|
/** Should be injected when Smolban is run using SQLite.
|
||||||
|
*/
|
||||||
final class Sqlite extends CuratedSqlStates:
|
final class Sqlite extends CuratedSqlStates:
|
||||||
|
|
||||||
override val uniqueViolation: SqlState =
|
override val uniqueViolation: SqlState =
|
||||||
|
|
|
||||||
|
|
@ -52,18 +52,18 @@ object DoobieTagDb:
|
||||||
tag: TagValue,
|
tag: TagValue,
|
||||||
createdAt: CreatedAt
|
createdAt: CreatedAt
|
||||||
): Update0 = sql"""
|
): Update0 = sql"""
|
||||||
INSERT INTO tags (tag_value, created_at)
|
INSERT INTO tag (tag_value, created_at)
|
||||||
VALUES ($tag, $createdAt)
|
VALUES ($tag, $createdAt)
|
||||||
""".update
|
""".update
|
||||||
|
|
||||||
def readTag(tag: TagValue): Query0[Tag] = sql"""
|
def readTag(tag: TagValue): Query0[Tag] = sql"""
|
||||||
SELECT tag_value, created_at
|
SELECT tag_value, created_at
|
||||||
FROM tags
|
FROM tag
|
||||||
WHERE tag_value = $tag
|
WHERE tag_value = $tag
|
||||||
""".query[Tag]
|
""".query[Tag]
|
||||||
|
|
||||||
def deleteTag(tag: TagValue): Update0 = sql"""
|
def deleteTag(tag: TagValue): Update0 = sql"""
|
||||||
DELETE FROM tags
|
DELETE FROM tag
|
||||||
WHERE tag_value = $tag
|
WHERE tag_value = $tag
|
||||||
""".update
|
""".update
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package gs.smolban.db.doobie
|
||||||
|
|
||||||
|
import cats.effect.IO
|
||||||
|
import cats.effect.Resource
|
||||||
|
import cats.effect.unsafe.IORuntime
|
||||||
|
import doobie.*
|
||||||
|
import doobie.hikari.Config
|
||||||
|
import doobie.hikari.HikariTransactor
|
||||||
|
import doobie.implicits.*
|
||||||
|
import doobie.util.transactor.Transactor
|
||||||
|
import gs.smolban.db.TagDb
|
||||||
|
import gs.smolban.model.metadata.CreatedAt
|
||||||
|
import gs.smolban.model.metadata.Tag
|
||||||
|
import gs.smolban.model.metadata.TagValue
|
||||||
|
import java.time.Clock
|
||||||
|
import munit.Location
|
||||||
|
|
||||||
|
class DoobieTagDbTests extends munit.FunSuite:
|
||||||
|
given IORuntime = IORuntime.global
|
||||||
|
|
||||||
|
def iotest(
|
||||||
|
name: String
|
||||||
|
)(
|
||||||
|
f: => IO[Unit]
|
||||||
|
)(
|
||||||
|
using
|
||||||
|
Location
|
||||||
|
): Unit =
|
||||||
|
test(name)(f.unsafeRunSync())
|
||||||
|
|
||||||
|
private val dbConfig: Config = Config(
|
||||||
|
jdbcUrl = "jdbc:sqlite:test/doobie_tag_db_tests",
|
||||||
|
driverClassName = Some("org.sqlite.JDBC")
|
||||||
|
)
|
||||||
|
|
||||||
|
private val transactor: Resource[IO, Transactor[IO]] =
|
||||||
|
HikariTransactor.fromConfig[IO](dbConfig)
|
||||||
|
|
||||||
|
private val tagDb: TagDb[ConnectionIO] = new DoobieTagDb(
|
||||||
|
CuratedSqlStates.sqlite
|
||||||
|
)
|
||||||
|
|
||||||
|
private val clock = Clock.systemDefaultZone()
|
||||||
|
|
||||||
|
iotest("should create, read, and delete a tag") {
|
||||||
|
val tagValue = TagValue.validate("x").get
|
||||||
|
val createdAt = CreatedAt.now(clock)
|
||||||
|
transactor.use { xa =>
|
||||||
|
(for
|
||||||
|
_ <- dropTable().run
|
||||||
|
_ <- createTable().run
|
||||||
|
tag <- tagDb.createTag(tagValue, createdAt).value
|
||||||
|
t2 <- tagDb.readTag(tagValue)
|
||||||
|
result <- tagDb.deleteTag(tagValue)
|
||||||
|
yield
|
||||||
|
assertEquals(tag, Right(Tag(tagValue, createdAt)))
|
||||||
|
assertEquals(t2, tag.toOption)
|
||||||
|
assert(result)
|
||||||
|
).transact(xa)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def dropTable(): Update0 =
|
||||||
|
sql"""
|
||||||
|
DROP TABLE IF EXISTS tag
|
||||||
|
""".update
|
||||||
|
|
||||||
|
def createTable(): Update0 =
|
||||||
|
sql"""
|
||||||
|
CREATE TABLE IF NOT EXISTS tag(
|
||||||
|
tag_value TEXT NOT NULL PRIMARY KEY,
|
||||||
|
created_at DATETIME NOT NULL
|
||||||
|
)
|
||||||
|
""".update
|
||||||
|
|
@ -3,9 +3,13 @@ package gs.smolban.model.metadata
|
||||||
import cats.Show
|
import cats.Show
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
/** Describes an instant at which something was created. Opaque type for
|
/** Describes an instant at which something was created. Opaque type for
|
||||||
* `java.time.Instant`.
|
* `java.time.Instant`.
|
||||||
|
*
|
||||||
|
* This type always truncates to milliseconds. This is the lowest common
|
||||||
|
* denominator for database representations (SQLite).
|
||||||
*/
|
*/
|
||||||
opaque type CreatedAt = Instant
|
opaque type CreatedAt = Instant
|
||||||
|
|
||||||
|
|
@ -18,7 +22,8 @@ object CreatedAt:
|
||||||
* @return
|
* @return
|
||||||
* The new instance.
|
* The new instance.
|
||||||
*/
|
*/
|
||||||
def apply(timestamp: Instant): CreatedAt = timestamp
|
def apply(timestamp: Instant): CreatedAt =
|
||||||
|
timestamp.truncatedTo(ChronoUnit.MILLIS)
|
||||||
|
|
||||||
/** Instantiate a new [[CreatedAt]] representing the current instant.
|
/** Instantiate a new [[CreatedAt]] representing the current instant.
|
||||||
*
|
*
|
||||||
|
|
@ -27,7 +32,7 @@ object CreatedAt:
|
||||||
* @return
|
* @return
|
||||||
* The new [[CreatedAt]] instance.
|
* The new [[CreatedAt]] instance.
|
||||||
*/
|
*/
|
||||||
def now(clock: Clock): CreatedAt = clock.instant()
|
def now(clock: Clock): CreatedAt = apply(clock.instant())
|
||||||
|
|
||||||
/** Instantiate a new [[CreatedAt]].
|
/** Instantiate a new [[CreatedAt]].
|
||||||
*
|
*
|
||||||
|
|
|
||||||
6
test/README.md
Normal file
6
test/README.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Test Data
|
||||||
|
|
||||||
|
This directory provides a dumping ground for test data that is produced by tests
|
||||||
|
while running -- for example, SQLite databases.
|
||||||
|
|
||||||
|
All files (aside from this file) in this directory are ignored.
|
||||||
Loading…
Add table
Reference in a new issue