diff --git a/src/test/kotlin/kestrel/ExpressionLanguageTest.kt b/src/test/kotlin/kestrel/ExpressionLanguageTest.kt new file mode 100644 index 0000000..cecce8e --- /dev/null +++ b/src/test/kotlin/kestrel/ExpressionLanguageTest.kt @@ -0,0 +1,158 @@ +package kestrel + +import java.math.BigDecimal +import java.time.LocalDate +import kestrel.catalog.Catalog +import kestrel.engine.Executor +import kestrel.engine.ExplainResult +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 +import kotlin.test.assertTrue + +class ExpressionLanguageTest { + 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 text(value: String) = Value.TextValue(value) + + private fun seedUsers() { + run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER, bio TEXT)") + run( + "INSERT INTO users VALUES " + + "(1, 'ada', 36, 'mathematician'), (2, 'linus', 54, NULL), " + + "(3, 'grace', 85, 'rear admiral')" + ) + } + + @Test + fun scalarFunctionInProjection() { + seedUsers() + val result = select("SELECT UPPER(name) FROM users ORDER BY id ASC") + assertEquals(listOf("upper"), result.columns) + assertEquals(listOf(text("ADA"), text("LINUS"), text("GRACE")), result.rows.map { it[0] }) + } + + @Test + fun scalarFunctionInWhereClause() { + seedUsers() + val result = select("SELECT name FROM users WHERE LENGTH(name) > 4 ORDER BY name ASC") + assertEquals(listOf(listOf(text("grace")), listOf(text("linus"))), result.rows) + } + + @Test + fun scalarFunctionUsableInUpdateAssignment() { + seedUsers() + run("UPDATE users SET name = UPPER(name) WHERE id = 1") + val result = select("SELECT name FROM users WHERE id = 1") + assertEquals(listOf(listOf(text("ADA"))), result.rows) + } + + @Test + fun nestedFunctionCalls() { + seedUsers() + val result = select("SELECT SUBSTR(UPPER(name), 1, 1) FROM users WHERE id = 1") + assertEquals(listOf(listOf(text("A"))), result.rows) + } + + @Test + fun isNullFiltersRowsWithMissingValues() { + seedUsers() + val result = select("SELECT name FROM users WHERE bio IS NULL") + assertEquals(listOf(listOf(text("linus"))), result.rows) + val negated = select("SELECT COUNT(*) FROM users WHERE bio IS NOT NULL") + assertEquals(listOf(listOf(int(2))), negated.rows) + } + + @Test + fun inClauseMatchesAgainstLiteralSet() { + seedUsers() + val result = select("SELECT name FROM users WHERE age IN (36, 85) ORDER BY age ASC") + assertEquals(listOf(listOf(text("ada")), listOf(text("grace"))), result.rows) + } + + @Test + fun notInExcludesMatchingRows() { + seedUsers() + val result = select("SELECT name FROM users WHERE age NOT IN (36, 85)") + assertEquals(listOf(listOf(text("linus"))), result.rows) + } + + @Test + fun betweenSelectsInclusiveRange() { + seedUsers() + val result = select("SELECT name FROM users WHERE age BETWEEN 40 AND 90 ORDER BY age ASC") + assertEquals(listOf(listOf(text("linus")), listOf(text("grace"))), result.rows) + } + + @Test + fun likeMatchesWildcardPattern() { + seedUsers() + val result = select("SELECT name FROM users WHERE bio LIKE '%admiral' ") + assertEquals(listOf(listOf(text("grace"))), result.rows) + } + + @Test + fun caseExpressionInProjection() { + seedUsers() + val result = select( + "SELECT name, CASE WHEN age < 50 THEN 'young' WHEN age < 80 THEN 'mid' ELSE 'senior' END " + + "FROM users ORDER BY id ASC" + ) + assertEquals( + listOf( + listOf(text("ada"), text("young")), + listOf(text("linus"), text("mid")), + listOf(text("grace"), text("senior")) + ), + result.rows + ) + } + + @Test + fun caseExpressionUsableInWhereClause() { + seedUsers() + val result = select( + "SELECT name FROM users WHERE CASE WHEN age > 40 THEN TRUE ELSE FALSE END ORDER BY name ASC" + ) + assertEquals(listOf(listOf(text("grace")), listOf(text("linus"))), result.rows) + } + + @Test + fun combiningPredicatesWithLogicalOperators() { + seedUsers() + val result = select( + "SELECT name FROM users WHERE age BETWEEN 30 AND 60 AND name LIKE 'a%'" + ) + assertEquals(listOf(listOf(text("ada"))), result.rows) + } + + @Test + fun aggregateOfScalarFunctionResult() { + seedUsers() + val result = select("SELECT SUM(LENGTH(name)) FROM users") + assertEquals(listOf(listOf(int(3 + 5 + 5))), result.rows) + } + + @Test + fun groupByWithFunctionOnGroupedColumn() { + run("CREATE TABLE t (category TEXT, amount INTEGER)") + run("INSERT INTO t VALUES ('Eng', 10), ('eng', 20), ('Ops', 5)") + val result = select("SELECT LOWER(category), SUM(amount) FROM t GROUP BY LOWER(category) ORDER BY LOWER(category) ASC") + assertEquals( + listOf(listOf(text("eng"), int(30)), listOf(text("ops"), int(5))), + result.rows + ) + } +