WIP: More db prepping to run tests with sqlite
This commit is contained in:
parent
f25c9658eb
commit
8d0567195a
11 changed files with 79 additions and 338 deletions
13
build.sbt
13
build.sbt
|
|
@ -28,6 +28,13 @@ val Deps = new {
|
|||
|
||||
val Doobie = new {
|
||||
val Core: ModuleID = "org.tpolecat" %% "doobie-core" % "1.0.0-RC11"
|
||||
val Hikari: ModuleID = "org.tpolecat" %% "doobie-hikari" % "1.0.0-RC11"
|
||||
val Postgres: ModuleID = "org.tpolecat" %% "doobie-postgres" % "1.0.0-RC11"
|
||||
}
|
||||
|
||||
val JdbcDriver = new {
|
||||
val Sqlite: ModuleID = "org.xerial" % "sqlite-jdbc" % "3.51.1.0"
|
||||
val PostgreSQL: ModuleID = "org.postgresql" % "postgresql" % "42.7.9"
|
||||
}
|
||||
|
||||
val Http4s = new {
|
||||
|
|
@ -96,7 +103,11 @@ lazy val db = project
|
|||
.settings(name := s"${gsProjectName.value}-db")
|
||||
.settings(
|
||||
libraryDependencies ++= Seq(
|
||||
Deps.Doobie.Core
|
||||
Deps.Doobie.Core,
|
||||
Deps.Doobie.Hikari,
|
||||
Deps.Doobie.Postgres,
|
||||
Deps.JdbcDriver.Sqlite,
|
||||
Deps.JdbcDriver.PostgreSQL
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
package gs.smolban.db
|
||||
|
||||
object DbConstants:
|
||||
|
||||
val UniqueConstraintFailed: List[Int] =
|
||||
List(Sqlite.UniqueConstraintFailed)
|
||||
|
||||
object Sqlite:
|
||||
|
||||
val UniqueConstraintFailed: Int = 2067
|
||||
|
||||
end Sqlite
|
||||
|
||||
end DbConstants
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
package gs.smolban.db
|
||||
|
||||
import _root_.doobie.enumerated.SqlState
|
||||
import gs.smolban.auth.CredentialId
|
||||
import gs.smolban.model.SmolbanError
|
||||
import gs.smolban.model.account.AccountId
|
||||
import gs.smolban.model.account.AccountName
|
||||
import gs.smolban.model.account.AccountType
|
||||
import gs.smolban.model.metadata.TagValue
|
||||
|
||||
/** Parent type of all database errors in Smolban.
|
||||
*/
|
||||
|
|
@ -12,6 +14,10 @@ sealed trait DbError extends SmolbanError
|
|||
|
||||
object DbError:
|
||||
|
||||
case class GenericDatabaseError(sqlState: SqlState) extends DbError
|
||||
|
||||
case class TagAlreadyExists(value: TagValue) extends DbError
|
||||
|
||||
/** Produced when creating any new account fails because an account with the
|
||||
* same name already exists.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
package gs.smolban.db
|
||||
|
||||
import cats.data.EitherT
|
||||
import gs.smolban.model.Group
|
||||
|
||||
/** Database interface for [[Group]].
|
||||
*/
|
||||
trait GroupDb[F[_]]:
|
||||
/** Create a new [[Group]] in the database.
|
||||
*
|
||||
* @param group
|
||||
* The [[Group]] to store.
|
||||
* @return
|
||||
* The [[Group]] that was stored, or an error.
|
||||
*/
|
||||
def createGroup(group: Group): EitherT[F, DbError, Group]
|
||||
|
||||
/** Read the specified [[Group]] from the database.
|
||||
*
|
||||
* @param id
|
||||
* The unique identifier of the [[Group]] to retrieve.
|
||||
* @return
|
||||
* The [[Group]], or `None` if the specified ID does not exist.
|
||||
*/
|
||||
def readGroup(id: Group.Id): F[Option[Group]]
|
||||
|
||||
/** Delete the specified [[Group]]. Note that this operation deletes all
|
||||
* [[Ticket]] associated with the group and should be used with care.
|
||||
*
|
||||
* @param id
|
||||
* The unique identifier of the [[Group]] to delete.
|
||||
* @return
|
||||
* `true` if the group was deleted, `false` otherwise.
|
||||
*/
|
||||
def deleteGroup(id: Group.Id): F[Boolean]
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
package gs.smolban.db
|
||||
|
||||
import cats.data.EitherT
|
||||
import gs.smolban.model.CreatedAt
|
||||
import gs.smolban.model.Tag
|
||||
import gs.smolban.model.metadata.CreatedAt
|
||||
import gs.smolban.model.metadata.Tag
|
||||
import gs.smolban.model.metadata.TagValue
|
||||
|
||||
trait TagDb[F[_]]:
|
||||
|
||||
|
|
@ -16,24 +17,24 @@ trait TagDb[F[_]]:
|
|||
* The new [[Tag]], or an error if the value already exists.
|
||||
*/
|
||||
def createTag(
|
||||
tag: Tag.Value,
|
||||
tag: TagValue,
|
||||
createdAt: CreatedAt
|
||||
): EitherT[F, DbError, Tag]
|
||||
|
||||
/** Get the value of some [[Tag]] by ID.
|
||||
/** Get the value of some [[Tag]] by value.
|
||||
*
|
||||
* @param id
|
||||
* The internal identifier of the [[Tag]].
|
||||
* @param tag
|
||||
* The value of the [[Tag]].
|
||||
* @return
|
||||
* The found [[Tag]], or `None` if the ID does not exist.
|
||||
* The found [[Tag]], or `None` if the value does not exist.
|
||||
*/
|
||||
def readTag(id: Tag.Id): F[Option[Tag]]
|
||||
def readTag(tag: TagValue): F[Option[Tag]]
|
||||
|
||||
/** Delete a [[Tag]] by ID.
|
||||
/** Delete a [[Tag]] by value.
|
||||
*
|
||||
* @param id
|
||||
* The unique ID of the [[Tag]] to delete.
|
||||
* @param tag
|
||||
* The unique value of the [[Tag]] to delete.
|
||||
* @return
|
||||
* `true` if the [[Tag]] was deleted, `false` otherwise.
|
||||
*/
|
||||
def deleteTag(id: Tag.Id): F[Boolean]
|
||||
def deleteTag(tag: TagValue): F[Boolean]
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
package gs.smolban.db
|
||||
|
||||
import cats.data.EitherT
|
||||
import gs.smolban.model.Ticket
|
||||
|
||||
/** Database interface for [[Ticket]].
|
||||
*/
|
||||
trait TicketDb[F[_]]:
|
||||
/** Create a new [[Ticket]] in the database.
|
||||
*
|
||||
* @param ticket
|
||||
* The [[Ticket]] to store.
|
||||
* @return
|
||||
* The [[Ticket]] that was created, or an error.
|
||||
*/
|
||||
def createTicket(ticket: Ticket): EitherT[F, DbError, Ticket]
|
||||
|
||||
/** Read the specified [[Ticket]] from the database.
|
||||
*
|
||||
* @param ref
|
||||
* The reference to the [[Ticket]] to retrieve.
|
||||
* @return
|
||||
* The value of the [[Ticket]], or `None` if no such [[Ticket]] exists.
|
||||
*/
|
||||
def readTicket(ref: Ticket.Reference): F[Option[Ticket]]
|
||||
|
||||
/** Given a reference to a [[Ticket]] and some new value for that [[Ticket]],
|
||||
* update the [[Ticket]] in the database.
|
||||
*
|
||||
* @param ref
|
||||
* The refrence to the [[Ticket]] to update.
|
||||
* @param newValue
|
||||
* The new values with which to overwrite the [[Ticket]].
|
||||
* @return
|
||||
* The modified [[Ticket]] if the operation succeeded, or `None` if the
|
||||
* given reference does not exist.
|
||||
*/
|
||||
def updateTicket(
|
||||
ref: Ticket.Reference,
|
||||
newValue: Ticket
|
||||
): F[Option[Ticket]]
|
||||
|
||||
/** Delete the specified [[Ticket]].
|
||||
*
|
||||
* @param ref
|
||||
* The reference to the [[Ticket]] to delete.
|
||||
* @return
|
||||
* `true` if the operation deleted a [[Ticket]], `false` otherwise.
|
||||
*/
|
||||
def deleteTicket(ref: Ticket.Reference): F[Boolean]
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package gs.smolban.db.doobie
|
||||
|
||||
import doobie.enumerated.SqlState
|
||||
|
||||
sealed trait CuratedSqlStates:
|
||||
def uniqueViolation: SqlState
|
||||
|
||||
object CuratedSqlStates:
|
||||
|
||||
final class PostgreSQL extends CuratedSqlStates:
|
||||
|
||||
override val uniqueViolation: SqlState =
|
||||
doobie.postgres.sqlstate.class23.UNIQUE_VIOLATION
|
||||
|
||||
final class Sqlite extends CuratedSqlStates:
|
||||
|
||||
override val uniqueViolation: SqlState =
|
||||
SqlState("2067")
|
||||
|
||||
end CuratedSqlStates
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
package gs.smolban.db.doobie
|
||||
|
||||
import cats.MonadError
|
||||
import cats.data.EitherT
|
||||
import cats.syntax.all.*
|
||||
import doobie.*
|
||||
import doobie.implicits.*
|
||||
import gs.smolban.db.DbError
|
||||
import gs.smolban.db.GroupDb
|
||||
import gs.smolban.db.doobie.DoobieTypes.*
|
||||
import gs.smolban.model.CreatedAt
|
||||
import gs.smolban.model.Group
|
||||
|
||||
final class DoobieGroupDb() extends GroupDb[ConnectionIO]:
|
||||
|
||||
import DoobieGroupDb.Sql
|
||||
import gs.smolban.db.DbConstants
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
override def createGroup(
|
||||
group: Group
|
||||
): EitherT[ConnectionIO, DbError, Group] =
|
||||
EitherT(
|
||||
Sql.createGroup(group).run.attemptSql.flatMap {
|
||||
case Left(ex) =>
|
||||
if DbConstants.UniqueConstraintFailed.contains(ex.getErrorCode()) then
|
||||
Left(
|
||||
DbError.GroupAlreadyExists(group.id, group.slug)
|
||||
).pure[ConnectionIO]
|
||||
else MonadError[ConnectionIO, Throwable].raiseError(ex)
|
||||
case Right(_) =>
|
||||
Right(group).pure[ConnectionIO]
|
||||
}
|
||||
)
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
override def readGroup(id: Group.Id): ConnectionIO[Option[Group]] =
|
||||
Sql.readGroup(id).option
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
override def deleteGroup(id: Group.Id): ConnectionIO[Boolean] =
|
||||
for
|
||||
_ <- Sql.deleteAllTicketsForGroup(id).run
|
||||
rows <- Sql.deleteGroup(id).run
|
||||
yield rows > 0
|
||||
|
||||
object DoobieGroupDb:
|
||||
|
||||
private object Sql:
|
||||
|
||||
def createGroup(group: Group): Update0 = sql"""
|
||||
INSERT INTO groups (group_id, slug, created_at)
|
||||
VALUES (${group.id}, ${group.slug}, ${group.createdAt})
|
||||
""".update
|
||||
|
||||
def readGroup(groupId: Group.Id): Query0[Group] = sql"""
|
||||
SELECT slug, created_at
|
||||
FROM groups
|
||||
WHERE group_id = $groupId
|
||||
""".query[Group]
|
||||
|
||||
def deleteAllTicketsForGroup(groupId: Group.Id): Update0 = sql"""
|
||||
DELETE FROM tickets
|
||||
WHERE group_id = $groupId
|
||||
""".update
|
||||
|
||||
def deleteGroup(groupId: Group.Id): Update0 = sql"""
|
||||
DELETE FROM groups
|
||||
WHERE group_id = $groupId
|
||||
""".update
|
||||
|
||||
end Sql
|
||||
|
||||
object ErrorCodes:
|
||||
|
||||
val UniqueConstraintFailed: List[Int] =
|
||||
List(Sqlite.UniqueConstraintFailed)
|
||||
|
||||
object Sqlite:
|
||||
|
||||
val UniqueConstraintFailed: Int = 2067
|
||||
|
||||
end Sqlite
|
||||
|
||||
end ErrorCodes
|
||||
|
||||
end DoobieGroupDb
|
||||
|
|
@ -1,51 +1,47 @@
|
|||
package gs.smolban.db.doobie
|
||||
|
||||
import cats.MonadError
|
||||
import cats.data.EitherT
|
||||
import cats.syntax.all.*
|
||||
import doobie.*
|
||||
import doobie.implicits.*
|
||||
import gs.smolban.db.DbError
|
||||
import gs.smolban.db.TagDb
|
||||
import gs.smolban.db.doobie.DoobieTypes.*
|
||||
import gs.smolban.model.CreatedAt
|
||||
import gs.smolban.model.Tag
|
||||
import gs.smolban.model.metadata.CreatedAt
|
||||
import gs.smolban.model.metadata.Tag
|
||||
import gs.smolban.model.metadata.TagValue
|
||||
|
||||
final class DoobieTagDb() extends TagDb[ConnectionIO]:
|
||||
final class DoobieTagDb(
|
||||
sqlStates: CuratedSqlStates
|
||||
) extends TagDb[ConnectionIO]:
|
||||
|
||||
import DoobieTagDb.Sql
|
||||
import gs.smolban.db.DbConstants
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
override def createTag(
|
||||
value: Tag.Value,
|
||||
value: TagValue,
|
||||
createdAt: CreatedAt
|
||||
): EitherT[ConnectionIO, DbError, Tag] =
|
||||
EitherT(
|
||||
Sql.createTag(value, createdAt).option.attemptSql.flatMap {
|
||||
case Left(ex) =>
|
||||
if DbConstants.UniqueConstraintFailed.contains(ex.getErrorCode()) then
|
||||
Left(
|
||||
DbError.TagAlreadyExists(value)
|
||||
).pure[ConnectionIO]
|
||||
else MonadError[ConnectionIO, Throwable].raiseError(ex)
|
||||
case Right(None) =>
|
||||
Left(DbError.SilentInsertFailure("doobie-tag-db")).pure[ConnectionIO]
|
||||
case Right(Some(newId)) =>
|
||||
Right(Tag(newId, value, createdAt)).pure[ConnectionIO]
|
||||
Sql.createTag(value, createdAt).run.attemptSqlState.map {
|
||||
case Left(sqlState) =>
|
||||
if sqlState.value == sqlStates.uniqueViolation.value then
|
||||
Left(DbError.TagAlreadyExists(value))
|
||||
else Left(DbError.GenericDatabaseError(sqlState))
|
||||
case Right(_) =>
|
||||
Right(Tag(value, createdAt))
|
||||
}
|
||||
)
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
override def readTag(id: Tag.Id): ConnectionIO[Option[Tag]] =
|
||||
Sql.readTag(id).option
|
||||
override def readTag(tag: TagValue): ConnectionIO[Option[Tag]] =
|
||||
Sql.readTag(tag).option
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
override def deleteTag(id: Tag.Id): ConnectionIO[Boolean] =
|
||||
for rows <- Sql.deleteTag(id).run
|
||||
override def deleteTag(tag: TagValue): ConnectionIO[Boolean] =
|
||||
for rows <- Sql.deleteTag(tag).run
|
||||
yield rows > 0
|
||||
|
||||
object DoobieTagDb:
|
||||
|
|
@ -53,23 +49,22 @@ object DoobieTagDb:
|
|||
private object Sql:
|
||||
|
||||
def createTag(
|
||||
tv: Tag.Value,
|
||||
tag: TagValue,
|
||||
createdAt: CreatedAt
|
||||
): Query0[Tag.Id] = sql"""
|
||||
): Update0 = sql"""
|
||||
INSERT INTO tags (tag_value, created_at)
|
||||
VALUES ($tv, $createdAt)
|
||||
RETURNING id
|
||||
""".query[Tag.Id]
|
||||
VALUES ($tag, $createdAt)
|
||||
""".update
|
||||
|
||||
def readTag(tagId: Tag.Id): Query0[Tag] = sql"""
|
||||
SELECT id, value, created_at
|
||||
def readTag(tag: TagValue): Query0[Tag] = sql"""
|
||||
SELECT tag_value, created_at
|
||||
FROM tags
|
||||
WHERE id = $tagId
|
||||
WHERE tag_value = $tag
|
||||
""".query[Tag]
|
||||
|
||||
def deleteTag(tagId: Tag.Id): Update0 = sql"""
|
||||
def deleteTag(tag: TagValue): Update0 = sql"""
|
||||
DELETE FROM tags
|
||||
WHERE id = $tagId
|
||||
WHERE tag_value = $tag
|
||||
""".update
|
||||
|
||||
end Sql
|
||||
|
|
|
|||
|
|
@ -1,101 +0,0 @@
|
|||
package gs.smolban.db.doobie
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.syntax.all.*
|
||||
import doobie.*
|
||||
import doobie.implicits.*
|
||||
import gs.smolban.db.DbError
|
||||
import gs.smolban.db.TicketDb
|
||||
import gs.smolban.db.doobie.DoobieTypes.*
|
||||
import gs.smolban.model.CreatedAt
|
||||
import gs.smolban.model.Group
|
||||
import gs.smolban.model.Tag
|
||||
import gs.smolban.model.Ticket
|
||||
import java.time.Instant
|
||||
|
||||
final class DoobieTicketDb() extends TicketDb[ConnectionIO]:
|
||||
import DoobieTicketDb.Sql
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
override def createTicket(ticket: Ticket)
|
||||
: EitherT[ConnectionIO, DbError, Ticket] = ???
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
override def readTicket(ref: Ticket.Reference): ConnectionIO[Option[Ticket]] =
|
||||
Sql.readTicket(ref.number, ref.group).option.flatMap {
|
||||
case None => None.pure[ConnectionIO]
|
||||
case Some(ticketContents) =>
|
||||
for
|
||||
tags <- Sql.readTagsForTicket(ticketContents.id).stream.compile.toList
|
||||
history <- Sql
|
||||
.readHistoryForTicket(ticketContents.id)
|
||||
.stream
|
||||
.compile
|
||||
.toList
|
||||
yield Some(
|
||||
Ticket(
|
||||
number = ref.number,
|
||||
group = ref.group,
|
||||
createdAt = ticketContents.createdAt,
|
||||
title = ticketContents.title,
|
||||
description = ticketContents.description,
|
||||
tags = tags,
|
||||
status = ticketContents.status,
|
||||
statusHistory = history
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
override def updateTicket(
|
||||
ref: Ticket.Reference,
|
||||
newValue: Ticket
|
||||
): ConnectionIO[Option[Ticket]] = ???
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
override def deleteTicket(ref: Ticket.Reference): ConnectionIO[Boolean] = ???
|
||||
|
||||
object DoobieTicketDb:
|
||||
|
||||
private object Sql:
|
||||
|
||||
case class TicketContents(
|
||||
id: Long,
|
||||
createdAt: CreatedAt,
|
||||
title: String,
|
||||
description: String,
|
||||
status: Ticket.Status
|
||||
)
|
||||
|
||||
def readTicket(
|
||||
ticketNumber: Ticket.Number,
|
||||
groupId: Group.Id
|
||||
): Query0[TicketContents] = sql"""
|
||||
SELECT id, created_at, created_by, title, description, status, assignee
|
||||
FROM tickets
|
||||
WHERE ticket_number = $ticketNumber AND group_id = $groupId
|
||||
""".query[TicketContents]
|
||||
|
||||
def readTagsForTicket(
|
||||
ticketId: Long
|
||||
): Query0[Tag] = sql"""
|
||||
SELECT id, tag_value, created_at
|
||||
FROM tags
|
||||
WHERE ticket_id = $ticketId
|
||||
""".query[Tag]
|
||||
|
||||
def readHistoryForTicket(
|
||||
ticketId: Long
|
||||
): Query0[(Ticket.Status, Instant)] = sql"""
|
||||
SELECT status, set_at
|
||||
FROM ticket_history
|
||||
WHERE ticket_id = $ticketId
|
||||
""".query[(Ticket.Status, Instant)]
|
||||
|
||||
end Sql
|
||||
|
||||
end DoobieTicketDb
|
||||
|
|
@ -3,10 +3,8 @@ package gs.smolban.db.doobie
|
|||
import DoobieTypes.ErrorMessages
|
||||
import doobie.*
|
||||
import gs.smolban.model.metadata.CreatedAt
|
||||
import gs.smolban.model.metadata.Tag
|
||||
import gs.smolban.model.metadata.TagValue
|
||||
import gs.smolban.model.ticket.CommentId
|
||||
import gs.smolban.model.ticket.Ticket
|
||||
import gs.smolban.model.ticket.TicketStatus
|
||||
import gs.uuid.v0.UUID
|
||||
import gs.uuid.v0.UUIDFormat
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue