Add more ways to query for attachments

- find items with a specified attachment count
- find items by attachment id
This commit is contained in:
Eike Kettner 2021-03-08 09:30:47 +01:00
parent 2b2f913e85
commit 30c901ddf1
9 changed files with 89 additions and 25 deletions

View File

@ -2,7 +2,7 @@ package docspell.query
import cats.data.{NonEmptyList => Nel} 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 /** A query evaluates to `true` or `false` given enough details about
* an item. * an item.
@ -40,6 +40,7 @@ object ItemQuery {
object Attr { object Attr {
sealed trait StringAttr extends Attr sealed trait StringAttr extends Attr
sealed trait DateAttr extends Attr sealed trait DateAttr extends Attr
sealed trait IntAttr extends Attr
case object ItemName extends StringAttr case object ItemName extends StringAttr
case object ItemSource extends StringAttr case object ItemSource extends StringAttr
@ -47,6 +48,7 @@ object ItemQuery {
case object ItemId extends StringAttr case object ItemId extends StringAttr
case object Date extends DateAttr case object Date extends DateAttr
case object DueDate extends DateAttr case object DueDate extends DateAttr
case object AttachCount extends IntAttr
object Correspondent { object Correspondent {
case object OrgId extends StringAttr case object OrgId extends StringAttr
@ -72,12 +74,16 @@ object ItemQuery {
object Property { object Property {
final case class StringProperty(attr: StringAttr, value: String) extends Property final case class StringProperty(attr: StringAttr, value: String) extends Property
final case class DateProperty(attr: DateAttr, value: Date) 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 = def apply(sa: StringAttr, value: String): Property =
StringProperty(sa, value) StringProperty(sa, value)
def apply(da: DateAttr, value: Date): Property = def apply(da: DateAttr, value: Date): Property =
DateProperty(da, value) DateProperty(da, value)
def apply(na: IntAttr, value: Int): Property =
IntProperty(na, value)
} }
sealed trait Expr { sealed trait Expr {
@ -111,6 +117,7 @@ object ItemQuery {
final case class Fulltext(query: String) extends Expr final case class Fulltext(query: String) extends Expr
final case class ChecksumMatch(checksum: 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 // things that can be expressed with terms above
sealed trait MacroExpr extends Expr { sealed trait MacroExpr extends Expr {

View File

@ -62,6 +62,14 @@ object AttrParser {
val folderName: P[Attr.StringAttr] = val folderName: P[Attr.StringAttr] =
P.ignoreCase("folder").as(Attr.Folder.FolderName) 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] = val dateAttr: P[Attr.DateAttr] =
P.oneOf(List(date, dueDate)) P.oneOf(List(date, dueDate))
@ -86,5 +94,5 @@ object AttrParser {
) )
val anyAttr: P[Attr] = val anyAttr: P[Attr] =
P.oneOf(List(dateAttr, stringAttr)) P.oneOf(List(dateAttr, stringAttr, intAttr))
} }

View File

@ -67,6 +67,8 @@ object ExprUtil {
expr expr
case ChecksumMatch(_) => case ChecksumMatch(_) =>
expr expr
case AttachId(_) =>
expr
} }
private def spliceAnd(nodes: Nel[Expr]): Nel[Expr] = private def spliceAnd(nodes: Nel[Expr]): Nel[Expr] =

View File

@ -1,5 +1,6 @@
package docspell.query.internal package docspell.query.internal
import cats.parse.Numbers
import cats.parse.{Parser => P} import cats.parse.{Parser => P}
import docspell.query.ItemQuery._ import docspell.query.ItemQuery._
@ -18,6 +19,9 @@ object SimpleExprParser {
private[this] val inOrOpDate = private[this] val inOrOpDate =
P.eitherOr(op ~ DateParser.date, inOp *> DateParser.dateOrMore) P.eitherOr(op ~ DateParser.date, inOp *> DateParser.dateOrMore)
private[this] val opInt =
op ~ Numbers.digits.map(_.toInt)
val stringExpr: P[Expr] = val stringExpr: P[Expr] =
(AttrParser.stringAttr ~ inOrOpStr).map { (AttrParser.stringAttr ~ inOrOpStr).map {
case (attr, Right((op, value))) => case (attr, Right((op, value))) =>
@ -34,6 +38,11 @@ object SimpleExprParser {
Expr.InDateExpr(attr, values) 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] = val existsExpr: P[Expr.Exists] =
(P.ignoreCase("exists:") *> AttrParser.anyAttr).map(attr => Expr.Exists(attr)) (P.ignoreCase("exists:") *> AttrParser.anyAttr).map(attr => Expr.Exists(attr))
@ -79,11 +88,15 @@ object SimpleExprParser {
val checksumExpr: P[Expr.ChecksumMatch] = val checksumExpr: P[Expr.ChecksumMatch] =
(P.string("checksum:") *> BasicParser.singleString).map(Expr.ChecksumMatch.apply) (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] = val simpleExpr: P[Expr] =
P.oneOf( P.oneOf(
List( List(
dateExpr, dateExpr,
stringExpr, stringExpr,
intExpr,
existsExpr, existsExpr,
fulltextExpr, fulltextExpr,
tagIdExpr, tagIdExpr,
@ -93,7 +106,8 @@ object SimpleExprParser {
customFieldExpr, customFieldExpr,
inboxExpr, inboxExpr,
dirExpr, dirExpr,
checksumExpr checksumExpr,
attachIdExpr
) )
) )
} }

View File

@ -94,6 +94,10 @@ object ItemQueryGenerator {
val noLikeOp = if (op == Operator.Like) Operator.Eq else op val noLikeOp = if (op == Operator.Like) Operator.Eq else op
Condition.CompareVal(col, makeOp(noLikeOp), dt) 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) => case Expr.InExpr(attr, values) =>
val col = stringColumn(tables)(attr) val col = stringColumn(tables)(attr)
if (values.tail.isEmpty) col === values.head if (values.tail.isEmpty) col === values.head
@ -157,6 +161,15 @@ object ItemQueryGenerator {
val select = QItem.findByChecksumQuery(checksum, coll, Set.empty) val select = QItem.findByChecksumQuery(checksum, coll, Set.empty)
tables.item.id.in(select.withSelect(Nel.of(RItem.as("i").id.s))) 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(_) => case Expr.Fulltext(_) =>
// not supported here // not supported here
Condition.unit Condition.unit
@ -196,6 +209,8 @@ object ItemQueryGenerator {
stringColumn(tables)(s) stringColumn(tables)(s)
case t: Attr.DateAttr => case t: Attr.DateAttr =>
timestampColumn(tables)(t) timestampColumn(tables)(t)
case n: Attr.IntAttr =>
intColumn(tables)(n)
} }
private def timestampColumn(tables: Tables)(attr: Attr.DateAttr) = private def timestampColumn(tables: Tables)(attr: Attr.DateAttr) =
@ -224,6 +239,11 @@ object ItemQueryGenerator {
case Attr.Folder.FolderName => tables.folder.name 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 = private def makeOp(operator: Operator): QOp =
operator match { operator match {
case Operator.Eq => case Operator.Eq =>

View File

@ -1,5 +1,6 @@
package docspell.store.qb.generator package docspell.store.qb.generator
import docspell.store.queries.AttachCountTable
import docspell.store.records._ import docspell.store.records._
final case class Tables( final case class Tables(
@ -10,5 +11,6 @@ final case class Tables(
concEquip: REquipment.Table, concEquip: REquipment.Table,
folder: RFolder.Table, folder: RFolder.Table,
attach: RAttachment.Table, attach: RAttachment.Table,
meta: RAttachmentMeta.Table meta: RAttachmentMeta.Table,
attachCount: AttachCountTable
) )

View File

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

View File

@ -122,14 +122,7 @@ object QItem {
} }
private def findItemsBase(q: Query.Fix, noteMaxLen: Int): Select = { private def findItemsBase(q: Query.Fix, noteMaxLen: Int): Select = {
object Attachs extends TableDef { val attachs = AttachCountTable("cta")
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 coll = q.account.collective
Select( Select(
@ -142,7 +135,7 @@ object QItem {
i.source.s, i.source.s,
i.incoming.s, i.incoming.s,
i.created.s, i.created.s,
coalesce(Attachs.num.s, const(0)).s, coalesce(attachs.num.s, const(0)).s,
org.oid.s, org.oid.s,
org.name.s, org.name.s,
pers0.pid.s, pers0.pid.s,
@ -162,14 +155,14 @@ object QItem {
.leftJoin(f, f.id === i.folder && f.collective === coll) .leftJoin(f, f.id === i.folder && f.collective === coll)
.leftJoin( .leftJoin(
Select( Select(
select(countAll.as(Attachs.num), a.itemId.as(Attachs.itemId)), select(countAll.as(attachs.num), a.itemId.as(attachs.itemId)),
from(a) from(a)
.innerJoin(i, i.id === a.itemId), .innerJoin(i, i.id === a.itemId),
i.cid === q.account.collective, i.cid === q.account.collective,
GroupBy(a.itemId) GroupBy(a.itemId)
), ),
Attachs.aliasName, attachs.aliasName,
Attachs.itemId === i.id attachs.itemId === i.id
) )
.leftJoin(pers0, pers0.pid === i.corrPerson && pers0.cid === coll) .leftJoin(pers0, pers0.pid === i.corrPerson && pers0.cid === coll)
.leftJoin(org, org.oid === i.corrOrg && org.cid === coll) .leftJoin(org, org.oid === i.corrOrg && org.cid === coll)
@ -229,7 +222,7 @@ object QItem {
.map(itemIds => i.id.in(itemIds)) .map(itemIds => i.id.in(itemIds))
def queryCondFromExpr(today: LocalDate, coll: Ident, q: ItemQuery): Condition = { 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) ItemQueryGenerator.fromExpr(today, tables, coll)(q.expr)
} }

View File

@ -6,6 +6,7 @@ import docspell.store.records._
import minitest._ import minitest._
import docspell.common._ import docspell.common._
import docspell.query.ItemQueryParser import docspell.query.ItemQueryParser
import docspell.store.queries.AttachCountTable
import docspell.store.qb.DSL._ import docspell.store.qb.DSL._
import docspell.store.qb.generator.{ItemQueryGenerator, Tables} import docspell.store.qb.generator.{ItemQueryGenerator, Tables}
@ -20,7 +21,8 @@ object ItemQueryGeneratorTest extends SimpleTestSuite {
REquipment.as("ne"), REquipment.as("ne"),
RFolder.as("f"), RFolder.as("f"),
RAttachment.as("a"), RAttachment.as("a"),
RAttachmentMeta.as("m") RAttachmentMeta.as("m"),
AttachCountTable("cta")
) )
val now: LocalDate = LocalDate.of(2021, 2, 25) val now: LocalDate = LocalDate.of(2021, 2, 25)