first messy db test working

This commit is contained in:
Pat Garrity 2026-01-31 21:18:56 -06:00
parent 8d0567195a
commit f10f79ed95
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
7 changed files with 110 additions and 6 deletions

4
.gitignore vendored
View file

@ -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

View file

@ -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
) )
) )

View file

@ -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 =

View file

@ -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

View file

@ -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

View file

@ -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
View 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.