Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ ThisBuild / organization := "app.softnetwork"

name := "softclient4es"

ThisBuild / version := "0.7.0"
ThisBuild / version := "0.8.0"

ThisBuild / scalaVersion := scala213

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2319,4 +2319,103 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
.replaceAll("\\(double\\)(\\d)", "(double) $1")
}

it should "handle string function as script field and condition" in {
val select: ElasticSearchRequest =
SQLQuery(string)
val query = select.query
println(query)
query shouldBe
"""{
| "query": {
| "bool": {
| "filter": [
| {
| "script": {
| "script": {
| "lang": "painless",
| "source": "def left = (def e1 = (def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.trim() : null); e1 != null ? e1.length() : null); left == null ? false : left > 10"
| }
| }
| }
| ]
| }
| },
| "script_fields": {
| "len": {
| "script": {
| "lang": "painless",
| "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.length() : null)"
| }
| },
| "lower": {
| "script": {
| "lang": "painless",
| "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.lower() : null)"
| }
| },
| "upper": {
| "script": {
| "lang": "painless",
| "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.upper() : null)"
| }
| },
| "substr": {
| "script": {
| "lang": "painless",
| "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : ((1 - 1) < 0 || (1 - 1 + 3) > arg0.length()) ? null : arg0.substring((1 - 1), (1 - 1 + 3)))"
| }
| },
| "trim": {
| "script": {
| "lang": "painless",
| "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.trim() : null)"
| }
| },
| "concat": {
| "script": {
| "lang": "painless",
| "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : String.valueOf(arg0) + \"_test\" + String.valueOf(1))"
| }
| }
| },
| "_source": {
| "includes": [
| "identifier"
| ]
| }
|}""".stripMargin
.replaceAll("\\s+", "")
.replaceAll("defv", " def v")
.replaceAll("defa", "def a")
.replaceAll("defe", "def e")
.replaceAll("defl", "def l")
.replaceAll("def_", "def _")
.replaceAll("=_", " = _")
.replaceAll(",_", ", _")
.replaceAll(",\\(", ", (")
.replaceAll("if\\(", "if (")
.replaceAll("=\\(", " = (")
.replaceAll(":\\(", " : (")
.replaceAll(":0", " : 0")
.replaceAll(",(\\d)", ", $1")
.replaceAll("\\?", " ? ")
.replaceAll(":null", " : null")
.replaceAll("null:", "null : ")
.replaceAll("return", " return ")
.replaceAll(";", "; ")
.replaceAll("; if", ";if")
.replaceAll("==", " == ")
.replaceAll("\\+", " + ")
.replaceAll("-", " - ")
.replaceAll("\\*", " * ")
.replaceAll("/", " / ")
.replaceAll(">", " > ")
.replaceAll("<", " < ")
.replaceAll("!=", " != ")
.replaceAll("&&", " && ")
.replaceAll("\\|\\|", " || ")
.replaceAll(";\\s\\s", "; ")
.replaceAll("false:", "false : ")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2308,4 +2308,103 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
.replaceAll("\\(double\\)(\\d)", "(double) $1")
}

it should "handle string function as script field and condition" in {
val select: ElasticSearchRequest =
SQLQuery(string)
val query = select.query
println(query)
query shouldBe
"""{
| "query": {
| "bool": {
| "filter": [
| {
| "script": {
| "script": {
| "lang": "painless",
| "source": "def left = (def e1 = (def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.trim() : null); e1 != null ? e1.length() : null); left == null ? false : left > 10"
| }
| }
| }
| ]
| }
| },
| "script_fields": {
| "len": {
| "script": {
| "lang": "painless",
| "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.length() : null)"
| }
| },
| "lower": {
| "script": {
| "lang": "painless",
| "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.lower() : null)"
| }
| },
| "upper": {
| "script": {
| "lang": "painless",
| "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.upper() : null)"
| }
| },
| "substr": {
| "script": {
| "lang": "painless",
| "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : ((1 - 1) < 0 || (1 - 1 + 3) > arg0.length()) ? null : arg0.substring((1 - 1), (1 - 1 + 3)))"
| }
| },
| "trim": {
| "script": {
| "lang": "painless",
| "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.trim() : null)"
| }
| },
| "concat": {
| "script": {
| "lang": "painless",
| "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : String.valueOf(arg0) + \"_test\" + String.valueOf(1))"
| }
| }
| },
| "_source": {
| "includes": [
| "identifier"
| ]
| }
|}""".stripMargin
.replaceAll("\\s+", "")
.replaceAll("defv", " def v")
.replaceAll("defa", "def a")
.replaceAll("defe", "def e")
.replaceAll("defl", "def l")
.replaceAll("def_", "def _")
.replaceAll("=_", " = _")
.replaceAll(",_", ", _")
.replaceAll(",\\(", ", (")
.replaceAll("if\\(", "if (")
.replaceAll("=\\(", " = (")
.replaceAll(":\\(", " : (")
.replaceAll(":0", " : 0")
.replaceAll(",(\\d)", ", $1")
.replaceAll("\\?", " ? ")
.replaceAll(":null", " : null")
.replaceAll("null:", "null : ")
.replaceAll("return", " return ")
.replaceAll(";", "; ")
.replaceAll("; if", ";if")
.replaceAll("==", " == ")
.replaceAll("\\+", " + ")
.replaceAll("-", " - ")
.replaceAll("\\*", " * ")
.replaceAll("/", " / ")
.replaceAll(">", " > ")
.replaceAll("<", " < ")
.replaceAll("!=", " != ")
.replaceAll("&&", " && ")
.replaceAll("\\|\\|", " || ")
.replaceAll(";\\s\\s", "; ")
.replaceAll("false:", "false : ")
}

}
95 changes: 95 additions & 0 deletions sql/src/main/scala/app/softnetwork/elastic/sql/SQLFunction.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1025,3 +1025,98 @@ case class SQLAtan2(y: PainlessScript, x: PainlessScript) extends MathematicalFu
override def args: List[PainlessScript] = List(y, x)
override def nullable: Boolean = y.nullable || x.nullable
}

