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,6 +40,7 @@ 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
@ -47,6 +48,7 @@ object ItemQuery {
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,14 +122,7 @@ 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 attachs = AttachCountTable("cta")
val coll = q.account.collective
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)