Convert folder

This commit is contained in:
Eike Kettner 2020-12-13 00:26:58 +01:00
parent 87eb8c7f55
commit d6f28d3eca
21 changed files with 295 additions and 210 deletions

View File

@ -1,5 +1,14 @@
package docspell.store.qb 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 {} object Column {}

View File

@ -4,7 +4,13 @@ import cats.data.NonEmptyList
import doobie._ 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 { object Condition {

View File

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

View File

@ -21,7 +21,12 @@ object DBFunction {
case class Power(expr: SelectExpr, base: Int) extends 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
}
} }

View File

@ -48,7 +48,7 @@ object DML {
cond: Option[Condition], cond: Option[Condition],
setter: Seq[Setter[_]] setter: Seq[Setter[_]]
): Fragment = { ): 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" ++ fr"UPDATE" ++ FromExprBuilder.buildTable(table) ++ fr"SET" ++
setter setter
.map(s => buildSetter(s)) .map(s => buildSetter(s))

View File

@ -3,30 +3,39 @@ package docspell.store.qb
import cats.data.NonEmptyList import cats.data.NonEmptyList
import docspell.store.impl.DoobieMeta import docspell.store.impl.DoobieMeta
import docspell.store.qb.impl.DoobieQuery import docspell.store.qb.impl.SelectBuilder
import doobie.{Fragment, Put} import doobie.{Fragment, Put}
trait DSL extends DoobieMeta { trait DSL extends DoobieMeta {
def run(projection: Seq[SelectExpr], from: FromExpr): Fragment = 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 = def run(projection: Seq[SelectExpr], from: FromExpr, where: Condition): Fragment =
DoobieQuery(Select(projection, from, where)) SelectBuilder(Select(projection, from, where))
def runDistinct( def runDistinct(
projection: Seq[SelectExpr], projection: Seq[SelectExpr],
from: FromExpr, from: FromExpr,
where: Condition where: Condition
): Fragment = ): 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] = def select(dbf: DBFunction): Seq[SelectExpr] =
Seq(SelectExpr.SelectFun(dbf, None)) Seq(SelectExpr.SelectFun(dbf, None))
def select(e: SelectExpr, es: SelectExpr*): Seq[SelectExpr] =
es.prepended(e)
def select(c: Column[_], cs: Column[_]*): Seq[SelectExpr] = 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] = def select(seq: Seq[Column[_]], seqs: Seq[Column[_]]*): Seq[SelectExpr] =
(seq ++ seqs.flatten).map(c => SelectExpr.SelectColumn(c, None)) (seq ++ seqs.flatten).map(c => SelectExpr.SelectColumn(c, None))
@ -43,6 +52,9 @@ trait DSL extends DoobieMeta {
def count(c: Column[_]): DBFunction = def count(c: Column[_]): DBFunction =
DBFunction.Count(c) DBFunction.Count(c)
def countAll: DBFunction =
DBFunction.CountAll
def max(c: Column[_]): DBFunction = def max(c: Column[_]): DBFunction =
DBFunction.Max(c) DBFunction.Max(c)
@ -58,11 +70,11 @@ trait DSL extends DoobieMeta {
def lit[A](value: A)(implicit P: Put[A]): SelectExpr.SelectLit[A] = def lit[A](value: A)(implicit P: Put[A]): SelectExpr.SelectLit[A] =
SelectExpr.SelectLit(value, None) SelectExpr.SelectLit(value, None)
def plus(expr: SelectExpr, more: SelectExpr*): DBFunction = def plus(left: SelectExpr, right: SelectExpr): DBFunction =
DBFunction.Plus(expr, more.toVector) DBFunction.Calc(DBFunction.Operator.Plus, left, right)
def mult(expr: SelectExpr, more: SelectExpr*): DBFunction = def mult(left: SelectExpr, right: SelectExpr): DBFunction =
DBFunction.Mult(expr, more.toVector) DBFunction.Calc(DBFunction.Operator.Mult, left, right)
def and(c: Condition, cs: Condition*): Condition = def and(c: Condition, cs: Condition*): Condition =
c match { c match {
@ -205,8 +217,22 @@ trait DSL extends DoobieMeta {
def <>[A](value: A)(implicit P: Put[A]): Condition = def <>[A](value: A)(implicit P: Put[A]): Condition =
Condition.CompareFVal(dbf, Operator.Neq, value) 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

View File

@ -1,15 +1,15 @@
package docspell.store.qb package docspell.store.qb
import docspell.store.qb.impl.DoobieQuery import docspell.store.qb.impl.SelectBuilder
import doobie._ import doobie._
sealed trait Select { sealed trait Select {
def distinct: Fragment =
DoobieQuery.distinct(this)
def run: Fragment = 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 = def orderBy(ob: OrderBy, obs: OrderBy*): Select.Ordered =
Select.Ordered(this, ob, obs.toVector) Select.Ordered(this, ob, obs.toVector)
@ -20,27 +20,29 @@ sealed trait Select {
def limit(n: Int): Select = def limit(n: Int): Select =
this match { this match {
case Select.Limit(q, _) => Select.Limit(q, n) case Select.Limit(q, _) => Select.Limit(q, n)
case _ => case _ => Select.Limit(this, n)
Select.Limit(this, n)
} }
} }
object Select { object Select {
def apply(projection: Seq[SelectExpr], from: FromExpr) =
SimpleSelect(false, projection, from, None, None)
def apply( def apply(
projection: Seq[SelectExpr], projection: Seq[SelectExpr],
from: FromExpr, from: FromExpr,
where: Condition where: Condition
) = SimpleSelect(projection, from, Some(where), None) ) = SimpleSelect(false, projection, from, Some(where), None)
def apply( def apply(
projection: Seq[SelectExpr], projection: Seq[SelectExpr],
from: FromExpr, from: FromExpr,
where: Option[Condition] = None, where: Condition,
groupBy: Option[GroupBy] = None groupBy: GroupBy
) = SimpleSelect(projection, from, where, groupBy) ) = SimpleSelect(false, projection, from, Some(where), Some(groupBy))
case class SimpleSelect( case class SimpleSelect(
distinctFlag: Boolean,
projection: Seq[SelectExpr], projection: Seq[SelectExpr],
from: FromExpr, from: FromExpr,
where: Option[Condition], where: Option[Condition],
@ -48,6 +50,9 @@ object Select {
) extends Select { ) extends Select {
def group(gb: GroupBy): SimpleSelect = def group(gb: GroupBy): SimpleSelect =
copy(groupBy = Some(gb)) copy(groupBy = Some(gb))
def distinct: SimpleSelect =
copy(distinctFlag = true)
} }
case class Union(q: Select, qs: Vector[Select]) extends Select case class Union(q: Select, qs: Vector[Select]) extends Select
@ -58,4 +63,6 @@ object Select {
extends Select extends Select
case class Limit(q: Select, limit: Int) extends Select case class Limit(q: Select, limit: Int) extends Select
case class WithCte(cte: CteBind, ctes: Vector[CteBind], query: Select) extends Select
} }

View File

@ -2,7 +2,7 @@ package docspell.store.qb
import doobie.Put import doobie.Put
sealed trait SelectExpr { self => sealed trait SelectExpr {
def as(alias: String): SelectExpr def as(alias: String): SelectExpr
} }
@ -24,4 +24,14 @@ object SelectExpr {
copy(alias = Some(a)) 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))
}
} }

View File

@ -9,8 +9,11 @@ trait TableDef {
object TableDef { object TableDef {
def apply(table: String, aliasName: Option[String] = None): TableDef = def apply(table: String, aliasName: Option[String] = None): TableDef =
new TableDef { BasicTable(table, aliasName)
def tableName: String = table
def alias: Option[String] = aliasName final case class BasicTable(tableName: String, alias: Option[String]) extends TableDef {
def as(alias: String): BasicTable =
copy(alias = Some(alias))
} }
} }

View File

@ -6,8 +6,8 @@ import _root_.doobie.implicits._
import _root_.doobie.{Query => _, _} import _root_.doobie.{Query => _, _}
object ConditionBuilder { object ConditionBuilder {
val or = fr"OR" val or = fr" OR"
val and = fr"AND" val and = fr" AND"
val comma = fr"," val comma = fr","
val parenOpen = Fragment.const0("(") val parenOpen = Fragment.const0("(")
val parenClose = Fragment.const0(")") val parenClose = Fragment.const0(")")
@ -46,7 +46,7 @@ object ConditionBuilder {
c1Frag ++ operator(op) ++ c2Frag c1Frag ++ operator(op) ++ c2Frag
case Condition.InSubSelect(col, subsel) => case Condition.InSubSelect(col, subsel) =>
val sub = DoobieQuery(subsel) val sub = SelectBuilder(subsel)
SelectExprBuilder.column(col) ++ sql" IN (" ++ sub ++ parenClose SelectExprBuilder.column(col) ++ sql" IN (" ++ sub ++ parenClose
case c @ Condition.InValues(col, values, toLower) => case c @ Condition.InValues(col, values, toLower) =>

View File

@ -29,12 +29,20 @@ object DBFunctionBuilder extends CommonBuilder {
case DBFunction.Power(expr, base) => case DBFunction.Power(expr, base) =>
sql"POWER($base, " ++ SelectExprBuilder.build(expr) ++ sql")" sql"POWER($base, " ++ SelectExprBuilder.build(expr) ++ sql")"
case DBFunction.Plus(expr, more) => case DBFunction.Calc(op, left, right) =>
val v = more.prepended(expr).map(SelectExprBuilder.build) SelectExprBuilder.build(left) ++
v.reduce(_ ++ fr" +" ++ _) 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" *"
} }
} }

View File

@ -17,7 +17,7 @@ object FromExprBuilder {
joins.map(buildJoin).foldLeft(Fragment.empty)(_ ++ _) joins.map(buildJoin).foldLeft(Fragment.empty)(_ ++ _)
case FromExpr.SubSelect(sel, name) => 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 = def buildTable(table: TableDef): Fragment =

View File

@ -5,7 +5,7 @@ import docspell.store.qb._
import _root_.doobie.implicits._ import _root_.doobie.implicits._
import _root_.doobie.{Query => _, _} import _root_.doobie.{Query => _, _}
object DoobieQuery { object SelectBuilder {
val comma = fr"," val comma = fr","
val asc = fr" ASC" val asc = fr" ASC"
val desc = fr" DESC" val desc = fr" DESC"
@ -13,29 +13,30 @@ object DoobieQuery {
val union = fr"UNION ALL" val union = fr"UNION ALL"
def apply(q: Select): Fragment = def apply(q: Select): Fragment =
build(false)(q) build(q)
def distinct(q: Select): Fragment = def build(q: Select): Fragment =
build(true)(q)
def build(distinct: Boolean)(q: Select): Fragment =
q match { q match {
case sq: Select.SimpleSelect => 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) sel ++ buildSimple(sq)
case Select.Union(q, qs) => 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) => 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) => case Select.Ordered(q, ob, obs) =>
val order = obs.prepended(ob).map(orderBy).reduce(_ ++ comma ++ _) 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) => 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 = { def buildSimple(sq: Select.SimpleSelect): Fragment = {
@ -71,4 +72,7 @@ object DoobieQuery {
val f1 = gb.having.map(cond).getOrElse(Fragment.empty) val f1 = gb.having.map(cond).getOrElse(Fragment.empty)
fr"GROUP BY" ++ f0 ++ f1 fr"GROUP BY" ++ f0 ++ f1
} }
def buildCte(bind: CteBind): Fragment =
Fragment.const(bind.name.tableName) ++ sql"AS (" ++ build(bind.select) ++ sql")"
} }

View File

@ -2,7 +2,8 @@ package docspell.store.qb.impl
import docspell.store.qb._ import docspell.store.qb._
import _root_.doobie.{Query => _, _} import doobie._
import doobie.implicits._
object SelectExprBuilder extends CommonBuilder { object SelectExprBuilder extends CommonBuilder {
@ -17,6 +18,11 @@ object SelectExprBuilder extends CommonBuilder {
case SelectExpr.SelectFun(fun, alias) => case SelectExpr.SelectFun(fun, alias) =>
DBFunctionBuilder.build(fun) ++ appendAs(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)
} }
} }

View File

@ -4,7 +4,8 @@ import cats.data.OptionT
import cats.implicits._ import cats.implicits._
import docspell.common._ import docspell.common._
import docspell.store.impl.Implicits._ import docspell.store.qb.DSL._
import docspell.store.qb._
import docspell.store.records._ import docspell.store.records._
import doobie._ import doobie._
@ -137,21 +138,15 @@ object QFolder {
def findById(id: Ident, account: AccountId): ConnectionIO[Option[FolderDetail]] = { def findById(id: Ident, account: AccountId): ConnectionIO[Option[FolderDetail]] = {
val user = RUser.as("u") val user = RUser.as("u")
val mUserId = RFolderMember.Columns.user.prefix("m") val member = RFolderMember.as("m")
val mFolderId = RFolderMember.Columns.folder.prefix("m") val folder = RFolder.as("s")
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" ++ val memberQ = run(
Fragment.const(user.tableName) ++ fr"u ON" ++ mUserId.is(uId) ++ fr"INNER JOIN" ++ select(user.uid, user.login),
RFolder.table ++ fr"s ON" ++ mFolderId.is(sId) from(member)
.innerJoin(user, member.user === user.uid)
val memberQ = selectSimple( .innerJoin(folder, member.folder === folder.id),
Seq(uId, uLogin), member.folder === id && folder.collective === account.collective
from,
and(mFolderId.is(id), sColl.is(account.collective))
).query[IdRef].to[Vector] ).query[IdRef].to[Vector]
(for { (for {
@ -189,60 +184,61 @@ object QFolder {
// where s.cid = 'eike'; // where s.cid = 'eike';
val user = RUser.as("u") val user = RUser.as("u")
val uId = user.uid.column val member = RFolderMember.as("m")
val uLogin = user.login.column val folder = RFolder.as("s")
val sId = RFolder.Columns.id.prefix("s") val memlogin = TableDef("memberlogin")
val sOwner = RFolder.Columns.owner.prefix("s") val memloginFolder = member.folder.inTable(memlogin)
val sName = RFolder.Columns.name.prefix("s") val memloginLogn = user.login.inTable(memlogin)
val sColl = RFolder.Columns.collective.prefix("s")
val mUser = RFolderMember.Columns.user.prefix("m")
val mFolder = RFolderMember.Columns.folder.prefix("m")
//CTE val sql =
val cte: Fragment = { withCte(
val from1 = RFolderMember.table ++ fr"m INNER JOIN" ++ memlogin -> union(
Fragment.const(user.tableName) ++ fr"u ON" ++ uId.is(mUser) ++ fr"INNER JOIN" ++ Select(
RFolder.table ++ fr"s ON" ++ sId.is(mFolder) select(member.folder, user.login),
from(member)
val from2 = RFolder.table ++ fr"s INNER JOIN" ++ .innerJoin(user, user.uid === member.user)
Fragment.const(user.tableName) ++ fr"u ON" ++ uId.is(sOwner) .innerJoin(folder, folder.id === member.folder),
folder.collective === account.collective
withCTE( ),
"memberlogin" -> Select(
(selectSimple(Seq(mFolder, uLogin), from1, sColl.is(account.collective)) ++ select(folder.id, user.login),
fr"UNION ALL" ++ from(folder)
selectSimple(Seq(sId, uLogin), from2, sColl.is(account.collective))) .innerJoin(user, user.uid === folder.owner),
folder.collective === account.collective
) )
} )
)
val isMember = .select(
fr"SELECT COUNT(*) > 0 FROM memberlogin WHERE" ++ mFolder.prefix("").is(sId) ++ Select(
fr"AND" ++ uLogin.prefix("").is(account.user) select(
folder.id.s,
val memberCount = folder.name.s,
fr"SELECT COUNT(*) - 1 FROM memberlogin WHERE" ++ mFolder.prefix("").is(sId) folder.owner.s,
user.login.s,
//Query folder.created.s,
val cols = Seq( Select(
sId.f, select(countAll > 0),
sName.f, from(memlogin),
sOwner.f, memloginFolder === folder.id && memloginLogn === account.user
uLogin.f, ).as("member"),
RFolder.Columns.created.prefix("s").f, Select(
fr"(" ++ isMember ++ fr") as mem", select(countAll - 1),
fr"(" ++ memberCount ++ fr") as cnt" 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 from = RFolder.table ++ fr"s INNER JOIN" ++ sql.run
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)))
.query[FolderItem] .query[FolderItem]
.to[Vector] .to[Vector]
} }
@ -250,30 +246,22 @@ object QFolder {
/** Select all folder_id where the given account is member or owner. */ /** Select all folder_id where the given account is member or owner. */
def findMemberFolderIds(account: AccountId): Fragment = { def findMemberFolderIds(account: AccountId): Fragment = {
val user = RUser.as("u") val user = RUser.as("u")
val fId = RFolder.Columns.id.prefix("f") val f = RFolder.as("f")
val fOwner = RFolder.Columns.owner.prefix("f") val m = RFolderMember.as("m")
val fColl = RFolder.Columns.collective.prefix("f") union(
val uId = user.uid.column Select(
val uLogin = user.login.column select(f.id),
val mFolder = RFolderMember.Columns.folder.prefix("m") from(f).innerJoin(user, f.owner === user.uid),
val mUser = RFolderMember.Columns.user.prefix("m") f.collective === account.collective && user.login === account.user
),
selectSimple( Select(
Seq(fId), select(m.folder),
RFolder.table ++ fr"f INNER JOIN" ++ Fragment.const( from(m)
user.tableName .innerJoin(f, f.id === m.folder)
) ++ fr"u ON" ++ fOwner.is(uId), .innerJoin(user, user.uid === m.user),
and(fColl.is(account.collective), uLogin.is(account.user)) f.collective === account.collective && user.login === 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))
) )
).run
} }
def getMemberFolders(account: AccountId): ConnectionIO[Set[Ident]] = def getMemberFolders(account: AccountId): ConnectionIO[Set[Ident]] =

View File

@ -91,6 +91,7 @@ object QItem {
val org = ROrganization.as("o") val org = ROrganization.as("o")
val pers0 = RPerson.as("p0") val pers0 = RPerson.as("p0")
val pers1 = RPerson.as("p1") val pers1 = RPerson.as("p1")
val f = RFolder.as("f")
val IC = RItem.Columns.all.map(_.prefix("i")) val IC = RItem.Columns.all.map(_.prefix("i"))
val OC = org.all.map(_.column) val OC = org.all.map(_.column)
@ -98,7 +99,7 @@ object QItem {
val P1C = pers1.all.map(_.column) val P1C = pers1.all.map(_.column)
val EC = equip.all.map(_.oldColumn).map(_.prefix("e")) val EC = equip.all.map(_.oldColumn).map(_.prefix("e"))
val ICC = List(RItem.Columns.id, RItem.Columns.name).map(_.prefix("ref")) 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 = val cq =
selectSimple( selectSimple(
@ -129,9 +130,11 @@ object QItem {
fr"LEFT JOIN" ++ RItem.table ++ fr"ref ON" ++ RItem.Columns.inReplyTo fr"LEFT JOIN" ++ RItem.table ++ fr"ref ON" ++ RItem.Columns.inReplyTo
.prefix("i") .prefix("i")
.is(RItem.Columns.id.prefix("ref")) ++ .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") .prefix("i")
.is(RFolder.Columns.id.prefix("f")) ++ .is(f.id.column) ++
fr"WHERE" ++ RItem.Columns.id.prefix("i").is(id) fr"WHERE" ++ RItem.Columns.id.prefix("i").is(id)
val q = cq val q = cq
@ -322,13 +325,13 @@ object QItem {
val org = ROrganization.as("o0") val org = ROrganization.as("o0")
val pers0 = RPerson.as("p0") val pers0 = RPerson.as("p0")
val pers1 = RPerson.as("p1") val pers1 = RPerson.as("p1")
val f = RFolder.as("f1")
val IC = RItem.Columns val IC = RItem.Columns
val AC = RAttachment.Columns val AC = RAttachment.Columns
val FC = RFolder.Columns
val itemCols = IC.all val itemCols = IC.all
val equipCols = List(equip.eid.oldColumn, equip.name.oldColumn) 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 cvItem = RCustomFieldValue.Columns.itemId.prefix("cv")
val finalCols = commas( val finalCols = commas(
@ -350,8 +353,8 @@ object QItem {
pers1.name.column.f, pers1.name.column.f,
equip.eid.oldColumn.prefix("e1").f, equip.eid.oldColumn.prefix("e1").f,
equip.name.oldColumn.prefix("e1").f, equip.name.oldColumn.prefix("e1").f,
FC.id.prefix("f1").f, f.id.column.f,
FC.name.prefix("f1").f, f.name.column.f,
// sql uses 1 for first character // sql uses 1 for first character
IC.notes.prefix("i").substring(1, noteMaxLen), IC.notes.prefix("i").substring(1, noteMaxLen),
// last column is only for sorting // last column is only for sorting
@ -384,7 +387,11 @@ object QItem {
equip.cid.oldColumn.is(q.account.collective) equip.cid.oldColumn.is(q.account.collective)
) )
val withFolder = 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 ++ 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")" 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 fr"LEFT JOIN equips e1 ON" ++ IC.concEquipment
.prefix("i") .prefix("i")
.is(equip.eid.oldColumn.prefix("e1")) ++ .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 (if (q.customValues.isEmpty) Fragment.empty
else else
fr"INNER JOIN customvalues cv ON" ++ cvItem.is(IC.id.prefix("i"))) fr"INNER JOIN customvalues cv ON" ++ cvItem.is(IC.id.prefix("i")))
@ -425,6 +432,7 @@ object QItem {
val org = ROrganization.as("o0") val org = ROrganization.as("o0")
val pers0 = RPerson.as("p0") val pers0 = RPerson.as("p0")
val pers1 = RPerson.as("p1") val pers1 = RPerson.as("p1")
val f = RFolder.as("f1")
val IC = RItem.Columns val IC = RItem.Columns
// inclusive tags are AND-ed // inclusive tags are AND-ed
@ -468,7 +476,7 @@ object QItem {
org.oid.column.isOrDiscard(q.corrOrg), org.oid.column.isOrDiscard(q.corrOrg),
pers1.pid.column.isOrDiscard(q.concPerson), pers1.pid.column.isOrDiscard(q.concPerson),
equip.eid.oldColumn.prefix("e1").isOrDiscard(q.concEquip), 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 if (q.tagsInclude.isEmpty && q.tagCategoryIncl.isEmpty) Fragment.empty
else else
IC.id.prefix("i") ++ sql" IN (" ++ tagSelectsIncl IC.id.prefix("i") ++ sql" IN (" ++ tagSelectsIncl

View File

@ -27,7 +27,7 @@ object RContact {
val personId = Column[Ident]("pid", this) val personId = Column[Ident]("pid", this)
val orgId = Column[Ident]("oid", this) val orgId = Column[Ident]("oid", this)
val created = Column[Timestamp]("created", 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) private val T = Table(None)

View File

@ -4,8 +4,8 @@ import cats.effect._
import cats.implicits._ import cats.implicits._
import docspell.common._ import docspell.common._
import docspell.store.impl.Column import docspell.store.qb.DSL._
import docspell.store.impl.Implicits._ import docspell.store.qb._
import doobie._ import doobie._
import doobie.implicits._ import doobie.implicits._
@ -26,61 +26,58 @@ object RFolder {
now <- Timestamp.current[F] now <- Timestamp.current[F]
} yield RFolder(nId, name, account.collective, account.user, now) } 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[Ident]("id", this)
val name = Column[String]("name", this)
val id = Column("id") val collective = Column[Ident]("cid", this)
val name = Column("name") val owner = Column[Ident]("owner", this)
val collective = Column("cid") val created = Column[Timestamp]("created", this)
val owner = Column("owner")
val created = Column("created")
val all = List(id, name, collective, owner, created) 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] = { def insert(value: RFolder): ConnectionIO[Int] =
val sql = insertRow( DML.insert(
table, T,
all, T.all,
fr"${value.id},${value.name},${value.collectiveId},${value.owner},${value.created}" fr"${value.id},${value.name},${value.collectiveId},${value.owner},${value.created}"
) )
sql.update.run
}
def update(v: RFolder): ConnectionIO[Int] = def update(v: RFolder): ConnectionIO[Int] =
updateRow( DML.update(
table, T,
and(id.is(v.id), collective.is(v.collectiveId), owner.is(v.owner)), T.id === v.id && T.collective === v.collectiveId && T.owner === v.owner,
name.setTo(v.name) DML.set(T.name.setTo(v.name))
).update.run )
def existsByName(coll: Ident, folderName: String): ConnectionIO[Boolean] = 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] .query[Int]
.unique .unique
.map(_ > 0) .map(_ > 0)
def findById(folderId: Ident): ConnectionIO[Option[RFolder]] = { 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 sql.query[RFolder].option
} }
def findAll( def findAll(
coll: Ident, coll: Ident,
nameQ: Option[String], nameQ: Option[String],
order: Columns.type => Column order: Table => Column[_]
): ConnectionIO[Vector[RFolder]] = { ): ConnectionIO[Vector[RFolder]] = {
val q = Seq(collective.is(coll)) ++ (nameQ match { val nameFilter = nameQ.map(n => T.name.like(s"%${n.toLowerCase}%"))
case Some(str) => Seq(name.lowerLike(s"%${str.toLowerCase}%")) val sql = Select(select(T.all), from(T), T.collective === coll &&? nameFilter)
case None => Seq.empty .orderBy(order(T))
}) sql.run.query[RFolder].to[Vector]
val sql = selectSimple(all, table, and(q)) ++ orderBy(order(Columns).f)
sql.query[RFolder].to[Vector]
} }
def delete(folderId: Ident): ConnectionIO[Int] = def delete(folderId: Ident): ConnectionIO[Int] =
deleteFrom(table, id.is(folderId)).update.run DML.delete(T, T.id === folderId)
} }

View File

@ -4,8 +4,8 @@ import cats.effect._
import cats.implicits._ import cats.implicits._
import docspell.common._ import docspell.common._
import docspell.store.impl.Column import docspell.store.qb.DSL._
import docspell.store.impl.Implicits._ import docspell.store.qb._
import doobie._ import doobie._
import doobie.implicits._ import doobie.implicits._
@ -25,37 +25,36 @@ object RFolderMember {
now <- Timestamp.current[F] now <- Timestamp.current[F]
} yield RFolderMember(nId, folder, user, now) } 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[Ident]("id", this)
val folder = Column[Ident]("folder_id", this)
val id = Column("id") val user = Column[Ident]("user_id", this)
val folder = Column("folder_id") val created = Column[Timestamp]("created", this)
val user = Column("user_id")
val created = Column("created")
val all = List(id, folder, user, created) 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] = { def insert(value: RFolderMember): ConnectionIO[Int] =
val sql = insertRow( DML.insert(
table, T,
all, T.all,
fr"${value.id},${value.folderId},${value.userId},${value.created}" fr"${value.id},${value.folderId},${value.userId},${value.created}"
) )
sql.update.run
}
def findByUserId(userId: Ident, folderId: Ident): ConnectionIO[Option[RFolderMember]] = 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] .query[RFolderMember]
.option .option
def delete(userId: Ident, folderId: Ident): ConnectionIO[Int] = 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] = def deleteAll(folderId: Ident): ConnectionIO[Int] =
deleteFrom(table, folder.is(folderId)).update.run DML.delete(T, T.folder === folderId)
} }

View File

@ -27,7 +27,7 @@ object RTag {
val name = Column[String]("name", this) val name = Column[String]("name", this)
val category = Column[String]("category", this) val category = Column[String]("category", this)
val created = Column[Timestamp]("created", 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) val T = Table(None)
def as(alias: String): Table = def as(alias: String): Table =

View File

@ -22,7 +22,7 @@ object DoobieQueryTest extends SimpleTestSuite {
) )
val q = Select(proj, table, cond) val q = Select(proj, table, cond)
val frag = DoobieQuery(q) val frag = SelectBuilder(q)
assertEquals( assertEquals(
frag.toString, 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 = ? )")""" """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 = ? )")"""