test: cover expression evaluator and aggregates
This commit is contained in:
1 parent
7ffbc94191
commit
2df0306440
2 files changed
+232
No files matched your search
@@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user