Skip to content

Commit

Permalink
Handle Option[x] values in FieldPredicate for SQL
Browse files Browse the repository at this point in the history
  • Loading branch information
alekslitvinenk committed Dec 18, 2023
1 parent 266d39d commit 84baa35
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 44 deletions.
16 changes: 4 additions & 12 deletions src/main/scala/io/dockovpn/metastore/db/Queries.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

package io.dockovpn.metastore.db

import io.dockovpn.metastore.util.Strings.toCamelCase
import io.dockovpn.metastore.util.{FieldPredicate, Predicate}
import io.dockovpn.metastore.util.Sql.TableSchema
import io.dockovpn.metastore.util.{Predicate, Sql}
import slick.jdbc.GetResult
import slick.jdbc.MySQLProfile.api._
import slick.sql.SqlStreamingAction
Expand All @@ -18,8 +18,8 @@ object Queries {
|WHERE #$field = $k
|""".stripMargin.as[V]

def filter[V](predicate: Predicate, table: String)(implicit rconv: GetResult[V]): SqlStreamingAction[Vector[V], V, Effect] = {
val sqlPredicate = predicateToSql(predicate)
def filter[V](predicate: Predicate, table: String, schema: TableSchema)(implicit rconv: GetResult[V]): SqlStreamingAction[Vector[V], V, Effect] = {
val sqlPredicate = Sql.predicateToSql(predicate, schema)

sql"""SELECT * FROM #$table
|WHERE #$sqlPredicate
Expand Down Expand Up @@ -53,12 +53,4 @@ object Queries {
sql"""SELECT * FROM #$table
|""".stripMargin.as[V]
}

// TODO: Implement Predicate -> SQL materializer
private def predicateToSql(predicate: Predicate): String = {
predicate match {
case FieldPredicate(field, _, value) => s"${toCamelCase(field)} = '$value'"
case _ => "1=1" // not implemented
}
}
}
41 changes: 12 additions & 29 deletions src/main/scala/io/dockovpn/metastore/store/DBStore.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ package io.dockovpn.metastore.store

import io.dockovpn.metastore.db.{DBRef, Queries}
import io.dockovpn.metastore.provider.AbstractTableMetadataProvider
import io.dockovpn.metastore.util.Sql.TableSchema
import io.dockovpn.metastore.util.Strings.toCamelCase
import io.dockovpn.metastore.util.{Predicate, Types}
import io.dockovpn.metastore.util.{Predicate, Sql, Types}

