Skip to content

Commit

Permalink
Cleanup and improvements to filter builder
Browse files Browse the repository at this point in the history
  • Loading branch information
darkfrog26 committed Sep 2, 2024
1 parent 6dbbd02 commit 30d3fb0
Show file tree
Hide file tree
Showing 16 changed files with 106 additions and 82 deletions.
1 change: 0 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ ThisBuild / version := "0.13.0-SNAPSHOT"
ThisBuild / scalaVersion := scala213
ThisBuild / crossScalaVersions := allScalaVersions
ThisBuild / scalacOptions ++= Seq("-unchecked", "-deprecation")
ThisBuild / javacOptions ++= Seq("-source", "1.8", "-target", "1.8")

ThisBuild / sonatypeCredentialHost := "s01.oss.sonatype.org"
ThisBuild / sonatypeRepository := "https://s01.oss.sonatype.org/service/local"
Expand Down
23 changes: 12 additions & 11 deletions core/src/main/scala/lightdb/Field.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import fabric.io.JsonParser
import fabric.rw._
import lightdb.aggregate.AggregateSupport
import lightdb.distance.Distance
import lightdb.doc.Document
import lightdb.filter.{Filter, FilterSupport}
import lightdb.materialized.Materializable
import lightdb.spatial.GeoPoint

