feat(engine): extend Evaluator with evaluateBetween and evaluateLike
This commit is contained in:
1 parent
b165621a2a
commit
644c83f436
1 file changed
+156
-5
@@ -284,6 +284,62 @@ class Evaluator(
|
|||||||
if (candidate is Value.NullValue) {
|
if (candidate is Value.NullValue) {
|
||||||
sawNull = true
|
sawNull = true
|
||||||
continue
|
continue
|
||||||
|
}
|
||||||
|
if (compareOperands(operand, candidate, collation) == 0) return Value.BooleanValue(!expression.negated)
|
||||||
|
}
|
||||||
|
return if (sawNull) Value.NullValue else Value.BooleanValue(expression.negated)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun evaluateBetween(expression: BetweenExpression): Value {
|
||||||
|
val operand = evaluate(expression.operand)
|
||||||
|
val low = evaluate(expression.low)
|
||||||
|
val high = evaluate(expression.high)
|
||||||
|
if (operand is Value.NullValue || low is Value.NullValue || high is Value.NullValue) return Value.NullValue
|
||||||
|
val withinRange = compareOperands(operand, low, pairCollation(expression.operand, expression.low)) >= 0 &&
|
||||||
|
compareOperands(operand, high, pairCollation(expression.operand, expression.high)) <= 0
|
||||||
|
return Value.BooleanValue(if (expression.negated) !withinRange else withinRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun evaluateLike(expression: LikeExpression): Value {
|
||||||
|
val operand = evaluate(expression.operand)
|
||||||
|
val pattern = evaluate(expression.pattern)
|
||||||
|
if (operand is Value.NullValue || pattern is Value.NullValue) return Value.NullValue
|
||||||
|
if (operand !is Value.TextValue || pattern !is Value.TextValue) {
|
||||||
|
throw KestrelException("LIKE requires text operands but got ${operand.typeName} and ${pattern.typeName}")
|
||||||
|
}
|
||||||
|
val matches = likeToRegex(pattern.value).matches(operand.value)
|
||||||
|
return Value.BooleanValue(if (expression.negated) !matches else matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun likeToRegex(pattern: String): Regex {
|
||||||
|
val builder = StringBuilder("(?s)")
|
||||||
|
for (character in pattern) {
|
||||||
|
when (character) {
|
||||||
|
'%' -> builder.append(".*")
|
||||||
|
'_' -> builder.append(".")
|
||||||
|
else -> builder.append(Regex.escape(character.toString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Regex(builder.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun evaluateCase(expression: CaseExpression): Value {
|
||||||
|
val operand = expression.operand?.let { evaluate(it) }
|
||||||
|
for (branch in expression.branches) {
|
||||||
|
val matched = if (operand != null) {
|
||||||
|
if (operand is Value.NullValue) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
val candidate = evaluate(branch.condition)
|
||||||
|
candidate !is Value.NullValue &&
|
||||||
|
compareOperands(operand, candidate, pairCollation(expression.operand!!, branch.condition)) == 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
asBooleanOrNull(evaluate(branch.condition)) == true
|
||||||
|
}
|
||||||
|
if (matched) return evaluate(branch.result)
|
||||||
|
}
|
||||||
|
return expression.elseExpression?.let { evaluate(it) } ?: Value.NullValue
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun evaluateColumn(expression: ColumnExpression): Value {
|
private fun evaluateColumn(expression: ColumnExpression): Value {
|
||||||
@@ -298,6 +354,7 @@ class Evaluator(
|
|||||||
UnaryOperator.Negate -> when (operand) {
|
UnaryOperator.Negate -> when (operand) {
|
||||||
is Value.IntegerValue -> Value.IntegerValue(-operand.value)
|
is Value.IntegerValue -> Value.IntegerValue(-operand.value)
|
||||||
is Value.RealValue -> Value.RealValue(-operand.value)
|
is Value.RealValue -> Value.RealValue(-operand.value)
|
||||||
|
is Value.DecimalValue -> Value.DecimalValue(-operand.value)
|
||||||
else -> throw KestrelException("unary minus requires a numeric operand but got ${operand.typeName}")
|
else -> throw KestrelException("unary minus requires a numeric operand but got ${operand.typeName}")
|
||||||
}
|
}
|
||||||
UnaryOperator.Not -> when (operand) {
|
UnaryOperator.Not -> when (operand) {
|
||||||
@@ -317,7 +374,7 @@ class Evaluator(
|
|||||||
return when (operator) {
|
return when (operator) {
|
||||||
BinaryOperator.Add, BinaryOperator.Subtract,
|
BinaryOperator.Add, BinaryOperator.Subtract,
|
||||||
BinaryOperator.Multiply, BinaryOperator.Divide -> evaluateArithmetic(operator, left, right)
|
BinaryOperator.Multiply, BinaryOperator.Divide -> evaluateArithmetic(operator, left, right)
|
||||||
else -> evaluateComparison(operator, left, right)
|
else -> evaluateComparison(operator, left, right, pairCollation(expression.left, expression.right))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,6 +404,12 @@ class Evaluator(
|
|||||||
|
|
||||||
private fun evaluateArithmetic(operator: BinaryOperator, left: Value, right: Value): Value {
|
private fun evaluateArithmetic(operator: BinaryOperator, left: Value, right: Value): Value {
|
||||||
if (left is Value.NullValue || right is Value.NullValue) return Value.NullValue
|
if (left is Value.NullValue || right is Value.NullValue) return Value.NullValue
|
||||||
|
if (left is Value.DateValue || right is Value.DateValue) {
|
||||||
|
return evaluateDateArithmetic(operator, left, right)
|
||||||
|
}
|
||||||
|
if (left is Value.DecimalValue || right is Value.DecimalValue) {
|
||||||
|
return evaluateDecimalArithmetic(operator, left, right)
|
||||||
|
}
|
||||||
if (!isNumeric(left) || !isNumeric(right)) {
|
if (!isNumeric(left) || !isNumeric(right)) {
|
||||||
throw KestrelException("arithmetic requires numeric operands but got ${left.typeName} and ${right.typeName}")
|
throw KestrelException("arithmetic requires numeric operands but got ${left.typeName} and ${right.typeName}")
|
||||||
}
|
}
|
||||||
@@ -382,9 +445,9 @@ class Evaluator(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun evaluateComparison(operator: BinaryOperator, left: Value, right: Value): Value {
|
private fun evaluateComparison(operator: BinaryOperator, left: Value, right: Value, collation: Collation): Value {
|
||||||
if (left is Value.NullValue || right is Value.NullValue) return Value.NullValue
|
if (left is Value.NullValue || right is Value.NullValue) return Value.NullValue
|
||||||
val ordering = compareOperands(left, right)
|
val ordering = compareOperands(left, right, collation)
|
||||||
val result = when (operator) {
|
val result = when (operator) {
|
||||||
BinaryOperator.Equal -> ordering == 0
|
BinaryOperator.Equal -> ordering == 0
|
||||||
BinaryOperator.NotEqual -> ordering != 0
|
BinaryOperator.NotEqual -> ordering != 0
|
||||||
@@ -397,14 +460,59 @@ class Evaluator(
|
|||||||
return Value.BooleanValue(result)
|
return Value.BooleanValue(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun compareOperands(left: Value, right: Value): Int = when {
|
/**
|
||||||
|
* [collation] only ever changes the outcome for two [Value.TextValue]s - every other pairing
|
||||||
|
* ignores it, matching SQLite (COLLATE is meaningless outside of text comparison). Defaults to
|
||||||
|
* [Collation.Binary] (this engine's behavior before COLLATE existed) at every call site that
|
||||||
|
* has no [CollateExpression] to resolve one from.
|
||||||
|
*/
|
||||||
|
private fun compareOperands(left: Value, right: Value, collation: Collation = Collation.Binary): Int = when {
|
||||||
left is Value.IntegerValue && right is Value.IntegerValue -> left.value.compareTo(right.value)
|
left is Value.IntegerValue && right is Value.IntegerValue -> left.value.compareTo(right.value)
|
||||||
|
left is Value.DecimalValue || right is Value.DecimalValue -> {
|
||||||
|
val a = decimalOf(left) ?: throw KestrelException("cannot compare ${left.typeName} with ${right.typeName}")
|
||||||
|
val b = decimalOf(right) ?: throw KestrelException("cannot compare ${left.typeName} with ${right.typeName}")
|
||||||
|
a.compareTo(b)
|
||||||
|
}
|
||||||
isNumeric(left) && isNumeric(right) -> asDouble(left).compareTo(asDouble(right))
|
isNumeric(left) && isNumeric(right) -> asDouble(left).compareTo(asDouble(right))
|
||||||
left is Value.TextValue && right is Value.TextValue -> left.value.compareTo(right.value)
|
left is Value.TextValue && right is Value.TextValue -> collation.compare(left.value, right.value)
|
||||||
left is Value.BooleanValue && right is Value.BooleanValue -> left.value.compareTo(right.value)
|
left is Value.BooleanValue && right is Value.BooleanValue -> left.value.compareTo(right.value)
|
||||||
|
left is Value.DateValue && right is Value.DateValue -> left.value.compareTo(right.value)
|
||||||
else -> throw KestrelException("cannot compare ${left.typeName} with ${right.typeName}")
|
else -> throw KestrelException("cannot compare ${left.typeName} with ${right.typeName}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The [Collation] an explicit `COLLATE` on [expression] itself names, or `null` when it is not a [CollateExpression] at all. */
|
||||||
|
private fun explicitCollationOf(expression: Expression): Collation? = (expression as? CollateExpression)?.collation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [expression]'s own declared `COLLATE` (see [kestrel.catalog.Column.collation]) when it is a
|
||||||
|
* bare [ColumnExpression] whose [resolver] reports one - `null` for anything else, including a
|
||||||
|
* column that never declared one at all ([ColumnResolver.declaredCollation] reporting
|
||||||
|
* [Collation.Binary], its class default, is treated the exact same as reporting nothing: a
|
||||||
|
* column that never opted into a non-default sequence should not out-rank an operand on the
|
||||||
|
* *other* side of a comparison that did, purely by virtue of appearing on the left - see
|
||||||
|
* [pairCollation]).
|
||||||
|
*/
|
||||||
|
private fun columnDeclaredCollationOf(expression: Expression): Collation? =
|
||||||
|
(expression as? ColumnExpression)?.let { resolver?.declaredCollation(it) }?.takeIf { it != Collation.Binary }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [Collation] [expression] compares with on its own, combining both of [pairCollation]'s
|
||||||
|
* tiers into one - used where there is only a single operand to consult
|
||||||
|
* ([evaluateIn]/[evaluateInSubquery]'s list/subquery operand), which can still tell "no COLLATE
|
||||||
|
* here at all" apart from "explicitly COLLATE BINARY" (both [explicitCollationOf] and
|
||||||
|
* [columnDeclaredCollationOf] return `null` only for the former).
|
||||||
|
*/
|
||||||
|
private fun collationOf(expression: Expression): Collation? = explicitCollationOf(expression) ?: columnDeclaredCollationOf(expression)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegates to the shared [kestrel.engine.pairCollation] (SQLite's two-tier binary-comparison
|
||||||
|
* rule) bound to this evaluator's own [resolver] - see that function's doc for the precedence
|
||||||
|
* rule itself and why it must stay two separate passes rather than one combined
|
||||||
|
* [collationOf]-per-side check.
|
||||||
|
*/
|
||||||
|
private fun pairCollation(left: Expression, right: Expression): Collation =
|
||||||
|
kestrel.engine.pairCollation(left, right) { resolver?.declaredCollation(it) }
|
||||||
|
|
||||||
private fun isNumeric(value: Value): Boolean = value is Value.IntegerValue || value is Value.RealValue
|
private fun isNumeric(value: Value): Boolean = value is Value.IntegerValue || value is Value.RealValue
|
||||||
|
|
||||||
private fun asDouble(value: Value): Double = when (value) {
|
private fun asDouble(value: Value): Double = when (value) {
|
||||||
@@ -412,4 +520,47 @@ class Evaluator(
|
|||||||
is Value.RealValue -> value.value
|
is Value.RealValue -> value.value
|
||||||
else -> error("not a numeric value")
|
else -> error("not a numeric value")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun decimalOf(value: Value): BigDecimal? = when (value) {
|
||||||
|
is Value.DecimalValue -> value.value
|
||||||
|
is Value.IntegerValue -> BigDecimal(value.value)
|
||||||
|
is Value.RealValue -> BigDecimal.valueOf(value.value)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun evaluateDateArithmetic(operator: BinaryOperator, left: Value, right: Value): Value = when {
|
||||||
|
left is Value.DateValue && right is Value.DateValue -> when (operator) {
|
||||||
|
BinaryOperator.Subtract -> Value.IntegerValue(left.value.toEpochDay() - right.value.toEpochDay())
|
||||||
|
else -> throw KestrelException("DATE ${operator.symbol} DATE is not supported; only subtraction (day difference) is")
|
||||||
|
}
|
||||||
|
left is Value.DateValue && right is Value.IntegerValue -> when (operator) {
|
||||||
|
BinaryOperator.Add -> Value.DateValue(left.value.plusDays(right.value))
|
||||||
|
BinaryOperator.Subtract -> Value.DateValue(left.value.minusDays(right.value))
|
||||||
|
else -> throw KestrelException("DATE only supports + and - with an INTEGER day offset")
|
||||||
|
}
|
||||||
|
left is Value.IntegerValue && right is Value.DateValue && operator == BinaryOperator.Add ->
|
||||||
|
Value.DateValue(right.value.plusDays(left.value))
|
||||||
|
else -> throw KestrelException("arithmetic requires numeric operands but got ${left.typeName} and ${right.typeName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun evaluateDecimalArithmetic(operator: BinaryOperator, left: Value, right: Value): Value {
|
||||||
|
val a = decimalOf(left) ?: throw KestrelException("arithmetic requires numeric operands but got ${left.typeName} and ${right.typeName}")
|
||||||
|
val b = decimalOf(right) ?: throw KestrelException("arithmetic requires numeric operands but got ${left.typeName} and ${right.typeName}")
|
||||||
|
return Value.DecimalValue(
|
||||||
|
when (operator) {
|
||||||
|
BinaryOperator.Add -> a + b
|
||||||
|
BinaryOperator.Subtract -> a - b
|
||||||
|
BinaryOperator.Multiply -> a * b
|
||||||
|
BinaryOperator.Divide -> {
|
||||||
|
if (b.signum() == 0) throw KestrelException("division by zero")
|
||||||
|
a.divide(b, maxOf(a.scale(), b.scale(), MIN_DIVISION_SCALE), RoundingMode.HALF_UP)
|
||||||
|
}
|
||||||
|
else -> error("unreachable arithmetic operator")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val MIN_DIVISION_SCALE = 10
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user