import scala.concurrent.{ExecutionContext, Future}
import scala.reflect.ClassTag
Expand All @@ -23,8 +24,6 @@ class DBStore[V <: Product](implicit ec: ExecutionContext,
tag: ClassTag[V],
dbRef: => DBRef) extends AbstractStore[V] {

private type TableSchema = Map[String, String]

private val tableMetadata = metadataProvider.getTableMetadata[V]

private lazy val tableSchemaFuture = {
Expand All @@ -35,18 +34,14 @@ class DBStore[V <: Product](implicit ec: ExecutionContext,

import tableMetadata.implicits._

override def filter(predicate: Predicate): Future[Seq[V]] = dbRef.run(
Queries.filter(predicate, tableMetadata.tableName)
).map(_.asInstanceOf[Vector[V]])

override def contains(k: String): Future[Boolean] = get(k).map(_.nonEmpty)

override def get(k: String): Future[Option[V]] =
dbRef.run(
Queries.getRecordsByKey(k, tableMetadata.tableName, tableMetadata.fieldName)
).map(_.headOption.asInstanceOf[Option[V]])

override def put(k: String, v: V): Future[Unit] = {
override def put(k: String, v: V): Future[Unit] =
withSchema { schema =>
val fieldToValueMap = getColumnNameToSqlValueMap(v, schema)
val keys = fieldToValueMap.keys.toSeq
Expand All @@ -56,7 +51,6 @@ class DBStore[V <: Product](implicit ec: ExecutionContext,
Queries.insertRecordIntoTable(keys, values, tableMetadata.tableName)
).map(_ => ())
}
}

override def update(k: String, v: V): Future[Unit] = {
withSchema { schema =>
Expand All @@ -70,6 +64,13 @@ class DBStore[V <: Product](implicit ec: ExecutionContext,
}
}

override def filter(predicate: Predicate): Future[Seq[V]] =
withSchema { schema =>
dbRef.run(
Queries.filter(predicate, tableMetadata.tableName, schema)
).map(_.asInstanceOf[Vector[V]])
}

override def getAll(): Future[Seq[V]] =
dbRef.run(
Queries.getAllRecords(tableMetadata.tableName)
Expand All @@ -82,29 +83,11 @@ class DBStore[V <: Product](implicit ec: ExecutionContext,
val kvt = (kv._1, kv._2, schema(kv._1))
val (columnName, maybeOptValue, sqlType) = kvt

// unwrap Option if any
val maybeNullValue = maybeOptValue match {
case option: Option[Any] =>
option.orNull
case _ => maybeOptValue
}

// decide which values need to be wrapped in single quotes
val maybeQuotedValue = sqlType match {
case a if a startsWith "varchar" => s"'$maybeNullValue'"
case "tinyint(1)" => if (maybeNullValue.asInstanceOf[Boolean]) 1 else 0
case "timestamp" => s"'$maybeNullValue'"
case "uuid" => s"'$maybeNullValue'"
case _ => maybeNullValue
}

val sqlValue = if (maybeQuotedValue == "'null'")
"NULL"
else maybeQuotedValue
val sqlValue = Sql.valueToSqlType(maybeOptValue, sqlType)

(columnName, sqlValue)
}

// TODO: consider using fast Future from Akka to avoid scheduling of a Future which result is already available
private def withSchema(f: TableSchema => Future[Unit]): Future[Unit] = tableSchemaFuture.flatMap(f)
private def withSchema[R](f: TableSchema => Future[R]): Future[R] = tableSchemaFuture.flatMap(f)
}
46 changes: 46 additions & 0 deletions src/main/scala/io/dockovpn/metastore/util/Sql.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.dockovpn.metastore.util

import io.dockovpn.metastore.util.Strings.toCamelCase

object Sql {

type TableSchema = Map[String, String]

def valueToSqlType(maybeOptValue: Any, sqlType: String): Any = {
// unwrap Option if any
val maybeNullValue = maybeOptValue match {
case option: Option[Any] =>
option.orNull
case _ => maybeOptValue
}

// decide which values need to be wrapped in single quotes
val maybeQuotedValue = sqlType match {
case a if a startsWith "varchar" => s"'$maybeNullValue'"
case "tinyint(1)" => if (maybeNullValue.asInstanceOf[Boolean]) 1 else 0
case "timestamp" => s"'$maybeNullValue'"
case "uuid" => s"'$maybeNullValue'"
case _ => maybeNullValue
}

val sqlValue = if (maybeQuotedValue == "'null'" || maybeQuotedValue == null)
"NULL"
else maybeQuotedValue

sqlValue
}

def predicateToSql(predicate: Predicate, schema: TableSchema): String = {
predicate match {
case FieldPredicate(field, _, value) =>
val columnName = toCamelCase(field)
val sqlType = schema(columnName)
val sqlValue = valueToSqlType(value, sqlType)
if (sqlValue == "NULL")
s"$columnName IS NULL"
else
s"$columnName = $sqlValue"
case _ => "1=1" // not implemented
}
}
}
6 changes: 3 additions & 3 deletions src/test/scala/io/dockovpn/metastore/store/DBStoreSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ class DBStoreSpec extends AnyWordSpec
val fieldValue = Some(1)
val value = OptIntRecord(id = key, value = fieldValue)
val opResult: Unit = testStore.put(key, value).futureValue
opResult should be (())
opResult should be(())

val getResult = testStore.filter(FieldPredicate("value", "=", fieldValue)).futureValue
getResult should be(Seq(value))
Expand All @@ -356,7 +356,7 @@ class DBStoreSpec extends AnyWordSpec
val fieldValue = None
val value = OptIntRecord(id = key, value = fieldValue)
val opResult: Unit = testStore.put(key, value).futureValue
opResult should be (())
opResult should be(())

val getResult = testStore.filter(FieldPredicate("value", "=", fieldValue)).futureValue
getResult should be(Seq(value))
Expand Down Expand Up @@ -408,7 +408,7 @@ class DBStoreSpec extends AnyWordSpec
"value is OptTimestampRecord and filed is Some(_)" in {
val testStore: AbstractStore[OptTimestampRecord] = StoreProvider.getStoreByType(dbStoreType)
val key = "key"
val fieldValue = Some(Timestamp.from(Instant.now()))
val fieldValue = Some(Timestamp.from(baseInstant))
val value = OptTimestampRecord(id = key, value = fieldValue)
val opResult: Unit = testStore.put(key, value).futureValue
opResult should be (())
Expand Down

0 comments on commit 84baa35

Please sign in to comment.