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 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 {

View File

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

View File

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

View File

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

View File

@ -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 =>

View File

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

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

View File

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