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
Original file line number Diff line number Diff line change
Expand Up @@ -121,22 +121,34 @@ case class ElasticBridge(filter: ElasticFilter) {
val leftQuery = ElasticBridge(leftNested)
.query(innerHitsNames /*++ leftNested.innerHitsName.toSet*/, leftBoolQuery)

// 🔴 NOT-FOLD: this consumer of `Predicate.not` deliberately does NOT fold.
//
// Round 10 (LOW-1) proposed aligning this site with the other three by reading
// `Predicate.emittedRight`. That was tried and MEASURED WRONG: under a nested
// relation the right criterion is evaluated over a CHILD document and the query
// wraps it in an EXISTENTIAL, so `NOT` outside and `NOT` inside are different
// questions. `WHERE MATCH(comments.content) AGAINST ('Nice') AND NOT
// replies.lastUpdated < LAST_DAY(…)` means "no reply is before that date", which
// includes a blog with NO replies; folded, it became "SOME reply is not before
// it", which excludes that blog and admits one with replies on both sides. The
// `SQLQuerySpec` "predicate with distinct nested" fixture caught the flip:
// `must_not[nested(…)] + filter[…]` became `must[nested(…), nested(… == false)]`.
//
// So the fold applies where the negated criterion is evaluated over the SAME
// document, and not here. Enumerated on `Predicate.emittedRight`.
val rightNested = ElasticNested(p.rightCriteria, p.rightCriteria.limit)
val rightBoolQuery = Option(ElasticBoolQuery(group = true))
val rightQuery = ElasticBridge(rightNested)
.query(innerHitsNames /*++ rightNested.innerHitsName.toSet*/, rightBoolQuery)

val negate = p.not.isDefined
p.operator match {
case AND =>
p.not match {
case Some(_) => not(rightQuery).filter(leftQuery)
case _ => must(leftQuery, rightQuery)
}
if (negate) not(rightQuery).filter(leftQuery)
else must(leftQuery, rightQuery)
case _ =>
p.not match {
case Some(_) => not(rightQuery).should(leftQuery)
case _ => should(leftQuery, rightQuery)
}
if (negate) not(rightQuery).should(leftQuery)
else should(leftQuery, rightQuery)
}
case _ =>
val boolQuery = Option(ElasticBoolQuery(group = true))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,34 @@ package object bridge {
)
}

/** True when the ES query DSL cannot address this operand by field NAME -- it is a computed
* value, so the predicate has to run as a script.
*
* 🔴 Story BIDC-8 (review L-8). This condition was inline in `expressionToQuery` only, so `IN`
* and `BETWEEN` over a function-wrapped operand fell through to `termsQuery` / `rangeQuery`
* keyed on `identifier.name` -- which is the EMPTY STRING for a function wrapper. MEASURED on
* live ES 8.18.3: `WHERE UPPER(status) IN ('A','B')` emitted `{"terms":{"":["A","B"]}}` and
* `WHERE ABS(amount) BETWEEN 1 AND 100` emitted `{"range":{"":{...}}}`, both rejected with the
* opaque `x_content_parse_exception: [bool] failed to parse field [filter]`. `Distance` is
* excluded because the geo-distance query DOES address it natively.
*/
private[bridge] def requiresScript(identifier: Identifier): Boolean =
identifier.functions.nonEmpty && (identifier.functions.size > 1 || (identifier.functions.head match {
case _: Distance => false
case _ => true
}))

private[bridge] def scriptQueryOf(criteria: Criteria)(implicit
timestamp: Long,
contextType: PainlessContextType
): Query = {
val context = PainlessContext(context = contextType)
val script = criteria.painless(Some(context))
scriptQuery(
now(Script(script = s"$context$script").lang("painless").scriptType("source"))
)
}

