Skip to content

Commit 5193c94

Browse files
authored
Merge pull request #18 from SOFTNETWORK-APP/feature/stringFunctions
add support for SQL string functions : - UPPER() - LOWER() - TRIM() - LENGTH() - CONCAT() - SUBSTRING()
2 parents 47032c1 + 1d2426b commit 5193c94

File tree

8 files changed

+380
-4
lines changed

8 files changed

+380
-4
lines changed

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ ThisBuild / organization := "app.softnetwork"
1919

2020
name := "softclient4es"
2121

22-
ThisBuild / version := "0.7.0"
22+
ThisBuild / version := "0.8.0"
2323

2424
ThisBuild / scalaVersion := scala213
2525

es6/sql-bridge/src/test/scala/app/softnetwork/elastic/sql/SQLQuerySpec.scala

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2319,4 +2319,103 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
23192319
.replaceAll("\\(double\\)(\\d)", "(double) $1")
23202320
}
23212321

2322+
it should "handle string function as script field and condition" in {
2323+
val select: ElasticSearchRequest =
2324+
SQLQuery(string)
2325+
val query = select.query
2326+
println(query)
2327+
query shouldBe
2328+
"""{
2329+
| "query": {
2330+
| "bool": {
2331+
| "filter": [
2332+
| {
2333+
| "script": {
2334+
| "script": {
2335+
| "lang": "painless",
2336+
| "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"
2337+
| }
2338+
| }
2339+
| }
2340+
| ]
2341+
| }
2342+
| },
2343+
| "script_fields": {
2344+
| "len": {
2345+
| "script": {
2346+
| "lang": "painless",
2347+
| "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.length() : null)"
2348+
| }
2349+
| },
2350+
| "lower": {
2351+
| "script": {
2352+
| "lang": "painless",
2353+
| "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.lower() : null)"
2354+
| }
2355+
| },
2356+
| "upper": {
2357+
| "script": {
2358+
| "lang": "painless",
2359+
| "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.upper() : null)"
2360+
| }
2361+
| },
2362+
| "substr": {
2363+
| "script": {
2364+
| "lang": "painless",
2365+
| "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)))"
2366+
| }
2367+
| },
2368+
| "trim": {
2369+
| "script": {
2370+
| "lang": "painless",
2371+
| "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.trim() : null)"
2372+
| }
2373+
| },
2374+
| "concat": {
2375+
| "script": {
2376+
| "lang": "painless",
2377+
| "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : String.valueOf(arg0) + \"_test\" + String.valueOf(1))"
2378+
| }
2379+
| }
2380+
| },
2381+
| "_source": {
2382+
| "includes": [
2383+
| "identifier"
2384+
| ]
2385+
| }
2386+
|}""".stripMargin
2387+
.replaceAll("\\s+", "")
2388+
.replaceAll("defv", " def v")
2389+
.replaceAll("defa", "def a")
2390+
.replaceAll("defe", "def e")
2391+
.replaceAll("defl", "def l")
2392+
.replaceAll("def_", "def _")
2393+
.replaceAll("=_", " = _")
2394+
.replaceAll(",_", ", _")
2395+
.replaceAll(",\\(", ", (")
2396+
.replaceAll("if\\(", "if (")
2397+
.replaceAll("=\\(", " = (")
2398+
.replaceAll(":\\(", " : (")
2399+
.replaceAll(":0", " : 0")
2400+
.replaceAll(",(\\d)", ", $1")
2401+
.replaceAll("\\?", " ? ")
2402+
.replaceAll(":null", " : null")
2403+
.replaceAll("null:", "null : ")
2404+
.replaceAll("return", " return ")
2405+
.replaceAll(";", "; ")
2406+
.replaceAll("; if", ";if")
2407+
.replaceAll("==", " == ")
2408+
.replaceAll("\\+", " + ")
2409+
.replaceAll("-", " - ")
2410+
.replaceAll("\\*", " * ")
2411+
.replaceAll("/", " / ")
2412+
.replaceAll(">", " > ")
2413+
.replaceAll("<", " < ")
2414+
.replaceAll("!=", " != ")
2415+
.replaceAll("&&", " && ")
2416+
.replaceAll("\\|\\|", " || ")
2417+
.replaceAll(";\\s\\s", "; ")
2418+
.replaceAll("false:", "false : ")
2419+
}
2420+
23222421
}

sql/bridge/src/test/scala/app/softnetwork/elastic/sql/SQLQuerySpec.scala

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2308,4 +2308,103 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
23082308
.replaceAll("\\(double\\)(\\d)", "(double) $1")
23092309
}
23102310

