diff --git a/src/test/kotlin/kestrel/EvaluatorTest.kt b/src/test/kotlin/kestrel/EvaluatorTest.kt index e7962ae..245fff7 100644 --- a/src/test/kotlin/kestrel/EvaluatorTest.kt +++ b/src/test/kotlin/kestrel/EvaluatorTest.kt @@ -1,12 +1,22 @@ package kestrel +import java.math.BigDecimal +import java.time.LocalDate import kestrel.catalog.Column import kestrel.catalog.TableSchema import kestrel.engine.Evaluator +import kestrel.sql.ast.BetweenExpression import kestrel.sql.ast.BinaryExpression import kestrel.sql.ast.BinaryOperator +import kestrel.sql.ast.CallExpression +import kestrel.sql.ast.CaseBranch +import kestrel.sql.ast.CaseExpression +import kestrel.sql.ast.CastExpression import kestrel.sql.ast.ColumnExpression import kestrel.sql.ast.Expression +import kestrel.sql.ast.InExpression +import kestrel.sql.ast.IsNullExpression +import kestrel.sql.ast.LikeExpression import kestrel.sql.ast.LiteralExpression import kestrel.sql.ast.UnaryExpression import kestrel.sql.ast.UnaryOperator @@ -106,4 +116,177 @@ class EvaluatorTest { fun logicalOperatorOnNumberFails() { assertFailsWith { evalConstant(bin(BinaryOperator.And, intLit(1), trueLit)) } } + + @Test + fun stringFunctionsTransformText() { + assertEquals(Value.TextValue("ADA"), evalConstant(CallExpression("UPPER", listOf(textLit("ada"))))) + assertEquals(Value.TextValue("ada"), evalConstant(CallExpression("LOWER", listOf(textLit("ADA"))))) + assertEquals(Value.IntegerValue(3), evalConstant(CallExpression("LENGTH", listOf(textLit("ada"))))) + assertEquals(Value.TextValue("ada"), evalConstant(CallExpression("TRIM", listOf(textLit(" ada "))))) + } + + @Test + fun substrHandlesTwoAndThreeArgumentForms() { + assertEquals( + Value.TextValue("ell"), + evalConstant(CallExpression("SUBSTR", listOf(textLit("hello"), intLit(2), intLit(3)))) + ) + assertEquals(Value.TextValue("llo"), evalConstant(CallExpression("SUBSTR", listOf(textLit("hello"), intLit(3))))) + assertEquals(Value.TextValue(""), evalConstant(CallExpression("SUBSTR", listOf(textLit("hello"), intLit(99))))) + } + + @Test + fun replaceAndConcat() { + assertEquals( + Value.TextValue("hexxo"), + evalConstant(CallExpression("REPLACE", listOf(textLit("hello"), textLit("l"), textLit("x")))) + ) + assertEquals( + Value.TextValue("ab-cd"), + evalConstant(CallExpression("CONCAT", listOf(textLit("ab"), textLit("-"), textLit("cd")))) + ) + } + + @Test + fun numericFunctionsComputeCorrectly() { + assertEquals(Value.IntegerValue(4), evalConstant(CallExpression("ABS", listOf(intLit(-4))))) + assertEquals(Value.RealValue(4.0), evalConstant(CallExpression("ABS", listOf(realLit(-4.0))))) + assertEquals(Value.IntegerValue(3), evalConstant(CallExpression("CEIL", listOf(realLit(2.1))))) + assertEquals(Value.IntegerValue(2), evalConstant(CallExpression("FLOOR", listOf(realLit(2.9))))) + assertEquals(Value.RealValue(2.35), evalConstant(CallExpression("ROUND", listOf(realLit(2.346), intLit(2))))) + assertEquals(Value.IntegerValue(5), evalConstant(CallExpression("ROUND", listOf(intLit(5))))) + } + + @Test + fun nullHandlingFunctionsSeeRawNulls() { + assertEquals(Value.IntegerValue(7), evalConstant(CallExpression("COALESCE", listOf(nullLit, nullLit, intLit(7))))) + assertEquals(Value.NullValue, evalConstant(CallExpression("COALESCE", listOf(nullLit, nullLit)))) + assertEquals(Value.IntegerValue(1), evalConstant(CallExpression("IFNULL", listOf(intLit(1), intLit(2))))) + assertEquals(Value.IntegerValue(2), evalConstant(CallExpression("IFNULL", listOf(nullLit, intLit(2))))) + assertEquals(Value.NullValue, evalConstant(CallExpression("NULLIF", listOf(intLit(5), intLit(5))))) + assertEquals(Value.IntegerValue(5), evalConstant(CallExpression("NULLIF", listOf(intLit(5), intLit(6))))) + assertEquals(Value.TextValue("NULL"), evalConstant(CallExpression("TYPEOF", listOf(nullLit)))) + assertEquals(Value.TextValue("INTEGER"), evalConstant(CallExpression("TYPEOF", listOf(intLit(1))))) + } + + @Test + fun mostFunctionsPropagateNullArguments() { + assertEquals(Value.NullValue, evalConstant(CallExpression("UPPER", listOf(nullLit)))) + assertEquals(Value.NullValue, evalConstant(CallExpression("ABS", listOf(nullLit)))) + } + + @Test + fun functionArityIsEnforced() { + assertFailsWith { evalConstant(CallExpression("UPPER", listOf(textLit("a"), textLit("b")))) } + assertFailsWith { evalConstant(CallExpression("SUBSTR", listOf(textLit("a")))) } + } + + @Test + fun unknownFunctionFails() { + assertFailsWith { evalConstant(CallExpression("NOPE", listOf(intLit(1)))) } + } + + @Test + fun currentDateReturnsTodaysWallClockDate() { + assertEquals(Value.DateValue(LocalDate.now()), evalConstant(CallExpression("CURRENT_DATE", emptyList()))) + } + + @Test + fun dateComponentFunctionsExtractYearMonthDay() { + val date = dateLit(LocalDate.of(2024, 3, 15)) + assertEquals(Value.IntegerValue(2024), evalConstant(CallExpression("YEAR", listOf(date)))) + assertEquals(Value.IntegerValue(3), evalConstant(CallExpression("MONTH", listOf(date)))) + assertEquals(Value.IntegerValue(15), evalConstant(CallExpression("DAY", listOf(date)))) + assertEquals(Value.IntegerValue(5), evalConstant(CallExpression("DAYOFWEEK", listOf(date)))) + } + + @Test + fun dateComponentFunctionsPropagateNullAndRejectNonDateArguments() { + assertEquals(Value.NullValue, evalConstant(CallExpression("YEAR", listOf(nullLit)))) + assertFailsWith { evalConstant(CallExpression("MONTH", listOf(intLit(1)))) } + } + + @Test + fun dateAddAndDateSubShiftByWholeDaysInEitherDirection() { + val date = dateLit(LocalDate.of(2024, 3, 15)) + assertEquals( + Value.DateValue(LocalDate.of(2024, 3, 25)), + evalConstant(CallExpression("DATE_ADD", listOf(date, intLit(10)))) + ) + assertEquals( + Value.DateValue(LocalDate.of(2024, 3, 5)), + evalConstant(CallExpression("DATE_SUB", listOf(date, intLit(10)))) + ) + // Crossing a month boundary exercises java.time's own carry logic rather than hand-rolled arithmetic. + assertEquals( + Value.DateValue(LocalDate.of(2024, 4, 2)), + evalConstant(CallExpression("DATE_ADD", listOf(date, intLit(18)))) + ) + assertEquals( + Value.DateValue(LocalDate.of(2024, 3, 9)), + evalConstant(CallExpression("DATE_ADD", listOf(date, intLit(-6)))) + ) + } + + @Test + fun datediffReturnsTheSignedDayCountBetweenTwoDates() { + val later = dateLit(LocalDate.of(2024, 3, 20)) + val earlier = dateLit(LocalDate.of(2024, 3, 15)) + assertEquals(Value.IntegerValue(5), evalConstant(CallExpression("DATEDIFF", listOf(later, earlier)))) + assertEquals(Value.IntegerValue(-5), evalConstant(CallExpression("DATEDIFF", listOf(earlier, later)))) + assertEquals(Value.IntegerValue(0), evalConstant(CallExpression("DATEDIFF", listOf(later, later)))) + } + + @Test + fun repeatBuildsTheStringNTimesAndTreatsNonPositiveCountsAsEmpty() { + assertEquals(Value.TextValue("ababab"), evalConstant(CallExpression("REPEAT", listOf(textLit("ab"), intLit(3))))) + assertEquals(Value.TextValue(""), evalConstant(CallExpression("REPEAT", listOf(textLit("ab"), intLit(0))))) + assertEquals(Value.TextValue(""), evalConstant(CallExpression("REPEAT", listOf(textLit("ab"), intLit(-1))))) + } + + @Test + fun lpadAndRpadPadToTheTargetLengthOrTruncateWhenAlreadyLongEnough() { + assertEquals(Value.TextValue("--ab"), evalConstant(CallExpression("LPAD", listOf(textLit("ab"), intLit(4), textLit("-"))))) + assertEquals(Value.TextValue("ab--"), evalConstant(CallExpression("RPAD", listOf(textLit("ab"), intLit(4), textLit("-"))))) + assertEquals(Value.TextValue(" ab"), evalConstant(CallExpression("LPAD", listOf(textLit("ab"), intLit(4))))) + assertEquals(Value.TextValue("he"), evalConstant(CallExpression("LPAD", listOf(textLit("hello"), intLit(2), textLit("-"))))) + assertEquals(Value.TextValue("he"), evalConstant(CallExpression("RPAD", listOf(textLit("hello"), intLit(2), textLit("-"))))) + assertEquals(Value.TextValue(""), evalConstant(CallExpression("LPAD", listOf(textLit("hello"), intLit(0), textLit("-"))))) + } + + @Test + fun lpadCyclesAMultiCharacterPadStringAndTrimsItToExactlyFitTheTargetLength() { + assertEquals(Value.TextValue("xyxab"), evalConstant(CallExpression("LPAD", listOf(textLit("ab"), intLit(5), textLit("xy"))))) + } + + @Test + fun padFunctionsRejectAnEmptyPadStringWhenPaddingIsActuallyNeeded() { + assertFailsWith { + evalConstant(CallExpression("LPAD", listOf(textLit("ab"), intLit(4), textLit("")))) + } + } + + @Test + fun instrReturnsAOneBasedPositionOrZeroWhenNotFound() { + assertEquals(Value.IntegerValue(3), evalConstant(CallExpression("INSTR", listOf(textLit("hello"), textLit("llo"))))) + assertEquals(Value.IntegerValue(1), evalConstant(CallExpression("INSTR", listOf(textLit("hello"), textLit("h"))))) + assertEquals(Value.IntegerValue(0), evalConstant(CallExpression("INSTR", listOf(textLit("hello"), textLit("z"))))) + } + + @Test + fun powerStaysExactForTwoIntegersButFallsBackToRealOtherwise() { + assertEquals(Value.IntegerValue(1024), evalConstant(CallExpression("POWER", listOf(intLit(2), intLit(10))))) + assertEquals(Value.IntegerValue(1), evalConstant(CallExpression("POWER", listOf(intLit(5), intLit(0))))) + assertEquals(Value.RealValue(0.25), evalConstant(CallExpression("POWER", listOf(intLit(2), intLit(-2))))) + assertEquals(Value.RealValue(6.25), evalConstant(CallExpression("POWER", listOf(realLit(2.5), intLit(2))))) + } + + @Test + fun modComputesRemainderAndRejectsDivisionByZero() { + assertEquals(Value.IntegerValue(1), evalConstant(CallExpression("MOD", listOf(intLit(7), intLit(3))))) + assertEquals(Value.IntegerValue(-1), evalConstant(CallExpression("MOD", listOf(intLit(-7), intLit(3))))) + assertEquals(Value.RealValue(0.5), evalConstant(CallExpression("MOD", listOf(realLit(2.5), intLit(1))))) + assertFailsWith { evalConstant(CallExpression("MOD", listOf(intLit(1), intLit(0)))) } + } + }