First draft of ast and parser

This commit is contained in:
Eike Kettner
2021-02-23 01:24:24 +01:00
parent 74a79a79d9
commit be5c7ffb88
25 changed files with 1190 additions and 24 deletions

View File

@ -3,6 +3,9 @@ package docspell.store.qb
case class Column[A](name: String, table: TableDef) {
def inTable(t: TableDef): Column[A] =
copy(table = t)
def cast[B]: Column[B] =
this.asInstanceOf[Column[B]]
}
object Column {}

View File

@ -174,13 +174,13 @@ trait DSL extends DoobieMeta {
Condition.CompareVal(col, Operator.LowerEq, value)
def ====(value: String): Condition =
Condition.CompareVal(col.asInstanceOf[Column[String]], Operator.Eq, value)
Condition.CompareVal(col.cast[String], Operator.Eq, value)
def like(value: A)(implicit P: Put[A]): Condition =
Condition.CompareVal(col, Operator.LowerLike, value)
def likes(value: String): Condition =
Condition.CompareVal(col.asInstanceOf[Column[String]], Operator.LowerLike, value)
Condition.CompareVal(col.cast[String], Operator.LowerLike, value)
def <=(value: A)(implicit P: Put[A]): Condition =
Condition.CompareVal(col, Operator.Lte, value)

View File

@ -0,0 +1,195 @@
package docspell.store.qb.generator
import cats.data.NonEmptyList
import docspell.common._
import docspell.query.ItemQuery
import docspell.query.ItemQuery.Attr._
import docspell.query.ItemQuery.Property.{DateProperty, StringProperty}
import docspell.query.ItemQuery.{Attr, Expr, Operator, TagOperator}
import docspell.store.qb.{Operator => QOp, _}
import docspell.store.qb.DSL._
import docspell.store.records.{RCustomField, RCustomFieldValue, TagItemName}
import doobie.util.Put
object ItemQueryGenerator {
def apply(tables: Tables, coll: Ident)(q: ItemQuery)(implicit
PT: Put[Timestamp]
): Condition =
fromExpr(tables, coll)(q.expr)
final def fromExpr(tables: Tables, coll: Ident)(
expr: Expr
)(implicit PT: Put[Timestamp]): Condition =
expr match {
case Expr.AndExpr(inner) =>
Condition.And(inner.map(fromExpr(tables, coll)))
case Expr.OrExpr(inner) =>
Condition.Or(inner.map(fromExpr(tables, coll)))
case Expr.NotExpr(inner) =>
inner match {
case Expr.Exists(notExists) =>
anyColumn(tables)(notExists).isNull
case Expr.TagIdsMatch(op, tags) =>
val ids = tags.toList.flatMap(s => Ident.fromString(s).toOption)
NonEmptyList
.fromList(ids)
.map { nel =>
op match {
case TagOperator.AnyMatch =>
tables.item.id.notIn(TagItemName.itemsWithEitherTag(nel))
case TagOperator.AllMatch =>
tables.item.id.notIn(TagItemName.itemsWithAllTags(nel))
}
}
.getOrElse(Condition.unit)
case Expr.TagsMatch(op, tags) =>
op match {
case TagOperator.AllMatch =>
tables.item.id.notIn(TagItemName.itemsWithAllTagNameOrIds(tags))
case TagOperator.AnyMatch =>
tables.item.id.notIn(TagItemName.itemsWithEitherTagNameOrIds(tags))
}
case Expr.TagCategoryMatch(op, cats) =>
op match {
case TagOperator.AllMatch =>
tables.item.id.notIn(TagItemName.itemsInAllCategories(cats))
case TagOperator.AnyMatch =>
tables.item.id.notIn(TagItemName.itemsInEitherCategory(cats))
}
case Expr.Fulltext(_) =>
Condition.unit
case _ =>
Condition.Not(fromExpr(tables, coll)(inner))
}
case Expr.Exists(field) =>
anyColumn(tables)(field).isNotNull
case Expr.SimpleExpr(op, StringProperty(attr, value)) =>
val col = stringColumn(tables)(attr)
op match {
case Operator.Like =>
Condition.CompareVal(col, makeOp(op), value.toLowerCase)
case _ =>
Condition.CompareVal(col, makeOp(op), value)
}
case Expr.SimpleExpr(op, DateProperty(attr, value)) =>
val dt = Timestamp.atUtc(value.atStartOfDay())
val col = timestampColumn(tables)(attr)
Condition.CompareVal(col, makeOp(op), dt)
case Expr.InExpr(attr, values) =>
val col = stringColumn(tables)(attr)
if (values.tail.isEmpty) col === values.head
else col.in(values)
case Expr.TagIdsMatch(op, tags) =>
val ids = tags.toList.flatMap(s => Ident.fromString(s).toOption)
NonEmptyList
.fromList(ids)
.map { nel =>
op match {
case TagOperator.AnyMatch =>
tables.item.id.in(TagItemName.itemsWithEitherTag(nel))
case TagOperator.AllMatch =>
tables.item.id.in(TagItemName.itemsWithAllTags(nel))
}
}
.getOrElse(Condition.unit)
case Expr.TagsMatch(op, tags) =>
op match {
case TagOperator.AllMatch =>
tables.item.id.in(TagItemName.itemsWithAllTagNameOrIds(tags))
case TagOperator.AnyMatch =>
tables.item.id.in(TagItemName.itemsWithEitherTagNameOrIds(tags))
}
case Expr.TagCategoryMatch(op, cats) =>
op match {
case TagOperator.AllMatch =>
tables.item.id.in(TagItemName.itemsInAllCategories(cats))
case TagOperator.AnyMatch =>
tables.item.id.in(TagItemName.itemsInEitherCategory(cats))
}
case Expr.CustomFieldMatch(field, op, value) =>
tables.item.id.in(itemsWithCustomField(coll, field, makeOp(op), value))
case Expr.Fulltext(_) =>
// not supported here
Condition.unit
}
private def anyColumn(tables: Tables)(attr: Attr): Column[_] =
attr match {
case s: StringAttr =>
stringColumn(tables)(s)
case t: DateAttr =>
timestampColumn(tables)(t)
}
private def timestampColumn(tables: Tables)(attr: DateAttr) =
attr match {
case Attr.Date =>
tables.item.itemDate
case Attr.DueDate =>
tables.item.dueDate
}
private def stringColumn(tables: Tables)(attr: StringAttr): Column[String] =
attr match {
case Attr.ItemId => tables.item.id.cast[String]
case Attr.ItemName => tables.item.name
case Attr.ItemSource => tables.item.source
case Attr.Correspondent.OrgId => tables.corrOrg.oid.cast[String]
case Attr.Correspondent.OrgName => tables.corrOrg.name
case Attr.Correspondent.PersonId => tables.corrPers.pid.cast[String]
case Attr.Correspondent.PersonName => tables.corrPers.name
case Attr.Concerning.PersonId => tables.concPers.pid.cast[String]
case Attr.Concerning.PersonName => tables.concPers.name
case Attr.Concerning.EquipId => tables.concEquip.eid.cast[String]
case Attr.Concerning.EquipName => tables.concEquip.name
case Attr.Folder.FolderId => tables.folder.id.cast[String]
case Attr.Folder.FolderName => tables.folder.name
}
private def makeOp(operator: Operator): QOp =
operator match {
case Operator.Eq =>
QOp.Eq
case Operator.Like =>
QOp.LowerLike
case Operator.Gt =>
QOp.Gt
case Operator.Lt =>
QOp.Lt
case Operator.Gte =>
QOp.Gte
case Operator.Lte =>
QOp.Lte
}
def itemsWithCustomField(coll: Ident, field: String, op: QOp, value: String): Select = {
val cf = RCustomField.as("cf")
val cfv = RCustomFieldValue.as("cfv")
val v = if (op == QOp.LowerLike) value.toLowerCase else value
Select(
select(cfv.itemId),
from(cfv).innerJoin(cf, cf.id === cfv.field),
cf.cid === coll && cf.name ==== field && Condition.CompareVal(cfv.value, op, v)
)
}
}

View File

@ -0,0 +1,14 @@
package docspell.store.qb.generator
import docspell.store.records._
final case class Tables(
item: RItem.Table,
corrOrg: ROrganization.Table,
corrPers: RPerson.Table,
concPers: RPerson.Table,
concEquip: REquipment.Table,
folder: RFolder.Table,
attach: RAttachment.Table,
meta: RAttachmentMeta.Table
)

View File

@ -42,9 +42,27 @@ object TagItemName {
def itemsWithEitherTag(tags: NonEmptyList[Ident]): Select =
Select(ti.itemId.s, from(ti), orTags(tags)).distinct
def itemsWithEitherTagNameOrIds(tags: NonEmptyList[String]): Select =
Select(
ti.itemId.s,
from(ti).innerJoin(t, t.tid === ti.tagId),
ti.tagId.cast[String].in(tags) || t.name.inLower(tags.map(_.toLowerCase))
).distinct
def itemsWithAllTags(tags: NonEmptyList[Ident]): Select =
intersect(tags.map(tid => Select(ti.itemId.s, from(ti), ti.tagId === tid).distinct))
def itemsWithAllTagNameOrIds(tags: NonEmptyList[String]): Select =
intersect(
tags.map(tag =>
Select(
ti.itemId.s,
from(ti).innerJoin(t, t.tid === ti.tagId),
ti.tagId ==== tag || t.name.lowerEq(tag.toLowerCase)
).distinct
)
)
def itemsWithEitherTagOrCategory(
tags: NonEmptyList[Ident],
cats: NonEmptyList[String]