def applyNumericOp[A](n: NumericValue[_])(
longOp: Long => A,
doubleOp: Double => A
Expand All @@ -666,18 +694,7 @@ package object bridge {
import expression._
if (isAggregation)
return matchAllQuery()
if (
identifier.functions.nonEmpty && (identifier.functions.size > 1 || (identifier.functions.head match {
case _: Distance => false
case _ => true
}))
) {
val context = PainlessContext(context = contextType)
val script = painless(Some(context))
return scriptQuery(
now(Script(script = s"$context$script").lang("painless").scriptType("source"))
)
}
if (requiresScript(identifier)) return scriptQueryOf(expression)
// Geo distance special case
identifier.functions.headOption match {
case Some(d: Distance) =>
Expand All @@ -692,11 +709,14 @@ package object bridge {
}) match {
case Some(g) =>
maybeNot match {
case Some(_) =>
// `maybeNegated` is total (story BIDC-8, review M-6): a comparison with no
// negated spelling declines the geo shortcut and falls through to the generic
// path rather than dying with a `MatchError`.
case Some(_) if o.maybeNegated.isDefined =>
return geoDistanceToQuery(
DistanceCriteria(
d,
o.not,
o.maybeNegated.get,
g
)
)
Expand Down Expand Up @@ -880,14 +900,18 @@ package object bridge {
case op: ComparisonOperator =>
i.script match {
case Some(script) =>
val o = if (maybeNot.isDefined) op.not else op
// `maybeNegated` is total (story BIDC-8, review M-6). A comparison the range
// query cannot express -- one with no negated spelling, or any operator outside
// the six below -- runs as a script instead of reaching a `MatchError`.
val o = if (maybeNot.isDefined) op.maybeNegated.getOrElse(op) else op
o match {
case GT => rangeQuery(identifier.name) gt script
case GE => rangeQuery(identifier.name) gte script
case LT => rangeQuery(identifier.name) lt script
case LE => rangeQuery(identifier.name) lte script
case EQ => rangeQuery(identifier.name) gte script lte script
case NE | DIFF => not(rangeQuery(identifier.name) gte script lte script)
case _ => scriptQueryOf(expression)
}
case _ =>
val context = PainlessContext(context = contextType)
Expand Down Expand Up @@ -943,7 +967,11 @@ package object bridge {
existsQuery(identifier.name)
}

implicit def inToQuery[R, T <: Value[R]](in: InExpr[R, T]): Query = {
implicit def inToQuery[R, T <: Value[R]](in: InExpr[R, T])(implicit
timestamp: Long,
contextType: PainlessContextType = PainlessContextType.Query
): Query = {
if (requiresScript(in.identifier)) return scriptQueryOf(in)
import in._
val _values: Seq[Any] = values.innerValues
val t =
Expand All @@ -969,6 +997,7 @@ package object bridge {
contextType: PainlessContextType = PainlessContextType.Query
): Query = {
import between._
if (requiresScript(identifier)) return scriptQueryOf(between)
// Geo distance special case
identifier.functions.headOption match {
case Some(d: Distance) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ class AggregationNamingSpec extends AnyFlatSpec with Matchers {
terms,
""","aggs":{"count_x":{"value_count":{"field":"x"}},""",
""""having_filter":{"bucket_selector":{"buckets_path":{"count_x":"count_x"},""",
""""script":{"source":"(params.count_x == null ? false : (!(params.count_x >= 1 && params.count_x <= 5)))"}}}}}}}"""
""""script":{"source":"(params.count_x == null ? false : !(params.count_x >= 1 && params.count_x <= 5))"}}}}}}}"""
).mkString
}

Expand All @@ -265,7 +265,7 @@ class AggregationNamingSpec extends AnyFlatSpec with Matchers {
terms,
""","aggs":{"max_x":{"max":{"field":"x"}},""",
""""having_filter":{"bucket_selector":{"buckets_path":{"max_x":"max_x"},""",
""""script":{"source":"(params.max_x == null ? false : (!(params.max_x == 1 || params.max_x == 2)))"}}}}}}}"""
""""script":{"source":"(params.max_x == null ? false : !(params.max_x == 1 || params.max_x == 2))"}}}}}}}"""
).mkString
}

Expand All @@ -290,7 +290,7 @@ class AggregationNamingSpec extends AnyFlatSpec with Matchers {
terms,
""","aggs":{"count_x":{"value_count":{"field":"x"}},""",
""""having_filter":{"bucket_selector":{"buckets_path":{"count_x":"count_x"},""",
""""script":{"source":"(params.count_x == null ? false : (!(params.count_x == 1 || params.count_x == 2)))"}}}}}}}"""
""""script":{"source":"(params.count_x == null ? false : !(params.count_x == 1 || params.count_x == 2))"}}}}}}}"""
).mkString
}

Expand Down
Loading
Loading