Skip to content

Commit

Permalink
finos#1296 make FilterColumnValueParser independent of SQL use-case
Browse files Browse the repository at this point in the history
- it is now just a utility to convert column value (String) from ANTLR
  to external value (original external date type) that will then be used
  with external data source for filtering.
- In the process makes it responsible only for one thing i.e. parse column
  value (String) to external value's data type. Should not care about how
  we convert the resulting external value to a String understood by SQL.
  That logic's been moved back to IgniteSqlFilterClause.
- also renames SqlStringConverterContainer to less verbose ToSqlStringContainer.
  • Loading branch information
junaidzm13 committed Apr 30, 2024
1 parent 3dd67cf commit 3d6e70a
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 212 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package org.finos.vuu.feature.ignite

import org.finos.vuu.core.filter.FilterSpecParser
import org.finos.vuu.core.sort.ModelType.SortSpecInternal
import org.finos.vuu.feature.ignite.filter.{IgniteSqlFilterClause, IgniteSqlFilterTreeVisitor, SqlStringConverterContainer, SqlStringConverterContainerBuilder}
import org.finos.vuu.feature.ignite.filter.{IgniteSqlFilterClause, IgniteSqlFilterTreeVisitor, ToSqlStringContainer, ToSqlStringContainerBuilder}
import org.finos.vuu.feature.ignite.sort.IgniteSqlSortBuilder
import org.finos.vuu.net.FilterSpec
import org.finos.vuu.util.schema.SchemaMapper
Expand All @@ -14,16 +14,16 @@ trait FilterAndSortSpecToSql {

object FilterAndSortSpecToSql {
def apply(schemaMapper: SchemaMapper): FilterAndSortSpecToSql = {
new FilterAndSortSpecToSqlImpl(schemaMapper, SqlStringConverterContainerBuilder().build())
new FilterAndSortSpecToSqlImpl(schemaMapper, ToSqlStringContainerBuilder().build())
}

def apply(schemaMapper: SchemaMapper, toSqlStringContainer: SqlStringConverterContainer): FilterAndSortSpecToSql = {
def apply(schemaMapper: SchemaMapper, toSqlStringContainer: ToSqlStringContainer): FilterAndSortSpecToSql = {
new FilterAndSortSpecToSqlImpl(schemaMapper, toSqlStringContainer)
}
}

private class FilterAndSortSpecToSqlImpl(private val schemaMapper: SchemaMapper,
private val toSqlStringContainer: SqlStringConverterContainer) extends FilterAndSortSpecToSql {
private val toSqlStringContainer: ToSqlStringContainer) extends FilterAndSortSpecToSql {
// @Todo convert IgniteSqlFilterTreeVisitor & IgniteSqlSortBuilder to objects?
private val filterTreeVisitor = new IgniteSqlFilterTreeVisitor
private val igniteSqlSortBuilder = new IgniteSqlSortBuilder
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.finos.vuu.feature.ignite.filter

import com.typesafe.scalalogging.StrictLogging
import org.finos.vuu.core.table.{Column, DataType}
import org.finos.vuu.feature.ignite.filter.FilterColumnValueParser.{ErrorMessage, ParsedResult}
import org.finos.vuu.util.schema.{SchemaField, SchemaMapper}

protected trait FilterColumnValueParser {
def parse(columnName: String, columnValue: String): Either[ErrorMessage, ParsedResult[Any]]
def parse(columnName: String, columnValues: List[String]): Either[ErrorMessage, ParsedResult[List[Any]]]
}

protected object FilterColumnValueParser {
def apply(schemaMapper: SchemaMapper): FilterColumnValueParser = {
new ColumnValueParser(schemaMapper)
}

case class ParsedResult[T](externalField: SchemaField, externalData: T)

type ErrorMessage = String

val STRING_DATA_TYPE: Class[String] = classOf[String]
}

private class ColumnValueParser(private val mapper: SchemaMapper) extends FilterColumnValueParser with StrictLogging {

override def parse(columnName: String, columnValue: String): Either[ErrorMessage, ParsedResult[Any]] = {
mapper.externalSchemaField(columnName) match {
case Some(f) => RawColumnValueParser(f).parse(columnValue).map(ParsedResult(f, _))
case None => Left(externalFieldNotFoundError(columnName))
}
}

override def parse(columnName: String, columnValues: List[String]): Either[ErrorMessage, ParsedResult[List[Any]]] = {
mapper.externalSchemaField(columnName) match {
case Some(f) => parseValues(RawColumnValueParser(f), columnValues)
case None => Left(externalFieldNotFoundError(columnName))
}
}

private def parseValues(parser: RawColumnValueParser,
columnValues: List[String]): Either[ErrorMessage, ParsedResult[List[Any]]] = {
val (errors, parsedValues) = columnValues.partitionMap(parser.parse)
val combinedError = errors.mkString("\n")

if (parsedValues.isEmpty) {
Left(combinedError)
} else {
if (errors.nonEmpty) logger.error(
s"Failed to parse some of the column values corresponding to the column ${parser.column.name}: \n $combinedError"
)
Right(ParsedResult(parser.field, parsedValues))
}
}

private def externalFieldNotFoundError(columnName: String): String =
s"Failed to find mapped external field for column `$columnName`"

private case class RawColumnValueParser(field: SchemaField) {
val column: Column = mapper.tableColumn(field.name).get

def parse(columnValue: String): Either[ErrorMessage, Any] = {
parseStringToColumnDataType(columnValue).flatMap(convertColumnValueToExternalFieldType)
}

private def parseStringToColumnDataType(value: String): Either[ErrorMessage, Any] =
DataType.parseToDataType(value, column.dataType)

private def convertColumnValueToExternalFieldType(columnValue: Any): Either[ErrorMessage, Any] =
mapper.toMappedExternalFieldType(column.name, columnValue)
.toRight(s"Failed to convert column value `$columnValue` from `${column.dataType}` to external type `${field.dataType}`")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,70 +2,82 @@ package org.finos.vuu.feature.ignite.filter

import com.typesafe.scalalogging.StrictLogging
import org.finos.vuu.feature.ignite.filter.IgniteSqlFilterClause.EMPTY_SQL
import org.finos.vuu.feature.ignite.filter.SqlFilterColumnValueParser.{ParsedResult, STRING_DATA_TYPE}
import org.finos.vuu.feature.ignite.filter.FilterColumnValueParser.{ParsedResult, STRING_DATA_TYPE}
import org.finos.vuu.util.schema.{SchemaField, SchemaMapper}
import org.finos.vuu.util.types.TypeUtils

private object IgniteSqlFilterClause {
val EMPTY_SQL = ""
}

trait IgniteSqlFilterClause {
def toSql(schemaMapper: SchemaMapper, toStringContainer: SqlStringConverterContainer): String
def toSql(schemaMapper: SchemaMapper, toStringContainer: ToSqlStringContainer): String
}

case class OrIgniteSqlFilterClause(clauses:List[IgniteSqlFilterClause]) extends IgniteSqlFilterClause {
override def toSql(schemaMapper: SchemaMapper, toStringContainer: SqlStringConverterContainer): String = {
override def toSql(schemaMapper: SchemaMapper, toStringContainer: ToSqlStringContainer): String = {
val sql = clauses.map(c => c.toSql(schemaMapper, toStringContainer)).filter(_ != EMPTY_SQL).mkString(" OR ")
if (clauses.length > 1) s"($sql)" else sql
}
}

case class AndIgniteSqlFilterClause(clauses:List[IgniteSqlFilterClause]) extends IgniteSqlFilterClause {
override def toSql(schemaMapper: SchemaMapper, toStringContainer: SqlStringConverterContainer): String = {
override def toSql(schemaMapper: SchemaMapper, toStringContainer: ToSqlStringContainer): String = {
val sql = clauses.map(c => c.toSql(schemaMapper, toStringContainer)).filter(_ != EMPTY_SQL).mkString(" AND ")
if (clauses.length > 1) s"($sql)" else sql
}
}

case class EqIgniteSqlFilterClause(columnName: String, value: String) extends IgniteSqlFilterClause {
override def toSql(schemaMapper: SchemaMapper, toStringContainer: SqlStringConverterContainer): String =
SqlFilterColumnValueParser(schemaMapper, toStringContainer).parseColumnValue(columnName, value) match {
case Right(ParsedResult(f, parsedValue)) => eqSql(f.name, parsedValue)
override def toSql(schemaMapper: SchemaMapper, toStringContainer: ToSqlStringContainer): String = {
def toString = getStringConverter(toStringContainer)

FilterColumnValueParser(schemaMapper).parse(columnName, value) match {
case Right(ParsedResult(f, externalValue)) => eqSql(f.name, toString(externalValue, f.dataType))
case Left(errMsg) => logErrorAndReturnEmptySql(errMsg)
}
}

private def eqSql(field: String, processedVal: String): String = {
s"$field = $processedVal"
}
}

case class NeqIgniteSqlFilterClause(columnName: String, value: String) extends IgniteSqlFilterClause {
override def toSql(schemaMapper: SchemaMapper, toStringContainer: SqlStringConverterContainer): String =
SqlFilterColumnValueParser(schemaMapper, toStringContainer).parseColumnValue(columnName, value) match {
case Right(ParsedResult(f, parsedValue)) => neqSql(f.name, parsedValue)
override def toSql(schemaMapper: SchemaMapper, toStringContainer: ToSqlStringContainer): String = {
def toString = getStringConverter(toStringContainer)

FilterColumnValueParser(schemaMapper).parse(columnName, value) match {
case Right(ParsedResult(f, externalValue)) => neqSql(f.name, toString(externalValue, f.dataType))
case Left(errMsg) => logErrorAndReturnEmptySql(errMsg)
}
}

private def neqSql(field: String, processedVal: String): String = {
s"$field != $processedVal"
}
}

case class RangeIgniteSqlFilterClause(op: RangeOp)(columnName: String, value: String) extends IgniteSqlFilterClause {
override def toSql(schemaMapper: SchemaMapper, toStringContainer: SqlStringConverterContainer): String =
SqlFilterColumnValueParser(schemaMapper, toStringContainer).parseColumnValue(columnName, value) match {
case Right(ParsedResult(f, parsedValue)) => rangeSql(f.name, parsedValue)
override def toSql(schemaMapper: SchemaMapper, toStringContainer: ToSqlStringContainer): String = {
def toString = getStringConverter(toStringContainer)

FilterColumnValueParser(schemaMapper).parse(columnName, value) match {
case Right(ParsedResult(f, externalValue)) => rangeSql(f.name, toString(externalValue, f.dataType))
case Left(errMsg) => logErrorAndReturnEmptySql(errMsg)
}
}

private def rangeSql(field: String, processedVal: String): String = s"$field ${op.value} $processedVal"
override def toString = s"RangeIgniteSqlFilterClause[$op]($columnName, $value)"
}

case class StartsIgniteSqlFilterClause(columnName: String, value: String) extends IgniteSqlFilterClause with StrictLogging {
override def toSql(schemaMapper: SchemaMapper, toStringContainer: SqlStringConverterContainer): String = {
SqlFilterColumnValueParser(schemaMapper, toStringContainer).parseColumnValue(columnName, value) match {
case Right(ParsedResult(f, parsedValue)) => startsSql(f, parsedValue)
override def toSql(schemaMapper: SchemaMapper, toStringContainer: ToSqlStringContainer): String = {
def toString = getStringConverter(toStringContainer)

FilterColumnValueParser(schemaMapper).parse(columnName, value) match {
case Right(ParsedResult(f, externalValue)) => startsSql(f, toString(externalValue, f.dataType))
case Left(errMsg) => logErrorAndReturnEmptySql(errMsg)
}
}
Expand All @@ -77,9 +89,11 @@ case class StartsIgniteSqlFilterClause(columnName: String, value: String) extend
}

case class EndsIgniteSqlFilterClause(columnName: String, value: String) extends IgniteSqlFilterClause with StrictLogging {
override def toSql(schemaMapper: SchemaMapper, toStringContainer: SqlStringConverterContainer): String = {
SqlFilterColumnValueParser(schemaMapper, toStringContainer).parseColumnValue(columnName, value) match {
case Right(ParsedResult(f, parsedValue)) => endsSql(f, parsedValue)
override def toSql(schemaMapper: SchemaMapper, toStringContainer: ToSqlStringContainer): String = {
def toString = getStringConverter(toStringContainer)

FilterColumnValueParser(schemaMapper).parse(columnName, value) match {
case Right(ParsedResult(f, externalValue)) => endsSql(f, toString(externalValue, f.dataType))
case Left(errMsg) => logErrorAndReturnEmptySql(errMsg)
}
}
Expand All @@ -91,11 +105,14 @@ case class EndsIgniteSqlFilterClause(columnName: String, value: String) extends
}

case class InIgniteSqlFilterClause(columnName: String, values: List[String]) extends IgniteSqlFilterClause with StrictLogging {
override def toSql(schemaMapper: SchemaMapper, toStringContainer: SqlStringConverterContainer): String =
SqlFilterColumnValueParser(schemaMapper, toStringContainer).parseColumnValues(columnName, values) match {
case Right(ParsedResult(f, parsedValues)) => inQuery(f.name, parsedValues)
override def toSql(schemaMapper: SchemaMapper, toStringContainer: ToSqlStringContainer): String = {
def toString = getStringConverter(toStringContainer)

FilterColumnValueParser(schemaMapper).parse(columnName, values) match {
case Right(ParsedResult(f, externalValues)) => inQuery(f.name, externalValues.map(toString(_, f.dataType)))
case Left(errMsg) => logErrorAndReturnEmptySql(errMsg)
}
}

private def inQuery(field: String, processedValues: List[String]) = {
s"$field IN (${processedValues.mkString(",")})"
Expand All @@ -110,6 +127,37 @@ object RangeOp {
final case object LTE extends RangeOp(value = "<=")
}


private object getStringConverter {
def apply(toStringContainer: ToSqlStringContainer): (Any, Class[_]) => String = (value, dataType) =>
if (TypeUtils.areTypesEqual(dataType, STRING_DATA_TYPE)) {
quotedString(defaultToString(value))
} else {
toStringContainer
.toString(value, dataType.asInstanceOf[Class[Any]])
.getOrElse(addQuotesIfRequired(defaultToString(value), dataType))
}

private def defaultToString(value: Any): String = Option(value).map(_.toString).orNull
}

private object quotedString {
def apply(s: String) = s"'$s'"
}

private object addQuotesIfRequired {
def apply(v: String, dataType: Class[_]): String = if (requireQuotes(dataType)) quotedString(v) else v

private def requireQuotes(dt: Class[_]): Boolean = dataTypesRequiringQuotes.contains(dt)

private val dataTypesRequiringQuotes: Set[Class[_]] = Set(
classOf[String],
classOf[Char],
classOf[java.lang.Character],
classOf[java.sql.Date],
)
}

private object logErrorAndReturnEmptySql extends StrictLogging {
def apply(error: String): String = {
logger.error(error)
Expand Down

This file was deleted.

Loading

0 comments on commit 3d6e70a

Please sign in to comment.