2311+
it should "handle string function as script field and condition" in {
2312+
val select: ElasticSearchRequest =
2313+
SQLQuery(string)
2314+
val query = select.query
2315+
println(query)
2316+
query shouldBe
2317+
"""{
2318+
| "query": {
2319+
| "bool": {
2320+
| "filter": [
2321+
| {
2322+
| "script": {
2323+
| "script": {
2324+
| "lang": "painless",
2325+
| "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"
2326+
| }
2327+
| }
2328+
| }
2329+
| ]
2330+
| }
2331+
| },
2332+
| "script_fields": {
2333+
| "len": {
2334+
| "script": {
2335+
| "lang": "painless",
2336+
| "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.length() : null)"
2337+
| }
2338+
| },
2339+
| "lower": {
2340+
| "script": {
2341+
| "lang": "painless",
2342+
| "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.lower() : null)"
2343+
| }
2344+
| },
2345+
| "upper": {
2346+
| "script": {
2347+
| "lang": "painless",
2348+
| "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.upper() : null)"
2349+
| }
2350+
| },
2351+
| "substr": {
2352+
| "script": {
2353+
| "lang": "painless",
2354+
| "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)))"
2355+
| }
2356+
| },
2357+
| "trim": {
2358+
| "script": {
2359+
| "lang": "painless",
2360+
| "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.trim() : null)"
2361+
| }
2362+
| },
2363+
| "concat": {
2364+
| "script": {
2365+
| "lang": "painless",
2366+
| "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : String.valueOf(arg0) + \"_test\" + String.valueOf(1))"
2367+
| }
2368+
| }
2369+
| },
2370+
| "_source": {
2371+
| "includes": [
2372+
| "identifier"
2373+
| ]
2374+
| }
2375+
|}""".stripMargin
2376+
.replaceAll("\\s+", "")
2377+
.replaceAll("defv", " def v")
2378+
.replaceAll("defa", "def a")
2379+
.replaceAll("defe", "def e")
2380+
.replaceAll("defl", "def l")
2381+
.replaceAll("def_", "def _")
2382+
.replaceAll("=_", " = _")
2383+
.replaceAll(",_", ", _")
2384+
.replaceAll(",\\(", ", (")
2385+
.replaceAll("if\\(", "if (")
2386+
.replaceAll("=\\(", " = (")
2387+
.replaceAll(":\\(", " : (")
2388+
.replaceAll(":0", " : 0")
2389+
.replaceAll(",(\\d)", ", $1")
2390+
.replaceAll("\\?", " ? ")
2391+
.replaceAll(":null", " : null")
2392+
.replaceAll("null:", "null : ")
2393+
.replaceAll("return", " return ")
2394+
.replaceAll(";", "; ")
2395+
.replaceAll("; if", ";if")
2396+
.replaceAll("==", " == ")
2397+
.replaceAll("\\+", " + ")
2398+
.replaceAll("-", " - ")
2399+
.replaceAll("\\*", " * ")
2400+
.replaceAll("/", " / ")
2401+
.replaceAll(">", " > ")
2402+
.replaceAll("<", " < ")
2403+
.replaceAll("!=", " != ")
2404+
.replaceAll("&&", " && ")
2405+
.replaceAll("\\|\\|", " || ")
2406+
.replaceAll(";\\s\\s", "; ")
2407+
.replaceAll("false:", "false : ")
2408+
}
2409+
23112410
}

