Skip to content

Commit 2c6f244

Browse files
committed
Minimal range query support
1 parent f3d2752 commit 2c6f244

4 files changed

Lines changed: 65 additions & 3 deletions

File tree

oolong-elasticsearch/src/main/scala/ru/tinkoff/oolong/elasticsearch/ElasticQueryCompiler.scala

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ object ElasticQueryCompiler extends Backend[QExpr, ElasticQueryNode, JsonNode] {
1919
case QExpr.Eq(QExpr.Prop(path), QExpr.Constant(s)) => EQN.Term(EQN.Field(path), EQN.Constant(s))
2020
case QExpr.Ne(QExpr.Prop(path), QExpr.Constant(s)) =>
2121
EQN.Bool(mustNot = EQN.Term(EQN.Field(path), EQN.Constant(s)) :: Nil)
22-
case QExpr.And(exprs) => EQN.Bool(must = exprs map opt)
23-
case QExpr.Or(exprs) => EQN.Bool(should = exprs map opt)
24-
case QExpr.Not(expr) => EQN.Bool(mustNot = opt(expr) :: Nil)
22+
case QExpr.Gte(QExpr.Prop(path), QExpr.Constant(s)) => EQN.Range(EQN.Field(path), gte = Some(EQN.Constant(s)))
23+
case QExpr.Lte(QExpr.Prop(path), QExpr.Constant(s)) => EQN.Range(EQN.Field(path), lte = Some(EQN.Constant(s)))
24+
case QExpr.Gt(QExpr.Prop(path), QExpr.Constant(s)) => EQN.Range(EQN.Field(path), gt = Some(EQN.Constant(s)))
25+
case QExpr.Lt(QExpr.Prop(path), QExpr.Constant(s)) => EQN.Range(EQN.Field(path), lt = Some(EQN.Constant(s)))
26+
case QExpr.And(exprs) => EQN.Bool(must = exprs map opt)
27+
case QExpr.Or(exprs) => EQN.Bool(should = exprs map opt)
28+
case QExpr.Not(expr) => EQN.Bool(mustNot = opt(expr) :: Nil)
2529
case QExpr.Exists(QExpr.Prop(path), QExpr.Constant(true)) => EQN.Exists(EQN.Field(path))
2630
case QExpr.Exists(QExpr.Prop(path), QExpr.Constant(false)) =>
2731
EQN.Bool(mustNot = EQN.Exists(EQN.Field(path)) :: Nil)
@@ -49,13 +53,24 @@ object ElasticQueryCompiler extends Backend[QExpr, ElasticQueryNode, JsonNode] {
4953
case EQN.Constant(s: Any) => s.toString
5054
case EQN.Exists(EQN.Field(path)) =>
5155
s"""{ "exists": { "field": "${path.mkString(".")}" }}"""
56+
case EQN.Range(EQN.Field(path), gt, gte, lt, lte) =>
57+
val bounds = Seq(
58+
renderKeyMap(""""gt"""", gt),
59+
renderKeyMap(""""gte"""", gte),
60+
renderKeyMap(""""lt"""", lt),
61+
renderKeyMap(""""lte"""", lte)
62+
).flatten
63+
s"""{"range": {"${path.mkString(".")}": {${bounds.mkString(",")}}}}"""
5264
case EQN.Field(field) =>
5365
// TODO: adjust error message
5466
report.errorAndAbort(s"There is no filter condition on field ${field.mkString(".")}")
5567
case _ => "AST can't be rendered"
5668
}
5769
}
5870

71+
private def renderKeyMap(key: String, node: Option[ElasticQueryNode])(using quotes: Quotes): Option[String] =
72+
node.map(render(_)).map(v => s"""$key:$v""")
73+
5974
override def target(optRepr: ElasticQueryNode)(using quotes: Quotes): Expr[JsonNode] = {
6075
import quotes.reflect.*
6176

@@ -74,6 +89,19 @@ object ElasticQueryCompiler extends Backend[QExpr, ElasticQueryNode, JsonNode] {
7489
'{ JsonNode.obj("term" -> JsonNode.obj(${ Expr(path.mkString(".")) } -> ${ handleValues(x) })) }
7590
case EQN.Exists(EQN.Field(path)) =>
7691
'{ JsonNode.obj("exists" -> JsonNode.obj("field" -> JsonNode.Str(${ Expr(path.mkString(".")) }))) }
92+
case EQN.Range(EQN.Field(path), gt, gte, lt, lte) =>
93+
'{
94+
JsonNode.obj(
95+
"range" -> JsonNode.obj(
96+
${ Expr(path.mkString(".")) } -> JsonNode.obj(
97+
"gt" -> ${ gt.map(handleValues(_)).getOrElse('{ JsonNode.`null` }) },
98+
"gte" -> ${ gte.map(handleValues(_)).getOrElse('{ JsonNode.`null` }) },
99+
"lt" -> ${ lt.map(handleValues(_)).getOrElse('{ JsonNode.`null` }) },
100+
"lte" -> ${ lte.map(handleValues(_)).getOrElse('{ JsonNode.`null` }) }
101+
)
102+
)
103+
)
104+
}
77105
case _ => report.errorAndAbort("given node can't be in that position")
78106
}
79107
}

oolong-elasticsearch/src/main/scala/ru/tinkoff/oolong/elasticsearch/ElasticQueryNode.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,12 @@ object ElasticQueryNode {
3838
}
3939
}
4040
}
41+
42+
case class Range(
43+
field: Field,
44+
gt: Option[ElasticQueryNode] = None,
45+
gte: Option[ElasticQueryNode] = None,
46+
lt: Option[ElasticQueryNode] = None,
47+
lte: Option[ElasticQueryNode] = None
48+
) extends ElasticQueryNode
4149
}

oolong-elasticsearch/src/test/scala/ru/tinkoff/oolong/elasticsearch/QuerySpec.scala

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,28 @@ class QuerySpec extends AnyFunSuite {
4747

4848
q.render shouldBe """{"query":{"bool":{"must":[],"should":[],"must_not":[{"exists":{"field":"field3.optionalInnerField"}}]}}}"""
4949
}
50+
51+
test("> query") {
52+
val q = query[TestClass](_.field2 > 4)
53+
54+
q.render shouldBe """{"query":{"range":{"field2":{"gt":4,"gte":null,"lt":null,"lte":null}}}}"""
55+
}
56+
57+
test(">= query") {
58+
val q = query[TestClass](_.field2 >= 4)
59+
60+
q.render shouldBe """{"query":{"range":{"field2":{"gt":null,"gte":4,"lt":null,"lte":null}}}}"""
61+
}
62+
63+
test("< query") {
64+
val q = query[TestClass](_.field2 < 4)
65+
66+
q.render shouldBe """{"query":{"range":{"field2":{"gt":null,"gte":null,"lt":4,"lte":null}}}}"""
67+
}
68+
69+
test("<= query") {
70+
val q = query[TestClass](_.field2 <= 4)
71+
72+
q.render shouldBe """{"query":{"range":{"field2":{"gt":null,"gte":null,"lt":null,"lte":4}}}}"""
73+
}
5074
}

oolong-json/src/main/scala/ru/tinkoff/oolong/JsonNode.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ private[oolong] object JsonNode {
2828
override def render: String = value.map((k, v) => s"\"$k\":${v.render}").mkString("{", ",", "}")
2929
}
3030

31+
val `null`: JsonNode = Null
32+
3133
def obj(head: (String, JsonNode), tail: (String, JsonNode)*): Obj =
3234
Obj((head +: tail).to(Map))
3335
}

0 commit comments

Comments
 (0)