diff --git a/src/test/kotlin/kestrel/AggregateTest.kt b/src/test/kotlin/kestrel/AggregateTest.kt new file mode 100644 index 0000000..8a5f919 --- /dev/null +++ b/src/test/kotlin/kestrel/AggregateTest.kt @@ -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 = + 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 { 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]) + } +} diff --git a/src/test/kotlin/kestrel/EvaluatorTest.kt b/src/test/kotlin/kestrel/EvaluatorTest.kt new file mode 100644 index 0000000..e7962ae --- /dev/null +++ b/src/test/kotlin/kestrel/EvaluatorTest.kt @@ -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 { 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 { evalConstant(bin(BinaryOperator.Add, textLit("a"), intLit(1))) } + } + + @Test + fun logicalOperatorOnNumberFails() { + assertFailsWith { evalConstant(bin(BinaryOperator.And, intLit(1), trueLit)) } + } +}