From 30c901ddf1824a0b5f084a33dec6eb50073377c0 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Mon, 8 Mar 2021 09:30:47 +0100 Subject: [PATCH] Add more ways to query for attachments - find items with a specified attachment count - find items by attachment id --- .../main/scala/docspell/query/ItemQuery.scala | 21 ++++++++++++------- .../docspell/query/internal/AttrParser.scala | 10 ++++++++- .../docspell/query/internal/ExprUtil.scala | 2 ++ .../query/internal/SimpleExprParser.scala | 16 +++++++++++++- .../qb/generator/ItemQueryGenerator.scala | 20 ++++++++++++++++++ .../docspell/store/qb/generator/Tables.scala | 4 +++- .../store/queries/AttachCountTable.scala | 16 ++++++++++++++ .../scala/docspell/store/queries/QItem.scala | 21 +++++++------------ .../generator/ItemQueryGeneratorTest.scala | 4 +++- 9 files changed, 89 insertions(+), 25 deletions(-) create mode 100644 modules/store/src/main/scala/docspell/store/queries/AttachCountTable.scala diff --git a/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala b/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala index 4fce13d2..94f3868f 100644 --- a/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala +++ b/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala @@ -2,7 +2,7 @@ package docspell.query import cats.data.{NonEmptyList => Nel} -import docspell.query.ItemQuery.Attr.{DateAttr, StringAttr} +import docspell.query.ItemQuery.Attr.{DateAttr, IntAttr, StringAttr} /** A query evaluates to `true` or `false` given enough details about * an item. @@ -40,13 +40,15 @@ object ItemQuery { object Attr { sealed trait StringAttr extends Attr sealed trait DateAttr extends Attr + sealed trait IntAttr extends Attr - case object ItemName extends StringAttr - case object ItemSource extends StringAttr - case object ItemNotes extends StringAttr - case object ItemId extends StringAttr - case object Date extends DateAttr - case object DueDate extends DateAttr + case object ItemName extends StringAttr + case object ItemSource extends StringAttr + case object ItemNotes extends StringAttr + case object ItemId extends StringAttr + case object Date extends DateAttr + case object DueDate extends DateAttr + case object AttachCount extends IntAttr object Correspondent { case object OrgId extends StringAttr @@ -72,12 +74,16 @@ object ItemQuery { object Property { final case class StringProperty(attr: StringAttr, value: String) extends Property final case class DateProperty(attr: DateAttr, value: Date) extends Property + final case class IntProperty(attr: IntAttr, value: Int) extends Property def apply(sa: StringAttr, value: String): Property = StringProperty(sa, value) def apply(da: DateAttr, value: Date): Property = DateProperty(da, value) + + def apply(na: IntAttr, value: Int): Property = + IntProperty(na, value) } sealed trait Expr { @@ -111,6 +117,7 @@ object ItemQuery { final case class Fulltext(query: String) extends Expr final case class ChecksumMatch(checksum: String) extends Expr + final case class AttachId(id: String) extends Expr // things that can be expressed with terms above sealed trait MacroExpr extends Expr { diff --git a/modules/query/shared/src/main/scala/docspell/query/internal/AttrParser.scala b/modules/query/shared/src/main/scala/docspell/query/internal/AttrParser.scala index d5289c67..d7aab3ed 100644 --- a/modules/query/shared/src/main/scala/docspell/query/internal/AttrParser.scala +++ b/modules/query/shared/src/main/scala/docspell/query/internal/AttrParser.scala @@ -62,6 +62,14 @@ object AttrParser { val folderName: P[Attr.StringAttr] = P.ignoreCase("folder").as(Attr.Folder.FolderName) + val attachCountAttr: P[Attr.IntAttr] = + P.ignoreCase("attach.count").as(Attr.AttachCount) + + // combining grouped by type + + val intAttr: P[Attr.IntAttr] = + attachCountAttr + val dateAttr: P[Attr.DateAttr] = P.oneOf(List(date, dueDate)) @@ -86,5 +94,5 @@ object AttrParser { ) val anyAttr: P[Attr] = - P.oneOf(List(dateAttr, stringAttr)) + P.oneOf(List(dateAttr, stringAttr, intAttr)) } diff --git a/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala b/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala index f4f8193c..df81983f 100644 --- a/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala +++ b/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala @@ -67,6 +67,8 @@ object ExprUtil { expr case ChecksumMatch(_) => expr + case AttachId(_) => + expr } private def spliceAnd(nodes: Nel[Expr]): Nel[Expr] = diff --git a/modules/query/shared/src/main/scala/docspell/query/internal/SimpleExprParser.scala b/modules/query/shared/src/main/scala/docspell/query/internal/SimpleExprParser.scala index 00bf5aa0..56a0077a 100644 --- a/modules/query/shared/src/main/scala/docspell/query/internal/SimpleExprParser.scala +++ b/modules/query/shared/src/main/scala/docspell/query/internal/SimpleExprParser.scala @@ -1,5 +1,6 @@ package docspell.query.internal +import cats.parse.Numbers import cats.parse.{Parser => P} import docspell.query.ItemQuery._ @@ -18,6 +19,9 @@ object SimpleExprParser { private[this] val inOrOpDate = P.eitherOr(op ~ DateParser.date, inOp *> DateParser.dateOrMore) + private[this] val opInt = + op ~ Numbers.digits.map(_.toInt) + val stringExpr: P[Expr] = (AttrParser.stringAttr ~ inOrOpStr).map { case (attr, Right((op, value))) => @@ -34,6 +38,11 @@ object SimpleExprParser { Expr.InDateExpr(attr, values) } + val intExpr: P[Expr] = + (AttrParser.intAttr ~ opInt).map { case (attr, (op, value)) => + Expr.SimpleExpr(op, Property(attr, value)) + } + val existsExpr: P[Expr.Exists] = (P.ignoreCase("exists:") *> AttrParser.anyAttr).map(attr => Expr.Exists(attr)) @@ -79,11 +88,15 @@ object SimpleExprParser { val checksumExpr: P[Expr.ChecksumMatch] = (P.string("checksum:") *> BasicParser.singleString).map(Expr.ChecksumMatch.apply) + val attachIdExpr: P[Expr.AttachId] = + (P.ignoreCase("attach.id:") *> BasicParser.singleString).map(Expr.AttachId.apply) + val simpleExpr: P[Expr] = P.oneOf( List( dateExpr, stringExpr, + intExpr, existsExpr, fulltextExpr, tagIdExpr, @@ -93,7 +106,8 @@ object SimpleExprParser { customFieldExpr, inboxExpr, dirExpr, - checksumExpr + checksumExpr, + attachIdExpr ) ) } diff --git a/modules/store/src/main/scala/docspell/store/qb/generator/ItemQueryGenerator.scala b/modules/store/src/main/scala/docspell/store/qb/generator/ItemQueryGenerator.scala index c92367fe..a99516ff 100644 --- a/modules/store/src/main/scala/docspell/store/qb/generator/ItemQueryGenerator.scala +++ b/modules/store/src/main/scala/docspell/store/qb/generator/ItemQueryGenerator.scala @@ -94,6 +94,10 @@ object ItemQueryGenerator { val noLikeOp = if (op == Operator.Like) Operator.Eq else op Condition.CompareVal(col, makeOp(noLikeOp), dt) + case Expr.SimpleExpr(op, Property.IntProperty(attr, value)) => + val col = intColumn(tables)(attr) + Condition.CompareVal(col, makeOp(op), value) + case Expr.InExpr(attr, values) => val col = stringColumn(tables)(attr) if (values.tail.isEmpty) col === values.head @@ -157,6 +161,15 @@ object ItemQueryGenerator { val select = QItem.findByChecksumQuery(checksum, coll, Set.empty) tables.item.id.in(select.withSelect(Nel.of(RItem.as("i").id.s))) + case Expr.AttachId(id) => + tables.item.id.in( + Select( + select(RAttachment.T.itemId), + from(RAttachment.T), + RAttachment.T.id.cast[String] === id + ).distinct + ) + case Expr.Fulltext(_) => // not supported here Condition.unit @@ -196,6 +209,8 @@ object ItemQueryGenerator { stringColumn(tables)(s) case t: Attr.DateAttr => timestampColumn(tables)(t) + case n: Attr.IntAttr => + intColumn(tables)(n) } private def timestampColumn(tables: Tables)(attr: Attr.DateAttr) = @@ -224,6 +239,11 @@ object ItemQueryGenerator { case Attr.Folder.FolderName => tables.folder.name } + private def intColumn(tables: Tables)(attr: Attr.IntAttr): Column[Int] = + attr match { + case Attr.AttachCount => tables.attachCount.num + } + private def makeOp(operator: Operator): QOp = operator match { case Operator.Eq => diff --git a/modules/store/src/main/scala/docspell/store/qb/generator/Tables.scala b/modules/store/src/main/scala/docspell/store/qb/generator/Tables.scala index 966b129d..0d30c99c 100644 --- a/modules/store/src/main/scala/docspell/store/qb/generator/Tables.scala +++ b/modules/store/src/main/scala/docspell/store/qb/generator/Tables.scala @@ -1,5 +1,6 @@ package docspell.store.qb.generator +import docspell.store.queries.AttachCountTable import docspell.store.records._ final case class Tables( @@ -10,5 +11,6 @@ final case class Tables( concEquip: REquipment.Table, folder: RFolder.Table, attach: RAttachment.Table, - meta: RAttachmentMeta.Table + meta: RAttachmentMeta.Table, + attachCount: AttachCountTable ) diff --git a/modules/store/src/main/scala/docspell/store/queries/AttachCountTable.scala b/modules/store/src/main/scala/docspell/store/queries/AttachCountTable.scala new file mode 100644 index 00000000..2ed6fc3c --- /dev/null +++ b/modules/store/src/main/scala/docspell/store/queries/AttachCountTable.scala @@ -0,0 +1,16 @@ +package docspell.store.queries + +import docspell.common.Ident +import docspell.store.qb.Column +import docspell.store.qb.TableDef + +final case class AttachCountTable(aliasName: String) extends TableDef { + val tableName = "attachs" + val alias: Option[String] = Some(aliasName) + + val num = Column[Int]("num", this) + val itemId = Column[Ident]("item_id", this) + + def as(alias: String): AttachCountTable = + copy(aliasName = alias) +} 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 2e1e1296..c1ee5f2c 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala @@ -122,15 +122,8 @@ object QItem { } private def findItemsBase(q: Query.Fix, noteMaxLen: Int): Select = { - object Attachs extends TableDef { - val tableName = "attachs" - val aliasName = "cta" - val alias = Some(aliasName) - val num = Column[Int]("num", this) - val itemId = Column[Ident]("item_id", this) - } - - val coll = q.account.collective + val attachs = AttachCountTable("cta") + val coll = q.account.collective Select( select( @@ -142,7 +135,7 @@ object QItem { i.source.s, i.incoming.s, i.created.s, - coalesce(Attachs.num.s, const(0)).s, + coalesce(attachs.num.s, const(0)).s, org.oid.s, org.name.s, pers0.pid.s, @@ -162,14 +155,14 @@ object QItem { .leftJoin(f, f.id === i.folder && f.collective === coll) .leftJoin( Select( - select(countAll.as(Attachs.num), a.itemId.as(Attachs.itemId)), + select(countAll.as(attachs.num), a.itemId.as(attachs.itemId)), from(a) .innerJoin(i, i.id === a.itemId), i.cid === q.account.collective, GroupBy(a.itemId) ), - Attachs.aliasName, - Attachs.itemId === i.id + attachs.aliasName, + attachs.itemId === i.id ) .leftJoin(pers0, pers0.pid === i.corrPerson && pers0.cid === coll) .leftJoin(org, org.oid === i.corrOrg && org.cid === coll) @@ -229,7 +222,7 @@ object QItem { .map(itemIds => i.id.in(itemIds)) def queryCondFromExpr(today: LocalDate, coll: Ident, q: ItemQuery): Condition = { - val tables = Tables(i, org, pers0, pers1, equip, f, a, m) + val tables = Tables(i, org, pers0, pers1, equip, f, a, m, AttachCountTable("cta")) ItemQueryGenerator.fromExpr(today, tables, coll)(q.expr) } diff --git a/modules/store/src/test/scala/docspell/store/generator/ItemQueryGeneratorTest.scala b/modules/store/src/test/scala/docspell/store/generator/ItemQueryGeneratorTest.scala index 3d9e5b2e..7d511026 100644 --- a/modules/store/src/test/scala/docspell/store/generator/ItemQueryGeneratorTest.scala +++ b/modules/store/src/test/scala/docspell/store/generator/ItemQueryGeneratorTest.scala @@ -6,6 +6,7 @@ import docspell.store.records._ import minitest._ import docspell.common._ import docspell.query.ItemQueryParser +import docspell.store.queries.AttachCountTable import docspell.store.qb.DSL._ import docspell.store.qb.generator.{ItemQueryGenerator, Tables} @@ -20,7 +21,8 @@ object ItemQueryGeneratorTest extends SimpleTestSuite { REquipment.as("ne"), RFolder.as("f"), RAttachment.as("a"), - RAttachmentMeta.as("m") + RAttachmentMeta.as("m"), + AttachCountTable("cta") ) val now: LocalDate = LocalDate.of(2021, 2, 25)