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 7187e147..fb5f097c 100644 --- a/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala +++ b/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala @@ -136,32 +136,21 @@ object RegexNerFile { object Sql { import doobie._ - import doobie.implicits._ - import docspell.store.impl.Implicits._ - import docspell.store.impl.Column + import docspell.store.qb.DSL._ + import docspell.store.qb._ def latestUpdate(collective: Ident): ConnectionIO[Option[Timestamp]] = { - def max(col: Column, table: Fragment, cidCol: Column): Fragment = - selectSimple(col.max ++ fr"as t", table, cidCol.is(collective)) + def max_(col: Column[_], cidCol: Column[Ident]): Select = + Select(select(max(col).as("t")), from(col.table), cidCol === collective) - val equip = REquipment.as("e") - val sql = - List( - max( - ROrganization.Columns.updated, - ROrganization.table, - ROrganization.Columns.cid - ), - max(RPerson.Columns.updated, RPerson.table, RPerson.Columns.cid), - max( - equip.updated.oldColumn, - Fragment.const(equip.tableName), - equip.cid.oldColumn - ) - ) - .reduce(_ ++ fr"UNION ALL" ++ _) + val sql = union( + max_(ROrganization.T.updated, ROrganization.T.cid), + max_(RPerson.T.updated, RPerson.T.cid), + max_(REquipment.T.updated, REquipment.T.cid) + ) + val t = Column[Timestamp]("t", TableDef("")) - selectSimple(fr"MAX(t)", fr"(" ++ sql ++ fr") as x", Fragment.empty) + run(select(max(t)), fromSubSelect(sql).as("x")) .query[Option[Timestamp]] .option .map(_.flatten) 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 1e597efc..fbbaac6f 100644 --- a/modules/store/src/main/scala/docspell/store/qb/DBFunction.scala +++ b/modules/store/src/main/scala/docspell/store/qb/DBFunction.scala @@ -23,4 +23,9 @@ object DBFunction { def as(a: String) = copy(alias = a) } + + case class Max(column: Column[_], alias: String) extends DBFunction { + def as(a: String) = + copy(alias = a) + } } 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 0b7b62a2..7eb12b55 100644 --- a/modules/store/src/main/scala/docspell/store/qb/DSL.scala +++ b/modules/store/src/main/scala/docspell/store/qb/DSL.scala @@ -9,9 +9,19 @@ import doobie.{Fragment, Put} trait DSL extends DoobieMeta { + def run(projection: Seq[SelectExpr], from: FromExpr): Fragment = + DoobieQuery(Select(projection, from, None)) + def run(projection: Seq[SelectExpr], from: FromExpr, where: Condition): Fragment = DoobieQuery(Select(projection, from, where)) + def runDistinct( + projection: Seq[SelectExpr], + from: FromExpr, + where: Condition + ): Fragment = + DoobieQuery.distinct(Select(projection, from, where)) + def select(dbf: DBFunction): Seq[SelectExpr] = Seq(SelectExpr.SelectFun(dbf)) @@ -21,12 +31,21 @@ trait DSL extends DoobieMeta { def select(seq: Seq[Column[_]], seqs: Seq[Column[_]]*): Seq[SelectExpr] = (seq ++ seqs.flatten).map(SelectExpr.SelectColumn.apply) - def from(table: TableDef): FromExpr = + def union(s1: Select, sn: Select*): Select = + Select.Union(s1, sn.toVector) + + def from(table: TableDef): FromExpr.From = FromExpr.From(table) + def fromSubSelect(sel: Select): FromExpr.SubSelect = + FromExpr.SubSelect(sel, "x") + def count(c: Column[_]): DBFunction = DBFunction.Count(c, "cn") + def max(c: Column[_]): DBFunction = + DBFunction.Max(c, "mn") + def and(c: Condition, cs: Condition*): Condition = c match { case Condition.And(head, tail) => diff --git a/modules/store/src/main/scala/docspell/store/qb/FromExpr.scala b/modules/store/src/main/scala/docspell/store/qb/FromExpr.scala index f066cccc..d78f3143 100644 --- a/modules/store/src/main/scala/docspell/store/qb/FromExpr.scala +++ b/modules/store/src/main/scala/docspell/store/qb/FromExpr.scala @@ -2,9 +2,9 @@ package docspell.store.qb sealed trait FromExpr { - def innerJoin(other: TableDef, on: Condition): FromExpr - - def leftJoin(other: TableDef, on: Condition): FromExpr +// def innerJoin(other: TableDef, on: Condition): FromExpr +// +// def leftJoin(other: TableDef, on: Condition): FromExpr } object FromExpr { @@ -23,6 +23,10 @@ object FromExpr { def leftJoin(other: TableDef, on: Condition): Joined = Joined(from, joins :+ Join.LeftJoin(other, on)) + } + case class SubSelect(sel: Select, name: String) extends FromExpr { + def as(name: String): SubSelect = + copy(name = name) } } 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 13da97cf..072d98c5 100644 --- a/modules/store/src/main/scala/docspell/store/qb/TableDef.scala +++ b/modules/store/src/main/scala/docspell/store/qb/TableDef.scala @@ -5,3 +5,12 @@ trait TableDef { def alias: Option[String] } + +object TableDef { + + def apply(table: String, aliasName: Option[String] = None): TableDef = + new TableDef { + def tableName: String = table + def alias: Option[String] = aliasName + } +} 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 39e10864..71ee3606 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 @@ -15,6 +15,9 @@ object FromExprBuilder { case FromExpr.Joined(from, joins) => build(from) ++ joins.map(buildJoin).foldLeft(Fragment.empty)(_ ++ _) + + case FromExpr.SubSelect(sel, name) => + sql" FROM (" ++ DoobieQuery(sel) ++ fr") AS" ++ Fragment.const(name) } def buildTable(table: TableDef): Fragment = 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 e60308bd..f59d1fe5 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 @@ -13,16 +13,19 @@ object SelectExprBuilder { column(col) case SelectExpr.SelectFun(DBFunction.CountAll(alias)) => - fr"COUNT(*) AS" ++ Fragment.const(alias) + sql"COUNT(*) AS" ++ Fragment.const(alias) case SelectExpr.SelectFun(DBFunction.Count(col, alias)) => - fr"COUNT(" ++ column(col) ++ fr") AS" ++ Fragment.const(alias) + sql"COUNT(" ++ column(col) ++ fr") AS" ++ Fragment.const(alias) + + case SelectExpr.SelectFun(DBFunction.Max(col, alias)) => + sql"MAX(" ++ column(col) ++ fr") AS" ++ Fragment.const(alias) } def column(col: Column[_]): Fragment = { - val prefix = - Fragment.const0(col.table.alias.getOrElse(col.table.tableName)) - prefix ++ Fragment.const0(".") ++ Fragment.const0(col.name) + val prefix = col.table.alias.getOrElse(col.table.tableName) + if (prefix.isEmpty) columnNoPrefix(col) + else Fragment.const0(prefix) ++ Fragment.const0(".") ++ Fragment.const0(col.name) } def columnNoPrefix(col: Column[_]): Fragment = diff --git a/modules/store/src/main/scala/docspell/store/queries/QCollective.scala b/modules/store/src/main/scala/docspell/store/queries/QCollective.scala index a180623f..62b23ab0 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QCollective.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QCollective.scala @@ -91,61 +91,28 @@ object QCollective { sql.run.query[TagCount].to[List] } -// def tagCloud2(coll: Ident): ConnectionIO[List[TagCount]] = { -// val tagItem = RTagItem.as("r") -// val TC = RTag.Columns -// -// val q3 = fr"SELECT" ++ commas( -// TC.all.map(_.prefix("t").f) ++ Seq(fr"count(" ++ tagItem.itemId.column.f ++ fr")") -// ) ++ -// fr"FROM" ++ Fragment.const(tagItem.tableName) ++ fr"r" ++ -// fr"INNER JOIN" ++ RTag.table ++ fr"t ON" ++ tagItem.tagId.column -// .prefix("r") -// .is(TC.tid.prefix("t")) ++ -// fr"WHERE" ++ TC.cid.prefix("t").is(coll) ++ -// fr"GROUP BY" ++ commas( -// TC.name.prefix("t").f, -// TC.tid.prefix("t").f, -// TC.category.prefix("t").f -// ) -// -// q3.query[TagCount].to[List] -// } def getContacts( coll: Ident, query: Option[String], kind: Option[ContactKind] ): Stream[ConnectionIO, RContact] = { - val RO = ROrganization - val RP = RPerson - val RC = RContact + import docspell.store.qb.DSL._ + import docspell.store.qb._ - val orgCond = selectSimple(Seq(RO.Columns.oid), RO.table, RO.Columns.cid.is(coll)) - val persCond = selectSimple(Seq(RP.Columns.pid), RP.table, RP.Columns.cid.is(coll)) - val queryCond = query match { - case Some(q) => - Seq(RC.Columns.value.lowerLike(s"%${q.toLowerCase}%")) - case None => - Seq.empty - } - val kindCond = kind match { - case Some(k) => - Seq(RC.Columns.kind.is(k)) - case None => - Seq.empty - } + val ro = ROrganization.as("o") + val rp = RPerson.as("p") + val rc = RContact.as("c") - val q = selectSimple( - RC.Columns.all, - RC.table, - and( - Seq( - or(RC.Columns.orgId.isIn(orgCond), RC.Columns.personId.isIn(persCond)) - ) ++ queryCond ++ kindCond - ) - ) ++ orderBy(RC.Columns.value.f) + val orgCond = Select(select(ro.oid), from(ro), ro.cid === coll) + val persCond = Select(select(rp.pid), from(rp), rp.cid === coll) + val valueFilter = query.map(s => rc.value.like(s"%${s.toLowerCase}%")) + val kindFilter = kind.map(k => rc.kind === k) - q.query[RContact].stream + Select( + select(rc.all), + from(rc), + (rc.orgId.in(orgCond) || rc.personId.in(persCond)) &&? valueFilter &&? kindFilter + ).orderBy(rc.value).run.query[RContact].stream } } 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 29d6ea89..b41ebfa6 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala @@ -88,13 +88,17 @@ object QItem { def findItem(id: Ident): ConnectionIO[Option[ItemData]] = { val equip = REquipment.as("e") - val IC = RItem.Columns.all.map(_.prefix("i")) - val OC = ROrganization.Columns.all.map(_.prefix("o")) - val P0C = RPerson.Columns.all.map(_.prefix("p0")) - val P1C = RPerson.Columns.all.map(_.prefix("p1")) - 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 org = ROrganization.as("o") + val pers0 = RPerson.as("p0") + val pers1 = RPerson.as("p1") + + val IC = RItem.Columns.all.map(_.prefix("i")) + val OC = org.all.map(_.column) + val P0C = pers0.all.map(_.column) + 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 cq = selectSimple( @@ -102,15 +106,21 @@ object QItem { RItem.table ++ fr"i", Fragment.empty ) ++ - fr"LEFT JOIN" ++ ROrganization.table ++ fr"o ON" ++ RItem.Columns.corrOrg + fr"LEFT JOIN" ++ Fragment.const( + org.tableName + ) ++ fr"o ON" ++ RItem.Columns.corrOrg .prefix("i") - .is(ROrganization.Columns.oid.prefix("o")) ++ - fr"LEFT JOIN" ++ RPerson.table ++ fr"p0 ON" ++ RItem.Columns.corrPerson + .is(org.oid.column) ++ + fr"LEFT JOIN" ++ Fragment.const( + pers0.tableName + ) ++ fr"p0 ON" ++ RItem.Columns.corrPerson .prefix("i") - .is(RPerson.Columns.pid.prefix("p0")) ++ - fr"LEFT JOIN" ++ RPerson.table ++ fr"p1 ON" ++ RItem.Columns.concPerson + .is(pers0.pid.column) ++ + fr"LEFT JOIN" ++ Fragment.const( + pers1.tableName + ) ++ fr"p1 ON" ++ RItem.Columns.concPerson .prefix("i") - .is(RPerson.Columns.pid.prefix("p1")) ++ + .is(pers1.pid.column) ++ fr"LEFT JOIN" ++ Fragment.const( equip.tableName ) ++ fr"e ON" ++ RItem.Columns.concEquipment @@ -308,15 +318,15 @@ object QItem { moreCols: Seq[Fragment], ctes: (String, Fragment)* ): Fragment = { - val equip = REquipment.as("e1") + val equip = REquipment.as("e1") + val org = ROrganization.as("o0") + val pers0 = RPerson.as("p0") + val pers1 = RPerson.as("p1") + val IC = RItem.Columns val AC = RAttachment.Columns - val PC = RPerson.Columns - val OC = ROrganization.Columns val FC = RFolder.Columns val itemCols = IC.all - val personCols = List(PC.pid, PC.name) - val orgCols = List(OC.oid, OC.name) val equipCols = List(equip.eid.oldColumn, equip.name.oldColumn) val folderCols = List(FC.id, FC.name) val cvItem = RCustomFieldValue.Columns.itemId.prefix("cv") @@ -332,12 +342,12 @@ object QItem { IC.incoming.prefix("i").f, IC.created.prefix("i").f, fr"COALESCE(a.num, 0)", - OC.oid.prefix("o0").f, - OC.name.prefix("o0").f, - PC.pid.prefix("p0").f, - PC.name.prefix("p0").f, - PC.pid.prefix("p1").f, - PC.name.prefix("p1").f, + org.oid.column.f, + org.name.column.f, + pers0.pid.column.f, + pers0.name.column.f, + pers1.pid.column.f, + pers1.name.column.f, equip.eid.oldColumn.prefix("e1").f, equip.name.oldColumn.prefix("e1").f, FC.id.prefix("f1").f, @@ -356,9 +366,17 @@ object QItem { val withItem = selectSimple(itemCols, RItem.table, IC.cid.is(q.account.collective)) val withPerson = - selectSimple(personCols, RPerson.table, PC.cid.is(q.account.collective)) + selectSimple( + List(RPerson.T.pid.column, RPerson.T.name.column), + Fragment.const(RPerson.T.tableName), + RPerson.T.cid.column.is(q.account.collective) + ) val withOrgs = - selectSimple(orgCols, ROrganization.table, OC.cid.is(q.account.collective)) + selectSimple( + List(ROrganization.T.oid.column, ROrganization.T.name.column), + Fragment.const(ROrganization.T.tableName), + ROrganization.T.cid.column.is(q.account.collective) + ) val withEquips = selectSimple( equipCols, @@ -386,9 +404,9 @@ object QItem { ) ++ selectKW ++ finalCols ++ fr" FROM items i" ++ fr"LEFT JOIN attachs a ON" ++ IC.id.prefix("i").is(AC.itemId.prefix("a")) ++ - fr"LEFT JOIN persons p0 ON" ++ IC.corrPerson.prefix("i").is(PC.pid.prefix("p0")) ++ - fr"LEFT JOIN orgs o0 ON" ++ IC.corrOrg.prefix("i").is(OC.oid.prefix("o0")) ++ - fr"LEFT JOIN persons p1 ON" ++ IC.concPerson.prefix("i").is(PC.pid.prefix("p1")) ++ + fr"LEFT JOIN persons p0 ON" ++ IC.corrPerson.prefix("i").is(pers0.pid.column) ++ + fr"LEFT JOIN orgs o0 ON" ++ IC.corrOrg.prefix("i").is(org.oid.column) ++ + fr"LEFT JOIN persons p1 ON" ++ IC.concPerson.prefix("i").is(pers1.pid.column) ++ fr"LEFT JOIN equips e1 ON" ++ IC.concEquipment .prefix("i") .is(equip.eid.oldColumn.prefix("e1")) ++ @@ -404,9 +422,10 @@ object QItem { batch: Batch ): Stream[ConnectionIO, ListItem] = { val equip = REquipment.as("e1") + val org = ROrganization.as("o0") + val pers0 = RPerson.as("p0") + val pers1 = RPerson.as("p1") val IC = RItem.Columns - val PC = RPerson.Columns - val OC = ROrganization.Columns // inclusive tags are AND-ed val tagSelectsIncl = q.tagsInclude @@ -436,18 +455,18 @@ object QItem { allNames .map(n => or( - OC.name.prefix("o0").lowerLike(n), - PC.name.prefix("p0").lowerLike(n), - PC.name.prefix("p1").lowerLike(n), + org.name.column.lowerLike(n), + pers0.name.column.lowerLike(n), + pers1.name.column.lowerLike(n), equip.name.oldColumn.prefix("e1").lowerLike(n), IC.name.prefix("i").lowerLike(n), IC.notes.prefix("i").lowerLike(n) ) ) .getOrElse(Fragment.empty), - RPerson.Columns.pid.prefix("p0").isOrDiscard(q.corrPerson), - ROrganization.Columns.oid.prefix("o0").isOrDiscard(q.corrOrg), - RPerson.Columns.pid.prefix("p1").isOrDiscard(q.concPerson), + pers0.pid.column.isOrDiscard(q.corrPerson), + 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), if (q.tagsInclude.isEmpty && q.tagCategoryIncl.isEmpty) Fragment.empty diff --git a/modules/store/src/main/scala/docspell/store/queries/QOrganization.scala b/modules/store/src/main/scala/docspell/store/queries/QOrganization.scala index d0234973..6d41a841 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QOrganization.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QOrganization.scala @@ -4,10 +4,8 @@ import cats.implicits._ import fs2._ import docspell.common._ -import docspell.store.impl.Column -import docspell.store.impl.Implicits._ -import docspell.store.records.ROrganization.{Columns => OC} -import docspell.store.records.RPerson.{Columns => PC} +import docspell.store.qb.DSL._ +import docspell.store.qb._ import docspell.store.records._ import docspell.store.{AddResult, Store} @@ -19,29 +17,22 @@ object QOrganization { def findOrgAndContact( coll: Ident, query: Option[String], - order: OC.type => Column + order: ROrganization.Table => Column[_] ): Stream[ConnectionIO, (ROrganization, Vector[RContact])] = { - val oColl = ROrganization.Columns.cid.prefix("o") - val oName = ROrganization.Columns.name.prefix("o") - val oNotes = ROrganization.Columns.notes.prefix("o") - val oId = ROrganization.Columns.oid.prefix("o") - val cOrg = RContact.Columns.orgId.prefix("c") - val cVal = RContact.Columns.value.prefix("c") + val org = ROrganization.as("o") + val c = RContact.as("c") - val cols = ROrganization.Columns.all.map(_.prefix("o")) ++ RContact.Columns.all - .map(_.prefix("c")) - val from = ROrganization.table ++ fr"o LEFT JOIN" ++ - RContact.table ++ fr"c ON" ++ cOrg.is(oId) + val valFilter = query.map { q => + val v = s"%$q%" + c.value.like(v) || org.name.like(v) || org.notes.like(v) + } + val sql = Select( + select(org.all, c.all), + from(org).leftJoin(c, c.orgId === org.oid), + org.cid === coll &&? valFilter + ).orderBy(order(org)) - val q = Seq(oColl.is(coll)) ++ (query match { - case Some(str) => - val v = s"%$str%" - Seq(or(cVal.lowerLike(v), oName.lowerLike(v), oNotes.lowerLike(v))) - case None => - Seq.empty - }) - - (selectSimple(cols, from, and(q)) ++ orderBy(order(OC).prefix("o").f)) + sql.run .query[(ROrganization, Option[RContact])] .stream .groupAdjacentBy(_._1) @@ -55,18 +46,16 @@ object QOrganization { coll: Ident, orgId: Ident ): ConnectionIO[Option[(ROrganization, Vector[RContact])]] = { - val oColl = ROrganization.Columns.cid.prefix("o") - val oId = ROrganization.Columns.oid.prefix("o") - val cOrg = RContact.Columns.orgId.prefix("c") + val org = ROrganization.as("o") + val c = RContact.as("c") - val cols = ROrganization.Columns.all.map(_.prefix("o")) ++ RContact.Columns.all - .map(_.prefix("c")) - val from = ROrganization.table ++ fr"o LEFT JOIN" ++ - RContact.table ++ fr"c ON" ++ cOrg.is(oId) + val sql = run( + select(org.all, c.all), + from(org).leftJoin(c, c.orgId === org.oid), + org.cid === coll && org.oid === orgId + ) - val q = and(oColl.is(coll), oId.is(orgId)) - - selectSimple(cols, from, q) + sql .query[(ROrganization, Option[RContact])] .stream .groupAdjacentBy(_._1) @@ -81,33 +70,23 @@ object QOrganization { def findPersonAndContact( coll: Ident, query: Option[String], - order: PC.type => Column + order: RPerson.Table => Column[_] ): Stream[ConnectionIO, (RPerson, Option[ROrganization], Vector[RContact])] = { - val pColl = PC.cid.prefix("p") - val pName = RPerson.Columns.name.prefix("p") - val pNotes = RPerson.Columns.notes.prefix("p") - val pId = RPerson.Columns.pid.prefix("p") - val cPers = RContact.Columns.personId.prefix("c") - val cVal = RContact.Columns.value.prefix("c") - val oId = ROrganization.Columns.oid.prefix("o") - val pOid = RPerson.Columns.oid.prefix("p") + val pers = RPerson.as("p") + val org = ROrganization.as("o") + val c = RContact.as("c") + val valFilter = query + .map(s => s"%$s%") + .map(v => c.value.like(v) || pers.name.like(v) || pers.notes.like(v)) + val sql = Select( + select(pers.all, org.all, c.all), + from(pers) + .leftJoin(org, org.oid === pers.oid) + .leftJoin(c, c.personId === pers.pid), + pers.cid === coll &&? valFilter + ).orderBy(order(pers)) - val cols = RPerson.Columns.all.map(_.prefix("p")) ++ - ROrganization.Columns.all.map(_.prefix("o")) ++ - RContact.Columns.all.map(_.prefix("c")) - val from = RPerson.table ++ fr"p LEFT JOIN" ++ - ROrganization.table ++ fr"o ON" ++ pOid.is(oId) ++ fr"LEFT JOIN" ++ - RContact.table ++ fr"c ON" ++ cPers.is(pId) - - val q = Seq(pColl.is(coll)) ++ (query match { - case Some(str) => - val v = s"%${str.toLowerCase}%" - Seq(or(cVal.lowerLike(v), pName.lowerLike(v), pNotes.lowerLike(v))) - case None => - Seq.empty - }) - - (selectSimple(cols, from, and(q)) ++ orderBy(order(PC).prefix("p").f)) + sql.run .query[(RPerson, Option[ROrganization], Option[RContact])] .stream .groupAdjacentBy(_._1) @@ -122,22 +101,19 @@ object QOrganization { coll: Ident, persId: Ident ): ConnectionIO[Option[(RPerson, Option[ROrganization], Vector[RContact])]] = { - val pColl = PC.cid.prefix("p") - val pId = RPerson.Columns.pid.prefix("p") - val cPers = RContact.Columns.personId.prefix("c") - val oId = ROrganization.Columns.oid.prefix("o") - val pOid = RPerson.Columns.oid.prefix("p") + val pers = RPerson.as("p") + val org = ROrganization.as("o") + val c = RContact.as("c") + val sql = + run( + select(pers.all, org.all, c.all), + from(pers) + .leftJoin(org, pers.oid === org.oid) + .leftJoin(c, c.personId === pers.pid), + pers.cid === coll && pers.pid === persId + ) - val cols = RPerson.Columns.all.map(_.prefix("p")) ++ - ROrganization.Columns.all.map(_.prefix("o")) ++ - RContact.Columns.all.map(_.prefix("c")) - val from = RPerson.table ++ fr"p LEFT JOIN" ++ - ROrganization.table ++ fr"o ON" ++ pOid.is(oId) ++ fr"LEFT JOIN" ++ - RContact.table ++ fr"c ON" ++ cPers.is(pId) - - val q = and(pColl.is(coll), pId.is(persId)) - - selectSimple(cols, from, q) + sql .query[(RPerson, Option[ROrganization], Option[RContact])] .stream .groupAdjacentBy(_._1) @@ -156,23 +132,15 @@ object QOrganization { ck: Option[ContactKind], concerning: Option[Boolean] ): Stream[ConnectionIO, RPerson] = { - val pColl = PC.cid.prefix("p") - val pConc = PC.concerning.prefix("p") - val pId = PC.pid.prefix("p") - val cPers = RContact.Columns.personId.prefix("c") - val cVal = RContact.Columns.value.prefix("c") - val cKind = RContact.Columns.kind.prefix("c") - - val from = RPerson.table ++ fr"p INNER JOIN" ++ - RContact.table ++ fr"c ON" ++ cPers.is(pId) - val q = Seq( - cVal.lowerLike(s"%${value.toLowerCase}%"), - pColl.is(coll) - ) ++ concerning.map(pConc.is(_)).toSeq ++ ck.map(cKind.is(_)).toSeq - - selectDistinct(PC.all.map(_.prefix("p")), from, and(q)) - .query[RPerson] - .stream + val p = RPerson.as("p") + val c = RContact.as("c") + runDistinct( + select(p.all), + from(p).innerJoin(c, c.personId === p.pid), + c.value.like(s"%${value.toLowerCase}%") && p.cid === coll &&? + concerning.map(c => p.concerning === c) &&? + ck.map(k => c.kind === k) + ).query[RPerson].stream } def addOrg[F[_]]( 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 f6d3598a..7fd64778 100644 --- a/modules/store/src/main/scala/docspell/store/records/RContact.scala +++ b/modules/store/src/main/scala/docspell/store/records/RContact.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._ @@ -18,64 +18,62 @@ case class RContact( object RContact { - val table = fr"contact" + final case class Table(alias: Option[String]) extends TableDef { + val tableName = "contact" - object Columns { - val contactId = Column("contactid") - val value = Column("value") - val kind = Column("kind") - val personId = Column("pid") - val orgId = Column("oid") - val created = Column("created") + val contactId = Column[Ident]("contactid", this) + val value = Column[String]("value", this) + val kind = Column[ContactKind]("kind", this) + 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) } - import Columns._ + private val T = Table(None) + def as(alias: String): Table = + Table(Some(alias)) - def insert(v: RContact): ConnectionIO[Int] = { - val sql = insertRow( - table, - all, + def insert(v: RContact): ConnectionIO[Int] = + DML.insert( + T, + T.all, fr"${v.contactId},${v.value},${v.kind},${v.personId},${v.orgId},${v.created}" ) - sql.update.run - } - def update(v: RContact): ConnectionIO[Int] = { - val sql = updateRow( - table, - contactId.is(v.contactId), - commas( - value.setTo(v.value), - kind.setTo(v.kind), - personId.setTo(v.personId), - orgId.setTo(v.orgId) + def update(v: RContact): ConnectionIO[Int] = + DML.update( + T, + T.contactId === v.contactId, + DML.set( + T.value.setTo(v.value), + T.kind.setTo(v.kind), + T.personId.setTo(v.personId), + T.orgId.setTo(v.orgId) ) ) - sql.update.run - } def delete(v: RContact): ConnectionIO[Int] = - deleteFrom(table, contactId.is(v.contactId)).update.run + DML.delete(T, T.contactId === v.contactId) def deleteOrg(oid: Ident): ConnectionIO[Int] = - deleteFrom(table, orgId.is(oid)).update.run + DML.delete(T, T.orgId === oid) def deletePerson(pid: Ident): ConnectionIO[Int] = - deleteFrom(table, personId.is(pid)).update.run + DML.delete(T, T.personId === pid) def findById(id: Ident): ConnectionIO[Option[RContact]] = { - val sql = selectSimple(all, table, contactId.is(id)) + val sql = run(select(T.all), from(T), T.contactId === id) sql.query[RContact].option } def findAllPerson(pid: Ident): ConnectionIO[Vector[RContact]] = { - val sql = selectSimple(all, table, personId.is(pid)) + val sql = run(select(T.all), from(T), T.personId === pid) sql.query[RContact].to[Vector] } def findAllOrg(oid: Ident): ConnectionIO[Vector[RContact]] = { - val sql = selectSimple(all, table, orgId.is(oid)) + val sql = run(select(T.all), from(T), T.orgId === oid) sql.query[RContact].to[Vector] } } diff --git a/modules/store/src/main/scala/docspell/store/records/REquipment.scala b/modules/store/src/main/scala/docspell/store/records/REquipment.scala index 7883731e..3e0276ea 100644 --- a/modules/store/src/main/scala/docspell/store/records/REquipment.scala +++ b/modules/store/src/main/scala/docspell/store/records/REquipment.scala @@ -27,6 +27,7 @@ object REquipment { val all = List(eid, cid, name, created, updated) } + val T = Table(None) def as(alias: String): Table = Table(Some(alias)) diff --git a/modules/store/src/main/scala/docspell/store/records/ROrganization.scala b/modules/store/src/main/scala/docspell/store/records/ROrganization.scala index 76a55d2a..2aa0a743 100644 --- a/modules/store/src/main/scala/docspell/store/records/ROrganization.scala +++ b/modules/store/src/main/scala/docspell/store/records/ROrganization.scala @@ -4,8 +4,8 @@ import cats.Eq import fs2.Stream import docspell.common.{IdRef, _} -import docspell.store.impl.Implicits._ -import docspell.store.impl._ +import docspell.store.qb.DSL._ +import docspell.store.qb._ import doobie._ import doobie.implicits._ @@ -27,73 +27,73 @@ object ROrganization { implicit val orgEq: Eq[ROrganization] = Eq.by[ROrganization, Ident](_.oid) - val table = fr"organization" + final case class Table(alias: Option[String]) extends TableDef { + val tableName = "organization" - object Columns { - val oid = Column("oid") - val cid = Column("cid") - val name = Column("name") - val street = Column("street") - val zip = Column("zip") - val city = Column("city") - val country = Column("country") - val notes = Column("notes") - val created = Column("created") - val updated = Column("updated") + val oid = Column[Ident]("oid", this) + val cid = Column[Ident]("cid", this) + val name = Column[String]("name", this) + val street = Column[String]("street", this) + val zip = Column[String]("zip", this) + val city = Column[String]("city", this) + val country = Column[String]("country", this) + val notes = Column[String]("notes", this) + val created = Column[Timestamp]("created", this) + val updated = Column[Timestamp]("updated", this) val all = List(oid, cid, name, street, zip, city, country, notes, created, updated) } - import Columns._ + val T = Table(None) + def as(alias: String): Table = + Table(Some(alias)) - def insert(v: ROrganization): ConnectionIO[Int] = { - val sql = insertRow( - table, - all, + def insert(v: ROrganization): ConnectionIO[Int] = + DML.insert( + T, + T.all, fr"${v.oid},${v.cid},${v.name},${v.street},${v.zip},${v.city},${v.country},${v.notes},${v.created},${v.updated}" ) - sql.update.run - } def update(v: ROrganization): ConnectionIO[Int] = { def sql(now: Timestamp) = - updateRow( - table, - and(oid.is(v.oid), cid.is(v.cid)), - commas( - cid.setTo(v.cid), - name.setTo(v.name), - street.setTo(v.street), - zip.setTo(v.zip), - city.setTo(v.city), - country.setTo(v.country), - notes.setTo(v.notes), - updated.setTo(now) + DML.update( + T, + T.oid === v.oid && T.cid === v.cid, + DML.set( + T.cid.setTo(v.cid), + T.name.setTo(v.name), + T.street.setTo(v.street), + T.zip.setTo(v.zip), + T.city.setTo(v.city), + T.country.setTo(v.country), + T.notes.setTo(v.notes), + T.updated.setTo(now) ) ) for { now <- Timestamp.current[ConnectionIO] - n <- sql(now).update.run + n <- sql(now) } yield n } def existsByName(coll: Ident, oname: String): ConnectionIO[Boolean] = - selectCount(oid, table, and(cid.is(coll), name.is(oname))) + run(select(count(T.oid)), from(T), T.cid === coll && T.name === oname) .query[Int] .unique .map(_ > 0) def findById(id: Ident): ConnectionIO[Option[ROrganization]] = { - val sql = selectSimple(all, table, cid.is(id)) + val sql = run(select(T.all), from(T), T.cid === id) sql.query[ROrganization].option } def find(coll: Ident, orgName: String): ConnectionIO[Option[ROrganization]] = { - val sql = selectSimple(all, table, and(cid.is(coll), name.is(orgName))) + val sql = run(select(T.all), from(T), T.cid === coll && T.name === orgName) sql.query[ROrganization].option } def findLike(coll: Ident, orgName: String): ConnectionIO[Vector[IdRef]] = - selectSimple(List(oid, name), table, and(cid.is(coll), name.lowerLike(orgName))) + run(select(T.oid, T.name), from(T), T.cid === coll && T.name.like(orgName)) .query[IdRef] .to[Vector] @@ -102,42 +102,38 @@ object ROrganization { contactKind: ContactKind, value: String ): ConnectionIO[Vector[IdRef]] = { - val CC = RContact.Columns - val q = fr"SELECT DISTINCT" ++ commas(oid.prefix("o").f, name.prefix("o").f) ++ - fr"FROM" ++ table ++ fr"o" ++ - fr"INNER JOIN" ++ RContact.table ++ fr"c ON" ++ CC.orgId - .prefix("c") - .is(oid.prefix("o")) ++ - fr"WHERE" ++ and( - cid.prefix("o").is(coll), - CC.kind.prefix("c").is(contactKind), - CC.value.prefix("c").lowerLike(value) + val c = RContact.as("c") + val o = ROrganization.as("o") + runDistinct( + select(o.oid, o.name), + from(o).innerJoin(c, c.orgId === o.oid), + where( + o.cid === coll, + c.kind === contactKind, + c.value.like(value) ) - - q.query[IdRef].to[Vector] + ).query[IdRef].to[Vector] } def findAll( coll: Ident, - order: Columns.type => Column + order: Table => Column[_] ): Stream[ConnectionIO, ROrganization] = { - val sql = selectSimple(all, table, cid.is(coll)) ++ orderBy(order(Columns).f) - sql.query[ROrganization].stream + val sql = Select(select(T.all), from(T), T.cid === coll).orderBy(order(T)) + sql.run.query[ROrganization].stream } def findAllRef( coll: Ident, nameQ: Option[String], - order: Columns.type => Column + order: Table => Column[_] ): ConnectionIO[Vector[IdRef]] = { - val q = Seq(cid.is(coll)) ++ (nameQ match { - case Some(str) => Seq(name.lowerLike(s"%${str.toLowerCase}%")) - case None => Seq.empty - }) - val sql = selectSimple(List(oid, name), table, and(q)) ++ orderBy(order(Columns).f) - sql.query[IdRef].to[Vector] + val nameFilter = nameQ.map(s => T.name.like(s"%${s.toLowerCase}%")) + val sql = Select(select(T.oid, T.name), from(T), T.cid === coll &&? nameFilter) + .orderBy(order(T)) + sql.run.query[IdRef].to[Vector] } def delete(id: Ident, coll: Ident): ConnectionIO[Int] = - deleteFrom(table, and(oid.is(id), cid.is(coll))).update.run + DML.delete(T, T.oid === id && T.cid === coll) } diff --git a/modules/store/src/main/scala/docspell/store/records/RPerson.scala b/modules/store/src/main/scala/docspell/store/records/RPerson.scala index c7df6fed..04eb7831 100644 --- a/modules/store/src/main/scala/docspell/store/records/RPerson.scala +++ b/modules/store/src/main/scala/docspell/store/records/RPerson.scala @@ -6,8 +6,8 @@ import cats.effect._ import fs2.Stream import docspell.common.{IdRef, _} -import docspell.store.impl.Implicits._ -import docspell.store.impl._ +import docspell.store.qb.DSL._ +import docspell.store.qb._ import doobie._ import doobie.implicits._ @@ -31,21 +31,21 @@ object RPerson { implicit val personEq: Eq[RPerson] = Eq.by(_.pid) - val table = fr"person" + final case class Table(alias: Option[String]) extends TableDef { + val tableName = "person" - object Columns { - val pid = Column("pid") - val cid = Column("cid") - val name = Column("name") - val street = Column("street") - val zip = Column("zip") - val city = Column("city") - val country = Column("country") - val notes = Column("notes") - val concerning = Column("concerning") - val created = Column("created") - val updated = Column("updated") - val oid = Column("oid") + val pid = Column[Ident]("pid", this) + val cid = Column[Ident]("cid", this) + val name = Column[String]("name", this) + val street = Column[String]("street", this) + val zip = Column[String]("zip", this) + val city = Column[String]("city", this) + val country = Column[String]("country", this) + val notes = Column[String]("notes", this) + val concerning = Column[Boolean]("concerning", this) + val created = Column[Timestamp]("created", this) + val updated = Column[Timestamp]("updated", this) + val oid = Column[Ident]("oid", this) val all = List( pid, cid, @@ -62,54 +62,54 @@ object RPerson { ) } - import Columns._ + val T = Table(None) + def as(alias: String): Table = + Table(Some(alias)) - def insert(v: RPerson): ConnectionIO[Int] = { - val sql = insertRow( - table, - all, + def insert(v: RPerson): ConnectionIO[Int] = + DML.insert( + T, + T.all, fr"${v.pid},${v.cid},${v.name},${v.street},${v.zip},${v.city},${v.country},${v.notes},${v.concerning},${v.created},${v.updated},${v.oid}" ) - sql.update.run - } def update(v: RPerson): ConnectionIO[Int] = { def sql(now: Timestamp) = - updateRow( - table, - and(pid.is(v.pid), cid.is(v.cid)), - commas( - cid.setTo(v.cid), - name.setTo(v.name), - street.setTo(v.street), - zip.setTo(v.zip), - city.setTo(v.city), - country.setTo(v.country), - concerning.setTo(v.concerning), - notes.setTo(v.notes), - oid.setTo(v.oid), - updated.setTo(now) + DML.update( + T, + T.pid === v.pid && T.cid === v.cid, + DML.set( + T.cid.setTo(v.cid), + T.name.setTo(v.name), + T.street.setTo(v.street), + T.zip.setTo(v.zip), + T.city.setTo(v.city), + T.country.setTo(v.country), + T.concerning.setTo(v.concerning), + T.notes.setTo(v.notes), + T.oid.setTo(v.oid), + T.updated.setTo(now) ) ) for { now <- Timestamp.current[ConnectionIO] - n <- sql(now).update.run + n <- sql(now) } yield n } def existsByName(coll: Ident, pname: String): ConnectionIO[Boolean] = - selectCount(pid, table, and(cid.is(coll), name.is(pname))) + run(select(count(T.pid)), from(T), T.cid === coll && T.name === pname) .query[Int] .unique .map(_ > 0) def findById(id: Ident): ConnectionIO[Option[RPerson]] = { - val sql = selectSimple(all, table, cid.is(id)) + val sql = run(select(T.all), from(T), T.cid === id) sql.query[RPerson].option } def find(coll: Ident, personName: String): ConnectionIO[Option[RPerson]] = { - val sql = selectSimple(all, table, and(cid.is(coll), name.is(personName))) + val sql = run(select(T.all), from(T), T.cid === coll && T.name === personName) sql.query[RPerson].option } @@ -118,10 +118,10 @@ object RPerson { personName: String, concerningOnly: Boolean ): ConnectionIO[Vector[IdRef]] = - selectSimple( - List(pid, name), - table, - and(cid.is(coll), concerning.is(concerningOnly), name.lowerLike(personName)) + run( + select(T.pid, T.name), + from(T), + where(T.cid === coll, T.concerning === concerningOnly, T.name.like(personName)) ).query[IdRef].to[Vector] def findLike( @@ -130,53 +130,52 @@ object RPerson { value: String, concerningOnly: Boolean ): ConnectionIO[Vector[IdRef]] = { - val CC = RContact.Columns - val q = fr"SELECT DISTINCT" ++ commas(pid.prefix("p").f, name.prefix("p").f) ++ - fr"FROM" ++ table ++ fr"p" ++ - fr"INNER JOIN" ++ RContact.table ++ fr"c ON" ++ CC.personId - .prefix("c") - .is(pid.prefix("p")) ++ - fr"WHERE" ++ and( - cid.prefix("p").is(coll), - CC.kind.prefix("c").is(contactKind), - concerning.prefix("p").is(concerningOnly), - CC.value.prefix("c").lowerLike(value) - ) + val p = RPerson.as("p") + val c = RContact.as("c") - q.query[IdRef].to[Vector] + runDistinct( + select(p.pid, p.name), + from(p).innerJoin(c, p.pid === c.personId), + where( + p.cid === coll, + c.kind === contactKind, + p.concerning === concerningOnly, + c.value.like(value) + ) + ).query[IdRef].to[Vector] } def findAll( coll: Ident, - order: Columns.type => Column + order: Table => Column[_] ): Stream[ConnectionIO, RPerson] = { - val sql = selectSimple(all, table, cid.is(coll)) ++ orderBy(order(Columns).f) - sql.query[RPerson].stream + val sql = Select(select(T.all), from(T), T.cid === coll).orderBy(order(T)) + sql.run.query[RPerson].stream } def findAllRef( coll: Ident, nameQ: Option[String], - order: Columns.type => Column + order: Table => Column[_] ): ConnectionIO[Vector[IdRef]] = { - val q = Seq(cid.is(coll)) ++ (nameQ match { - case Some(str) => Seq(name.lowerLike(s"%${str.toLowerCase}%")) - case None => Seq.empty - }) - val sql = selectSimple(List(pid, name), table, and(q)) ++ orderBy(order(Columns).f) - sql.query[IdRef].to[Vector] + + val nameFilter = nameQ.map(s => T.name.like(s"%${s.toLowerCase}%")) + + val sql = Select(select(T.pid, T.name), from(T), T.cid === coll &&? nameFilter) + .orderBy(order(T)) + sql.run.query[IdRef].to[Vector] } def delete(personId: Ident, coll: Ident): ConnectionIO[Int] = - deleteFrom(table, and(pid.is(personId), cid.is(coll))).update.run + DML.delete(T, T.pid === personId && T.cid === coll) - def findOrganization(ids: Set[Ident]): ConnectionIO[Vector[PersonRef]] = { - val cols = Seq(pid, name, oid) + def findOrganization(ids: Set[Ident]): ConnectionIO[Vector[PersonRef]] = NonEmptyList.fromList(ids.toList) match { case Some(nel) => - selectSimple(cols, table, pid.isIn(nel)).query[PersonRef].to[Vector] + run(select(T.pid, T.name, T.oid), from(T), T.pid.in(nel)) + .query[PersonRef] + .to[Vector] case None => Sync[ConnectionIO].pure(Vector.empty) } - } } diff --git a/modules/store/src/main/scala/docspell/store/records/RRememberMe.scala b/modules/store/src/main/scala/docspell/store/records/RRememberMe.scala index 36d0b4f9..464c0576 100644 --- a/modules/store/src/main/scala/docspell/store/records/RRememberMe.scala +++ b/modules/store/src/main/scala/docspell/store/records/RRememberMe.scala @@ -4,8 +4,8 @@ import cats.effect.Sync import cats.implicits._ 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._ @@ -13,18 +13,20 @@ import doobie.implicits._ case class RRememberMe(id: Ident, accountId: AccountId, created: Timestamp, uses: Int) {} object RRememberMe { + final case class Table(alias: Option[String]) extends TableDef { + val tableName = "rememberme" - val table = fr"rememberme" - - object Columns { - val id = Column("id") - val cid = Column("cid") - val username = Column("login") - val created = Column("created") - val uses = Column("uses") + val id = Column[Ident]("id", this) + val cid = Column[Ident]("cid", this) + val username = Column[Ident]("login", this) + val created = Column[Timestamp]("created", this) + val uses = Column[Int]("uses", this) val all = List(id, cid, username, created, uses) } - import Columns._ + + private val T = Table(None) + def as(alias: String): Table = + Table(Some(alias)) def generate[F[_]: Sync](account: AccountId): F[RRememberMe] = for { @@ -33,29 +35,29 @@ object RRememberMe { } yield RRememberMe(i, account, c, 0) def insert(v: RRememberMe): ConnectionIO[Int] = - insertRow( - table, - all, + DML.insert( + T, + T.all, fr"${v.id},${v.accountId.collective},${v.accountId.user},${v.created},${v.uses}" - ).update.run + ) def insertNew(acc: AccountId): ConnectionIO[RRememberMe] = generate[ConnectionIO](acc).flatMap(v => insert(v).map(_ => v)) def findById(rid: Ident): ConnectionIO[Option[RRememberMe]] = - selectSimple(all, table, id.is(rid)).query[RRememberMe].option + run(select(T.all), from(T), T.id === rid).query[RRememberMe].option def delete(rid: Ident): ConnectionIO[Int] = - deleteFrom(table, id.is(rid)).update.run + DML.delete(T, T.id === rid) def incrementUse(rid: Ident): ConnectionIO[Int] = - updateRow(table, id.is(rid), uses.increment(1)).update.run + DML.update(T, T.id === rid, DML.set(T.uses.increment(1))) def useRememberMe( rid: Ident, minCreated: Timestamp ): ConnectionIO[Option[RRememberMe]] = { - val get = selectSimple(all, table, and(id.is(rid), created.isGt(minCreated))) + val get = run(select(T.all), from(T), T.id === rid && T.created > minCreated) .query[RRememberMe] .option for { @@ -65,5 +67,5 @@ object RRememberMe { } def deleteOlderThan(ts: Timestamp): ConnectionIO[Int] = - deleteFrom(table, created.isLt(ts)).update.run + DML.delete(T, T.created < ts) }