From d6f28d3eca4a1c0d8777bc17381517de871d4acf Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Sun, 13 Dec 2020 00:26:58 +0100 Subject: [PATCH] Convert folder --- .../main/scala/docspell/store/qb/Column.scala | 11 +- .../scala/docspell/store/qb/Condition.scala | 8 +- .../scala/docspell/store/qb/CteBind.scala | 9 + .../scala/docspell/store/qb/DBFunction.scala | 9 +- .../main/scala/docspell/store/qb/DML.scala | 2 +- .../main/scala/docspell/store/qb/DSL.scala | 48 +++-- .../main/scala/docspell/store/qb/Select.scala | 29 +-- .../scala/docspell/store/qb/SelectExpr.scala | 12 +- .../scala/docspell/store/qb/TableDef.scala | 11 +- .../store/qb/impl/ConditionBuilder.scala | 6 +- .../store/qb/impl/DBFunctionBuilder.scala | 20 +- .../store/qb/impl/FromExprBuilder.scala | 2 +- ...{DoobieQuery.scala => SelectBuilder.scala} | 26 +-- .../store/qb/impl/SelectExprBuilder.scala | 8 +- .../docspell/store/queries/QFolder.scala | 172 ++++++++---------- .../scala/docspell/store/queries/QItem.scala | 28 ++- .../docspell/store/records/RContact.scala | 2 +- .../docspell/store/records/RFolder.scala | 61 +++---- .../store/records/RFolderMember.scala | 37 ++-- .../scala/docspell/store/records/RTag.scala | 2 +- .../store/qb/impl/DoobieQueryTest.scala | 2 +- 21 files changed, 295 insertions(+), 210 deletions(-) create mode 100644 modules/store/src/main/scala/docspell/store/qb/CteBind.scala rename modules/store/src/main/scala/docspell/store/qb/impl/{DoobieQuery.scala => SelectBuilder.scala} (71%) 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 a8465417..60d49abc 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,14 @@ package docspell.store.qb -case class Column[A](name: String, table: TableDef) +case class Column[A](name: String, table: TableDef) { + def inTable(t: TableDef): Column[A] = + copy(table = t) + + def s: SelectExpr = + SelectExpr.SelectColumn(this, None) + + def as(alias: String): SelectExpr = + SelectExpr.SelectColumn(this, 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 4738545a..264889f7 100644 --- a/modules/store/src/main/scala/docspell/store/qb/Condition.scala +++ b/modules/store/src/main/scala/docspell/store/qb/Condition.scala @@ -4,7 +4,13 @@ import cats.data.NonEmptyList import doobie._ -sealed trait Condition {} +sealed trait Condition { + def s: SelectExpr.SelectCondition = + SelectExpr.SelectCondition(this, None) + + def as(alias: String): SelectExpr.SelectCondition = + SelectExpr.SelectCondition(this, Some(alias)) +} object Condition { diff --git a/modules/store/src/main/scala/docspell/store/qb/CteBind.scala b/modules/store/src/main/scala/docspell/store/qb/CteBind.scala new file mode 100644 index 00000000..0a22a056 --- /dev/null +++ b/modules/store/src/main/scala/docspell/store/qb/CteBind.scala @@ -0,0 +1,9 @@ +package docspell.store.qb + +case class CteBind(name: TableDef, select: Select) {} + +object CteBind { + + def apply(t: (TableDef, Select)): CteBind = + CteBind(t._1, t._2) +} diff --git a/modules/store/src/main/scala/docspell/store/qb/DBFunction.scala b/modules/store/src/main/scala/docspell/store/qb/DBFunction.scala index 52b50024..24de3520 100644 --- a/modules/store/src/main/scala/docspell/store/qb/DBFunction.scala +++ b/modules/store/src/main/scala/docspell/store/qb/DBFunction.scala @@ -21,7 +21,12 @@ object DBFunction { case class Power(expr: SelectExpr, base: Int) extends DBFunction - case class Plus(expr: SelectExpr, exprs: Vector[SelectExpr]) extends DBFunction + case class Calc(op: Operator, left: SelectExpr, right: SelectExpr) extends DBFunction - case class Mult(expr: SelectExpr, exprs: Vector[SelectExpr]) extends DBFunction + sealed trait Operator + object Operator { + case object Plus extends Operator + case object Minus extends Operator + case object Mult extends Operator + } } diff --git a/modules/store/src/main/scala/docspell/store/qb/DML.scala b/modules/store/src/main/scala/docspell/store/qb/DML.scala index 194ee3db..e8fd6dcf 100644 --- a/modules/store/src/main/scala/docspell/store/qb/DML.scala +++ b/modules/store/src/main/scala/docspell/store/qb/DML.scala @@ -48,7 +48,7 @@ object DML { cond: Option[Condition], setter: Seq[Setter[_]] ): Fragment = { - val condFrag = cond.map(DoobieQuery.cond).getOrElse(Fragment.empty) + val condFrag = cond.map(SelectBuilder.cond).getOrElse(Fragment.empty) fr"UPDATE" ++ FromExprBuilder.buildTable(table) ++ fr"SET" ++ setter .map(s => buildSetter(s)) 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 849305c4..6223ebc0 100644 --- a/modules/store/src/main/scala/docspell/store/qb/DSL.scala +++ b/modules/store/src/main/scala/docspell/store/qb/DSL.scala @@ -3,30 +3,39 @@ package docspell.store.qb import cats.data.NonEmptyList import docspell.store.impl.DoobieMeta -import docspell.store.qb.impl.DoobieQuery +import docspell.store.qb.impl.SelectBuilder import doobie.{Fragment, Put} trait DSL extends DoobieMeta { def run(projection: Seq[SelectExpr], from: FromExpr): Fragment = - DoobieQuery(Select(projection, from, None)) + SelectBuilder(Select(projection, from)) def run(projection: Seq[SelectExpr], from: FromExpr, where: Condition): Fragment = - DoobieQuery(Select(projection, from, where)) + SelectBuilder(Select(projection, from, where)) def runDistinct( projection: Seq[SelectExpr], from: FromExpr, where: Condition ): Fragment = - DoobieQuery.distinct(Select(projection, from, where)) + SelectBuilder(Select(projection, from, where).distinct) + + def withCte(cte: (TableDef, Select), more: (TableDef, Select)*): DSL.WithCteDsl = + DSL.WithCteDsl(CteBind(cte), more.map(CteBind.apply).toVector) + + def select(cond: Condition): Seq[SelectExpr] = + Seq(SelectExpr.SelectCondition(cond, None)) def select(dbf: DBFunction): Seq[SelectExpr] = Seq(SelectExpr.SelectFun(dbf, None)) + def select(e: SelectExpr, es: SelectExpr*): Seq[SelectExpr] = + es.prepended(e) + def select(c: Column[_], cs: Column[_]*): Seq[SelectExpr] = - select(c :: cs.toList) + cs.prepended(c).map(col => SelectExpr.SelectColumn(col, None)) def select(seq: Seq[Column[_]], seqs: Seq[Column[_]]*): Seq[SelectExpr] = (seq ++ seqs.flatten).map(c => SelectExpr.SelectColumn(c, None)) @@ -43,6 +52,9 @@ trait DSL extends DoobieMeta { def count(c: Column[_]): DBFunction = DBFunction.Count(c) + def countAll: DBFunction = + DBFunction.CountAll + def max(c: Column[_]): DBFunction = DBFunction.Max(c) @@ -58,11 +70,11 @@ trait DSL extends DoobieMeta { def lit[A](value: A)(implicit P: Put[A]): SelectExpr.SelectLit[A] = SelectExpr.SelectLit(value, None) - def plus(expr: SelectExpr, more: SelectExpr*): DBFunction = - DBFunction.Plus(expr, more.toVector) + def plus(left: SelectExpr, right: SelectExpr): DBFunction = + DBFunction.Calc(DBFunction.Operator.Plus, left, right) - def mult(expr: SelectExpr, more: SelectExpr*): DBFunction = - DBFunction.Mult(expr, more.toVector) + def mult(left: SelectExpr, right: SelectExpr): DBFunction = + DBFunction.Calc(DBFunction.Operator.Mult, left, right) def and(c: Condition, cs: Condition*): Condition = c match { @@ -205,8 +217,22 @@ trait DSL extends DoobieMeta { def <>[A](value: A)(implicit P: Put[A]): Condition = Condition.CompareFVal(dbf, Operator.Neq, value) + + def -[A](value: A)(implicit P: Put[A]): DBFunction = + DBFunction.Calc( + DBFunction.Operator.Minus, + SelectExpr.SelectFun(dbf, None), + SelectExpr.SelectLit(value, None) + ) + } +} + +object DSL extends DSL { + + final case class WithCteDsl(cte: CteBind, ctes: Vector[CteBind]) { + + def select(s: Select): Select.WithCte = + Select.WithCte(cte, ctes, s) } } - -object DSL extends DSL diff --git a/modules/store/src/main/scala/docspell/store/qb/Select.scala b/modules/store/src/main/scala/docspell/store/qb/Select.scala index c7076be5..ac6d0ca1 100644 --- a/modules/store/src/main/scala/docspell/store/qb/Select.scala +++ b/modules/store/src/main/scala/docspell/store/qb/Select.scala @@ -1,15 +1,15 @@ package docspell.store.qb -import docspell.store.qb.impl.DoobieQuery +import docspell.store.qb.impl.SelectBuilder import doobie._ sealed trait Select { - def distinct: Fragment = - DoobieQuery.distinct(this) - def run: Fragment = - DoobieQuery(this) + SelectBuilder(this) + + def as(alias: String): SelectExpr.SelectQuery = + SelectExpr.SelectQuery(this, Some(alias)) def orderBy(ob: OrderBy, obs: OrderBy*): Select.Ordered = Select.Ordered(this, ob, obs.toVector) @@ -20,27 +20,29 @@ sealed trait Select { def limit(n: Int): Select = this match { case Select.Limit(q, _) => Select.Limit(q, n) - case _ => - Select.Limit(this, n) + case _ => Select.Limit(this, n) } } object Select { + def apply(projection: Seq[SelectExpr], from: FromExpr) = + SimpleSelect(false, projection, from, None, None) def apply( projection: Seq[SelectExpr], from: FromExpr, where: Condition - ) = SimpleSelect(projection, from, Some(where), None) + ) = SimpleSelect(false, projection, from, Some(where), None) def apply( projection: Seq[SelectExpr], from: FromExpr, - where: Option[Condition] = None, - groupBy: Option[GroupBy] = None - ) = SimpleSelect(projection, from, where, groupBy) + where: Condition, + groupBy: GroupBy + ) = SimpleSelect(false, projection, from, Some(where), Some(groupBy)) case class SimpleSelect( + distinctFlag: Boolean, projection: Seq[SelectExpr], from: FromExpr, where: Option[Condition], @@ -48,6 +50,9 @@ object Select { ) extends Select { def group(gb: GroupBy): SimpleSelect = copy(groupBy = Some(gb)) + + def distinct: SimpleSelect = + copy(distinctFlag = true) } case class Union(q: Select, qs: Vector[Select]) extends Select @@ -58,4 +63,6 @@ object Select { extends Select case class Limit(q: Select, limit: Int) extends Select + + case class WithCte(cte: CteBind, ctes: Vector[CteBind], query: Select) extends Select } diff --git a/modules/store/src/main/scala/docspell/store/qb/SelectExpr.scala b/modules/store/src/main/scala/docspell/store/qb/SelectExpr.scala index fec6eee4..ba029f5c 100644 --- a/modules/store/src/main/scala/docspell/store/qb/SelectExpr.scala +++ b/modules/store/src/main/scala/docspell/store/qb/SelectExpr.scala @@ -2,7 +2,7 @@ package docspell.store.qb import doobie.Put -sealed trait SelectExpr { self => +sealed trait SelectExpr { def as(alias: String): SelectExpr } @@ -24,4 +24,14 @@ object SelectExpr { copy(alias = Some(a)) } + case class SelectQuery(query: Select, alias: Option[String]) extends SelectExpr { + def as(a: String): SelectQuery = + copy(alias = Some(a)) + } + + case class SelectCondition(cond: Condition, alias: Option[String]) extends SelectExpr { + def as(a: String): SelectCondition = + copy(alias = Some(a)) + } + } diff --git a/modules/store/src/main/scala/docspell/store/qb/TableDef.scala b/modules/store/src/main/scala/docspell/store/qb/TableDef.scala index 072d98c5..4ef6cfa4 100644 --- a/modules/store/src/main/scala/docspell/store/qb/TableDef.scala +++ b/modules/store/src/main/scala/docspell/store/qb/TableDef.scala @@ -9,8 +9,11 @@ trait TableDef { object TableDef { def apply(table: String, aliasName: Option[String] = None): TableDef = - new TableDef { - def tableName: String = table - def alias: Option[String] = aliasName - } + BasicTable(table, aliasName) + + final case class BasicTable(tableName: String, alias: Option[String]) extends TableDef { + def as(alias: String): BasicTable = + copy(alias = Some(alias)) + } + } 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 8f56738a..cc700b18 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 @@ -6,8 +6,8 @@ import _root_.doobie.implicits._ import _root_.doobie.{Query => _, _} object ConditionBuilder { - val or = fr"OR" - val and = fr"AND" + val or = fr" OR" + val and = fr" AND" val comma = fr"," val parenOpen = Fragment.const0("(") val parenClose = Fragment.const0(")") @@ -46,7 +46,7 @@ object ConditionBuilder { c1Frag ++ operator(op) ++ c2Frag case Condition.InSubSelect(col, subsel) => - val sub = DoobieQuery(subsel) + val sub = SelectBuilder(subsel) SelectExprBuilder.column(col) ++ sql" IN (" ++ sub ++ parenClose case c @ Condition.InValues(col, values, toLower) => diff --git a/modules/store/src/main/scala/docspell/store/qb/impl/DBFunctionBuilder.scala b/modules/store/src/main/scala/docspell/store/qb/impl/DBFunctionBuilder.scala index cbc48ec8..494ec66c 100644 --- a/modules/store/src/main/scala/docspell/store/qb/impl/DBFunctionBuilder.scala +++ b/modules/store/src/main/scala/docspell/store/qb/impl/DBFunctionBuilder.scala @@ -29,12 +29,20 @@ object DBFunctionBuilder extends CommonBuilder { case DBFunction.Power(expr, base) => sql"POWER($base, " ++ SelectExprBuilder.build(expr) ++ sql")" - case DBFunction.Plus(expr, more) => - val v = more.prepended(expr).map(SelectExprBuilder.build) - v.reduce(_ ++ fr" +" ++ _) + case DBFunction.Calc(op, left, right) => + SelectExprBuilder.build(left) ++ + buildOperator(op) ++ + SelectExprBuilder.build(right) - case DBFunction.Mult(expr, more) => - val v = more.prepended(expr).map(SelectExprBuilder.build) - v.reduce(_ ++ fr" *" ++ _) + } + + def buildOperator(op: DBFunction.Operator): Fragment = + op match { + case DBFunction.Operator.Minus => + fr" -" + case DBFunction.Operator.Plus => + fr" +" + case DBFunction.Operator.Mult => + fr" *" } } diff --git a/modules/store/src/main/scala/docspell/store/qb/impl/FromExprBuilder.scala b/modules/store/src/main/scala/docspell/store/qb/impl/FromExprBuilder.scala index 71ee3606..926ed330 100644 --- a/modules/store/src/main/scala/docspell/store/qb/impl/FromExprBuilder.scala +++ b/modules/store/src/main/scala/docspell/store/qb/impl/FromExprBuilder.scala @@ -17,7 +17,7 @@ object FromExprBuilder { joins.map(buildJoin).foldLeft(Fragment.empty)(_ ++ _) case FromExpr.SubSelect(sel, name) => - sql" FROM (" ++ DoobieQuery(sel) ++ fr") AS" ++ Fragment.const(name) + sql" FROM (" ++ SelectBuilder(sel) ++ fr") AS" ++ Fragment.const(name) } def buildTable(table: TableDef): Fragment = diff --git a/modules/store/src/main/scala/docspell/store/qb/impl/DoobieQuery.scala b/modules/store/src/main/scala/docspell/store/qb/impl/SelectBuilder.scala similarity index 71% rename from modules/store/src/main/scala/docspell/store/qb/impl/DoobieQuery.scala rename to modules/store/src/main/scala/docspell/store/qb/impl/SelectBuilder.scala index b1a45d70..3d64c67b 100644 --- a/modules/store/src/main/scala/docspell/store/qb/impl/DoobieQuery.scala +++ b/modules/store/src/main/scala/docspell/store/qb/impl/SelectBuilder.scala @@ -5,7 +5,7 @@ import docspell.store.qb._ import _root_.doobie.implicits._ import _root_.doobie.{Query => _, _} -object DoobieQuery { +object SelectBuilder { val comma = fr"," val asc = fr" ASC" val desc = fr" DESC" @@ -13,29 +13,30 @@ object DoobieQuery { val union = fr"UNION ALL" def apply(q: Select): Fragment = - build(false)(q) + build(q) - def distinct(q: Select): Fragment = - build(true)(q) - - def build(distinct: Boolean)(q: Select): Fragment = + def build(q: Select): Fragment = q match { case sq: Select.SimpleSelect => - val sel = if (distinct) fr"SELECT DISTINCT" else fr"SELECT" + val sel = if (sq.distinctFlag) fr"SELECT DISTINCT" else fr"SELECT" sel ++ buildSimple(sq) case Select.Union(q, qs) => - qs.prepended(q).map(build(false)).reduce(_ ++ union ++ _) + qs.prepended(q).map(build).reduce(_ ++ union ++ _) case Select.Intersect(q, qs) => - qs.prepended(q).map(build(false)).reduce(_ ++ intersect ++ _) + qs.prepended(q).map(build).reduce(_ ++ intersect ++ _) case Select.Ordered(q, ob, obs) => val order = obs.prepended(ob).map(orderBy).reduce(_ ++ comma ++ _) - build(distinct)(q) ++ fr"ORDER BY" ++ order + build(q) ++ fr" ORDER BY" ++ order case Select.Limit(q, n) => - build(distinct)(q) ++ fr" LIMIT $n" + build(q) ++ fr" LIMIT $n" + + case Select.WithCte(cte, moreCte, query) => + val ctes = moreCte.prepended(cte) + fr"WITH" ++ ctes.map(buildCte).reduce(_ ++ comma ++ _) ++ fr" " ++ build(query) } def buildSimple(sq: Select.SimpleSelect): Fragment = { @@ -71,4 +72,7 @@ object DoobieQuery { val f1 = gb.having.map(cond).getOrElse(Fragment.empty) fr"GROUP BY" ++ f0 ++ f1 } + + def buildCte(bind: CteBind): Fragment = + Fragment.const(bind.name.tableName) ++ sql"AS (" ++ build(bind.select) ++ sql")" } diff --git a/modules/store/src/main/scala/docspell/store/qb/impl/SelectExprBuilder.scala b/modules/store/src/main/scala/docspell/store/qb/impl/SelectExprBuilder.scala index b027b704..c3b5daab 100644 --- a/modules/store/src/main/scala/docspell/store/qb/impl/SelectExprBuilder.scala +++ b/modules/store/src/main/scala/docspell/store/qb/impl/SelectExprBuilder.scala @@ -2,7 +2,8 @@ package docspell.store.qb.impl import docspell.store.qb._ -import _root_.doobie.{Query => _, _} +import doobie._ +import doobie.implicits._ object SelectExprBuilder extends CommonBuilder { @@ -17,6 +18,11 @@ object SelectExprBuilder extends CommonBuilder { case SelectExpr.SelectFun(fun, alias) => DBFunctionBuilder.build(fun) ++ appendAs(alias) + case SelectExpr.SelectQuery(query, alias) => + sql"(" ++ SelectBuilder.build(query) ++ sql")" ++ appendAs(alias) + + case SelectExpr.SelectCondition(cond, alias) => + sql"(" ++ ConditionBuilder.build(cond) ++ sql")" ++ appendAs(alias) } } 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 2f71fe0d..79b6341f 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QFolder.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QFolder.scala @@ -4,7 +4,8 @@ import cats.data.OptionT import cats.implicits._ import docspell.common._ -import docspell.store.impl.Implicits._ +import docspell.store.qb.DSL._ +import docspell.store.qb._ import docspell.store.records._ import doobie._ @@ -136,22 +137,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 = user.uid.column - val uLogin = user.login.column - val sColl = RFolder.Columns.collective.prefix("s") - val sId = RFolder.Columns.id.prefix("s") + val user = RUser.as("u") + val member = RFolderMember.as("m") + val folder = RFolder.as("s") - val from = RFolderMember.table ++ fr"m 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( - Seq(uId, uLogin), - from, - and(mFolderId.is(id), sColl.is(account.collective)) + val memberQ = run( + select(user.uid, user.login), + from(member) + .innerJoin(user, member.user === user.uid) + .innerJoin(folder, member.folder === folder.id), + member.folder === id && folder.collective === account.collective ).query[IdRef].to[Vector] (for { @@ -188,92 +183,85 @@ object QFolder { // inner join user_ u on u.uid = s.owner // where s.cid = 'eike'; - 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") - val sColl = RFolder.Columns.collective.prefix("s") - val mUser = RFolderMember.Columns.user.prefix("m") - val mFolder = RFolderMember.Columns.folder.prefix("m") + val user = RUser.as("u") + val member = RFolderMember.as("m") + val folder = RFolder.as("s") + val memlogin = TableDef("memberlogin") + val memloginFolder = member.folder.inTable(memlogin) + val memloginLogn = user.login.inTable(memlogin) - //CTE - val cte: Fragment = { - val from1 = RFolderMember.table ++ fr"m 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" ++ - Fragment.const(user.tableName) ++ fr"u ON" ++ uId.is(sOwner) - - withCTE( - "memberlogin" -> - (selectSimple(Seq(mFolder, uLogin), from1, sColl.is(account.collective)) ++ - fr"UNION ALL" ++ - selectSimple(Seq(sId, uLogin), from2, sColl.is(account.collective))) + val sql = + withCte( + memlogin -> union( + Select( + select(member.folder, user.login), + from(member) + .innerJoin(user, user.uid === member.user) + .innerJoin(folder, folder.id === member.folder), + folder.collective === account.collective + ), + Select( + select(folder.id, user.login), + from(folder) + .innerJoin(user, user.uid === folder.owner), + folder.collective === account.collective + ) + ) ) - } + .select( + Select( + select( + folder.id.s, + folder.name.s, + folder.owner.s, + user.login.s, + folder.created.s, + Select( + select(countAll > 0), + from(memlogin), + memloginFolder === folder.id && memloginLogn === account.user + ).as("member"), + Select( + select(countAll - 1), + from(memlogin), + memloginFolder === folder.id + ).as("member_count") + ), + from(folder) + .innerJoin(user, user.uid === folder.owner), + where( + folder.collective === account.collective &&? + idQ.map(id => folder.id === id) &&? + nameQ.map(q => folder.name.like(s"%${q.toLowerCase}%")) &&? + ownerLogin.map(login => user.login === login) + ) + ).orderBy(folder.name.asc) + ) - val isMember = - fr"SELECT COUNT(*) > 0 FROM memberlogin WHERE" ++ mFolder.prefix("").is(sId) ++ - fr"AND" ++ uLogin.prefix("").is(account.user) - - val memberCount = - fr"SELECT COUNT(*) - 1 FROM memberlogin WHERE" ++ mFolder.prefix("").is(sId) - - //Query - val cols = Seq( - sId.f, - sName.f, - sOwner.f, - uLogin.f, - RFolder.Columns.created.prefix("s").f, - fr"(" ++ isMember ++ fr") as mem", - fr"(" ++ memberCount ++ fr") as cnt" - ) - - val from = RFolder.table ++ fr"s INNER JOIN" ++ - Fragment.const(user.tableName) ++ fr"u ON" ++ uId.is(sOwner) - - val where = - sColl.is(account.collective) :: idQ.toList - .map(id => sId.is(id)) ::: nameQ.toList.map(q => - sName.lowerLike(s"%${q.toLowerCase}%") - ) ::: ownerLogin.toList.map(login => uLogin.is(login)) - - (cte ++ selectSimple(commas(cols), from, and(where) ++ orderBy(sName.asc))) + sql.run .query[FolderItem] .to[Vector] } /** 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 = 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" ++ Fragment.const( - user.tableName - ) ++ fr"u ON" ++ fOwner.is(uId), - and(fColl.is(account.collective), uLogin.is(account.user)) - ) ++ - fr"UNION ALL" ++ - selectSimple( - Seq(mFolder), - RFolderMember.table ++ fr"m INNER JOIN" ++ RFolder.table ++ fr"f ON" ++ fId.is( - mFolder - ) ++ - fr"INNER JOIN" ++ Fragment.const(user.tableName) ++ fr"u ON" ++ uId.is(mUser), - and(fColl.is(account.collective), uLogin.is(account.user)) + val user = RUser.as("u") + val f = RFolder.as("f") + val m = RFolderMember.as("m") + union( + Select( + select(f.id), + from(f).innerJoin(user, f.owner === user.uid), + f.collective === account.collective && user.login === account.user + ), + Select( + select(m.folder), + from(m) + .innerJoin(f, f.id === m.folder) + .innerJoin(user, user.uid === m.user), + f.collective === account.collective && user.login === account.user ) + ).run } def getMemberFolders(account: AccountId): ConnectionIO[Set[Ident]] = diff --git a/modules/store/src/main/scala/docspell/store/queries/QItem.scala b/modules/store/src/main/scala/docspell/store/queries/QItem.scala index b41ebfa6..b7c50f44 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala @@ -91,6 +91,7 @@ object QItem { val org = ROrganization.as("o") val pers0 = RPerson.as("p0") val pers1 = RPerson.as("p1") + val f = RFolder.as("f") val IC = RItem.Columns.all.map(_.prefix("i")) val OC = org.all.map(_.column) @@ -98,7 +99,7 @@ object QItem { val P1C = pers1.all.map(_.column) val EC = equip.all.map(_.oldColumn).map(_.prefix("e")) val ICC = List(RItem.Columns.id, RItem.Columns.name).map(_.prefix("ref")) - val FC = List(RFolder.Columns.id, RFolder.Columns.name).map(_.prefix("f")) + val FC = List(f.id.column, f.name.column) val cq = selectSimple( @@ -129,9 +130,11 @@ object QItem { fr"LEFT JOIN" ++ RItem.table ++ fr"ref ON" ++ RItem.Columns.inReplyTo .prefix("i") .is(RItem.Columns.id.prefix("ref")) ++ - fr"LEFT JOIN" ++ RFolder.table ++ fr"f ON" ++ RItem.Columns.folder + fr"LEFT JOIN" ++ Fragment.const( + RFolder.T.tableName + ) ++ fr"f ON" ++ RItem.Columns.folder .prefix("i") - .is(RFolder.Columns.id.prefix("f")) ++ + .is(f.id.column) ++ fr"WHERE" ++ RItem.Columns.id.prefix("i").is(id) val q = cq @@ -322,13 +325,13 @@ object QItem { val org = ROrganization.as("o0") val pers0 = RPerson.as("p0") val pers1 = RPerson.as("p1") + val f = RFolder.as("f1") val IC = RItem.Columns val AC = RAttachment.Columns - val FC = RFolder.Columns val itemCols = IC.all val equipCols = List(equip.eid.oldColumn, equip.name.oldColumn) - val folderCols = List(FC.id, FC.name) + val folderCols = List(f.id.oldColumn, f.name.oldColumn) val cvItem = RCustomFieldValue.Columns.itemId.prefix("cv") val finalCols = commas( @@ -350,8 +353,8 @@ object QItem { pers1.name.column.f, equip.eid.oldColumn.prefix("e1").f, equip.name.oldColumn.prefix("e1").f, - FC.id.prefix("f1").f, - FC.name.prefix("f1").f, + f.id.column.f, + f.name.column.f, // sql uses 1 for first character IC.notes.prefix("i").substring(1, noteMaxLen), // last column is only for sorting @@ -384,7 +387,11 @@ object QItem { equip.cid.oldColumn.is(q.account.collective) ) val withFolder = - selectSimple(folderCols, RFolder.table, FC.collective.is(q.account.collective)) + selectSimple( + folderCols, + Fragment.const(f.tableName), + f.collective.oldColumn.is(q.account.collective) + ) val withAttach = fr"SELECT COUNT(" ++ AC.id.f ++ fr") as num, " ++ AC.itemId.f ++ fr"from" ++ RAttachment.table ++ fr"GROUP BY (" ++ AC.itemId.f ++ fr")" @@ -410,7 +417,7 @@ object QItem { fr"LEFT JOIN equips e1 ON" ++ IC.concEquipment .prefix("i") .is(equip.eid.oldColumn.prefix("e1")) ++ - fr"LEFT JOIN folders f1 ON" ++ IC.folder.prefix("i").is(FC.id.prefix("f1")) ++ + fr"LEFT JOIN folders f1 ON" ++ IC.folder.prefix("i").is(f.id.column) ++ (if (q.customValues.isEmpty) Fragment.empty else fr"INNER JOIN customvalues cv ON" ++ cvItem.is(IC.id.prefix("i"))) @@ -425,6 +432,7 @@ object QItem { val org = ROrganization.as("o0") val pers0 = RPerson.as("p0") val pers1 = RPerson.as("p1") + val f = RFolder.as("f1") val IC = RItem.Columns // inclusive tags are AND-ed @@ -468,7 +476,7 @@ object QItem { org.oid.column.isOrDiscard(q.corrOrg), pers1.pid.column.isOrDiscard(q.concPerson), equip.eid.oldColumn.prefix("e1").isOrDiscard(q.concEquip), - RFolder.Columns.id.prefix("f1").isOrDiscard(q.folder), + f.id.column.isOrDiscard(q.folder), if (q.tagsInclude.isEmpty && q.tagCategoryIncl.isEmpty) Fragment.empty else IC.id.prefix("i") ++ sql" IN (" ++ tagSelectsIncl diff --git a/modules/store/src/main/scala/docspell/store/records/RContact.scala b/modules/store/src/main/scala/docspell/store/records/RContact.scala index 7fd64778..40053665 100644 --- a/modules/store/src/main/scala/docspell/store/records/RContact.scala +++ b/modules/store/src/main/scala/docspell/store/records/RContact.scala @@ -27,7 +27,7 @@ object RContact { val personId = Column[Ident]("pid", this) val orgId = Column[Ident]("oid", this) val created = Column[Timestamp]("created", this) - val all = List(contactId, value, kind, personId, orgId, created) + val all = List[Column[_]](contactId, value, kind, personId, orgId, created) } private val T = Table(None) diff --git a/modules/store/src/main/scala/docspell/store/records/RFolder.scala b/modules/store/src/main/scala/docspell/store/records/RFolder.scala index 0b3b0ebb..d678fe9f 100644 --- a/modules/store/src/main/scala/docspell/store/records/RFolder.scala +++ b/modules/store/src/main/scala/docspell/store/records/RFolder.scala @@ -4,8 +4,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._ @@ -26,61 +26,58 @@ object RFolder { now <- Timestamp.current[F] } yield RFolder(nId, name, account.collective, account.user, now) - val table = fr"folder" + final case class Table(alias: Option[String]) extends TableDef { + val tableName = "folder" - object Columns { - - val id = Column("id") - val name = Column("name") - val collective = Column("cid") - val owner = Column("owner") - val created = Column("created") + val id = Column[Ident]("id", this) + val name = Column[String]("name", this) + val collective = Column[Ident]("cid", this) + val owner = Column[Ident]("owner", this) + val created = Column[Timestamp]("created", this) val all = List(id, name, collective, owner, created) } - import Columns._ + val T = Table(None) + def as(alias: String): Table = + Table(Some(alias)) - def insert(value: RFolder): ConnectionIO[Int] = { - val sql = insertRow( - table, - all, + def insert(value: RFolder): ConnectionIO[Int] = + DML.insert( + T, + T.all, fr"${value.id},${value.name},${value.collectiveId},${value.owner},${value.created}" ) - sql.update.run - } def update(v: RFolder): ConnectionIO[Int] = - updateRow( - table, - and(id.is(v.id), collective.is(v.collectiveId), owner.is(v.owner)), - name.setTo(v.name) - ).update.run + DML.update( + T, + T.id === v.id && T.collective === v.collectiveId && T.owner === v.owner, + DML.set(T.name.setTo(v.name)) + ) def existsByName(coll: Ident, folderName: String): ConnectionIO[Boolean] = - selectCount(id, table, and(collective.is(coll), name.is(folderName))) + run(select(count(T.id)), from(T), T.collective === coll && T.name === folderName) .query[Int] .unique .map(_ > 0) def findById(folderId: Ident): ConnectionIO[Option[RFolder]] = { - val sql = selectSimple(all, table, id.is(folderId)) + val sql = run(select(T.all), from(T), T.id === folderId) sql.query[RFolder].option } def findAll( coll: Ident, nameQ: Option[String], - order: Columns.type => Column + order: Table => Column[_] ): ConnectionIO[Vector[RFolder]] = { - val q = Seq(collective.is(coll)) ++ (nameQ match { - case Some(str) => Seq(name.lowerLike(s"%${str.toLowerCase}%")) - case None => Seq.empty - }) - val sql = selectSimple(all, table, and(q)) ++ orderBy(order(Columns).f) - sql.query[RFolder].to[Vector] + val nameFilter = nameQ.map(n => T.name.like(s"%${n.toLowerCase}%")) + val sql = Select(select(T.all), from(T), T.collective === coll &&? nameFilter) + .orderBy(order(T)) + sql.run.query[RFolder].to[Vector] } def delete(folderId: Ident): ConnectionIO[Int] = - deleteFrom(table, id.is(folderId)).update.run + DML.delete(T, T.id === folderId) } diff --git a/modules/store/src/main/scala/docspell/store/records/RFolderMember.scala b/modules/store/src/main/scala/docspell/store/records/RFolderMember.scala index cb7b5f21..f16d880d 100644 --- a/modules/store/src/main/scala/docspell/store/records/RFolderMember.scala +++ b/modules/store/src/main/scala/docspell/store/records/RFolderMember.scala @@ -4,8 +4,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._ @@ -25,37 +25,36 @@ object RFolderMember { now <- Timestamp.current[F] } yield RFolderMember(nId, folder, user, now) - val table = fr"folder_member" + final case class Table(alias: Option[String]) extends TableDef { + val tableName = "folder_member" - object Columns { - - val id = Column("id") - val folder = Column("folder_id") - val user = Column("user_id") - val created = Column("created") + val id = Column[Ident]("id", this) + val folder = Column[Ident]("folder_id", this) + val user = Column[Ident]("user_id", this) + val created = Column[Timestamp]("created", this) val all = List(id, folder, user, created) } - import Columns._ + val T = Table(None) + def as(alias: String): Table = + Table(Some(alias)) - def insert(value: RFolderMember): ConnectionIO[Int] = { - val sql = insertRow( - table, - all, + def insert(value: RFolderMember): ConnectionIO[Int] = + DML.insert( + T, + T.all, fr"${value.id},${value.folderId},${value.userId},${value.created}" ) - sql.update.run - } def findByUserId(userId: Ident, folderId: Ident): ConnectionIO[Option[RFolderMember]] = - selectSimple(all, table, and(folder.is(folderId), user.is(userId))) + run(select(T.all), from(T), T.folder === folderId && T.user === userId) .query[RFolderMember] .option def delete(userId: Ident, folderId: Ident): ConnectionIO[Int] = - deleteFrom(table, and(folder.is(folderId), user.is(userId))).update.run + DML.delete(T, T.folder === folderId && T.user === userId) def deleteAll(folderId: Ident): ConnectionIO[Int] = - deleteFrom(table, folder.is(folderId)).update.run + DML.delete(T, T.folder === folderId) } diff --git a/modules/store/src/main/scala/docspell/store/records/RTag.scala b/modules/store/src/main/scala/docspell/store/records/RTag.scala index 3285fbc4..577e6e5d 100644 --- a/modules/store/src/main/scala/docspell/store/records/RTag.scala +++ b/modules/store/src/main/scala/docspell/store/records/RTag.scala @@ -27,7 +27,7 @@ object RTag { val name = Column[String]("name", this) val category = Column[String]("category", this) val created = Column[Timestamp]("created", this) - val all = List(tid, cid, name, category, created) + val all = List[Column[_]](tid, cid, name, category, created) } val T = Table(None) def as(alias: String): Table = diff --git a/modules/store/src/test/scala/docspell/store/qb/impl/DoobieQueryTest.scala b/modules/store/src/test/scala/docspell/store/qb/impl/DoobieQueryTest.scala index a5b22f81..2d920352 100644 --- a/modules/store/src/test/scala/docspell/store/qb/impl/DoobieQueryTest.scala +++ b/modules/store/src/test/scala/docspell/store/qb/impl/DoobieQueryTest.scala @@ -22,7 +22,7 @@ object DoobieQueryTest extends SimpleTestSuite { ) val q = Select(proj, table, cond) - val frag = DoobieQuery(q) + val frag = SelectBuilder(q) assertEquals( frag.toString, """Fragment("SELECT c.id, c.name, c.owner_id, c.lecturer_id, c.lessons FROM course c INNER JOIN person o ON c.owner_id = o.id LEFT JOIN person l ON c.lecturer_id = l.id WHERE (LOWER(c.name) LIKE ? AND o.name = ? )")"""