sql/src/main/scala/app/softnetwork/elastic/sql/SQLFunction.scala

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,3 +1025,98 @@ case class SQLAtan2(y: PainlessScript, x: PainlessScript) extends MathematicalFu
10251025
override def args: List[PainlessScript] = List(y, x)
10261026
override def nullable: Boolean = y.nullable || x.nullable
10271027
}
1028+
1029+
sealed trait StringFunction[Out <: SQLType]
1030+
extends SQLTransformFunction[SQLVarchar, Out]
1031+
with SQLFunctionWithIdentifier {
1032+
override def inputType: SQLVarchar = SQLTypes.Varchar
1033+
1034+
override def outputType: Out
1035+
1036+
def operator: SQLStringOperator
1037+
1038+
override def fun: Option[PainlessScript] = Some(operator)
1039+
1040+
override def identifier: SQLIdentifier = SQLIdentifier("", functions = this :: Nil)
1041+
1042+
override def toSQL(base: String): String = s"$sql($base)"
1043+
1044+
override def sql: String =
1045+
if (args.isEmpty)
1046+
s"${fun.map(_.sql).getOrElse("")}"
1047+
else
1048+
super.sql
1049+
}
1050+
1051+
case class SQLStringFunction(operator: SQLStringOperator) extends StringFunction[SQLVarchar] {
1052+
override def outputType: SQLVarchar = SQLTypes.Varchar
1053+
override def args: List[PainlessScript] = List.empty
1054+
1055+
}
1056+
1057+
case class SQLSubstring(str: PainlessScript, start: Int, length: Option[Int])
1058+
extends StringFunction[SQLVarchar] {
1059+
override def outputType: SQLVarchar = SQLTypes.Varchar
1060+
override def operator: SQLStringOperator = Substring
1061+
1062+
override def args: List[PainlessScript] =
1063+
List(str, SQLIntValue(start)) ++ length.map(l => SQLIntValue(l)).toList
1064+
1065+
override def nullable: Boolean = str.nullable
1066+
1067+
override def toPainlessCall(callArgs: List[String]): String = {
1068+
callArgs match {
1069+
// SUBSTRING(expr, start, length)
1070+
case List(arg0, arg1, arg2) =>
1071+
s"(($arg1 - 1) < 0 || ($arg1 - 1 + $arg2) > $arg0.length()) ? null : $arg0.substring(($arg1 - 1), ($arg1 - 1 + $arg2))"
1072+
1073+
// SUBSTRING(expr, start)
1074+
case List(arg0, arg1) =>
1075+
s"(($arg1 - 1) < 0 || ($arg1 - 1) >= $arg0.length()) ? null : $arg0.substring(($arg1 - 1))"
1076+
1077+
case _ => throw new IllegalArgumentException("SUBSTRING requires 2 or 3 arguments")
1078+
}
1079+
}
1080+
1081+
override def validate(): Either[String, Unit] =
1082+
if (start < 1)
1083+
Left("SUBSTRING start position must be greater than or equal to 1 (SQL is 1-based)")
1084+
else if (length.exists(_ < 0))
1085+
Left("SUBSTRING length must be non-negative")
1086+
else Right(())
1087+
1088+
override def toSQL(base: String): String = sql
1089+
1090+
}
1091+
1092+
case class SQLConcat(values: List[PainlessScript]) extends StringFunction[SQLVarchar] {
1093+
override def outputType: SQLVarchar = SQLTypes.Varchar
1094+
override def operator: SQLStringOperator = Concat
1095+
1096+
override def args: List[PainlessScript] = values
1097+
1098+
override def nullable: Boolean = values.exists(_.nullable)
1099+
1100+
override def toPainlessCall(callArgs: List[String]): String = {
1101+
if (callArgs.isEmpty)
1102+
throw new IllegalArgumentException("CONCAT requires at least one argument")
1103+
else
1104+
callArgs.zipWithIndex
1105+
.map { case (arg, idx) =>
1106+
SQLTypeUtils.coerce(arg, values(idx).out, SQLTypes.Varchar, nullable = false)
1107+
}
1108+
.mkString(operator.painless)
1109+
}
1110+
1111+
override def validate(): Either[String, Unit] =
1112+
if (values.isEmpty) Left("CONCAT requires at least one argument")
1113+
else Right(())
1114+
1115+
override def toSQL(base: String): String = sql
1116+
}
1117+
1118+
case object SQLLength extends StringFunction[SQLBigInt] {
1119+
override def outputType: SQLBigInt = SQLTypes.BigInt
1120+
override def operator: SQLStringOperator = Length
1121+
override def args: List[PainlessScript] = List.empty
1122+
}

sql/src/main/scala/app/softnetwork/elastic/sql/SQLOperator.scala

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,23 @@ case object IsNotNull extends SQLExpr("is not null") with SQLComparisonOperator
9999
case object Match extends SQLExpr("match") with SQLComparisonOperator
100100
case object Against extends SQLExpr("against") with SQLRegex
101101

102+
sealed trait SQLStringOperator extends SQLOperator {
103+
override def painless: String = s".${sql.toLowerCase()}()"
104+
}
105+
case object Concat extends SQLExpr("concat") with SQLStringOperator {
106+
override def painless: String = " + "
107+
}
108+
case object Lower extends SQLExpr("lower") with SQLStringOperator
109+
case object Upper extends SQLExpr("upper") with SQLStringOperator
110+
case object Trim extends SQLExpr("trim") with SQLStringOperator
111+
//case object LTrim extends SQLExpr("ltrim") with SQLStringOperator
112+
//case object RTrim extends SQLExpr("rtrim") with SQLStringOperator
113+
case object Substring extends SQLExpr("substring") with SQLStringOperator {
114+
override def painless: String = ".substring"
115+
}
116+
case object To extends SQLExpr("to") with SQLRegex
117+
case object Length extends SQLExpr("length") with SQLStringOperator
118+
102119
sealed trait SQLLogicalOperator extends SQLExpressionOperator
103120

104121
case object Not extends SQLExpr("not") with SQLLogicalOperator

0 commit comments

Comments
 (0)