sealed trait StringFunction[Out <: SQLType]
extends SQLTransformFunction[SQLVarchar, Out]
with SQLFunctionWithIdentifier {
override def inputType: SQLVarchar = SQLTypes.Varchar

override def outputType: Out

def operator: SQLStringOperator

override def fun: Option[PainlessScript] = Some(operator)

override def identifier: SQLIdentifier = SQLIdentifier("", functions = this :: Nil)

override def toSQL(base: String): String = s"$sql($base)"

override def sql: String =
if (args.isEmpty)
s"${fun.map(_.sql).getOrElse("")}"
else
super.sql
}

case class SQLStringFunction(operator: SQLStringOperator) extends StringFunction[SQLVarchar] {
override def outputType: SQLVarchar = SQLTypes.Varchar
override def args: List[PainlessScript] = List.empty

}

case class SQLSubstring(str: PainlessScript, start: Int, length: Option[Int])
extends StringFunction[SQLVarchar] {
override def outputType: SQLVarchar = SQLTypes.Varchar
override def operator: SQLStringOperator = Substring

override def args: List[PainlessScript] =
List(str, SQLIntValue(start)) ++ length.map(l => SQLIntValue(l)).toList

override def nullable: Boolean = str.nullable

override def toPainlessCall(callArgs: List[String]): String = {
callArgs match {
// SUBSTRING(expr, start, length)
case List(arg0, arg1, arg2) =>
s"(($arg1 - 1) < 0 || ($arg1 - 1 + $arg2) > $arg0.length()) ? null : $arg0.substring(($arg1 - 1), ($arg1 - 1 + $arg2))"

// SUBSTRING(expr, start)
case List(arg0, arg1) =>
s"(($arg1 - 1) < 0 || ($arg1 - 1) >= $arg0.length()) ? null : $arg0.substring(($arg1 - 1))"

case _ => throw new IllegalArgumentException("SUBSTRING requires 2 or 3 arguments")
}
}

override def validate(): Either[String, Unit] =
if (start < 1)
Left("SUBSTRING start position must be greater than or equal to 1 (SQL is 1-based)")
else if (length.exists(_ < 0))
Left("SUBSTRING length must be non-negative")
else Right(())

override def toSQL(base: String): String = sql

}

case class SQLConcat(values: List[PainlessScript]) extends StringFunction[SQLVarchar] {
override def outputType: SQLVarchar = SQLTypes.Varchar
override def operator: SQLStringOperator = Concat

override def args: List[PainlessScript] = values

override def nullable: Boolean = values.exists(_.nullable)

override def toPainlessCall(callArgs: List[String]): String = {
if (callArgs.isEmpty)
throw new IllegalArgumentException("CONCAT requires at least one argument")
else
callArgs.zipWithIndex
.map { case (arg, idx) =>
SQLTypeUtils.coerce(arg, values(idx).out, SQLTypes.Varchar, nullable = false)
}
.mkString(operator.painless)
}

override def validate(): Either[String, Unit] =
if (values.isEmpty) Left("CONCAT requires at least one argument")
else Right(())

override def toSQL(base: String): String = sql
}

case object SQLLength extends StringFunction[SQLBigInt] {
override def outputType: SQLBigInt = SQLTypes.BigInt
override def operator: SQLStringOperator = Length
override def args: List[PainlessScript] = List.empty
}
17 changes: 17 additions & 0 deletions sql/src/main/scala/app/softnetwork/elastic/sql/SQLOperator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,23 @@ case object IsNotNull extends SQLExpr("is not null") with SQLComparisonOperator
case object Match extends SQLExpr("match") with SQLComparisonOperator
case object Against extends SQLExpr("against") with SQLRegex

sealed trait SQLStringOperator extends SQLOperator {
override def painless: String = s".${sql.toLowerCase()}()"
}
case object Concat extends SQLExpr("concat") with SQLStringOperator {
override def painless: String = " + "
}
case object Lower extends SQLExpr("lower") with SQLStringOperator
case object Upper extends SQLExpr("upper") with SQLStringOperator
case object Trim extends SQLExpr("trim") with SQLStringOperator
//case object LTrim extends SQLExpr("ltrim") with SQLStringOperator
//case object RTrim extends SQLExpr("rtrim") with SQLStringOperator
case object Substring extends SQLExpr("substring") with SQLStringOperator {
override def painless: String = ".substring"
}
case object To extends SQLExpr("to") with SQLRegex
case object Length extends SQLExpr("length") with SQLStringOperator

sealed trait SQLLogicalOperator extends SQLExpressionOperator

case object Not extends SQLExpr("not") with SQLLogicalOperator
Expand Down
Loading