sealed class Field[Doc, V](val name: String,
val get: Doc => V,
val getRW: () => RW[V],
val indexed: Boolean = false) extends FilterSupport[V, Doc, Filter[Doc]] with AggregateSupport[Doc, V] with Materializable[Doc, V] {
sealed class Field[Doc <: Document[Doc], V](val name: String,
val get: Doc => V,
val getRW: () => RW[V],
val indexed: Boolean = false) extends FilterSupport[V, Doc, Filter[Doc]] with AggregateSupport[Doc, V] with Materializable[Doc, V] {
implicit def rw: RW[V] = getRW()

def isArr: Boolean = rw.definition match {
Expand Down Expand Up @@ -66,24 +67,24 @@ sealed class Field[Doc, V](val name: String,
override def toString: String = s"Field(name = $name)"
}

trait Indexed[Doc, V] extends Field[Doc, V]
trait Indexed[Doc <: Document[Doc], V] extends Field[Doc, V]

trait UniqueIndex[Doc, V] extends Indexed[Doc, V]
trait UniqueIndex[Doc <: Document[Doc], V] extends Indexed[Doc, V]

trait Tokenized[Doc] extends Indexed[Doc, String]
trait Tokenized[Doc <: Document[Doc]] extends Indexed[Doc, String]

object Field {
val NullString: String = "||NULL||"

var MaxIn: Option[Int] = Some(1_000)

def apply[Doc, V](name: String, get: Doc => V)(implicit getRW: => RW[V]): Field[Doc, V] = new Field[Doc, V](
def apply[Doc <: Document[Doc], V](name: String, get: Doc => V)(implicit getRW: => RW[V]): Field[Doc, V] = new Field[Doc, V](
name = name,
get = get,
getRW = () => getRW
)

def indexed[Doc, V](name: String, get: Doc => V)(implicit getRW: => RW[V]): Indexed[Doc, V] = new Field[Doc, V](
def indexed[Doc <: Document[Doc], V](name: String, get: Doc => V)(implicit getRW: => RW[V]): Indexed[Doc, V] = new Field[Doc, V](
name = name,
get = get,
getRW = () => getRW,
Expand All @@ -92,7 +93,7 @@ object Field {
override def toString: String = s"Indexed(name = ${this.name})"
}

def tokenized[Doc](name: String, get: Doc => String): Tokenized[Doc] = new Field[Doc, String](
def tokenized[Doc <: Document[Doc]](name: String, get: Doc => String): Tokenized[Doc] = new Field[Doc, String](
name = name,
get = get,
getRW = () => stringRW,
Expand All @@ -101,7 +102,7 @@ object Field {
override def toString: String = s"Tokenized(name = ${this.name})"
}

def unique[Doc, V](name: String, get: Doc => V)(implicit getRW: => RW[V]): UniqueIndex[Doc, V] = new Field[Doc, V](
def unique[Doc <: Document[Doc], V](name: String, get: Doc => V)(implicit getRW: => RW[V]): UniqueIndex[Doc, V] = new Field[Doc, V](
name = name,
get = get,
getRW = () => getRW,
Expand Down
5 changes: 3 additions & 2 deletions core/src/main/scala/lightdb/Sort.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package lightdb

import lightdb.doc.Document
import lightdb.spatial.GeoPoint

trait Sort
Expand All @@ -9,7 +10,7 @@ object Sort {

case object IndexOrder extends Sort

case class ByField[Doc, F](field: Field[Doc, F], direction: SortDirection = SortDirection.Ascending) extends Sort {
case class ByField[Doc <: Document[Doc], F](field: Field[Doc, F], direction: SortDirection = SortDirection.Ascending) extends Sort {
def direction(direction: SortDirection): ByField[Doc, F] = copy(direction = direction)

def ascending: ByField[Doc, F] = direction(SortDirection.Ascending)
Expand All @@ -21,7 +22,7 @@ object Sort {
def desc: ByField[Doc, F] = direction(SortDirection.Descending)
}

case class ByDistance[Doc](field: Field[Doc, Option[GeoPoint]],
case class ByDistance[Doc <: Document[Doc]](field: Field[Doc, Option[GeoPoint]],
from: GeoPoint,
direction: SortDirection = SortDirection.Ascending) extends Sort {
def direction(direction: SortDirection): ByDistance[Doc] = copy(direction = direction)
Expand Down
21 changes: 11 additions & 10 deletions core/src/main/scala/lightdb/aggregate/AggregateFilter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package lightdb.aggregate

import fabric.Json
import lightdb.Field
import lightdb.doc.Document
import lightdb.spatial.GeoPoint

sealed trait AggregateFilter[Doc] {
sealed trait AggregateFilter[Doc <: Document[Doc]] {
def &&(that: AggregateFilter[Doc]): AggregateFilter[Doc] = (this, that) match {
case (AggregateFilter.Combined(f1), AggregateFilter.Combined(f2)) => AggregateFilter.Combined(f1 ::: f2)
case (_, AggregateFilter.Combined(f)) => AggregateFilter.Combined(this :: f)
Expand All @@ -14,28 +15,28 @@ sealed trait AggregateFilter[Doc] {
}

object AggregateFilter {
def and[Doc](filters: AggregateFilter[Doc]*): AggregateFilter[Doc] = filters.tail
def and[Doc <: Document[Doc]](filters: AggregateFilter[Doc]*): AggregateFilter[Doc] = filters.tail
.foldLeft(filters.head)((combined, filter) => combined && filter)

case class Equals[Doc, F](name: String, field: Field[Doc, F], value: F) extends AggregateFilter[Doc] {
case class Equals[Doc <: Document[Doc], F](name: String, field: Field[Doc, F], value: F) extends AggregateFilter[Doc] {
def getJson: Json = field.rw.read(value)
}

case class NotEquals[Doc, F](name: String, field: Field[Doc, F], value: F) extends AggregateFilter[Doc] {
case class NotEquals[Doc <: Document[Doc], F](name: String, field: Field[Doc, F], value: F) extends AggregateFilter[Doc] {
def getJson: Json = field.rw.read(value)
}

case class In[Doc, F](name: String, field: Field[Doc, F], values: Seq[F]) extends AggregateFilter[Doc] {
case class In[Doc <: Document[Doc], F](name: String, field: Field[Doc, F], values: Seq[F]) extends AggregateFilter[Doc] {
def getJson: List[Json] = values.toList.map(field.rw.read)
}

case class Combined[Doc](filters: List[AggregateFilter[Doc]]) extends AggregateFilter[Doc]
case class Combined[Doc <: Document[Doc]](filters: List[AggregateFilter[Doc]]) extends AggregateFilter[Doc]

case class RangeLong[Doc](name: String, field: Field[Doc, Long], from: Option[Long], to: Option[Long]) extends AggregateFilter[Doc]
case class RangeLong[Doc <: Document[Doc]](name: String, field: Field[Doc, Long], from: Option[Long], to: Option[Long]) extends AggregateFilter[Doc]

case class RangeDouble[Doc](name: String, field: Field[Doc, Double], from: Option[Double], to: Option[Double]) extends AggregateFilter[Doc]
case class RangeDouble[Doc <: Document[Doc]](name: String, field: Field[Doc, Double], from: Option[Double], to: Option[Double]) extends AggregateFilter[Doc]

case class Parsed[Doc, F](name: String, field: Field[Doc, F], query: String, allowLeadingWildcard: Boolean) extends AggregateFilter[Doc]
case class Parsed[Doc <: Document[Doc], F](name: String, field: Field[Doc, F], query: String, allowLeadingWildcard: Boolean) extends AggregateFilter[Doc]

case class Distance[Doc](name: String, field: Field[Doc, GeoPoint], from: GeoPoint, radius: lightdb.distance.Distance) extends AggregateFilter[Doc]
case class Distance[Doc <: Document[Doc]](name: String, field: Field[Doc, GeoPoint], from: GeoPoint, radius: lightdb.distance.Distance) extends AggregateFilter[Doc]
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package lightdb.aggregate
import fabric.rw._
import lightdb.distance.Distance
import lightdb.Field
import lightdb.doc.Document
import lightdb.filter.FilterSupport
import lightdb.materialized.Materializable
import lightdb.spatial.GeoPoint

case class AggregateFunction[T, V, Doc](name: String, field: Field[Doc, V], `type`: AggregateType)
case class AggregateFunction[T, V, Doc <: Document[Doc]](name: String, field: Field[Doc, V], `type`: AggregateType)
(implicit val tRW: RW[T]) extends FilterSupport[V, Doc, AggregateFilter[Doc]] with Materializable[Doc, V] {
def rename(name: String): AggregateFunction[T, V, Doc] = copy(name = name)

Expand Down
3 changes: 2 additions & 1 deletion core/src/main/scala/lightdb/aggregate/AggregateSupport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package lightdb.aggregate

import fabric.rw._
import lightdb.Field
import lightdb.doc.Document

trait AggregateSupport[Doc, V] {
trait AggregateSupport[Doc <: Document[Doc], V] {
this: Field[Doc, V] =>

lazy val max: AggregateFunction[V, V, Doc] = AggregateFunction(s"${name}Max", this, AggregateType.Max)
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/lightdb/doc/DocumentModel.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lightdb.doc

import fabric.rw._
import lightdb.filter.FilterBuilder
import lightdb.{Field, Id, Indexed, Tokenized, Unique, UniqueIndex}

import scala.language.implicitConversions
Expand All @@ -23,6 +24,8 @@ trait DocumentModel[Doc <: Document[Doc]] {

def fields: List[Field[Doc, _]] = _fields

lazy val builder: FilterBuilder[Doc, this.type] = new FilterBuilder(this, 1, Nil)

object field {
private def add[V, F <: Field[Doc, V]](field: F): F = synchronized {
_fields = _fields ::: List(field)
Expand Down
50 changes: 13 additions & 37 deletions core/src/main/scala/lightdb/filter/Filter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,77 +2,53 @@ package lightdb.filter

import fabric.Json
import lightdb.Field
import lightdb.doc.{Document, DocumentModel}
import lightdb.spatial.GeoPoint

sealed trait Filter[Doc] {
sealed trait Filter[Doc <: Document[Doc]] {
def fields: List[Field[Doc, _]]

def &&(that: Filter[Doc]): Filter[Doc] = (this, that) match {
case (b1: Filter.Builder[Doc], b2: Filter.Builder[Doc]) if b1.minShould == b2.minShould =>
Filter.Builder[Doc](minShould = b1.minShould, filters = b1.filters ::: b2.filters)
case (_, b: Filter.Builder[Doc]) => b.must(this)
case (b: Filter.Builder[Doc], _) => b.must(that)
case _ => Filter.Builder[Doc](minShould = 1).must(this).must(that)
}

def ||(that: Filter[Doc]): Filter[Doc] = (this, that) match {
case (b1: Filter.Builder[Doc], b2: Filter.Builder[Doc]) if b1.minShould == b2.minShould =>
Filter.Builder[Doc](minShould = b1.minShould, filters = b1.filters ::: b2.filters)
case (_, b: Filter.Builder[Doc]) => b.should(this)
case (b: Filter.Builder[Doc], _) => b.should(that)
case _ => Filter.Builder[Doc](minShould = 1).should(this).should(that)
}
}

object Filter {
def and[Doc](filters: Filter[Doc]*): Filter[Doc] = filters.tail
def and[Doc <: Document[Doc]](filters: Filter[Doc]*): Filter[Doc] = filters.tail
.foldLeft(filters.head)((combined, filter) => combined && filter)

case class Equals[Doc, F](field: Field[Doc, F], value: F) extends Filter[Doc] {
case class Equals[Doc <: Document[Doc], F](field: Field[Doc, F], value: F) extends Filter[Doc] {
def getJson: Json = field.rw.read(value)

override lazy val fields: List[Field[Doc, _]] = List(field)
}

case class NotEquals[Doc, F](field: Field[Doc, F], value: F) extends Filter[Doc] {
case class NotEquals[Doc <: Document[Doc], F](field: Field[Doc, F], value: F) extends Filter[Doc] {
def getJson: Json = field.rw.read(value)

override lazy val fields: List[Field[Doc, _]] = List(field)
}

case class In[Doc, F](field: Field[Doc, F], values: Seq[F]) extends Filter[Doc] {
case class In[Doc <: Document[Doc], F](field: Field[Doc, F], values: Seq[F]) extends Filter[Doc] {
def getJson: List[Json] = values.toList.map(field.rw.read)

override lazy val fields: List[Field[Doc, _]] = List(field)
}

case class RangeLong[Doc](field: Field[Doc, Long], from: Option[Long], to: Option[Long]) extends Filter[Doc] {
case class RangeLong[Doc <: Document[Doc]](field: Field[Doc, Long], from: Option[Long], to: Option[Long]) extends Filter[Doc] {
override lazy val fields: List[Field[Doc, _]] = List(field)
}

case class RangeDouble[Doc](field: Field[Doc, Double], from: Option[Double], to: Option[Double]) extends Filter[Doc] {
case class RangeDouble[Doc <: Document[Doc]](field: Field[Doc, Double], from: Option[Double], to: Option[Double]) extends Filter[Doc] {
override lazy val fields: List[Field[Doc, _]] = List(field)
}

case class Parsed[Doc, F](field: Field[Doc, F], query: String, allowLeadingWildcard: Boolean) extends Filter[Doc] {
case class Parsed[Doc <: Document[Doc], F](field: Field[Doc, F], query: String, allowLeadingWildcard: Boolean) extends Filter[Doc] {
override lazy val fields: List[Field[Doc, _]] = List(field)
}

case class Distance[Doc](field: Field[Doc, Option[GeoPoint]], from: GeoPoint, radius: lightdb.distance.Distance) extends Filter[Doc] {
case class Distance[Doc <: Document[Doc]](field: Field[Doc, Option[GeoPoint]], from: GeoPoint, radius: lightdb.distance.Distance) extends Filter[Doc] {
override lazy val fields: List[Field[Doc, _]] = List(field)
}

case class Builder[Doc](minShould: Int = 0, filters: List[FilterClause[Doc]] = Nil) extends Filter[Doc] {
def minShould(i: Int): Builder[Doc] = copy(minShould = i)

def withFilter(filter: Filter[Doc], condition: Condition, boost: Option[Double] = None): Builder[Doc] = copy(
filters = filters ::: List(FilterClause(filter, condition, boost))
)

def must(filter: Filter[Doc], boost: Option[Double] = None): Builder[Doc] = withFilter(filter, Condition.Must, boost)
def mustNot(filter: Filter[Doc], boost: Option[Double] = None): Builder[Doc] = withFilter(filter, Condition.MustNot, boost)
def filter(filter: Filter[Doc], boost: Option[Double] = None): Builder[Doc] = withFilter(filter, Condition.Filter, boost)
def should(filter: Filter[Doc], boost: Option[Double] = None): Builder[Doc] = withFilter(filter, Condition.Should, boost)
case class Multi[Doc <: Document[Doc]](minShould: Int, filters: List[FilterClause[Doc]] = Nil) extends Filter[Doc] {
def conditional(filter: Filter[Doc], condition: Condition, boost: Option[Double] = None): Multi[Doc] =
copy(filters = filters ::: List(FilterClause(filter, condition, boost)))

override def fields: List[Field[Doc, _]] = filters.flatMap(_.filter.fields)
}
Expand Down
17 changes: 17 additions & 0 deletions core/src/main/scala/lightdb/filter/FilterBuilder.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package lightdb.filter

import lightdb.doc.{Document, DocumentModel}

class FilterBuilder[Doc <: Document[Doc], Model <: DocumentModel[Doc]](val model: Model,
minShould: Int,
filters: List[FilterClause[Doc]]) extends Filter.Multi[Doc](minShould, filters) {
def minShould(i: Int): FilterBuilder[Doc, Model] = new FilterBuilder(model, i, filters)

def withFilter(filter: Filter[Doc], condition: Condition, boost: Option[Double] = None): FilterBuilder[Doc, Model] =
new FilterBuilder(model, minShould, filters = filters ::: List(FilterClause(filter, condition, boost)))

def must(f: Model => Filter[Doc], boost: Option[Double] = None): FilterBuilder[Doc, Model] = withFilter(f(model), Condition.Must, boost)
def mustNot(f: Model => Filter[Doc], boost: Option[Double] = None): FilterBuilder[Doc, Model] = withFilter(f(model), Condition.MustNot, boost)
def filter(f: Model => Filter[Doc], boost: Option[Double] = None): FilterBuilder[Doc, Model] = withFilter(f(model), Condition.Filter, boost)
def should(f: Model => Filter[Doc], boost: Option[Double] = None): FilterBuilder[Doc, Model] = withFilter(f(model), Condition.Should, boost)
}
4 changes: 3 additions & 1 deletion core/src/main/scala/lightdb/filter/FilterClause.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package lightdb.filter

case class FilterClause[Doc](filter: Filter[Doc], condition: Condition, boost: Option[Double])
import lightdb.doc.Document

case class FilterClause[Doc <: Document[Doc]](filter: Filter[Doc], condition: Condition, boost: Option[Double])
21 changes: 21 additions & 0 deletions core/src/main/scala/lightdb/filter/package.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
package lightdb

import lightdb.doc.{Document, DocumentModel}

import scala.language.implicitConversions

package object filter {
implicit class ListFilterExtras[V, Doc, Filter](fs: FilterSupport[List[V], Doc, Filter]) {
def has(value: V): Filter = fs.is(List(value))
}
implicit class SetFilterExtras[V, Doc, Filter](fs: FilterSupport[Set[V], Doc, Filter]) {
def has(value: V): Filter = fs.is(Set(value))
}
implicit class FilterExtras[Doc <: Document[Doc]](val filter: Filter[Doc]) extends AnyVal {
def &&(that: Filter[Doc]): Filter[Doc] = (filter, that) match {
case (b1: Filter.Multi[Doc], b2: Filter.Multi[Doc]) if b1.minShould == b2.minShould =>
Filter.Multi(minShould = b1.minShould, filters = b1.filters ::: b2.filters)
case (_, b: Filter.Multi[Doc]) => b.conditional(filter, Condition.Must)
case (b: Filter.Multi[Doc], _) => b.conditional(that, Condition.Must)
case _ => Filter.Multi(minShould = 1).conditional(filter, Condition.Must).conditional(that, Condition.Must)
}

def ||(that: Filter[Doc]): Filter[Doc] = (filter, that) match {
case (b1: Filter.Multi[Doc], b2: Filter.Multi[Doc]) if b1.minShould == b2.minShould =>
Filter.Multi(minShould = b1.minShould, filters = b1.filters ::: b2.filters)
case (_, b: Filter.Multi[Doc]) => b.conditional(filter, Condition.Should)
case (b: Filter.Multi[Doc], _) => b.conditional(that, Condition.Should)
case _ => Filter.Multi(minShould = 1).conditional(filter, Condition.Should).conditional(that, Condition.Should)
}
}
}
10 changes: 5 additions & 5 deletions core/src/main/scala/lightdb/store/Conversion.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ import lightdb.spatial.{DistanceAndDoc, GeoPoint}
sealed trait Conversion[Doc, V]

object Conversion {
case class Value[Doc, F](field: Field[Doc, F]) extends Conversion[Doc, F]
case class Value[Doc <: Document[Doc], F](field: Field[Doc, F]) extends Conversion[Doc, F]

case class Doc[Doc]() extends Conversion[Doc, Doc]
case class Doc[Doc <: Document[Doc]]() extends Conversion[Doc, Doc]

case class Json[Doc](fields: List[Field[Doc, _]]) extends Conversion[Doc, fabric.Json]
case class Json[Doc <: Document[Doc]](fields: List[Field[Doc, _]]) extends Conversion[Doc, fabric.Json]

case class Materialized[Doc <: Document[Doc], Model <: DocumentModel[Doc]](fields: List[Field[Doc, _]]) extends Conversion[Doc, MaterializedIndex[Doc, Model]]

case class Converted[Doc, T](f: Doc => T) extends Conversion[Doc, T]
case class Converted[Doc <: Document[Doc], T](f: Doc => T) extends Conversion[Doc, T]

case class Distance[Doc](field: Field[Doc, Option[GeoPoint]],
case class Distance[Doc <: Document[Doc]](field: Field[Doc, Option[GeoPoint]],
from: GeoPoint,
sort: Boolean,
radius: Option[lightdb.distance.Distance]) extends Conversion[Doc, DistanceAndDoc[Doc]]
Expand Down
13 changes: 6 additions & 7 deletions core/src/test/scala/spec/AbstractBasicSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,11 @@ abstract class AbstractBasicSpec extends AnyWordSpec with Matchers { spec =>
"search using Filter.Builder and scoring" in {
if (filterBuilderSupported) {
db.people.transaction { implicit transaction =>
val results = db.people.query.scored.filter(p => Filter
.Builder()
.should(p.search.words("nica 13"), boost = Some(2.0))
.should(p.age <=> (10, 15))
val results = db.people.query.scored.filter(_
.builder
.minShould(0)
.should(_.search.words("nica 13"), boost = Some(2.0))
.should(_.age <=> (10, 15))
).search.docs
val people = results.list
people.map(_.name) should be(List("Veronica", "Brenda", "Diana", "Greg", "Charlie", "Evan", "Fiona", "Hanna", "Ian", "Jenna", "Kevin", "Mike", "Nancy", "Oscar", "Penny", "Quintin", "Ruth", "Sam", "Tori", "Uba", "Wyatt", "Xena", "Zoey", "Allan"))
Expand All @@ -227,9 +228,7 @@ abstract class AbstractBasicSpec extends AnyWordSpec with Matchers { spec =>
}
"search where city is set" in {
db.people.transaction { implicit transaction =>
val people = db.people.query.filter(p => Filter.Builder()
.mustNot(p.city === None)
).toList
val people = db.people.query.filter(_.builder.mustNot(_.city === None)).toList
people.map(_.name) should be(List("Evan"))
}
}
Expand Down
Loading

0 comments on commit 30d3fb0

Please sign in to comment.