test: cover expression evaluator and aggregates

This commit is contained in:
dualfroz committed 2026-08-27 18:43:09 +02:00
1 parent 7ffbc94191
commit 2df0306440
2 files changed
+232

No files matched your search

+123
View File
@@ -0,0 +1,123 @@
package kestrel
import kestrel.catalog.Catalog
import kestrel.engine.Executor
import kestrel.engine.SelectResult
import kestrel.engine.StatementResult
import kestrel.sql.lexer.Lexer
import kestrel.sql.parser.Parser
import kestrel.storage.Value
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class AggregateTest {
private val catalog = Catalog()
private val executor = Executor(catalog)
private fun run(sql: String): List<StatementResult> =
Parser(Lexer(sql).tokenize()).parseProgram().map { executor.execute(it) }
private fun select(sql: String): SelectResult = run(sql).single() as SelectResult
private fun int(value: Long) = Value.IntegerValue(value)
private fun real(value: Double) = Value.RealValue(value)
private fun text(value: String) = Value.TextValue(value)
private fun seed() {
run("CREATE TABLE sales (dept TEXT, region TEXT, amount INTEGER)")
run(
"INSERT INTO sales VALUES " +
"('eng', 'eu', 100), ('eng', 'us', 200), ('eng', 'us', NULL), " +
"('ops', 'eu', 50), ('ops', 'eu', 70)"
)
}
@Test
fun countStarCountsAllRows() {
seed()
assertEquals(listOf(listOf(int(5))), select("SELECT COUNT(*) FROM sales").rows)
}
@Test
fun countColumnIgnoresNulls() {
seed()
assertEquals(listOf(listOf(int(4))), select("SELECT COUNT(amount) FROM sales").rows)
}
@Test
fun sumAndAvgIgnoreNulls() {
seed()
val result = select("SELECT SUM(amount) AS total, AVG(amount) AS mean FROM sales")
assertEquals(listOf("total", "mean"), result.columns)
assertEquals(listOf(int(420), real(105.0)), result.rows[0])
}
@Test
fun minAndMaxIgnoreNulls() {
seed()
val result = select("SELECT MIN(amount) AS lo, MAX(amount) AS hi FROM sales")
assertEquals(listOf(int(50), int(200)), result.rows[0])
}
@Test
fun groupBySingleColumn() {
seed()
val result = select("SELECT dept, SUM(amount) AS total FROM sales GROUP BY dept ORDER BY dept ASC")
assertEquals(listOf(text("eng"), int(300)), result.rows[0])
assertEquals(listOf(text("ops"), int(120)), result.rows[1])
}
@Test
fun groupByMultipleColumns() {
seed()
val result = select(
"SELECT dept, region, COUNT(*) AS n FROM sales GROUP BY dept, region ORDER BY dept ASC"
)
assertEquals(listOf(text("eng"), text("eu"), int(1)), result.rows[0])
assertEquals(listOf(text("eng"), text("us"), int(2)), result.rows[1])
assertEquals(listOf(text("ops"), text("eu"), int(2)), result.rows[2])
}
@Test
fun havingFiltersGroups() {
seed()
val result = select("SELECT dept, SUM(amount) AS total FROM sales GROUP BY dept HAVING SUM(amount) > 200")
assertEquals(listOf(listOf(text("eng"), int(300))), result.rows)
}
@Test
fun sumOfAllNullsIsNull() {
run("CREATE TABLE t (x INTEGER)")
run("INSERT INTO t VALUES (NULL), (NULL)")
assertEquals(listOf(listOf(Value.NullValue)), select("SELECT SUM(x) FROM t").rows)
}
@Test
fun countStarOnEmptyTableIsZero() {
run("CREATE TABLE t (x INTEGER)")
assertEquals(listOf(listOf(int(0))), select("SELECT COUNT(*) FROM t").rows)
}
@Test
fun bareColumnOutsideGroupByIsRejected() {
seed()
assertFailsWith<KestrelException> { select("SELECT region, SUM(amount) FROM sales GROUP BY dept") }
}
@Test
fun orderByProjectionAlias() {
seed()
val result = select("SELECT dept, SUM(amount) AS total FROM sales GROUP BY dept ORDER BY total DESC")
assertEquals(listOf(text("eng"), int(300)), result.rows[0])
assertEquals(listOf(text("ops"), int(120)), result.rows[1])
}
@Test
fun aggregateExpressionArithmetic() {
seed()
val result = select("SELECT dept, SUM(amount) + COUNT(*) AS blended FROM sales GROUP BY dept ORDER BY dept ASC")
assertEquals(listOf(text("eng"), int(303)), result.rows[0])
assertEquals(listOf(text("ops"), int(122)), result.rows[1])
}
}
+109
View File
@@ -0,0 +1,109 @@
package kestrel
import kestrel.catalog.Column
import kestrel.catalog.TableSchema
import kestrel.engine.Evaluator
import kestrel.sql.ast.BinaryExpression
import kestrel.sql.ast.BinaryOperator
import kestrel.sql.ast.ColumnExpression
import kestrel.sql.ast.Expression
import kestrel.sql.ast.LiteralExpression
import kestrel.sql.ast.UnaryExpression
import kestrel.sql.ast.UnaryOperator
import kestrel.storage.ColumnType
import kestrel.storage.Row
import kestrel.storage.Value
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class EvaluatorTest {
private fun intLit(value: Long): Expression = LiteralExpression(Value.IntegerValue(value))
private fun realLit(value: Double): Expression = LiteralExpression(Value.RealValue(value))
private fun textLit(value: String): Expression = LiteralExpression(Value.TextValue(value))
private val nullLit: Expression = LiteralExpression(Value.NullValue)
private val trueLit: Expression = LiteralExpression(Value.BooleanValue(true))
private val falseLit: Expression = LiteralExpression(Value.BooleanValue(false))
private fun bin(operator: BinaryOperator, left: Expression, right: Expression) =
BinaryExpression(operator, left, right)
private fun evalConstant(expression: Expression): Value = Evaluator().evaluate(expression)
@Test
fun integerArithmetic() {
assertEquals(Value.IntegerValue(10), evalConstant(bin(BinaryOperator.Add, intLit(7), intLit(3))))
assertEquals(Value.IntegerValue(21), evalConstant(bin(BinaryOperator.Multiply, intLit(7), intLit(3))))
}
@Test
fun integerDivisionTruncates() {
assertEquals(Value.IntegerValue(3), evalConstant(bin(BinaryOperator.Divide, intLit(7), intLit(2))))
}
@Test
fun mixedArithmeticPromotesToReal() {
assertEquals(Value.RealValue(3.5), evalConstant(bin(BinaryOperator.Add, intLit(1), realLit(2.5))))
}
@Test
fun divisionByZeroFails() {
assertFailsWith<KestrelException> { evalConstant(bin(BinaryOperator.Divide, intLit(1), intLit(0))) }
}
@Test
fun numericComparison() {
assertEquals(Value.BooleanValue(true), evalConstant(bin(BinaryOperator.Greater, intLit(5), intLit(3))))
assertEquals(Value.BooleanValue(false), evalConstant(bin(BinaryOperator.Less, intLit(5), intLit(3))))
}
@Test
fun textComparison() {
assertEquals(Value.BooleanValue(true), evalConstant(bin(BinaryOperator.Less, textLit("abc"), textLit("abd"))))
}
@Test
fun comparisonWithNullYieldsNull() {
assertEquals(Value.NullValue, evalConstant(bin(BinaryOperator.Equal, intLit(1), nullLit)))
}
@Test
fun threeValuedAnd() {
assertEquals(Value.BooleanValue(false), evalConstant(bin(BinaryOperator.And, falseLit, nullLit)))
assertEquals(Value.NullValue, evalConstant(bin(BinaryOperator.And, trueLit, nullLit)))
}
@Test
fun threeValuedOr() {
assertEquals(Value.BooleanValue(true), evalConstant(bin(BinaryOperator.Or, trueLit, nullLit)))
assertEquals(Value.NullValue, evalConstant(bin(BinaryOperator.Or, falseLit, nullLit)))
}
@Test
fun notHandlesNull() {
assertEquals(Value.NullValue, evalConstant(UnaryExpression(UnaryOperator.Not, nullLit)))
assertEquals(Value.BooleanValue(false), evalConstant(UnaryExpression(UnaryOperator.Not, trueLit)))
}
@Test
fun unaryNegation() {
assertEquals(Value.IntegerValue(-4), evalConstant(UnaryExpression(UnaryOperator.Negate, intLit(4))))
}
@Test
fun columnReferenceReadsRowValue() {
val schema = TableSchema("t", listOf(Column("age", ColumnType.Integer, notNull = false, primaryKey = false)))
val row = Row(listOf(Value.IntegerValue(36)))
assertEquals(Value.IntegerValue(36), Evaluator(schema, row).evaluate(ColumnExpression("age")))
}
@Test
fun arithmeticOnTextFails() {
assertFailsWith<KestrelException> { evalConstant(bin(BinaryOperator.Add, textLit("a"), intLit(1))) }
}
@Test
fun logicalOperatorOnNumberFails() {
assertFailsWith<KestrelException> { evalConstant(bin(BinaryOperator.And, intLit(1), trueLit)) }
}
}