diff --git a/src/main/kotlin/kestrel/engine/Evaluator.kt b/src/main/kotlin/kestrel/engine/Evaluator.kt index 6b1099a..77ef36f 100644 --- a/src/main/kotlin/kestrel/engine/Evaluator.kt +++ b/src/main/kotlin/kestrel/engine/Evaluator.kt @@ -111,6 +111,179 @@ class Evaluator( is FunctionExpression -> throw KestrelException( "aggregate function ${expression.name} is not allowed here" ) + is WindowExpression -> throw KestrelException( + "window function ${expression.name} is not allowed here" + ) + is CallExpression -> evaluateCall(expression) + is IsNullExpression -> evaluateIsNull(expression) + is InExpression -> evaluateIn(expression) + is BetweenExpression -> evaluateBetween(expression) + is LikeExpression -> evaluateLike(expression) + is CaseExpression -> evaluateCase(expression) + is ScalarSubqueryExpression -> evaluateScalarSubquery(expression) + is ExistsExpression -> evaluateExists(expression) + is InSubqueryExpression -> evaluateInSubquery(expression) + is CastExpression -> evaluateCast(expression) + is NextValueExpression -> evaluateNextValue(expression) + is CurrentValueExpression -> evaluateCurrentValue(expression) + is MergeActionExpression -> Value.TextValue(mergeAction ?: throw KestrelException("MERGE_ACTION() is not allowed here")) + is ParameterExpression -> throw KestrelException( + "parameter reference \$${expression.index} is only valid inside a PREPARE-d statement, resolved by EXECUTE" + ) + } + + private fun evaluateNextValue(expression: NextValueExpression): Value = Value.IntegerValue( + (sequences ?: throw KestrelException("NEXTVAL is not allowed here")).nextValue(expression.sequenceName) + ) + + private fun evaluateCurrentValue(expression: CurrentValueExpression): Value = Value.IntegerValue( + (sequences ?: throw KestrelException("CURRVAL is not allowed here")).currentValue(expression.sequenceName) + ) + + private fun runSubquery(select: SelectStatement): SelectResult = + (subqueries ?: throw KestrelException("subqueries are not allowed here")).run(select, resolver) + + private fun evaluateScalarSubquery(expression: ScalarSubqueryExpression): Value { + val result = runSubquery(expression.select) + if (result.columns.size != 1) { + throw KestrelException("scalar subquery must return exactly one column but returned ${result.columns.size}") + } + return when { + result.rows.isEmpty() -> Value.NullValue + result.rows.size > 1 -> throw KestrelException("scalar subquery returned more than one row") + else -> result.rows.single().single() + } + } + + private fun evaluateExists(expression: ExistsExpression): Value = + Value.BooleanValue(runSubquery(expression.select).rows.isNotEmpty()) + + private fun evaluateInSubquery(expression: InSubqueryExpression): Value { + val operand = evaluate(expression.operand) + if (operand is Value.NullValue) return Value.NullValue + val collation = collationOf(expression.operand) ?: Collation.Binary + val result = runSubquery(expression.select) + if (result.columns.size != 1) { + throw KestrelException("IN subquery must return exactly one column but returned ${result.columns.size}") + } + var sawNull = false + for (row in result.rows) { + val candidate = row.single() + 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 evaluateCast(expression: CastExpression): Value { + val operand = evaluate(expression.operand) + if (operand is Value.NullValue) return Value.NullValue + return when (expression.targetType) { + ColumnType.Integer -> castToInteger(operand) + ColumnType.Real -> castToReal(operand) + ColumnType.Text -> Value.TextValue(castToText(operand)) + ColumnType.Boolean -> castToBoolean(operand) + ColumnType.Decimal -> castToDecimal(operand, expression.precision, expression.scale) + ColumnType.Date -> castToDate(operand) + } + } + + private fun castToInteger(value: Value): Value = Value.IntegerValue( + when (value) { + is Value.IntegerValue -> value.value + is Value.RealValue -> value.value.toLong() + is Value.DecimalValue -> value.value.toLong() + is Value.DateValue -> value.value.toEpochDay() + is Value.TextValue -> value.value.trim().toLongOrNull() + ?: throw KestrelException("cannot cast '${value.value}' to INTEGER") + else -> throw KestrelException("cannot cast ${value.typeName} to INTEGER") + } + ) + + private fun castToReal(value: Value): Value = Value.RealValue( + when (value) { + is Value.IntegerValue -> value.value.toDouble() + is Value.RealValue -> value.value + is Value.DecimalValue -> value.value.toDouble() + is Value.TextValue -> value.value.trim().toDoubleOrNull() + ?: throw KestrelException("cannot cast '${value.value}' to REAL") + else -> throw KestrelException("cannot cast ${value.typeName} to REAL") + } + ) + + private fun castToText(value: Value): String = when (value) { + is Value.IntegerValue -> value.value.toString() + is Value.RealValue -> value.value.toString() + is Value.DecimalValue -> value.value.toPlainString() + is Value.BooleanValue -> value.value.toString() + is Value.DateValue -> value.value.toString() + is Value.TextValue -> value.value + else -> throw KestrelException("cannot cast ${value.typeName} to TEXT") + } + + private fun castToBoolean(value: Value): Value = Value.BooleanValue( + when (value) { + is Value.BooleanValue -> value.value + is Value.IntegerValue -> value.value != 0L + is Value.TextValue -> when (value.value.trim().lowercase()) { + "true" -> true + "false" -> false + else -> throw KestrelException("cannot cast '${value.value}' to BOOLEAN") + } + else -> throw KestrelException("cannot cast ${value.typeName} to BOOLEAN") + } + ) + + private fun castToDecimal(value: Value, precision: Int?, scale: Int?): Value { + val raw = when (value) { + is Value.DecimalValue -> value.value + is Value.IntegerValue -> BigDecimal(value.value) + is Value.RealValue -> BigDecimal.valueOf(value.value) + is Value.TextValue -> value.value.trim().toBigDecimalOrNull() + ?: throw KestrelException("cannot cast '${value.value}' to DECIMAL") + else -> throw KestrelException("cannot cast ${value.typeName} to DECIMAL") + } + return Value.DecimalValue(DecimalColumns.normalize(raw, precision, scale, "CAST")) + } + + private fun castToDate(value: Value): Value = Value.DateValue( + when (value) { + is Value.DateValue -> value.value + is Value.IntegerValue -> LocalDate.ofEpochDay(value.value) + is Value.TextValue -> runCatching { LocalDate.parse(value.value.trim()) }.getOrElse { + throw KestrelException("cannot cast '${value.value}' to DATE") + } + else -> throw KestrelException("cannot cast ${value.typeName} to DATE") + } + ) + + private fun evaluateCall(expression: CallExpression): Value { + if (expression.name.equals("GROUPING", ignoreCase = true)) { + throw KestrelException("GROUPING() can only be used in the SELECT list, HAVING, or ORDER BY of a query with GROUP BY") + } + val function = ScalarFunctions.lookup(expression.name) + ?: throw KestrelException("unknown function: ${expression.name}") + return function.apply(expression.arguments.map { evaluate(it) }) + } + + private fun evaluateIsNull(expression: IsNullExpression): Value { + val isNull = evaluate(expression.operand).isNull + return Value.BooleanValue(if (expression.negated) !isNull else isNull) + } + + private fun evaluateIn(expression: InExpression): Value { + val operand = evaluate(expression.operand) + if (operand is Value.NullValue) return Value.NullValue + val collation = collationOf(expression.operand) ?: Collation.Binary + var sawNull = false + for (candidateExpression in expression.values) { + val candidate = evaluate(candidateExpression) + if (candidate is Value.NullValue) { + sawNull = true + continue } private fun evaluateColumn(expression: ColumnExpression): Value {