mirror of
				https://github.com/TheAnachronism/docspell.git
				synced 2025-10-30 21:40:12 +00:00 
			
		
		
		
	Introduce unit condition
This commit is contained in:
		| @@ -3,6 +3,7 @@ package docspell.backend.ops | ||||
| import cats.effect._ | ||||
| import cats.implicits._ | ||||
| import fs2.Stream | ||||
|  | ||||
| import docspell.backend.JobFactory | ||||
| import docspell.backend.ops.OItemSearch._ | ||||
| import docspell.common._ | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import cats.data.NonEmptyList | ||||
| import cats.data.OptionT | ||||
| import cats.effect.{Effect, Resource} | ||||
| import cats.implicits._ | ||||
|  | ||||
| import docspell.backend.JobFactory | ||||
| import docspell.common._ | ||||
| import docspell.ftsclient.FtsClient | ||||
| @@ -12,6 +13,7 @@ import docspell.store.queries.{QAttachment, QItem, QMoveAttachment} | ||||
| import docspell.store.queue.JobQueue | ||||
| import docspell.store.records._ | ||||
| import docspell.store.{AddResult, Store} | ||||
|  | ||||
| import doobie.implicits._ | ||||
| import org.log4s.getLogger | ||||
|  | ||||
|   | ||||
| @@ -7,9 +7,9 @@ import fs2.Stream | ||||
|  | ||||
| import docspell.backend.ops.OItemSearch._ | ||||
| import docspell.common._ | ||||
| import docspell.store._ | ||||
| import docspell.store.queries.{QAttachment, QItem} | ||||
| import docspell.store.records._ | ||||
| import docspell.store._ | ||||
|  | ||||
| import bitpeace.{FileMeta, RangeDef} | ||||
| import doobie.implicits._ | ||||
|   | ||||
| @@ -3,12 +3,14 @@ package docspell.joex.notify | ||||
| import cats.data.OptionT | ||||
| import cats.effect._ | ||||
| import cats.implicits._ | ||||
|  | ||||
| import docspell.backend.ops.OItemSearch.{Batch, ListItem, Query} | ||||
| import docspell.common._ | ||||
| import docspell.joex.mail.EmilHeader | ||||
| import docspell.joex.scheduler.{Context, Task} | ||||
| import docspell.store.queries.QItem | ||||
| import docspell.store.records._ | ||||
|  | ||||
| import emil._ | ||||
| import emil.builder._ | ||||
| import emil.javamail.syntax._ | ||||
|   | ||||
| @@ -7,6 +7,9 @@ import doobie._ | ||||
| sealed trait Condition | ||||
|  | ||||
| object Condition { | ||||
|   case object UnitCondition extends Condition | ||||
|  | ||||
|   val unit: Condition = UnitCondition | ||||
|  | ||||
|   case class CompareVal[A](column: Column[A], op: Operator, value: A)(implicit | ||||
|       val P: Put[A] | ||||
| @@ -26,36 +29,58 @@ object Condition { | ||||
|  | ||||
|   case class IsNull(col: Column[_]) extends Condition | ||||
|  | ||||
|   case class And(c: Condition, cs: Vector[Condition]) extends Condition { | ||||
|   case class And(inner: NonEmptyList[Condition]) extends Condition { | ||||
|     def append(other: Condition): And = | ||||
|       other match { | ||||
|         case And(oc, ocs) => | ||||
|           And(c, cs ++ (oc +: ocs)) | ||||
|         case And(otherInner) => | ||||
|           And(inner.concatNel(otherInner)) | ||||
|         case _ => | ||||
|           And(c, cs :+ other) | ||||
|           And(inner.append(other)) | ||||
|       } | ||||
|   } | ||||
|   object And { | ||||
|     def apply(c: Condition, cs: Condition*): And = | ||||
|       And(c, cs.toVector) | ||||
|       And(NonEmptyList(c, cs.toList)) | ||||
|  | ||||
|     object Inner extends InnerCondition { | ||||
|       def unapply(node: Condition): Option[NonEmptyList[Condition]] = | ||||
|         node match { | ||||
|           case n: And => | ||||
|             Option(n.inner) | ||||
|           case _ => | ||||
|             None | ||||
|         } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   case class Or(c: Condition, cs: Vector[Condition]) extends Condition { | ||||
|   case class Or(inner: NonEmptyList[Condition]) extends Condition { | ||||
|     def append(other: Condition): Or = | ||||
|       other match { | ||||
|         case Or(oc, ocs) => | ||||
|           Or(c, cs ++ (oc +: ocs)) | ||||
|         case Or(otherInner) => | ||||
|           Or(inner.concatNel(otherInner)) | ||||
|         case _ => | ||||
|           Or(c, cs :+ other) | ||||
|           Or(inner.append(other)) | ||||
|       } | ||||
|   } | ||||
|   object Or { | ||||
|     def apply(c: Condition, cs: Condition*): Or = | ||||
|       Or(c, cs.toVector) | ||||
|       Or(NonEmptyList(c, cs.toList)) | ||||
|  | ||||
|     object Inner extends InnerCondition { | ||||
|       def unapply(node: Condition): Option[NonEmptyList[Condition]] = | ||||
|         node match { | ||||
|           case n: Or => | ||||
|             Option(n.inner) | ||||
|           case _ => | ||||
|             None | ||||
|         } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   case class Not(c: Condition) extends Condition | ||||
|   object Not {} | ||||
|  | ||||
|   trait InnerCondition { | ||||
|     def unapply(node: Condition): Option[NonEmptyList[Condition]] | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -97,15 +97,15 @@ trait DSL extends DoobieMeta { | ||||
|       case a: Condition.And => | ||||
|         cs.foldLeft(a)(_.append(_)) | ||||
|       case _ => | ||||
|         Condition.And(c, cs.toVector) | ||||
|         Condition.And(c, cs: _*) | ||||
|     } | ||||
|  | ||||
|   def or(c: Condition, cs: Condition*): Condition = | ||||
|     c match { | ||||
|       case Condition.Or(head, tail) => | ||||
|         Condition.Or(head, tail ++ (c +: cs.toVector)) | ||||
|       case o: Condition.Or => | ||||
|         cs.foldLeft(o)(_.append(_)) | ||||
|       case _ => | ||||
|         Condition.Or(c, cs.toVector) | ||||
|         Condition.Or(c, cs: _*) | ||||
|     } | ||||
|  | ||||
|   def not(c: Condition): Condition = | ||||
|   | ||||
| @@ -46,35 +46,35 @@ sealed trait Select { | ||||
|  | ||||
| object Select { | ||||
|   def apply(projection: Nel[SelectExpr], from: FromExpr) = | ||||
|     SimpleSelect(false, projection, from, None, None) | ||||
|     SimpleSelect(false, projection, from, Condition.unit, None) | ||||
|  | ||||
|   def apply(projection: SelectExpr, from: FromExpr) = | ||||
|     SimpleSelect(false, Nel.of(projection), from, None, None) | ||||
|     SimpleSelect(false, Nel.of(projection), from, Condition.unit, None) | ||||
|  | ||||
|   def apply( | ||||
|       projection: Nel[SelectExpr], | ||||
|       from: FromExpr, | ||||
|       where: Condition | ||||
|   ) = SimpleSelect(false, projection, from, Some(where), None) | ||||
|   ) = SimpleSelect(false, projection, from, where, None) | ||||
|  | ||||
|   def apply( | ||||
|       projection: SelectExpr, | ||||
|       from: FromExpr, | ||||
|       where: Condition | ||||
|   ) = SimpleSelect(false, Nel.of(projection), from, Some(where), None) | ||||
|   ) = SimpleSelect(false, Nel.of(projection), from, where, None) | ||||
|  | ||||
|   def apply( | ||||
|       projection: Nel[SelectExpr], | ||||
|       from: FromExpr, | ||||
|       where: Condition, | ||||
|       groupBy: GroupBy | ||||
|   ) = SimpleSelect(false, projection, from, Some(where), Some(groupBy)) | ||||
|   ) = SimpleSelect(false, projection, from, where, Some(groupBy)) | ||||
|  | ||||
|   case class SimpleSelect( | ||||
|       distinctFlag: Boolean, | ||||
|       projection: Nel[SelectExpr], | ||||
|       from: FromExpr, | ||||
|       where: Option[Condition], | ||||
|       where: Condition, | ||||
|       groupBy: Option[GroupBy] | ||||
|   ) extends Select { | ||||
|     def group(gb: GroupBy): SimpleSelect = | ||||
| @@ -84,9 +84,10 @@ object Select { | ||||
|       copy(distinctFlag = true) | ||||
|  | ||||
|     def where(c: Option[Condition]): SimpleSelect = | ||||
|       copy(where = c) | ||||
|       where(c.getOrElse(Condition.unit)) | ||||
|  | ||||
|     def where(c: Condition): SimpleSelect = | ||||
|       copy(where = Some(c)) | ||||
|       copy(where = c) | ||||
|  | ||||
|     def appendSelect(e: SelectExpr): SimpleSelect = | ||||
|       copy(projection = projection.append(e)) | ||||
| @@ -95,7 +96,7 @@ object Select { | ||||
|       copy(from = f(from)) | ||||
|  | ||||
|     def changeWhere(f: Condition => Condition): SimpleSelect = | ||||
|       copy(where = where.map(f)) | ||||
|       copy(where = f(where)) | ||||
|  | ||||
|     def orderBy(ob: OrderBy, obs: OrderBy*): Select = | ||||
|       Ordered(this, ob, obs.toVector) | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| package docspell.store.qb.impl | ||||
|  | ||||
| import cats.data.NonEmptyList | ||||
|  | ||||
| import docspell.store.qb._ | ||||
|  | ||||
| import _root_.doobie.implicits._ | ||||
| @@ -12,8 +14,55 @@ object ConditionBuilder { | ||||
|   val parenOpen  = Fragment.const0("(") | ||||
|   val parenClose = Fragment.const0(")") | ||||
|  | ||||
|   def build(expr: Condition): Fragment = | ||||
|     expr match { | ||||
|   final def reduce(c: Condition): Condition = | ||||
|     c match { | ||||
|       case Condition.And(inner) => | ||||
|         NonEmptyList.fromList(flatten(inner.toList, Condition.And.Inner)) match { | ||||
|           case Some(rinner) => | ||||
|             if (rinner.tail.isEmpty) reduce(rinner.head) | ||||
|             else Condition.And(rinner.reverse.map(reduce)) | ||||
|           case None => | ||||
|             Condition.unit | ||||
|         } | ||||
|  | ||||
|       case Condition.Or(inner) => | ||||
|         NonEmptyList.fromList(flatten(inner.toList, Condition.Or.Inner)) match { | ||||
|           case Some(rinner) => | ||||
|             if (rinner.tail.isEmpty) reduce(rinner.head) | ||||
|             else Condition.Or(rinner.reverse.map(reduce)) | ||||
|           case None => | ||||
|             Condition.unit | ||||
|         } | ||||
|  | ||||
|       case Condition.Not(Condition.UnitCondition) => | ||||
|         Condition.unit | ||||
|  | ||||
|       case Condition.Not(Condition.Not(inner)) => | ||||
|         reduce(inner) | ||||
|  | ||||
|       case _ => | ||||
|         c | ||||
|     } | ||||
|  | ||||
|   private def flatten( | ||||
|       els: List[Condition], | ||||
|       nodePattern: Condition.InnerCondition, | ||||
|       result: List[Condition] = Nil | ||||
|   ): List[Condition] = | ||||
|     els match { | ||||
|       case Nil => | ||||
|         result | ||||
|       case nodePattern(more) :: tail => | ||||
|         val spliced = flatten(more.toList, nodePattern, result) | ||||
|         flatten(tail, nodePattern, spliced) | ||||
|       case Condition.UnitCondition :: tail => | ||||
|         flatten(tail, nodePattern, result) | ||||
|       case h :: tail => | ||||
|         flatten(tail, nodePattern, h :: result) | ||||
|     } | ||||
|  | ||||
|   final def build(expr: Condition): Fragment = | ||||
|     reduce(expr) match { | ||||
|       case c @ Condition.CompareVal(col, op, value) => | ||||
|         val opFrag  = operator(op) | ||||
|         val valFrag = buildValue(value)(c.P) | ||||
| @@ -58,14 +107,14 @@ object ConditionBuilder { | ||||
|       case Condition.IsNull(col) => | ||||
|         SelectExprBuilder.column(col) ++ fr" is null" | ||||
|  | ||||
|       case Condition.And(c, cs) => | ||||
|         val inner = cs.prepended(c).map(build).reduce(_ ++ and ++ _) | ||||
|         if (cs.isEmpty) inner | ||||
|       case Condition.And(ands) => | ||||
|         val inner = ands.map(build).reduceLeft(_ ++ and ++ _) | ||||
|         if (ands.tail.isEmpty) inner | ||||
|         else parenOpen ++ inner ++ parenClose | ||||
|  | ||||
|       case Condition.Or(c, cs) => | ||||
|         val inner = cs.prepended(c).map(build).reduce(_ ++ or ++ _) | ||||
|         if (cs.isEmpty) inner | ||||
|       case Condition.Or(ors) => | ||||
|         val inner = ors.map(build).reduceLeft(_ ++ or ++ _) | ||||
|         if (ors.tail.isEmpty) inner | ||||
|         else parenOpen ++ inner ++ parenClose | ||||
|  | ||||
|       case Condition.Not(Condition.IsNull(col)) => | ||||
| @@ -73,6 +122,9 @@ object ConditionBuilder { | ||||
|  | ||||
|       case Condition.Not(c) => | ||||
|         fr"NOT" ++ build(c) | ||||
|  | ||||
|       case Condition.UnitCondition => | ||||
|         Fragment.empty | ||||
|     } | ||||
|  | ||||
|   def operator(op: Operator): Fragment = | ||||
|   | ||||
| @@ -47,7 +47,7 @@ object SelectBuilder { | ||||
|   def buildSimple(sq: Select.SimpleSelect): Fragment = { | ||||
|     val f0 = sq.projection.map(selectExpr).reduceLeft(_ ++ comma ++ _) | ||||
|     val f1 = fromExpr(sq.from) | ||||
|     val f2 = sq.where.map(cond).getOrElse(Fragment.empty) | ||||
|     val f2 = cond(sq.where) | ||||
|     val f3 = sq.groupBy.map(groupBy).getOrElse(Fragment.empty) | ||||
|     f0 ++ f1 ++ f2 ++ f3 | ||||
|   } | ||||
| @@ -70,7 +70,12 @@ object SelectBuilder { | ||||
|     FromExprBuilder.build(fr) | ||||
|  | ||||
|   def cond(c: Condition): Fragment = | ||||
|     fr" WHERE" ++ ConditionBuilder.build(c) | ||||
|     c match { | ||||
|       case Condition.UnitCondition => | ||||
|         Fragment.empty | ||||
|       case _ => | ||||
|         fr" WHERE" ++ ConditionBuilder.build(c) | ||||
|     } | ||||
|  | ||||
|   def groupBy(gb: GroupBy): Fragment = { | ||||
|     val f0 = gb.names.prepended(gb.name).map(selectExpr).reduce(_ ++ comma ++ _) | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| package docspell.store.queries | ||||
|  | ||||
| import bitpeace.FileMeta | ||||
| import docspell.common._ | ||||
| import docspell.store.records._ | ||||
|  | ||||
| import bitpeace.FileMeta | ||||
|  | ||||
| case class ItemData( | ||||
|     item: RItem, | ||||
|     corrOrg: Option[ROrganization], | ||||
|   | ||||
| @@ -13,8 +13,8 @@ import docspell.store.qb.DSL._ | ||||
| import docspell.store.qb._ | ||||
| import docspell.store.records._ | ||||
|  | ||||
| import doobie.{Query => _, _} | ||||
| import doobie.implicits._ | ||||
| import doobie.{Query => _, _} | ||||
| import org.log4s._ | ||||
|  | ||||
| object QItem { | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| package docspell.store.queries | ||||
|  | ||||
| import cats.effect._ | ||||
| import cats.data.OptionT | ||||
| import cats.effect._ | ||||
| import cats.implicits._ | ||||
|  | ||||
| import docspell.common._ | ||||
| import docspell.store.records._ | ||||
|  | ||||
| import doobie.{Query => _, _} | ||||
| import doobie.implicits._ | ||||
| import doobie.{Query => _, _} | ||||
|  | ||||
| object QMoveAttachment { | ||||
|   def moveAttachmentBefore( | ||||
|   | ||||
| @@ -65,7 +65,7 @@ object QueryBuilderTest extends SimpleTestSuite { | ||||
|             fail("Unexpected result") | ||||
|         } | ||||
|         assertEquals(group, None) | ||||
|         assert(where.isDefined) | ||||
|         assert(where != Condition.unit) | ||||
|       case _ => | ||||
|         fail("Unexpected case") | ||||
|     } | ||||
|   | ||||
| @@ -0,0 +1,97 @@ | ||||
| package docspell.store.qb.impl | ||||
|  | ||||
| import minitest._ | ||||
| import docspell.store.qb._ | ||||
| import docspell.store.qb.DSL._ | ||||
| import docspell.store.qb.model.{CourseRecord, PersonRecord} | ||||
|  | ||||
| object ConditionBuilderTest extends SimpleTestSuite { | ||||
|  | ||||
|   val c = CourseRecord.as("c") | ||||
|   val p = PersonRecord.as("p") | ||||
|  | ||||
|   test("reduce ands") { | ||||
|     val cond = | ||||
|       c.lessons > 3 && (c.id === 5L && (p.name === "john" && Condition.unit && p.id === 1L)) | ||||
|     val expected = | ||||
|       and(c.lessons > 3, c.id === 5L, p.name === "john", p.id === 1L) | ||||
|  | ||||
|     assertEquals(ConditionBuilder.reduce(cond), expected) | ||||
|     assertEquals(ConditionBuilder.reduce(expected), expected) | ||||
|   } | ||||
|  | ||||
|   test("reduce ors") { | ||||
|     val cond = | ||||
|       c.lessons > 3 || (c.id === 5L || (p.name === "john" || Condition.unit || p.id === 1L)) | ||||
|     val expected = | ||||
|       or(c.lessons > 3, c.id === 5L, p.name === "john", p.id === 1L) | ||||
|  | ||||
|     assertEquals(ConditionBuilder.reduce(cond), expected) | ||||
|     assertEquals(ConditionBuilder.reduce(expected), expected) | ||||
|   } | ||||
|  | ||||
|   test("mixed and / or") { | ||||
|     val cond = c.lessons > 3 && (p.name === "john" || p.name === "mara") && c.id > 3 | ||||
|     val expected = | ||||
|       and(c.lessons > 3, or(p.name === "john", p.name === "mara"), c.id > 3) | ||||
|     assertEquals(ConditionBuilder.reduce(cond), expected) | ||||
|     assertEquals(ConditionBuilder.reduce(expected), expected) | ||||
|   } | ||||
|  | ||||
|   test("reduce double not") { | ||||
|     val cond = Condition.Not(Condition.Not(c.name === "scala")) | ||||
|     assertEquals(ConditionBuilder.reduce(cond), c.name === "scala") | ||||
|   } | ||||
|  | ||||
|   test("reduce triple not") { | ||||
|     val cond = Condition.Not(Condition.Not(Condition.Not(c.name === "scala"))) | ||||
|     assertEquals(ConditionBuilder.reduce(cond), not(c.name === "scala")) | ||||
|   } | ||||
|  | ||||
|   test("reduce not to unit") { | ||||
|     val cond = Condition.Not(Condition.Not(Condition.Not(Condition.Not(Condition.unit)))) | ||||
|     assertEquals(ConditionBuilder.reduce(cond), Condition.unit) | ||||
|   } | ||||
|  | ||||
|   test("remove units in and/or") { | ||||
|     val cond = | ||||
|       c.name === "scala" && Condition.unit && (c.name === "fp" || Condition.unit) && Condition.unit | ||||
|     assertEquals(ConditionBuilder.reduce(cond), and(c.name === "scala", c.name === "fp")) | ||||
|   } | ||||
|  | ||||
|   test("unwrap single and/ors") { | ||||
|     assertEquals( | ||||
|       ConditionBuilder.reduce(Condition.Or(c.name === "scala")), | ||||
|       c.name === "scala" | ||||
|     ) | ||||
|     assertEquals( | ||||
|       ConditionBuilder.reduce(Condition.And(c.name === "scala")), | ||||
|       c.name === "scala" | ||||
|     ) | ||||
|  | ||||
|     assertEquals( | ||||
|       ConditionBuilder.reduce(Condition.unit && c.name === "scala" && Condition.unit), | ||||
|       c.name === "scala" | ||||
|     ) | ||||
|     assertEquals( | ||||
|       ConditionBuilder.reduce(Condition.unit || c.name === "scala" || Condition.unit), | ||||
|       c.name === "scala" | ||||
|     ) | ||||
|  | ||||
|     assertEquals( | ||||
|       ConditionBuilder.reduce(and(and(and(c.name === "scala"), Condition.unit))), | ||||
|       c.name === "scala" | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   test("reduce empty and/or") { | ||||
|     assertEquals( | ||||
|       ConditionBuilder.reduce(Condition.unit && Condition.unit && Condition.unit), | ||||
|       Condition.unit | ||||
|     ) | ||||
|     assertEquals( | ||||
|       ConditionBuilder.reduce(Condition.unit || Condition.unit || Condition.unit), | ||||
|       Condition.unit | ||||
|     ) | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user