diff --git a/src/main/kotlin/kestrel/engine/Evaluator.kt b/src/main/kotlin/kestrel/engine/Evaluator.kt index 77ef36f..215a341 100644 --- a/src/main/kotlin/kestrel/engine/Evaluator.kt +++ b/src/main/kotlin/kestrel/engine/Evaluator.kt @@ -284,6 +284,62 @@ class Evaluator( if (candidate is Value.NullValue) { sawNull = true 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 { @@ -298,6 +354,7 @@ class Evaluator( UnaryOperator.Negate -> when (operand) { is Value.IntegerValue -> Value.IntegerValue(-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}") } UnaryOperator.Not -> when (operand) { @@ -317,7 +374,7 @@ class Evaluator( return when (operator) { BinaryOperator.Add, BinaryOperator.Subtract, 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 { 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)) { 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 - val ordering = compareOperands(left, right) + val ordering = compareOperands(left, right, collation) val result = when (operator) { BinaryOperator.Equal -> ordering == 0 BinaryOperator.NotEqual -> ordering != 0 @@ -397,14 +460,59 @@ class Evaluator( 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.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)) - 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.DateValue && right is Value.DateValue -> left.value.compareTo(right.value) 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 asDouble(value: Value): Double = when (value) { @@ -412,4 +520,47 @@ class Evaluator( is Value.RealValue -> value.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 + } }