From 30d3fb0c2b778cabae95361a996bee00fb01aa49 Mon Sep 17 00:00:00 2001 From: Matt Hicks Date: Mon, 2 Sep 2024 09:24:05 -0500 Subject: [PATCH] Cleanup and improvements to filter builder --- build.sbt | 1 - core/src/main/scala/lightdb/Field.scala | 23 +++++---- core/src/main/scala/lightdb/Sort.scala | 5 +- .../lightdb/aggregate/AggregateFilter.scala | 21 ++++---- .../lightdb/aggregate/AggregateFunction.scala | 3 +- .../lightdb/aggregate/AggregateSupport.scala | 3 +- .../scala/lightdb/doc/DocumentModel.scala | 3 ++ .../main/scala/lightdb/filter/Filter.scala | 50 +++++-------------- .../scala/lightdb/filter/FilterBuilder.scala | 17 +++++++ .../scala/lightdb/filter/FilterClause.scala | 4 +- .../main/scala/lightdb/filter/package.scala | 21 ++++++++ .../main/scala/lightdb/store/Conversion.scala | 10 ++-- .../test/scala/spec/AbstractBasicSpec.scala | 13 +++-- .../scala/lightdb/lucene/LuceneStore.scala | 7 +-- sql/src/main/scala/lightdb/sql/SQLArg.scala | 5 +- sql/src/main/scala/lightdb/sql/SQLStore.scala | 2 +- 16 files changed, 106 insertions(+), 82 deletions(-) create mode 100644 core/src/main/scala/lightdb/filter/FilterBuilder.scala diff --git a/build.sbt b/build.sbt index 80f74de0..da5ae284 100644 --- a/build.sbt +++ b/build.sbt @@ -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" diff --git a/core/src/main/scala/lightdb/Field.scala b/core/src/main/scala/lightdb/Field.scala index 26b884b4..31879186 100644 --- a/core/src/main/scala/lightdb/Field.scala +++ b/core/src/main/scala/lightdb/Field.scala @@ -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 { @@ -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, @@ -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, @@ -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, diff --git a/core/src/main/scala/lightdb/Sort.scala b/core/src/main/scala/lightdb/Sort.scala index b2d5c788..b3f7762c 100644 --- a/core/src/main/scala/lightdb/Sort.scala +++ b/core/src/main/scala/lightdb/Sort.scala @@ -1,5 +1,6 @@ package lightdb +import lightdb.doc.Document import lightdb.spatial.GeoPoint trait Sort @@ -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) @@ -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) diff --git a/core/src/main/scala/lightdb/aggregate/AggregateFilter.scala b/core/src/main/scala/lightdb/aggregate/AggregateFilter.scala index 26aa59ce..cf5fafca 100644 --- a/core/src/main/scala/lightdb/aggregate/AggregateFilter.scala +++ b/core/src/main/scala/lightdb/aggregate/AggregateFilter.scala @@ -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) @@ -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] } \ No newline at end of file diff --git a/core/src/main/scala/lightdb/aggregate/AggregateFunction.scala b/core/src/main/scala/lightdb/aggregate/AggregateFunction.scala index e3c9b461..52dfc4d0 100644 --- a/core/src/main/scala/lightdb/aggregate/AggregateFunction.scala +++ b/core/src/main/scala/lightdb/aggregate/AggregateFunction.scala @@ -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) diff --git a/core/src/main/scala/lightdb/aggregate/AggregateSupport.scala b/core/src/main/scala/lightdb/aggregate/AggregateSupport.scala index 0fd30f08..9b954cac 100644 --- a/core/src/main/scala/lightdb/aggregate/AggregateSupport.scala +++ b/core/src/main/scala/lightdb/aggregate/AggregateSupport.scala @@ -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) diff --git a/core/src/main/scala/lightdb/doc/DocumentModel.scala b/core/src/main/scala/lightdb/doc/DocumentModel.scala index 3b233146..3f7e1141 100644 --- a/core/src/main/scala/lightdb/doc/DocumentModel.scala +++ b/core/src/main/scala/lightdb/doc/DocumentModel.scala @@ -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 @@ -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) diff --git a/core/src/main/scala/lightdb/filter/Filter.scala b/core/src/main/scala/lightdb/filter/Filter.scala index 44dc22c5..815c8e1c 100644 --- a/core/src/main/scala/lightdb/filter/Filter.scala +++ b/core/src/main/scala/lightdb/filter/Filter.scala @@ -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) } diff --git a/core/src/main/scala/lightdb/filter/FilterBuilder.scala b/core/src/main/scala/lightdb/filter/FilterBuilder.scala new file mode 100644 index 00000000..f7e57206 --- /dev/null +++ b/core/src/main/scala/lightdb/filter/FilterBuilder.scala @@ -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) +} diff --git a/core/src/main/scala/lightdb/filter/FilterClause.scala b/core/src/main/scala/lightdb/filter/FilterClause.scala index e799bedf..559412b8 100644 --- a/core/src/main/scala/lightdb/filter/FilterClause.scala +++ b/core/src/main/scala/lightdb/filter/FilterClause.scala @@ -1,3 +1,5 @@ package lightdb.filter -case class FilterClause[Doc](filter: Filter[Doc], condition: Condition, boost: Option[Double]) \ No newline at end of file +import lightdb.doc.Document + +case class FilterClause[Doc <: Document[Doc]](filter: Filter[Doc], condition: Condition, boost: Option[Double]) \ No newline at end of file diff --git a/core/src/main/scala/lightdb/filter/package.scala b/core/src/main/scala/lightdb/filter/package.scala index 337f7096..8fa3f260 100644 --- a/core/src/main/scala/lightdb/filter/package.scala +++ b/core/src/main/scala/lightdb/filter/package.scala @@ -1,5 +1,9 @@ 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)) @@ -7,4 +11,21 @@ package object filter { 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) + } + } } diff --git a/core/src/main/scala/lightdb/store/Conversion.scala b/core/src/main/scala/lightdb/store/Conversion.scala index 658091aa..8a6c49e7 100644 --- a/core/src/main/scala/lightdb/store/Conversion.scala +++ b/core/src/main/scala/lightdb/store/Conversion.scala @@ -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]] diff --git a/core/src/test/scala/spec/AbstractBasicSpec.scala b/core/src/test/scala/spec/AbstractBasicSpec.scala index 91c3bed9..dd48dddc 100644 --- a/core/src/test/scala/spec/AbstractBasicSpec.scala +++ b/core/src/test/scala/spec/AbstractBasicSpec.scala @@ -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")) @@ -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")) } } diff --git a/lucene/src/main/scala/lightdb/lucene/LuceneStore.scala b/lucene/src/main/scala/lightdb/lucene/LuceneStore.scala index 94bc17cd..865b38f1 100644 --- a/lucene/src/main/scala/lightdb/lucene/LuceneStore.scala +++ b/lucene/src/main/scala/lightdb/lucene/LuceneStore.scala @@ -257,10 +257,11 @@ class LuceneStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]](directory: parser.parse(query) case Filter.Distance(index, from, radius) => LatLonPoint.newDistanceQuery(index.name, from.latitude, from.longitude, radius.toMeters) - case Filter.Builder(minShould, clauses) => + case Filter.Multi(minShould, clauses) => val b = new BooleanQuery.Builder val hasShould = clauses.exists(c => c.condition == Condition.Should || c.condition == Condition.Filter) - b.setMinimumNumberShouldMatch(if (hasShould) minShould else 0) + val minShouldMatch = if (hasShould) minShould else 0 + b.setMinimumNumberShouldMatch(minShouldMatch) clauses.foreach { c => val q = filter2Lucene(Some(c.filter)) val query = c.boost match { @@ -275,7 +276,7 @@ class LuceneStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]](directory: } b.add(query, occur) } - if (minShould == 0 && !clauses.exists(_.condition == Condition.Must)) { + if (minShouldMatch == 0 && !clauses.exists(_.condition == Condition.Must)) { b.add(new MatchAllDocsQuery, BooleanClause.Occur.MUST) } b.build() diff --git a/sql/src/main/scala/lightdb/sql/SQLArg.scala b/sql/src/main/scala/lightdb/sql/SQLArg.scala index 210584c6..955d39a5 100644 --- a/sql/src/main/scala/lightdb/sql/SQLArg.scala +++ b/sql/src/main/scala/lightdb/sql/SQLArg.scala @@ -4,6 +4,7 @@ import fabric._ import fabric.define.DefType import fabric.io.JsonFormatter import fabric.rw._ +import lightdb.doc.Document import lightdb.spatial.GeoPoint import lightdb.{Field, Id, Tokenized} @@ -14,7 +15,7 @@ trait SQLArg { } object SQLArg { - case class FieldArg[Doc, F](field: Field[Doc, F], value: F) extends SQLArg { + case class FieldArg[Doc <: Document[Doc], F](field: Field[Doc, F], value: F) extends SQLArg { private def setInternal(ps: PreparedStatement, index: Int, value: Any): Unit = { scribe.trace(s"SQLArg: $index / $value (${if (value != null) value.getClass.getName else "null"})") value match { @@ -54,7 +55,7 @@ object SQLArg { } object FieldArg { - def apply[Doc, F](doc: Doc, field: Field[Doc, F]): FieldArg[Doc, F] = apply(field, field.get(doc)) + def apply[Doc <: Document[Doc], F](doc: Doc, field: Field[Doc, F]): FieldArg[Doc, F] = apply(field, field.get(doc)) } case class StringArg(s: String) extends SQLArg { diff --git a/sql/src/main/scala/lightdb/sql/SQLStore.scala b/sql/src/main/scala/lightdb/sql/SQLStore.scala index 0920114d..b75fed1d 100644 --- a/sql/src/main/scala/lightdb/sql/SQLStore.scala +++ b/sql/src/main/scala/lightdb/sql/SQLStore.scala @@ -523,7 +523,7 @@ abstract class SQLStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]] exten }.toList SQLPart(parts.map(_ => s"${f.field.name} LIKE ?").mkString(" AND "), parts.map(s => SQLArg.StringArg(s))) case f: Filter.Distance[Doc] => distanceFilter(f) - case f: Filter.Builder[Doc] => + case f: Filter.Multi[Doc] => val (shoulds, others) = f.filters.partition(f => f.condition == Condition.Filter || f.condition == Condition.Should) if (f.minShould != 1 && shoulds.nonEmpty) { throw new UnsupportedOperationException("Should filtering only works in SQL for exactly one condition")