Skip to content

Commit 15d8cfc

Browse files
committed
add support for JOIN
1 parent db1b8bb commit 15d8cfc

File tree

9 files changed

+119
-42
lines changed

9 files changed

+119
-42
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.9.0"
22+
ThisBuild / version := "0.9.1"
2323

2424
ThisBuild / scalaVersion := scala213
2525

documentation/keywords.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ A list of reserved words recognized by the parser for this engine.
77
## Main clauses
88
SELECT
99
FROM
10+
JOIN
1011
UNNEST
1112
WHERE
1213
GROUP BY

documentation/request_structure.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Expand an array / nested field into rows. Mapped to Elasticsearch `nested` and i
4545
```sql
4646
SELECT id, phone
4747
FROM customers
48-
UNNEST(phones) AS phone;
48+
JOIN UNNEST(phones) AS phone;
4949
```
5050

5151
---

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
102102
it should "perform nested count" in {
103103
val results: Seq[ElasticAggregation] =
104104
SQLQuery(
105-
"select count(inner_emails.value) as email from index i, unnest(emails) as inner_emails where i.nom = \"Nom\""
105+
"select count(inner_emails.value) as email from index i join unnest(emails) as inner_emails where i.nom = \"Nom\""
106106
)
107107
results.size shouldBe 1
108108
val result = results.head
@@ -147,7 +147,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
147147
it should "perform nested count with nested criteria" in {
148148
val results: Seq[ElasticAggregation] =
149149
SQLQuery(
150-
"select count(inner_emails.value) as count_emails from index, unnest(emails) as inner_emails, unnest(profiles) as inner_profiles where nom = \"Nom\" and (inner_profiles.postalCode in (\"75001\",\"75002\"))"
150+
"select count(inner_emails.value) as count_emails from index join unnest(emails) as inner_emails join unnest(profiles) as inner_profiles where nom = \"Nom\" and (inner_profiles.postalCode in (\"75001\",\"75002\"))"
151151
)
152152
results.size shouldBe 1
153153
val result = results.head
@@ -206,7 +206,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
206206
it should "perform nested count with filter" in {
207207
val results: Seq[ElasticAggregation] =
208208
SQLQuery(
209-
"select count(inner_emails.value) as count_emails from index, unnest(emails) as inner_emails, unnest(profiles) as inner_profiles where nom = \"Nom\" and (inner_profiles.postalCode in (\"75001\",\"75002\")) having inner_emails.context = \"profile\""
209+
"select count(inner_emails.value) as count_emails from index join unnest(emails) as inner_emails join unnest(profiles) as inner_profiles where nom = \"Nom\" and (inner_profiles.postalCode in (\"75001\",\"75002\")) having inner_emails.context = \"profile\""
210210
)
211211
results.size shouldBe 1
212212
val result = results.head
@@ -276,7 +276,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
276276
it should "perform nested count with \"and not\" operator" in {
277277
val results: Seq[ElasticAggregation] =
278278
SQLQuery(
279-
"select count(distinct inner_emails.value) as count_emails from index, unnest(emails) as inner_emails, unnest(profiles) as inner_profiles where ((inner_profiles.postalCode = \"33600\") and (inner_profiles.postalCode <> \"75001\"))"
279+
"select count(distinct inner_emails.value) as count_emails from index join unnest(emails) as inner_emails join unnest(profiles) as inner_profiles where ((inner_profiles.postalCode = \"33600\") and (inner_profiles.postalCode <> \"75001\"))"
280280
)
281281
results.size shouldBe 1
282282
val result = results.head
@@ -352,7 +352,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
352352
it should "perform nested count with date filtering" in {
353353
val results: Seq[ElasticAggregation] =
354354
SQLQuery(
355-
"select count(distinct inner_emails.value) as count_distinct_emails from index, unnest(emails) as inner_emails, unnest(profiles) as inner_profiles where inner_profiles.postalCode = \"33600\" and inner_profiles.createdDate <= \"now-35M/M\""
355+
"select count(distinct inner_emails.value) as count_distinct_emails from index join unnest(emails) as inner_emails join unnest(profiles) as inner_profiles where inner_profiles.postalCode = \"33600\" and inner_profiles.createdDate <= \"now-35M/M\""
356356
)
357357
results.size shouldBe 1
358358
val result = results.head
@@ -428,7 +428,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
428428
|profile_ccm.lastName as lastName,
429429
|profile_ccm.postalCode as postalCode,
430430
|profile_ccm.birthYear as birthYear
431-
|FROM index, unnest(profiles) as profile_ccm
431+
|FROM index join unnest(profiles) as profile_ccm
432432
|WHERE
433433
|((profile_ccm.postalCode BETWEEN "10" AND "99999")
434434
|AND
@@ -621,8 +621,8 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
621621
| min(inner_products.price) as min_price,
622622
| max(inner_products.price) as max_price
623623
|FROM
624-
| stores store,
625-
| UNNEST(store.products LIMIT 10) as inner_products
624+
| stores store
625+
| JOIN UNNEST(store.products LIMIT 10) as inner_products
626626
|WHERE
627627
| (
628628
| firstName is not null AND

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
102102
it should "perform nested count" in {
103103
val results: Seq[ElasticAggregation] =
104104
SQLQuery(
105-
"select count(inner_emails.value) as email from index i, unnest(emails) as inner_emails where i.nom = \"Nom\""
105+
"select count(inner_emails.value) as email from index i join unnest(emails) as inner_emails where i.nom = \"Nom\""
106106
)
107107
results.size shouldBe 1
108108
val result = results.head
@@ -147,7 +147,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
147147
it should "perform nested count with nested criteria" in {
148148
val results: Seq[ElasticAggregation] =
149149
SQLQuery(
150-
"select count(inner_emails.value) as count_emails from index, unnest(emails) as inner_emails, unnest(profiles) as inner_profiles where nom = \"Nom\" and (inner_profiles.postalCode in (\"75001\",\"75002\"))"
150+
"select count(inner_emails.value) as count_emails from index join unnest(emails) as inner_emails join unnest(profiles) as inner_profiles where nom = \"Nom\" and (inner_profiles.postalCode in (\"75001\",\"75002\"))"
151151
)
152152
results.size shouldBe 1
153153
val result = results.head
@@ -206,7 +206,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
206206
it should "perform nested count with filter" in {
207207
val results: Seq[ElasticAggregation] =
208208
SQLQuery(
209-
"select count(inner_emails.value) as count_emails from index, unnest(emails) as inner_emails, unnest(profiles) as inner_profiles where nom = \"Nom\" and (inner_profiles.postalCode in (\"75001\",\"75002\")) having inner_emails.context = \"profile\""
209+
"select count(inner_emails.value) as count_emails from index join unnest(emails) as inner_emails join unnest(profiles) as inner_profiles where nom = \"Nom\" and (inner_profiles.postalCode in (\"75001\",\"75002\")) having inner_emails.context = \"profile\""
210210
)
211211
results.size shouldBe 1
212212
val result = results.head
@@ -276,7 +276,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
276276
it should "perform nested count with \"and not\" operator" in {
277277
val results: Seq[ElasticAggregation] =
278278
SQLQuery(
279-
"select count(distinct inner_emails.value) as count_emails from index, unnest(emails) as inner_emails, unnest(profiles) as inner_profiles where ((inner_profiles.postalCode = \"33600\") and (inner_profiles.postalCode <> \"75001\"))"
279+
"select count(distinct inner_emails.value) as count_emails from index join unnest(emails) as inner_emails join unnest(profiles) as inner_profiles where ((inner_profiles.postalCode = \"33600\") and (inner_profiles.postalCode <> \"75001\"))"
280280
)
281281
results.size shouldBe 1
282282
val result = results.head
@@ -352,7 +352,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
352352
it should "perform nested count with date filtering" in {
353353
val results: Seq[ElasticAggregation] =
354354
SQLQuery(
355-
"select count(distinct inner_emails.value) as count_distinct_emails from index, unnest(emails) as inner_emails, unnest(profiles) as inner_profiles where inner_profiles.postalCode = \"33600\" and inner_profiles.createdDate <= \"now-35M/M\""
355+
"select count(distinct inner_emails.value) as count_distinct_emails from index join unnest(emails) as inner_emails join unnest(profiles) as inner_profiles where inner_profiles.postalCode = \"33600\" and inner_profiles.createdDate <= \"now-35M/M\""
356356
)
357357
results.size shouldBe 1
358358
val result = results.head
@@ -428,7 +428,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
428428
|profile_ccm.lastName as lastName,
429429
|profile_ccm.postalCode as postalCode,
430430
|profile_ccm.birthYear as birthYear
431-
|FROM index, unnest(profiles) as profile_ccm
431+
|FROM index join unnest(profiles) as profile_ccm
432432
|WHERE
433433
|((profile_ccm.postalCode BETWEEN "10" AND "99999")
434434
|AND
@@ -621,8 +621,8 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
621621
| min(inner_products.price) as min_price,
622622
| max(inner_products.price) as max_price
623623
|FROM
624-
| stores store,
625-
| UNNEST(store.products LIMIT 10) as inner_products
624+
| stores store
625+
| JOIN UNNEST(store.products LIMIT 10) as inner_products
626626
|WHERE
627627
| (
628628
| firstName is not null AND
Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,51 @@
11
package app.softnetwork.elastic.sql.parser
22

3-
import app.softnetwork.elastic.sql.query.{From, Table, Unnest}
3+
import app.softnetwork.elastic.sql.query.{
4+
CrossJoin,
5+
From,
6+
FullJoin,
7+
InnerJoin,
8+
Join,
9+
JoinType,
10+
LeftJoin,
11+
On,
12+
RightJoin,
13+
Table,
14+
Unnest
15+
}
416

517
trait FromParser {
6-
self: Parser with LimitParser =>
18+
self: Parser with WhereParser with LimitParser =>
719

8-
def unnest: PackratParser[Table] =
9-
Unnest.regex ~ start ~ identifier ~ limit.? ~ end ~ alias ^^ { case _ ~ _ ~ i ~ l ~ _ ~ a =>
10-
Table(Unnest(i, l), Some(a))
20+
def unnest: PackratParser[Unnest] =
21+
Unnest.regex ~ start ~ identifier ~ limit.? ~ end ~ alias.? ^^ { case _ ~ i ~ l ~ _ ~ a =>
22+
Unnest(i, l, a)
1123
}
1224

13-
def table: PackratParser[Table] = identifier ~ alias.? ^^ { case i ~ a => Table(i, a) }
25+
def inner_join: PackratParser[JoinType] = InnerJoin.regex ^^ { _ => InnerJoin }
26+
def left_join: PackratParser[JoinType] = LeftJoin.regex ^^ { _ => LeftJoin }
27+
def right_join: PackratParser[JoinType] = RightJoin.regex ^^ { _ => RightJoin }
28+
def full_join: PackratParser[JoinType] = FullJoin.regex ^^ { _ => FullJoin }
29+
def cross_join: PackratParser[JoinType] = CrossJoin.regex ^^ { _ => CrossJoin }
30+
def join_type: PackratParser[JoinType] =
31+
inner_join | left_join | right_join | full_join | cross_join
32+
33+
def on: PackratParser[On] = On.regex ~> whereCriteria ^^ { rawTokens =>
34+
On(
35+
processTokens(rawTokens).getOrElse(throw new Exception("ON clause requires criteria"))
36+
)
37+
}
38+
39+
def join: PackratParser[Join] = opt(join_type) ~ Join.regex ~ unnest ~ opt(on) ^^ {
40+
case jt ~ _ ~ t ~ o => t // Unnest cannot have a join type or an ON clause
41+
}
42+
43+
def table: PackratParser[Table] = identifierRegex ~ alias.? ~ rep(join) ^^ { case i ~ a ~ js =>
44+
Table(i, a, js)
45+
}
1446

15-
def from: PackratParser[From] = From.regex ~ rep1sep(unnest | table, separator) ^^ {
16-
case _ ~ tables =>
17-
From(tables)
47+
def from: PackratParser[From] = From.regex ~ rep1sep(table, separator) ^^ { case _ ~ tables =>
48+
From(tables)
1849
}
1950

2051
}

sql/src/main/scala/app/softnetwork/elastic/sql/parser/Parser.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,13 @@ trait Parser
111111
private val reservedKeywords = Seq(
112112
"select",
113113
"from",
114+
"join",
114115
"where",
115116
"group",
116117
"having",
117118
"order",
118119
"limit",
120+
"offset",
119121
"as",
120122
"by",
121123
"except",
@@ -229,7 +231,7 @@ trait Parser
229231
private val identifierRegexStr =
230232
s"""(?i)(?!(?:${reservedKeywords.mkString("|")})\\b)[\\*a-zA-Z_\\-][a-zA-Z0-9_\\-.\\[\\]\\*]*"""
231233

232-
private val identifierRegex = identifierRegexStr.r // scala.util.matching.Regex
234+
val identifierRegex = identifierRegexStr.r // scala.util.matching.Regex
233235

234236
def identifier: PackratParser[Identifier] =
235237
(Distinct.regex.? ~ identifierRegex ^^ { case d ~ i =>

sql/src/main/scala/app/softnetwork/elastic/sql/query/From.scala

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,74 @@ import app.softnetwork.elastic.sql.{
1212

1313
case object From extends Expr("FROM") with TokenRegex
1414

15+
sealed trait JoinType extends TokenRegex
16+
17+
case object InnerJoin extends Expr("INNER") with JoinType
18+
19+
case object LeftJoin extends Expr("LEFT") with JoinType
20+
21+
case object RightJoin extends Expr("RIGHT") with JoinType
22+
23+
case object FullJoin extends Expr("FULL") with JoinType
24+
25+
case object CrossJoin extends Expr("CROSS") with JoinType
26+
27+
case object On extends Expr("ON") with TokenRegex
28+
29+
case class On(criteria: Criteria) extends Updateable {
30+
override def sql: String = s" $On $criteria"
31+
def update(request: SQLSearchRequest): On = this.copy(criteria = criteria.update(request))
32+
}
33+
34+
case object Join extends Expr("JOIN") with TokenRegex
35+
36+
sealed trait Join extends Updateable {
37+
def source: Source
38+
def joinType: Option[JoinType]
39+
def on: Option[On]
40+
def alias: Option[Alias]
41+
override def sql: String =
42+
s"${asString(joinType)} $Join $source${asString(on)}"
43+
44+
override def update(request: SQLSearchRequest): Join
45+
}
46+
1547
case object Unnest extends Expr("UNNEST") with TokenRegex
1648

17-
case class Unnest(identifier: Identifier, limit: Option[Limit]) extends Source {
18-
override def sql: String = s"$Unnest($identifier${asString(limit)})"
49+
case class Unnest(identifier: Identifier, limit: Option[Limit], alias: Option[Alias] = None)
50+
extends Source
51+
with Join {
52+
override def sql: String = s"$Join $Unnest($identifier${asString(limit)})"
1953
def update(request: SQLSearchRequest): Unnest =
2054
this.copy(identifier = identifier.update(request))
2155
override val name: String = identifier.name
56+
57+
override def source: Source = this
58+
59+
override def joinType: Option[JoinType] = None
60+
61+
override def on: Option[On] = None
2262
}
2363

24-
case class Table(source: Source, tableAlias: Option[Alias] = None) extends Updateable {
25-
override def sql: String = s"$source${asString(tableAlias)}"
26-
def update(request: SQLSearchRequest): Table = this.copy(source = source.update(request))
64+
case class Table(name: String, tableAlias: Option[Alias] = None, joins: Seq[Join] = Nil)
65+
extends Source {
66+
override def sql: String = s"$name${asString(tableAlias)}${joins.map(_.sql).mkString(" ")}"
67+
def update(request: SQLSearchRequest): Table = this.copy(joins = joins.map(_.update(request)))
2768
}
2869

2970
case class From(tables: Seq[Table]) extends Updateable {
3071
override def sql: String = s" $From ${tables.map(_.sql).mkString(",")}"
3172
lazy val tableAliases: Map[String, String] = tables
32-
.flatMap((table: Table) => table.tableAlias.map(alias => table.source.name -> alias.alias))
33-
.toMap
34-
lazy val unnests: Seq[(String, String, Option[Limit])] = tables.collect {
35-
case Table(u: Unnest, a) =>
36-
(a.map(_.alias).getOrElse(u.identifier.name), u.identifier.name, u.limit)
37-
}
73+
.flatMap((table: Table) => table.tableAlias.map(alias => table.name -> alias.alias))
74+
.toMap ++ unnests.map(unnest => unnest._2 -> unnest._1).toMap
75+
lazy val unnests: Seq[(String, String, Option[Limit])] = tables
76+
.map(_.joins)
77+
.collect { case j =>
78+
j.collect { case u: Unnest => // extract unnest info
79+
(u.alias.map(_.alias).getOrElse(u.identifier.name), u.identifier.name, u.limit)
80+
}
81+
}
82+
.flatten
3883
def update(request: SQLSearchRequest): From =
3984
this.copy(tables = tables.map(_.update(request)))
4085

sql/src/main/scala/app/softnetwork/elastic/sql/query/SQLSearchRequest.scala

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,7 @@ case class SQLSearchRequest(
5757

5858
lazy val excludes: Seq[String] = select.except.map(_.fields.map(_.sourceField)).getOrElse(Nil)
5959

60-
lazy val sources: Seq[String] = from.tables.collect { case Table(source: Identifier, _) =>
61-
source.sql
62-
}
60+
lazy val sources: Seq[String] = from.tables.map(_.name)
6361

6462
lazy val topHitsBuckets: Seq[Bucket] = topHitsAggs
6563
.flatMap(_.bucketNames)

0 commit comments

Comments
 (0)