Skip to content

Commit

Permalink
Added Regex support and Regex function to SQLite
Browse files Browse the repository at this point in the history
  • Loading branch information
darkfrog26 committed Sep 4, 2024
1 parent ebed784 commit c841289
Show file tree
Hide file tree
Showing 11 changed files with 59 additions and 5 deletions.
2 changes: 2 additions & 0 deletions core/src/main/scala/lightdb/Field.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ sealed class Field[Doc <: Document[Doc], V](val name: String,

override def !==(value: V): Filter[Doc] = Filter.NotEquals(name, value)

override def regex(expression: String): Filter[Doc] = Filter.Regex(name, expression)

override protected def rangeLong(from: Option[Long], to: Option[Long]): Filter[Doc] =
Filter.RangeLong(name, from, to)

Expand Down
5 changes: 4 additions & 1 deletion core/src/main/scala/lightdb/SortDirection.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package lightdb

import fabric.rw.RW

sealed trait SortDirection

object SortDirection {
case object Ascending extends SortDirection
implicit val rw: RW[SortDirection] = RW.enumeration(List(Ascending, Descending))

case object Ascending extends SortDirection
case object Descending extends SortDirection
}
6 changes: 5 additions & 1 deletion core/src/main/scala/lightdb/aggregate/AggregateFilter.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package lightdb.aggregate

import fabric.Json
import fabric.{Json, Str}
import lightdb.Field
import lightdb.doc.Document
import lightdb.spatial.GeoPoint
Expand All @@ -26,6 +26,10 @@ object AggregateFilter {
def getJson: Json = field.rw.read(value)
}

case class Regex[Doc <: Document[Doc], F](name: String, field: Field[Doc, F], expression: String) extends AggregateFilter[Doc] {
def getJson: Json = Str(expression)
}

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)
}
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/scala/lightdb/aggregate/AggregateFunction.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ case class AggregateFunction[T, V, Doc <: Document[Doc]](name: String, field: Fi

override def !==(value: V): AggregateFilter[Doc] = AggregateFilter.NotEquals(name, field, value)

override def regex(expression: String): AggregateFilter[Doc] = AggregateFilter.Regex(name, field, expression)

override protected def rangeLong(from: Option[Long], to: Option[Long]): AggregateFilter[Doc] =
AggregateFilter.RangeLong(name, field.asInstanceOf[Field[Doc, Long]], from, to)

Expand Down
4 changes: 4 additions & 0 deletions core/src/main/scala/lightdb/filter/Condition.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package lightdb.filter

import fabric.rw.RW

trait Condition

object Condition {
implicit val rw: RW[Condition] = RW.enumeration(List(Must, MustNot, Filter, Should))

case object Must extends Condition
case object MustNot extends Condition
case object Filter extends Condition
Expand Down
12 changes: 11 additions & 1 deletion core/src/main/scala/lightdb/filter/Filter.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package lightdb.filter

import fabric.Json
import fabric.{Json, Str}
import lightdb.Field
import lightdb.doc.{Document, DocumentModel}
import lightdb.spatial.GeoPoint
Expand Down Expand Up @@ -36,6 +36,16 @@ object Filter {
def apply[Doc <: Document[Doc], F](field: Field[Doc, F], value: F): NotEquals[Doc, F] = NotEquals(field.name, value)
}

case class Regex[Doc <: Document[Doc], F](fieldName: String, expression: String) extends Filter[Doc] {
def getJson(model: DocumentModel[Doc]): Json = Str(expression)
def field(model: DocumentModel[Doc]): Field[Doc, F] = model.fieldByName(fieldName)

override lazy val fieldNames: List[String] = List(fieldName)
}
object Regex {
def apply[Doc <: Document[Doc], F](field: Field[Doc, F], expression: String): Regex[Doc, F] = Regex(field.name, expression)
}

case class In[Doc <: Document[Doc], F](fieldName: String, values: Seq[F]) extends Filter[Doc] {
def getJson(model: DocumentModel[Doc]): List[Json] = values.toList.map(model.fieldByName[F](fieldName).rw.read)
def field(model: DocumentModel[Doc]): Field[Doc, F] = model.fieldByName(fieldName)
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/lightdb/filter/FilterSupport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ trait FilterSupport[F, Doc, Filter] {
def <(value: F)(implicit num: Numeric[F]): Filter = range(None, Some(value), includeTo = false)
def <=(value: F)(implicit num: Numeric[F]): Filter = range(None, Some(value))

def ~*(expression: String): Filter = regex(expression)
def regex(expression: String): Filter

def BETWEEN(tuple: (F, F))(implicit num: Numeric[F]): Filter = range(Some(tuple._1), Some(tuple._2))
def <=>(tuple: (F, F))(implicit num: Numeric[F]): Filter = range(Some(tuple._1), Some(tuple._2))

Expand Down
14 changes: 14 additions & 0 deletions core/src/test/scala/spec/AbstractBasicSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,20 @@ abstract class AbstractBasicSpec extends AnyWordSpec with Matchers { spec =>
people.map(_.name) should be(List("Veronica"))
}
}
"query name with regex match" in {
db.people.transaction { implicit transaction =>
val people = db.people.query.filter(_.name ~* "Han.+").toList
people.map(_.name) should be(List("Hanna"))
}
}
"query nicknames with regex match" in {
db.people.transaction { implicit transaction =>
val people = db.people.query
.filter(_.nicknames ~* ".+chy")
.toList
people.map(_.name) should be(List("Oscar"))
}
}
"query with single-value, multiple nicknames" in {
db.people.transaction { implicit transaction =>
val people = db.people.query
Expand Down
3 changes: 2 additions & 1 deletion lucene/src/main/scala/lightdb/lucene/LuceneStore.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import lightdb.store.{Conversion, Store, StoreManager, StoreMode}
import lightdb.transaction.Transaction
import lightdb.util.Aggregator
import org.apache.lucene.document.{DoubleField, DoublePoint, IntField, IntPoint, LatLonDocValuesField, LatLonPoint, LongField, LongPoint, NumericDocValuesField, SortedDocValuesField, SortedNumericDocValuesField, StoredField, StringField, TextField, Document => LuceneDocument, Field => LuceneField}
import org.apache.lucene.search.{BooleanClause, BooleanQuery, BoostQuery, FieldExistsQuery, IndexSearcher, MatchAllDocsQuery, ScoreDoc, SearcherFactory, SearcherManager, SortField, SortedNumericSortField, TermQuery, TopFieldCollector, TopFieldCollectorManager, TopFieldDocs, Query => LuceneQuery, Sort => LuceneSort}
import org.apache.lucene.search.{BooleanClause, BooleanQuery, BoostQuery, FieldExistsQuery, IndexSearcher, MatchAllDocsQuery, RegexpQuery, ScoreDoc, SearcherFactory, SearcherManager, SortField, SortedNumericSortField, TermQuery, TopFieldCollector, TopFieldCollectorManager, TopFieldDocs, Query => LuceneQuery, Sort => LuceneSort}
import org.apache.lucene.index.{StoredFields, Term}
import org.apache.lucene.queryparser.classic.QueryParser
import org.apache.lucene.util.BytesRef
Expand Down Expand Up @@ -242,6 +242,7 @@ class LuceneStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]](directory:
b.add(new MatchAllDocsQuery, BooleanClause.Occur.MUST)
b.add(exactQuery(f.field(collection.model), f.getJson(collection.model)), BooleanClause.Occur.MUST_NOT)
b.build()
case f: Filter.Regex[Doc, _] => new RegexpQuery(new Term(f.fieldName, f.expression))
case f: Filter.In[Doc, _] =>
val queries = f.getJson(collection.model).map(json => exactQuery(f.field(collection.model), json))
val b = new BooleanQuery.Builder
Expand Down
2 changes: 2 additions & 0 deletions sql/src/main/scala/lightdb/sql/SQLStore.scala
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ abstract class SQLStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]] exten
SQLPart.merge(parts: _*)
case f: Filter.NotEquals[Doc, _] if f.value == null | f.value == None => SQLPart(s"${f.fieldName} IS NOT NULL")
case f: Filter.NotEquals[Doc, _] => SQLPart(s"${f.fieldName} != ?", List(SQLArg.FieldArg(f.field(collection.model), f.value)))
case f: Filter.Regex[Doc, _] => SQLPart(s"${f.fieldName} REGEXP ?", List(SQLArg.StringArg(f.expression)))
case f: Filter.In[Doc, _] => SQLPart(s"${f.fieldName} IN (${f.values.map(_ => "?").mkString(", ")})", f.values.toList.map(v => SQLArg.FieldArg(f.field(collection.model), v)))
case f: Filter.RangeLong[Doc] => (f.from, f.to) match {
case (Some(from), Some(to)) => SQLPart(s"${f.fieldName} BETWEEN ? AND ?", List(SQLArg.LongArg(from), SQLArg.LongArg(to)))
Expand Down Expand Up @@ -557,6 +558,7 @@ abstract class SQLStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]] exten
private def af2Part(f: AggregateFilter[Doc]): SQLPart = f match {
case f: AggregateFilter.Equals[Doc, _] => SQLPart(s"${f.name} = ?", List(SQLArg.FieldArg(f.field, f.value)))
case f: AggregateFilter.NotEquals[Doc, _] => SQLPart(s"${f.name} != ?", List(SQLArg.FieldArg(f.field, f.value)))
case f: AggregateFilter.Regex[Doc, _] => SQLPart(s"${f.name} REGEXP ?", List(SQLArg.StringArg(f.expression)))
case f: AggregateFilter.In[Doc, _] => SQLPart(s"${f.name} IN (${f.values.map(_ => "?").mkString(", ")})", f.values.toList.map(v => SQLArg.FieldArg(f.field, v)))
case f: AggregateFilter.Combined[Doc] =>
val parts = f.filters.map(f => af2Part(f))
Expand Down
11 changes: 10 additions & 1 deletion sqlite/src/main/scala/lightdb/sql/SQLiteStore.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import org.sqlite.SQLiteConfig.{JournalMode, LockingMode, SynchronousMode, Trans
import java.io.File
import java.nio.file.{Files, Path, StandardCopyOption}
import java.sql.Connection
import java.util.regex.Pattern

class SQLiteStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]](val connectionManager: ConnectionManager,
val connectionShared: Boolean,
Expand All @@ -24,15 +25,23 @@ class SQLiteStore[Doc <: Document[Doc], Model <: DocumentModel[Doc]](val connect
private val OptPointRegex = """\[POINT\((.+) (.+)\)\]""".r

override protected def initTransaction()(implicit transaction: Transaction[Doc]): Unit = {
val c = connectionManager.getConnection
if (hasSpatial) {
scribe.info(s"${collection.name} has spatial features. Enabling...")
val c = connectionManager.getConnection
val s = c.createStatement()
s.executeUpdate(s"SELECT load_extension('${SQLiteStore.spatialitePath}');")
val hasGeometryColumns = this.tables(c).contains("geometry_columns")
if (!hasGeometryColumns) s.executeUpdate("SELECT InitSpatialMetaData()")
s.close()
}
org.sqlite.Function.create(c, "REGEXP", new org.sqlite.Function() {
override def xFunc(): Unit = {
val expression = value_text(0)
val value = Option(value_text(1)).getOrElse("")
val pattern = Pattern.compile(expression)
result(if (pattern.matcher(value).find()) 1 else 0)
}
})
super.initTransaction()
}

Expand Down

0 comments on commit c841289

Please sign in to comment.