diff --git a/async/src/main/scala/lightdb/async/AsyncQuery.scala b/async/src/main/scala/lightdb/async/AsyncQuery.scala index 4a212f50..d8fb3017 100644 --- a/async/src/main/scala/lightdb/async/AsyncQuery.scala +++ b/async/src/main/scala/lightdb/async/AsyncQuery.scala @@ -21,7 +21,8 @@ case class AsyncQuery[Doc <: Document[Doc], Model <: DocumentModel[Doc]](collect limit: Option[Int] = None, countTotal: Boolean = false, scoreDocs: Boolean = false, - minDocScore: Option[Double] = None) { query => + minDocScore: Option[Double] = None) { + query => private[async] def toQuery: Query[Doc, Model] = Query[Doc, Model](collection, filter, sort, offset, limit, countTotal, scoreDocs, minDocScore) def scored: AsyncQuery[Doc, Model] = copy(scoreDocs = true) @@ -82,11 +83,11 @@ case class AsyncQuery[Doc <: Document[Doc], Model <: DocumentModel[Doc]](collect (implicit transaction: Transaction[Doc]): fs2.Stream[IO, (MaterializedIndex[Doc, Model], Double)] = apply(Conversion.Materialized[Doc, Model](f(collection.model))) - def distance(f: Model => Field[Doc, Option[Geo.Point]], - from: Geo.Point, - sort: Boolean = true, - radius: Option[Distance] = None) - (implicit transaction: Transaction[Doc]): fs2.Stream[IO, (DistanceAndDoc[Doc], Double)] = + def distance[G <: Geo](f: Model => Field[Doc, List[G]], + from: Geo.Point, + sort: Boolean = true, + radius: Option[Distance] = None) + (implicit transaction: Transaction[Doc]): fs2.Stream[IO, (DistanceAndDoc[Doc], Double)] = apply(Conversion.Distance(f(collection.model), from, sort, radius)) } @@ -116,11 +117,11 @@ case class AsyncQuery[Doc <: Document[Doc], Model <: DocumentModel[Doc]](collect (implicit transaction: Transaction[Doc]): fs2.Stream[IO, MaterializedIndex[Doc, Model]] = apply(Conversion.Materialized[Doc, Model](f(collection.model))) - def distance(f: Model => Field[Doc, Option[Geo.Point]], - from: Geo.Point, - sort: Boolean = true, - radius: Option[Distance] = None) - (implicit transaction: Transaction[Doc]): fs2.Stream[IO, DistanceAndDoc[Doc]] = + def distance[G <: Geo](f: Model => Field[Doc, List[G]], + from: Geo.Point, + sort: Boolean = true, + radius: Option[Distance] = None) + (implicit transaction: Transaction[Doc]): fs2.Stream[IO, DistanceAndDoc[Doc]] = apply(Conversion.Distance(f(collection.model), from, sort, radius)) } @@ -159,11 +160,11 @@ case class AsyncQuery[Doc <: Document[Doc], Model <: DocumentModel[Doc]](collect (implicit transaction: Transaction[Doc]): IO[AsyncSearchResults[Doc, MaterializedIndex[Doc, Model]]] = apply(Conversion.Materialized(f(collection.model))) - def distance(f: Model => Field[Doc, Option[Geo.Point]], - from: Geo.Point, - sort: Boolean = true, - radius: Option[Distance] = None) - (implicit transaction: Transaction[Doc]): IO[AsyncSearchResults[Doc, DistanceAndDoc[Doc]]] = + def distance[G <: Geo](f: Model => Field[Doc, List[G]], + from: Geo.Point, + sort: Boolean = true, + radius: Option[Distance] = None) + (implicit transaction: Transaction[Doc]): IO[AsyncSearchResults[Doc, DistanceAndDoc[Doc]]] = apply(Conversion.Distance(f(collection.model), from, sort, radius)) } diff --git a/core/src/main/scala/lightdb/Field.scala b/core/src/main/scala/lightdb/Field.scala index d616c76e..6b0d90d1 100644 --- a/core/src/main/scala/lightdb/Field.scala +++ b/core/src/main/scala/lightdb/Field.scala @@ -22,12 +22,15 @@ sealed class Field[Doc <: Document[Doc], V](val name: String, case _ => false } - lazy val className: Option[String] = rw.definition match { - case DefType.Opt(DefType.Obj(_, Some(cn))) => Some(cn) - case DefType.Obj(_, Some(cn)) => Some(cn) - case DefType.Opt(DefType.Poly(_, Some(cn))) => Some(cn) - case DefType.Poly(_, Some(cn)) => Some(cn) - case _ => None + lazy val className: Option[String] = { + def lookup(d: DefType): Option[String] = d match { + case DefType.Opt(d) => lookup(d) + case DefType.Arr(d) => lookup(d) + case DefType.Poly(_, cn) => cn + case DefType.Obj(_, cn) => cn + case _ => None + } + lookup(rw.definition) } lazy val isSpatial: Boolean = className.exists(_.startsWith("lightdb.spatial.Geo")) @@ -73,6 +76,8 @@ sealed class Field[Doc <: Document[Doc], V](val name: String, def opt: Field[Doc, Option[V]] = new Field[Doc, Option[V]](name, doc => Option(get(doc)), () => implicitly[RW[Option[V]]], indexed) + def list: Field[Doc, List[V]] = new Field[Doc, List[V]](name, doc => List(get(doc)), () => implicitly[RW[List[V]]], indexed) + override def distance(from: Geo.Point, radius: Distance): Filter[Doc] = Filter.Distance(name, from, radius) @@ -134,7 +139,7 @@ object Field { }) case DefType.Opt(d) => string2Json(name, s, d) case DefType.Enum(_, _) => str(s) - case DefType.Arr(d) => arr(s.split(";;").toList.map(string2Json(name, _, d)): _*) + case DefType.Arr(d) if !s.startsWith("[") => arr(s.split(";;").toList.map(string2Json(name, _, d)): _*) case _ => try { JsonParser(s) } catch { diff --git a/core/src/main/scala/lightdb/Query.scala b/core/src/main/scala/lightdb/Query.scala index 48915eca..061484df 100644 --- a/core/src/main/scala/lightdb/Query.scala +++ b/core/src/main/scala/lightdb/Query.scala @@ -93,7 +93,7 @@ case class Query[Doc <: Document[Doc], Model <: DocumentModel[Doc]](collection: apply(Conversion.Materialized(fields)) } - def distance[G <: Geo](f: Model => Field[Doc, Option[G]], + def distance[G <: Geo](f: Model => Field[Doc, List[G]], from: Geo.Point, sort: Boolean = true, radius: Option[Distance] = None) @@ -121,9 +121,10 @@ case class Query[Doc <: Document[Doc], Model <: DocumentModel[Doc]](collection: def count(implicit transaction: Transaction[Doc]): Int = copy(limit = Some(1), countTotal = true) .search.docs.total.get - protected def distanceSearch[G <: Geo](field: Field[Doc, Option[G]], - from: Geo.Point, - sort: Boolean, radius: Option[Distance]) + protected def distanceSearch[G <: Geo](field: Field[Doc, List[G]], + from: Geo.Point, + sort: Boolean, + radius: Option[Distance]) (implicit transaction: Transaction[Doc]): SearchResults[Doc, DistanceAndDoc[Doc]] = { search(Conversion.Distance(field, from, sort, radius)) } diff --git a/core/src/main/scala/lightdb/Sort.scala b/core/src/main/scala/lightdb/Sort.scala index df48e5ad..9662c324 100644 --- a/core/src/main/scala/lightdb/Sort.scala +++ b/core/src/main/scala/lightdb/Sort.scala @@ -22,7 +22,7 @@ object Sort { def desc: ByField[Doc, F] = direction(SortDirection.Descending) } - case class ByDistance[Doc <: Document[Doc], G <: Geo](field: Field[Doc, Option[G]], + case class ByDistance[Doc <: Document[Doc], G <: Geo](field: Field[Doc, List[G]], from: Geo.Point, direction: SortDirection = SortDirection.Ascending) extends Sort { def direction(direction: SortDirection): ByDistance[Doc, G] = copy(direction = direction) diff --git a/core/src/main/scala/lightdb/spatial/DistanceAndDoc.scala b/core/src/main/scala/lightdb/spatial/DistanceAndDoc.scala index 78f651ca..539f0f81 100644 --- a/core/src/main/scala/lightdb/spatial/DistanceAndDoc.scala +++ b/core/src/main/scala/lightdb/spatial/DistanceAndDoc.scala @@ -2,4 +2,4 @@ package lightdb.spatial import lightdb.distance.Distance -case class DistanceAndDoc[Doc](doc: Doc, distance: Option[Distance]) \ No newline at end of file +case class DistanceAndDoc[Doc](doc: Doc, distance: List[Distance]) \ No newline at end of file diff --git a/core/src/main/scala/lightdb/store/Conversion.scala b/core/src/main/scala/lightdb/store/Conversion.scala index e6931163..4203d8a0 100644 --- a/core/src/main/scala/lightdb/store/Conversion.scala +++ b/core/src/main/scala/lightdb/store/Conversion.scala @@ -18,7 +18,7 @@ object Conversion { case class Converted[Doc <: Document[Doc], T](f: Doc => T) extends Conversion[Doc, T] - case class Distance[Doc <: Document[Doc], G <: Geo](field: Field[Doc, Option[G]], + case class Distance[Doc <: Document[Doc], G <: Geo](field: Field[Doc, List[G]], from: Geo.Point, sort: Boolean, radius: Option[lightdb.distance.Distance]) extends Conversion[Doc, DistanceAndDoc[Doc]] diff --git a/core/src/test/scala/spec/AbstractSpatialSpec.scala b/core/src/test/scala/spec/AbstractSpatialSpec.scala index 57589332..8c6441bb 100644 --- a/core/src/test/scala/spec/AbstractSpatialSpec.scala +++ b/core/src/test/scala/spec/AbstractSpatialSpec.scala @@ -44,21 +44,21 @@ abstract class AbstractSpatialSpec extends AnyWordSpec with Matchers { spec => name = "John Doe", age = 21, point = newYorkCity, - geo = None, + geo = Nil, _id = id1 ) private val p2 = Person( name = "Jane Doe", age = 19, point = noble, - geo = Some(moorePolygon), + geo = List(moorePolygon), _id = id2 ) private val p3 = Person( name = "Bob Dole", age = 123, point = yonkers, - geo = Some(chicago), + geo = List(chicago, yonkers), _id = id3 ) @@ -79,17 +79,17 @@ abstract class AbstractSpatialSpec extends AnyWordSpec with Matchers { spec => "sort by distance from Oklahoma City" in { DB.people.transaction { implicit transaction => val list = DB.people.query.search.distance( - _.point.opt, + _.point.list, from = oklahomaCity, radius = Some(1320.miles) ).iterator.toList val people = list.map(_.doc) - val distances = list.map(_.distance.get.mi) + val distances = list.map(_.distance.map(_.mi.toInt)) people.zip(distances).map { case (p, d) => p.name -> d } should be(List( - "Jane Doe" -> 28.55539552714398, - "John Doe" -> 1316.1301092705082 + "Jane Doe" -> List(28), + "John Doe" -> List(1316) )) } } @@ -101,12 +101,12 @@ abstract class AbstractSpatialSpec extends AnyWordSpec with Matchers { spec => radius = Some(10_000.miles) ).iterator.toList val people = list.map(_.doc) - val distances = list.map(_.distance.get.mi) + val distances = list.map(_.distance.map(_.mi)) people.zip(distances).map { case (p, d) => p.name -> d } should be(List( - "Jane Doe" -> 16.01508397712445, - "Bob Dole" -> 695.6419047674393 + "Jane Doe" -> List(16.01508397712445), + "Bob Dole" -> List(695.6419047674393, 1334.038796028706) )) } } @@ -133,7 +133,7 @@ abstract class AbstractSpatialSpec extends AnyWordSpec with Matchers { spec => case class Person(name: String, age: Int, point: Geo.Point, - geo: Option[Geo], + geo: List[Geo], _id: Id[Person] = Person.id()) extends Document[Person] object Person extends DocumentModel[Person] with JsonConversion[Person] { @@ -142,6 +142,6 @@ abstract class AbstractSpatialSpec extends AnyWordSpec with Matchers { spec => val name: F[String] = field("name", _.name) val age: F[Int] = field("age", _.age) val point: I[Geo.Point] = field.index("point", _.point) - val geo: I[Option[Geo]] = field.index("geo", _.geo) + val geo: I[List[Geo]] = field.index("geo", _.geo) } } diff --git a/lucene/src/main/scala/lightdb/lucene/LuceneStore.scala b/lucene/src/main/scala/lightdb/lucene/LuceneStore.scala index 24f14baa..13e1deb7 100644 --- a/lucene/src/main/scala/lightdb/lucene/LuceneStore.scala +++ b/lucene/src/main/scala/lightdb/lucene/LuceneStore.scala @@ -39,17 +39,11 @@ class LuceneStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]](directory: ) override def insert(doc: Doc)(implicit transaction: Transaction[Doc]): Unit = { - val luceneFields = fields.flatMap { field => - createLuceneFields(field, doc) - } - addDoc(id(doc), luceneFields, upsert = false) + addDoc(doc, upsert = false) } override def upsert(doc: Doc)(implicit transaction: Transaction[Doc]): Unit = { - val luceneFields = fields.flatMap { field => - createLuceneFields(field, doc) - } - addDoc(id(doc), luceneFields, upsert = true) + addDoc(doc, upsert = true) } private def createGeoFields(field: Field[Doc, _], @@ -71,16 +65,24 @@ class LuceneStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]](directory: val polygon = convert(p) LatLonShape.createIndexableFields(field.name, polygon) } - val geo = json.as[Geo] - geo match { - case p: Geo.Point => indexPoint(p) - case Geo.MultiPoint(points) => points.foreach(indexPoint) - case l: Geo.Line => indexLine(l) - case Geo.MultiLine(lines) => lines.foreach(indexLine) - case p: Geo.Polygon => indexPolygon(p) - case Geo.MultiPolygon(polygons) => polygons.foreach(indexPolygon) + val list = json match { + case Arr(value, _) => value.toList.map(_.as[Geo]) + case _ => List(json.as[Geo]) + } + list.foreach { geo => + geo match { + case p: Geo.Point => indexPoint(p) + case Geo.MultiPoint(points) => points.foreach(indexPoint) + case l: Geo.Line => indexLine(l) + case Geo.MultiLine(lines) => lines.foreach(indexLine) + case p: Geo.Polygon => indexPolygon(p) + case Geo.MultiPolygon(polygons) => polygons.foreach(indexPolygon) + } + add(new LatLonPoint(field.name, geo.center.latitude, geo.center.longitude)) + } + if (list.isEmpty) { + add(new LatLonPoint(field.name, 0.0, 0.0)) } - add(new LatLonPoint(field.name, geo.center.latitude, geo.center.longitude)) } add(new StoredField(field.name, JsonFormatter.Compact(json))) } @@ -123,17 +125,26 @@ class LuceneStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]](directory: add(sorted) case NumInt(l, _) => add(new NumericDocValuesField(fieldSortName, l)) case j if field.isSpatial && j != Null => - val g = j.as[Geo] - add(new LatLonDocValuesField(fieldSortName, g.center.latitude, g.center.longitude)) + val list = j match { + case Arr(values, _) => values.toList.map(_.as[Geo]) + case _ => List(j.as[Geo]) + } + list.foreach { g => + add(new LatLonDocValuesField(fieldSortName, g.center.latitude, g.center.longitude)) + } case _ => // Ignore } fields } } - private def addDoc(id: Id[Doc], fields: List[LuceneField], upsert: Boolean): Unit = if (fields.tail.nonEmpty) { + private def addDoc(doc: Doc, upsert: Boolean): Unit = if (fields.tail.nonEmpty) { + val id = this.id(doc) + val luceneFields = fields.flatMap { field => + createLuceneFields(field, doc) + } val document = new LuceneDocument - fields.foreach(document.add) + luceneFields.foreach(document.add) if (upsert) { index.indexWriter.updateDocument(new Term("_id", id.value), document) @@ -285,7 +296,10 @@ class LuceneStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]](directory: parser.setSplitOnWhitespace(true) parser.parse(query) case Filter.Distance(fieldName, from, radius) => - LatLonPoint.newDistanceQuery(fieldName, from.latitude, from.longitude, radius.toMeters) + val b = new BooleanQuery.Builder + b.add(LatLonPoint.newDistanceQuery(fieldName, from.latitude, from.longitude, radius.toMeters), BooleanClause.Occur.MUST) + b.add(LatLonPoint.newBoxQuery(fieldName, 0.0, 0.0, 0.0, 0.0), BooleanClause.Occur.MUST_NOT) + b.build() case Filter.Multi(minShould, clauses) => val b = new BooleanQuery.Builder val hasShould = clauses.exists(c => c.condition == Condition.Should || c.condition == Condition.Filter) diff --git a/sql/src/main/scala/lightdb/sql/SQLQueryBuilder.scala b/sql/src/main/scala/lightdb/sql/SQLQueryBuilder.scala index 01fa899f..b572f02a 100644 --- a/sql/src/main/scala/lightdb/sql/SQLQueryBuilder.scala +++ b/sql/src/main/scala/lightdb/sql/SQLQueryBuilder.scala @@ -91,7 +91,7 @@ case class SQLQueryBuilder[Doc <: Document[Doc]](collection: Collection[Doc, _], SQLResults(ps.executeQuery(), combinedSql, ps) } } catch { - case t: Throwable => throw new SQLException(s"Error executing query: $combinedSql", t) + case t: Throwable => throw new SQLException(s"Error executing query: $combinedSql (params: ${args.mkString(" | ")})", t) } } } diff --git a/sql/src/main/scala/lightdb/sql/SQLStore.scala b/sql/src/main/scala/lightdb/sql/SQLStore.scala index b3c6c1fe..1f32b315 100644 --- a/sql/src/main/scala/lightdb/sql/SQLStore.scala +++ b/sql/src/main/scala/lightdb/sql/SQLStore.scala @@ -10,7 +10,7 @@ import lightdb.distance.Distance import lightdb.doc.{Document, DocumentModel, JsonConversion} import lightdb.filter.{Condition, Filter} import lightdb.materialized.{MaterializedAggregate, MaterializedIndex} -import lightdb.spatial.DistanceAndDoc +import lightdb.spatial.{DistanceAndDoc, Geo} import lightdb.sql.connect.ConnectionManager import lightdb.store.{Conversion, Store, StoreMode} import lightdb.transaction.{Transaction, TransactionKey} @@ -276,9 +276,9 @@ abstract class SQLStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]] exten jsonFromFields(fields).asInstanceOf[V] case Conversion.Distance(field, _, _, _) => val fieldName = s"${field.name}Distance" - val distance = Option(rs.getObject(fieldName)).map(_.toString.toDouble).map(Distance.apply) + val distances = JsonParser(rs.getString(fieldName)).as[List[Double]].map(d => Distance(d)).toList val doc = getDoc(rs) - DistanceAndDoc(doc, distance).asInstanceOf[V] + DistanceAndDoc(doc, distances).asInstanceOf[V] } } } @@ -346,9 +346,7 @@ abstract class SQLStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]] exten case Sort.ByField(index, direction) => val dir = if (direction == SortDirection.Descending) "DESC" else "ASC" SQLPart(s"${index.name} $dir") - case Sort.ByDistance(field, _, direction) => - val dir = if (direction == SortDirection.Descending) "DESC" else "ASC" - SQLPart(s"${field.name}Distance $dir") + case Sort.ByDistance(field, _, direction) => sortByDistance(field, direction) }, limit = query.limit, offset = query.offset @@ -373,6 +371,11 @@ abstract class SQLStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]] exten ) } + protected def sortByDistance[G <: Geo](field: Field[_, List[G]], direction: SortDirection): SQLPart = { + val dir = if (direction == SortDirection.Descending) "DESC" else "ASC" + SQLPart(s"${field.name}Distance $dir") + } + private def aggregate2SQLQuery(query: AggregateQuery[Doc, Model]) (implicit transaction: Transaction[Doc]): SQLQueryBuilder[Doc] = { val fields = query.functions.map { f => diff --git a/sqlite/src/main/scala/lightdb/sql/SQLiteStore.scala b/sqlite/src/main/scala/lightdb/sql/SQLiteStore.scala index 7238f299..8166272a 100644 --- a/sqlite/src/main/scala/lightdb/sql/SQLiteStore.scala +++ b/sqlite/src/main/scala/lightdb/sql/SQLiteStore.scala @@ -5,14 +5,15 @@ import fabric.define.DefType import fabric.io.{JsonFormatter, JsonParser} import fabric.rw._ import lightdb.collection.Collection +import lightdb.distance.Distance import lightdb.sql.connect.{ConnectionManager, DBCPConnectionManager, SQLConfig, SingleConnectionManager} -import lightdb.{Field, LightDB} +import lightdb.{Field, LightDB, SortDirection} import lightdb.doc.{Document, DocumentModel} import lightdb.filter.Filter import lightdb.spatial.{Geo, Spatial} import lightdb.store.{Conversion, Store, StoreManager, StoreMode} import lightdb.transaction.Transaction -import org.sqlite.{SQLiteConfig, SQLiteOpenMode} +import org.sqlite.{Collation, SQLiteConfig, SQLiteOpenMode} import org.sqlite.SQLiteConfig.{JournalMode, LockingMode, SynchronousMode, TransactionMode} import java.io.File @@ -29,21 +30,45 @@ class SQLiteStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]](val connect scribe.info(s"${collection.name} has spatial features. Enabling...") org.sqlite.Function.create(c, "DISTANCE", new org.sqlite.Function() { override def xFunc(): Unit = { - def s(index: Int): Option[Geo] = Option(value_text(index)) - .map(s => JsonParser(s).as[Geo]) - val shape1 = s(0) - val shape2 = s(1) - val distance = (shape1, shape2) match { - case (Some(s1), Some(s2)) => - Some(Spatial.distance(s1, s2)) - case _ => None - } - distance match { - case Some(d) => - val meters = d.valueInMeters - result(meters) - case None => result() + def s(index: Int): List[Geo] = Option(value_text(index)) + .map(s => JsonParser(s)) + .map { + case Arr(vector, _) => vector.toList.map(_.as[Geo]) + case json => List(json.as[Geo]) + } + .getOrElse(Nil) + val shapes1 = s(0) + val shapes2 = s(1) + val distances = shapes1.flatMap { geo1 => + shapes2.map { geo2 => + Spatial.distance(geo1, geo2) + } } + result(JsonFormatter.Compact(distances.json)) + } + }) + org.sqlite.Function.create(c, "DISTANCE_LESS_THAN", new org.sqlite.Function() { + override def xFunc(): Unit = { + val distances = Option(value_text(0)) + .map(s => JsonParser(s).as[List[Distance]]) + .getOrElse(Nil) + val value = value_text(1).toDouble + val b = distances.exists(d => d.valueInMeters <= value) + result(if (b) 1 else 0) + } + }) + org.sqlite.Collation.create(c, "DISTANCE_SORT_ASCENDING", new Collation() { + override def xCompare(str1: String, str2: String): Int = { + val min1 = JsonParser(str1).as[List[Double]].min + val min2 = JsonParser(str2).as[List[Double]].min + min1.compareTo(min2) + } + }) + org.sqlite.Collation.create(c, "DISTANCE_SORT_DESCENDING", new Collation() { + override def xCompare(str1: String, str2: String): Int = { + val min1 = JsonParser(str1).as[List[Double]].min + val min2 = JsonParser(str2).as[List[Double]].min + min2.compareTo(min1) } }) } @@ -64,8 +89,12 @@ class SQLiteStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]](val connect List(SQLPart(s"DISTANCE(${d.field.name}, ?) AS ${d.field.name}Distance", List(SQLArg.JsonArg(d.from.json)))) override protected def distanceFilter(f: Filter.Distance[Doc]): SQLPart = - SQLPart(s"${f.fieldName}Distance <= ?", List(SQLArg.DoubleArg(f.radius.valueInMeters))) -// SQLPart(s"${f.fieldName} DISTANCE ? <= ?", List(SQLArg.JsonArg(f.from.json), SQLArg.DoubleArg(f.radius.m))) + SQLPart(s"DISTANCE_LESS_THAN(${f.fieldName}Distance, ?)", List(SQLArg.DoubleArg(f.radius.valueInMeters))) + + override protected def sortByDistance[G <: Geo](field: Field[_, List[G]], direction: SortDirection): SQLPart = direction match { + case SortDirection.Ascending => SQLPart(s"${field.name}Distance COLLATE DISTANCE_SORT_ASCENDING") + case SortDirection.Descending => SQLPart(s"${field.name}Distance COLLATE DISTANCE_SORT_DESCENDING") + } } object SQLiteStore extends StoreManager {