From 10b49fccf87800775bc4fa40789edebe3cc555ad Mon Sep 17 00:00:00 2001
From: Eike Kettner <eike.kettner@posteo.de>
Date: Wed, 9 Dec 2020 00:00:10 +0100
Subject: [PATCH] Converting user and userimap records

---
 .../docspell/joex/analysis/RegexNerFile.scala |   7 +-
 .../scala/docspell/store/impl/Implicits.scala |  11 ++
 .../main/scala/docspell/store/qb/Column.scala |   5 +-
 .../scala/docspell/store/qb/Condition.scala   |   2 +
 .../main/scala/docspell/store/qb/DSL.scala    |  11 ++
 .../store/qb/impl/ConditionBuilder.scala      |   4 +
 .../docspell/store/queries/QFolder.scala      |  29 ++--
 .../scala/docspell/store/queries/QLogin.scala |  12 +-
 .../scala/docspell/store/queries/QMails.scala |  19 +--
 .../scala/docspell/store/records/RUser.scala  | 115 +++++++-------
 .../docspell/store/records/RUserEmail.scala   |  25 +++-
 .../docspell/store/records/RUserImap.scala    | 140 ++++++++++--------
 12 files changed, 229 insertions(+), 151 deletions(-)

diff --git a/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala b/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala
index 6cff49f7..7187e147 100644
--- a/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala
+++ b/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala
@@ -144,6 +144,7 @@ object RegexNerFile {
       def max(col: Column, table: Fragment, cidCol: Column): Fragment =
         selectSimple(col.max ++ fr"as t", table, cidCol.is(collective))
 
+      val equip = REquipment.as("e")
       val sql =
         List(
           max(
@@ -152,7 +153,11 @@ object RegexNerFile {
             ROrganization.Columns.cid
           ),
           max(RPerson.Columns.updated, RPerson.table, RPerson.Columns.cid),
-          max(REquipment.Columns.updated, REquipment.table, REquipment.Columns.cid)
+          max(
+            equip.updated.oldColumn,
+            Fragment.const(equip.tableName),
+            equip.cid.oldColumn
+          )
         )
           .reduce(_ ++ fr"UNION ALL" ++ _)
 
diff --git a/modules/store/src/main/scala/docspell/store/impl/Implicits.scala b/modules/store/src/main/scala/docspell/store/impl/Implicits.scala
index edafa832..30cba7ca 100644
--- a/modules/store/src/main/scala/docspell/store/impl/Implicits.scala
+++ b/modules/store/src/main/scala/docspell/store/impl/Implicits.scala
@@ -5,5 +5,16 @@ object Implicits extends DoobieMeta with DoobieSyntax {
   implicit final class LegacySyntax(col: docspell.store.qb.Column[_]) {
     def oldColumn: Column =
       Column(col.name)
+
+    def column: Column = {
+      val c = col.alias match {
+        case Some(a) => oldColumn.as(a)
+        case None    => oldColumn
+      }
+      col.table.alias match {
+        case Some(p) => c.prefix(p)
+        case None    => c
+      }
+    }
   }
 }
diff --git a/modules/store/src/main/scala/docspell/store/qb/Column.scala b/modules/store/src/main/scala/docspell/store/qb/Column.scala
index 3f3fa1ab..90936f90 100644
--- a/modules/store/src/main/scala/docspell/store/qb/Column.scala
+++ b/modules/store/src/main/scala/docspell/store/qb/Column.scala
@@ -1,5 +1,8 @@
 package docspell.store.qb
 
-case class Column[A](name: String, table: TableDef, alias: Option[String] = None)
+case class Column[A](name: String, table: TableDef, alias: Option[String] = None) {
+  def as(alias: String): Column[A] =
+    copy(alias = Some(alias))
+}
 
 object Column {}
diff --git a/modules/store/src/main/scala/docspell/store/qb/Condition.scala b/modules/store/src/main/scala/docspell/store/qb/Condition.scala
index 45f9a4c7..a4414f31 100644
--- a/modules/store/src/main/scala/docspell/store/qb/Condition.scala
+++ b/modules/store/src/main/scala/docspell/store/qb/Condition.scala
@@ -13,6 +13,8 @@ object Condition {
   case class CompareCol[A](col1: Column[A], op: Operator, col2: Column[A])
       extends Condition
 
+  case class InSubSelect[A](col: Column[A], subSelect: Select) extends Condition
+
   case class And(c: Condition, cs: Vector[Condition]) extends Condition
   case class Or(c: Condition, cs: Vector[Condition])  extends Condition
   case class Not(c: Condition)                        extends Condition
diff --git a/modules/store/src/main/scala/docspell/store/qb/DSL.scala b/modules/store/src/main/scala/docspell/store/qb/DSL.scala
index 4844b662..76b52342 100644
--- a/modules/store/src/main/scala/docspell/store/qb/DSL.scala
+++ b/modules/store/src/main/scala/docspell/store/qb/DSL.scala
@@ -72,9 +72,17 @@ trait DSL extends DoobieMeta {
     def ===(value: A)(implicit P: Put[A]): Condition =
       Condition.CompareVal(col, Operator.Eq, value)
 
+    //TODO find some better way around the cast
+    def ====(value: String): Condition =
+      Condition.CompareVal(col.asInstanceOf[Column[String]], Operator.Eq, value)
+
     def like(value: A)(implicit P: Put[A]): Condition =
       Condition.CompareVal(col, Operator.LowerLike, value)
 
+    //TODO find some better way around the cast
+    def likes(value: String): Condition =
+      Condition.CompareVal(col.asInstanceOf[Column[String]], Operator.LowerLike, value)
+
     def <=(value: A)(implicit P: Put[A]): Condition =
       Condition.CompareVal(col, Operator.Lte, value)
 
@@ -87,6 +95,9 @@ trait DSL extends DoobieMeta {
     def <(value: A)(implicit P: Put[A]): Condition =
       Condition.CompareVal(col, Operator.Lt, value)
 
+    def in(subsel: Select): Condition =
+      Condition.InSubSelect(col, subsel)
+
     def ===(other: Column[A]): Condition =
       Condition.CompareCol(col, Operator.Eq, other)
   }
diff --git a/modules/store/src/main/scala/docspell/store/qb/impl/ConditionBuilder.scala b/modules/store/src/main/scala/docspell/store/qb/impl/ConditionBuilder.scala
index 5a37733f..a7c8f536 100644
--- a/modules/store/src/main/scala/docspell/store/qb/impl/ConditionBuilder.scala
+++ b/modules/store/src/main/scala/docspell/store/qb/impl/ConditionBuilder.scala
@@ -33,6 +33,10 @@ object ConditionBuilder {
         }
         c1Frag ++ operator(op) ++ c2Frag
 
+      case Condition.InSubSelect(col, subsel) =>
+        val sub = DoobieQuery(subsel)
+        SelectExprBuilder.column(col) ++ sql" IN (" ++ sub ++ sql")"
+
       case Condition.And(c, cs) =>
         val inner = cs.prepended(c).map(build).reduce(_ ++ and ++ _)
         if (cs.isEmpty) inner
diff --git a/modules/store/src/main/scala/docspell/store/queries/QFolder.scala b/modules/store/src/main/scala/docspell/store/queries/QFolder.scala
index 9c922d48..2f71fe0d 100644
--- a/modules/store/src/main/scala/docspell/store/queries/QFolder.scala
+++ b/modules/store/src/main/scala/docspell/store/queries/QFolder.scala
@@ -136,15 +136,16 @@ object QFolder {
   }
 
   def findById(id: Ident, account: AccountId): ConnectionIO[Option[FolderDetail]] = {
+    val user      = RUser.as("u")
     val mUserId   = RFolderMember.Columns.user.prefix("m")
     val mFolderId = RFolderMember.Columns.folder.prefix("m")
-    val uId       = RUser.Columns.uid.prefix("u")
-    val uLogin    = RUser.Columns.login.prefix("u")
+    val uId       = user.uid.column
+    val uLogin    = user.login.column
     val sColl     = RFolder.Columns.collective.prefix("s")
     val sId       = RFolder.Columns.id.prefix("s")
 
     val from = RFolderMember.table ++ fr"m INNER JOIN" ++
-      RUser.table ++ fr"u ON" ++ mUserId.is(uId) ++ fr"INNER JOIN" ++
+      Fragment.const(user.tableName) ++ fr"u ON" ++ mUserId.is(uId) ++ fr"INNER JOIN" ++
       RFolder.table ++ fr"s ON" ++ mFolderId.is(sId)
 
     val memberQ = selectSimple(
@@ -187,8 +188,9 @@ object QFolder {
 // inner join user_ u on u.uid = s.owner
 // where s.cid = 'eike';
 
-    val uId     = RUser.Columns.uid.prefix("u")
-    val uLogin  = RUser.Columns.login.prefix("u")
+    val user    = RUser.as("u")
+    val uId     = user.uid.column
+    val uLogin  = user.login.column
     val sId     = RFolder.Columns.id.prefix("s")
     val sOwner  = RFolder.Columns.owner.prefix("s")
     val sName   = RFolder.Columns.name.prefix("s")
@@ -199,11 +201,11 @@ object QFolder {
     //CTE
     val cte: Fragment = {
       val from1 = RFolderMember.table ++ fr"m INNER JOIN" ++
-        RUser.table ++ fr"u ON" ++ uId.is(mUser) ++ fr"INNER JOIN" ++
+        Fragment.const(user.tableName) ++ fr"u ON" ++ uId.is(mUser) ++ fr"INNER JOIN" ++
         RFolder.table ++ fr"s ON" ++ sId.is(mFolder)
 
       val from2 = RFolder.table ++ fr"s INNER JOIN" ++
-        RUser.table ++ fr"u ON" ++ uId.is(sOwner)
+        Fragment.const(user.tableName) ++ fr"u ON" ++ uId.is(sOwner)
 
       withCTE(
         "memberlogin" ->
@@ -232,7 +234,7 @@ object QFolder {
     )
 
     val from = RFolder.table ++ fr"s INNER JOIN" ++
-      RUser.table ++ fr"u ON" ++ uId.is(sOwner)
+      Fragment.const(user.tableName) ++ fr"u ON" ++ uId.is(sOwner)
 
     val where =
       sColl.is(account.collective) :: idQ.toList
@@ -247,17 +249,20 @@ object QFolder {
 
   /** Select all folder_id where the given account is member or owner. */
   def findMemberFolderIds(account: AccountId): Fragment = {
+    val user    = RUser.as("u")
     val fId     = RFolder.Columns.id.prefix("f")
     val fOwner  = RFolder.Columns.owner.prefix("f")
     val fColl   = RFolder.Columns.collective.prefix("f")
-    val uId     = RUser.Columns.uid.prefix("u")
-    val uLogin  = RUser.Columns.login.prefix("u")
+    val uId     = user.uid.column
+    val uLogin  = user.login.column
     val mFolder = RFolderMember.Columns.folder.prefix("m")
     val mUser   = RFolderMember.Columns.user.prefix("m")
 
     selectSimple(
       Seq(fId),
-      RFolder.table ++ fr"f INNER JOIN" ++ RUser.table ++ fr"u ON" ++ fOwner.is(uId),
+      RFolder.table ++ fr"f INNER JOIN" ++ Fragment.const(
+        user.tableName
+      ) ++ fr"u ON" ++ fOwner.is(uId),
       and(fColl.is(account.collective), uLogin.is(account.user))
     ) ++
       fr"UNION ALL" ++
@@ -266,7 +271,7 @@ object QFolder {
         RFolderMember.table ++ fr"m INNER JOIN" ++ RFolder.table ++ fr"f ON" ++ fId.is(
           mFolder
         ) ++
-          fr"INNER JOIN" ++ RUser.table ++ fr"u ON" ++ uId.is(mUser),
+          fr"INNER JOIN" ++ Fragment.const(user.tableName) ++ fr"u ON" ++ uId.is(mUser),
         and(fColl.is(account.collective), uLogin.is(account.user))
       )
   }
diff --git a/modules/store/src/main/scala/docspell/store/queries/QLogin.scala b/modules/store/src/main/scala/docspell/store/queries/QLogin.scala
index 4554772d..7dfdf59f 100644
--- a/modules/store/src/main/scala/docspell/store/queries/QLogin.scala
+++ b/modules/store/src/main/scala/docspell/store/queries/QLogin.scala
@@ -5,7 +5,6 @@ import cats.data.OptionT
 import docspell.common._
 import docspell.store.impl.Implicits._
 import docspell.store.records.RCollective.{Columns => CC}
-import docspell.store.records.RUser.{Columns => UC}
 import docspell.store.records.{RCollective, RRememberMe, RUser}
 
 import doobie._
@@ -23,16 +22,17 @@ object QLogin {
   )
 
   def findUser(acc: AccountId): ConnectionIO[Option[Data]] = {
-    val ucid   = UC.cid.prefix("u")
-    val login  = UC.login.prefix("u")
-    val pass   = UC.password.prefix("u")
-    val ustate = UC.state.prefix("u")
+    val user   = RUser.as("u")
+    val ucid   = user.cid.column
+    val login  = user.login.column
+    val pass   = user.password.column
+    val ustate = user.state.column
     val cstate = CC.state.prefix("c")
     val ccid   = CC.id.prefix("c")
 
     val sql = selectSimple(
       List(ucid, login, pass, cstate, ustate),
-      RUser.table ++ fr"u, " ++ RCollective.table ++ fr"c",
+      Fragment.const(user.tableName) ++ fr"u, " ++ RCollective.table ++ fr"c",
       and(ucid.is(ccid), login.is(acc.user), ucid.is(acc.collective))
     )
 
diff --git a/modules/store/src/main/scala/docspell/store/queries/QMails.scala b/modules/store/src/main/scala/docspell/store/queries/QMails.scala
index 90046d33..08330362 100644
--- a/modules/store/src/main/scala/docspell/store/queries/QMails.scala
+++ b/modules/store/src/main/scala/docspell/store/queries/QMails.scala
@@ -45,19 +45,20 @@ object QMails {
   }
 
   private def partialFind: (Seq[Column], Fragment) = {
-    val iId    = RItem.Columns.id.prefix("i")
-    val tItem  = RSentMailItem.Columns.itemId.prefix("t")
-    val tMail  = RSentMailItem.Columns.sentMailId.prefix("t")
-    val mId    = RSentMail.Columns.id.prefix("m")
-    val mUser  = RSentMail.Columns.uid.prefix("m")
-    val uId    = RUser.Columns.uid.prefix("u")
-    val uLogin = RUser.Columns.login.prefix("u")
+    val user  = RUser.as("u")
+    val iId   = RItem.Columns.id.prefix("i")
+    val tItem = RSentMailItem.Columns.itemId.prefix("t")
+    val tMail = RSentMailItem.Columns.sentMailId.prefix("t")
+    val mId   = RSentMail.Columns.id.prefix("m")
+    val mUser = RSentMail.Columns.uid.prefix("m")
 
-    val cols = RSentMail.Columns.all.map(_.prefix("m")) :+ uLogin
+    val cols = RSentMail.Columns.all.map(_.prefix("m")) :+ user.login.column
     val from = RSentMail.table ++ fr"m INNER JOIN" ++
       RSentMailItem.table ++ fr"t ON" ++ tMail.is(mId) ++
       fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ tItem.is(iId) ++
-      fr"INNER JOIN" ++ RUser.table ++ fr"u ON" ++ uId.is(mUser)
+      fr"INNER JOIN" ++ Fragment.const(user.tableName) ++ fr"u ON" ++ user.uid.column.is(
+        mUser
+      )
 
     (cols, from)
   }
diff --git a/modules/store/src/main/scala/docspell/store/records/RUser.scala b/modules/store/src/main/scala/docspell/store/records/RUser.scala
index a304cabf..5d21d08a 100644
--- a/modules/store/src/main/scala/docspell/store/records/RUser.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RUser.scala
@@ -1,8 +1,8 @@
 package docspell.store.records
 
 import docspell.common._
-import docspell.store.impl.Implicits._
-import docspell.store.impl._
+import docspell.store.qb.DSL._
+import docspell.store.qb._
 
 import doobie._
 import doobie.implicits._
@@ -20,86 +20,99 @@ case class RUser(
 ) {}
 
 object RUser {
+  final case class Table(alias: Option[String]) extends TableDef {
+    val tableName = "user_"
 
-  val table = fr"user_"
-
-  object Columns {
-    val uid        = Column("uid")
-    val cid        = Column("cid")
-    val login      = Column("login")
-    val password   = Column("password")
-    val state      = Column("state")
-    val email      = Column("email")
-    val loginCount = Column("logincount")
-    val lastLogin  = Column("lastlogin")
-    val created    = Column("created")
+    val uid        = Column[Ident]("uid", this)
+    val login      = Column[Ident]("login", this)
+    val cid        = Column[Ident]("cid", this)
+    val password   = Column[Password]("password", this)
+    val state      = Column[UserState]("state", this)
+    val email      = Column[String]("email", this)
+    val loginCount = Column[Int]("logincount", this)
+    val lastLogin  = Column[Timestamp]("lastlogin", this)
+    val created    = Column[Timestamp]("created", this)
 
     val all =
       List(uid, login, cid, password, state, email, loginCount, lastLogin, created)
   }
 
-  import Columns._
+  def as(alias: String): Table =
+    Table(Some(alias))
 
   def insert(v: RUser): ConnectionIO[Int] = {
-    val sql = insertRow(
-      table,
-      Columns.all,
+    val t = Table(None)
+    val sql = DML.insert(
+      t,
+      t.all,
       fr"${v.uid},${v.login},${v.cid},${v.password},${v.state},${v.email},${v.loginCount},${v.lastLogin},${v.created}"
     )
     sql.update.run
   }
 
   def update(v: RUser): ConnectionIO[Int] = {
-    val sql = updateRow(
-      table,
-      and(login.is(v.login), cid.is(v.cid)),
-      commas(
-        state.setTo(v.state),
-        email.setTo(v.email),
-        loginCount.setTo(v.loginCount),
-        lastLogin.setTo(v.lastLogin)
+    val t = Table(None)
+    DML.update(
+      t,
+      t.login === v.login && t.cid === v.cid,
+      DML.set(
+        t.state.setTo(v.state),
+        t.email.setTo(v.email),
+        t.loginCount.setTo(v.loginCount),
+        t.lastLogin.setTo(v.lastLogin)
       )
     )
-    sql.update.run
   }
 
-  def exists(loginName: Ident): ConnectionIO[Boolean] =
-    selectCount(uid, table, login.is(loginName)).query[Int].unique.map(_ > 0)
+  def exists(loginName: Ident): ConnectionIO[Boolean] = {
+    val t = Table(None)
+    run(select(count(t.uid)), from(t), t.login === loginName).query[Int].unique.map(_ > 0)
+  }
 
   def findByAccount(aid: AccountId): ConnectionIO[Option[RUser]] = {
-    val sql = selectSimple(all, table, and(cid.is(aid.collective), login.is(aid.user)))
+    val t = Table(None)
+    val sql =
+      run(select(t.all), from(t), t.cid === aid.collective && t.login === aid.user)
     sql.query[RUser].option
   }
 
   def findById(userId: Ident): ConnectionIO[Option[RUser]] = {
-    val sql = selectSimple(all, table, uid.is(userId))
+    val t   = Table(None)
+    val sql = run(select(t.all), from(t), t.uid === userId)
     sql.query[RUser].option
   }
 
-  def findAll(coll: Ident, order: Columns.type => Column): ConnectionIO[Vector[RUser]] = {
-    val sql = selectSimple(all, table, cid.is(coll)) ++ orderBy(order(Columns).f)
+  def findAll(coll: Ident, order: Table => Column[_]): ConnectionIO[Vector[RUser]] = {
+    val t   = Table(None)
+    val sql = Select(select(t.all), from(t), t.cid === coll).orderBy(order(t)).run
     sql.query[RUser].to[Vector]
   }
 
-  def updateLogin(accountId: AccountId): ConnectionIO[Int] =
-    currentTime.flatMap(t =>
-      updateRow(
-        table,
-        and(cid.is(accountId.collective), login.is(accountId.user)),
-        commas(
-          loginCount.f ++ fr"=" ++ loginCount.f ++ fr"+ 1",
-          lastLogin.setTo(t)
+  def updateLogin(accountId: AccountId): ConnectionIO[Int] = {
+    val t = Table(None)
+    def stmt(now: Timestamp) =
+      DML.update(
+        t,
+        t.cid === accountId.collective && t.login === accountId.user,
+        DML.set(
+          t.loginCount.increment(1),
+          t.lastLogin.setTo(now)
         )
-      ).update.run
+      )
+    Timestamp.current[ConnectionIO].flatMap(stmt)
+  }
+
+  def updatePassword(accountId: AccountId, hashedPass: Password): ConnectionIO[Int] = {
+    val t = Table(None)
+    DML.update(
+      t,
+      t.cid === accountId.collective && t.login === accountId.user,
+      DML.set(t.password.setTo(hashedPass))
     )
+  }
 
-  def updatePassword(accountId: AccountId, hashedPass: Password): ConnectionIO[Int] =
-    updateRow(
-      table,
-      and(cid.is(accountId.collective), login.is(accountId.user)),
-      password.setTo(hashedPass)
-    ).update.run
-
-  def delete(user: Ident, coll: Ident): ConnectionIO[Int] =
-    deleteFrom(table, and(cid.is(coll), login.is(user))).update.run
+  def delete(user: Ident, coll: Ident): ConnectionIO[Int] = {
+    val t = Table(None)
+    DML.delete(t, t.cid === coll && t.login === user).update.run
+  }
 }
diff --git a/modules/store/src/main/scala/docspell/store/records/RUserEmail.scala b/modules/store/src/main/scala/docspell/store/records/RUserEmail.scala
index 270df3c0..5c7b2802 100644
--- a/modules/store/src/main/scala/docspell/store/records/RUserEmail.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RUserEmail.scala
@@ -168,12 +168,16 @@ object RUserEmail {
       nameQ: Option[String],
       exact: Boolean
   ): Query0[RUserEmail] = {
+    val user   = RUser.as("u")
     val mUid   = uid.prefix("m")
     val mName  = name.prefix("m")
-    val uId    = RUser.Columns.uid.prefix("u")
-    val uColl  = RUser.Columns.cid.prefix("u")
-    val uLogin = RUser.Columns.login.prefix("u")
-    val from   = table ++ fr"m INNER JOIN" ++ RUser.table ++ fr"u ON" ++ mUid.is(uId)
+    val uId    = user.uid.column
+    val uColl  = user.cid.column
+    val uLogin = user.login.column
+    val from =
+      table ++ fr"m INNER JOIN" ++ Fragment.const(user.tableName) ++ fr"u ON" ++ mUid.is(
+        uId
+      )
     val cond = Seq(uColl.is(accId.collective), uLogin.is(accId.user)) ++ (nameQ match {
       case Some(str) if exact => Seq(mName.is(str))
       case Some(str)          => Seq(mName.lowerLike(s"%${str.toLowerCase}%"))
@@ -194,14 +198,19 @@ object RUserEmail {
     findByAccount0(accId, Some(name.id), true).option
 
   def delete(accId: AccountId, connName: Ident): ConnectionIO[Int] = {
-    val uId    = RUser.Columns.uid
-    val uColl  = RUser.Columns.cid
-    val uLogin = RUser.Columns.login
+    val user   = RUser.as("u")
+    val uId    = user.uid.column
+    val uColl  = user.cid.column
+    val uLogin = user.login.column
     val cond   = Seq(uColl.is(accId.collective), uLogin.is(accId.user))
 
     deleteFrom(
       table,
-      fr"uid in (" ++ selectSimple(Seq(uId), RUser.table, and(cond)) ++ fr") AND" ++ name
+      fr"uid in (" ++ selectSimple(
+        Seq(uId),
+        Fragment.const(user.tableName),
+        and(cond)
+      ) ++ fr") AND" ++ name
         .is(
           connName
         )
diff --git a/modules/store/src/main/scala/docspell/store/records/RUserImap.scala b/modules/store/src/main/scala/docspell/store/records/RUserImap.scala
index 4babbe76..5cff7c83 100644
--- a/modules/store/src/main/scala/docspell/store/records/RUserImap.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RUserImap.scala
@@ -5,8 +5,8 @@ import cats.effect._
 import cats.implicits._
 
 import docspell.common._
-import docspell.store.impl.Column
-import docspell.store.impl.Implicits._
+import docspell.store.qb.DSL._
+import docspell.store.qb._
 
 import doobie._
 import doobie.implicits._
@@ -92,19 +92,19 @@ object RUserImap {
       now
     )
 
-  val table = fr"userimap"
+  final case class Table(alias: Option[String]) extends TableDef {
+    val tableName = "userimap"
 
-  object Columns {
-    val id            = Column("id")
-    val uid           = Column("uid")
-    val name          = Column("name")
-    val imapHost      = Column("imap_host")
-    val imapPort      = Column("imap_port")
-    val imapUser      = Column("imap_user")
-    val imapPass      = Column("imap_password")
-    val imapSsl       = Column("imap_ssl")
-    val imapCertCheck = Column("imap_certcheck")
-    val created       = Column("created")
+    val id            = Column[Ident]("id", this)
+    val uid           = Column[Ident]("uid", this)
+    val name          = Column[Ident]("name", this)
+    val imapHost      = Column[String]("imap_host", this)
+    val imapPort      = Column[Int]("imap_port", this)
+    val imapUser      = Column[String]("imap_user", this)
+    val imapPass      = Column[Password]("imap_password", this)
+    val imapSsl       = Column[SSLType]("imap_ssl", this)
+    val imapCertCheck = Column[Boolean]("imap_certcheck", this)
+    val created       = Column[Timestamp]("created", this)
 
     val all = List(
       id,
@@ -120,52 +120,64 @@ object RUserImap {
     )
   }
 
-  import Columns._
+  def as(alias: String): Table =
+    Table(Some(alias))
 
-  def insert(v: RUserImap): ConnectionIO[Int] =
-    insertRow(
-      table,
-      all,
-      sql"${v.id},${v.uid},${v.name},${v.imapHost},${v.imapPort},${v.imapUser},${v.imapPassword},${v.imapSsl},${v.imapCertCheck},${v.created}"
-    ).update.run
-
-  def update(eId: Ident, v: RUserImap): ConnectionIO[Int] =
-    updateRow(
-      table,
-      id.is(eId),
-      commas(
-        name.setTo(v.name),
-        imapHost.setTo(v.imapHost),
-        imapPort.setTo(v.imapPort),
-        imapUser.setTo(v.imapUser),
-        imapPass.setTo(v.imapPassword),
-        imapSsl.setTo(v.imapSsl),
-        imapCertCheck.setTo(v.imapCertCheck)
+  def insert(v: RUserImap): ConnectionIO[Int] = {
+    val t = Table(None)
+    DML
+      .insert(
+        t,
+        t.all,
+        sql"${v.id},${v.uid},${v.name},${v.imapHost},${v.imapPort},${v.imapUser},${v.imapPassword},${v.imapSsl},${v.imapCertCheck},${v.created}"
       )
-    ).update.run
+      .update
+      .run
+  }
 
-  def findByUser(userId: Ident): ConnectionIO[Vector[RUserImap]] =
-    selectSimple(all, table, uid.is(userId)).query[RUserImap].to[Vector]
+  def update(eId: Ident, v: RUserImap): ConnectionIO[Int] = {
+    val t = Table(None)
+    DML.update(
+      t,
+      t.id === eId,
+      DML.set(
+        t.name.setTo(v.name),
+        t.imapHost.setTo(v.imapHost),
+        t.imapPort.setTo(v.imapPort),
+        t.imapUser.setTo(v.imapUser),
+        t.imapPass.setTo(v.imapPassword),
+        t.imapSsl.setTo(v.imapSsl),
+        t.imapCertCheck.setTo(v.imapCertCheck)
+      )
+    )
+  }
+
+  def findByUser(userId: Ident): ConnectionIO[Vector[RUserImap]] = {
+    val t = Table(None)
+    run(select(t.all), from(t), t.uid === userId).query[RUserImap].to[Vector]
+  }
 
   private def findByAccount0(
       accId: AccountId,
       nameQ: Option[String],
       exact: Boolean
   ): Query0[RUserImap] = {
-    val mUid   = uid.prefix("m")
-    val mName  = name.prefix("m")
-    val uId    = RUser.Columns.uid.prefix("u")
-    val uColl  = RUser.Columns.cid.prefix("u")
-    val uLogin = RUser.Columns.login.prefix("u")
-    val from   = table ++ fr"m INNER JOIN" ++ RUser.table ++ fr"u ON" ++ mUid.is(uId)
-    val cond = Seq(uColl.is(accId.collective), uLogin.is(accId.user)) ++ (nameQ match {
-      case Some(str) if exact => Seq(mName.is(str))
-      case Some(str)          => Seq(mName.lowerLike(s"%${str.toLowerCase}%"))
-      case None               => Seq.empty
-    })
+    val m = RUserImap.as("m")
+    val u = RUser.as("u")
 
-    (selectSimple(all.map(_.prefix("m")), from, and(cond)) ++ orderBy(mName.f))
-      .query[RUserImap]
+    val nameFilter =
+      nameQ.map { str =>
+        if (exact) m.name ==== str
+        else m.name.likes(s"%${str.toLowerCase}%")
+      }
+
+    val sql = Select(
+      select(m.all),
+      from(m).innerJoin(u, m.uid === u.uid),
+      u.cid === accId.collective && u.login === accId.user &&? nameFilter
+    ).orderBy(m.name).run
+
+    sql.query[RUserImap]
   }
 
   def findByAccount(
@@ -178,26 +190,28 @@ object RUserImap {
     findByAccount0(accId, Some(name.id), true).option
 
   def delete(accId: AccountId, connName: Ident): ConnectionIO[Int] = {
-    val uId    = RUser.Columns.uid
-    val uColl  = RUser.Columns.cid
-    val uLogin = RUser.Columns.login
-    val cond   = Seq(uColl.is(accId.collective), uLogin.is(accId.user))
+    val t = Table(None)
+    val u = RUser.as("u")
+    val subsel =
+      Select(select(u.uid), from(u), u.cid === accId.collective && u.login === accId.user)
 
-    deleteFrom(
-      table,
-      fr"uid in (" ++ selectSimple(Seq(uId), RUser.table, and(cond)) ++ fr") AND" ++ name
-        .is(
-          connName
-        )
-    ).update.run
+    DML
+      .delete(
+        t,
+        t.uid.in(subsel) && t.name === connName
+      )
+      .update
+      .run
   }
 
   def exists(accId: AccountId, name: Ident): ConnectionIO[Boolean] =
     getByName(accId, name).map(_.isDefined)
 
-  def exists(userId: Ident, connName: Ident): ConnectionIO[Boolean] =
-    selectCount(id, table, and(uid.is(userId), name.is(connName)))
+  def exists(userId: Ident, connName: Ident): ConnectionIO[Boolean] = {
+    val t = Table(None)
+    run(select(count(t.id)), from(t), t.uid === userId && t.name === connName)
       .query[Int]
       .unique
       .map(_ > 0)
+  }
 }