test: cover stringFunctionsTransformText

This commit is contained in:
dualfroz committed 2026-09-08 22:09:40 +02:00
1 parent 1614395134
commit 5eedad6404
1 file changed
+183
+183
View File
@@ -1,12 +1,22 @@
package kestrel package kestrel
import java.math.BigDecimal
import java.time.LocalDate
import kestrel.catalog.Column import kestrel.catalog.Column
import kestrel.catalog.TableSchema import kestrel.catalog.TableSchema
import kestrel.engine.Evaluator import kestrel.engine.Evaluator
import kestrel.sql.ast.BetweenExpression
import kestrel.sql.ast.BinaryExpression import kestrel.sql.ast.BinaryExpression
import kestrel.sql.ast.BinaryOperator 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.ColumnExpression
import kestrel.sql.ast.Expression 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.LiteralExpression
import kestrel.sql.ast.UnaryExpression import kestrel.sql.ast.UnaryExpression
import kestrel.sql.ast.UnaryOperator import kestrel.sql.ast.UnaryOperator
@@ -106,4 +116,177 @@ class EvaluatorTest {
fun logicalOperatorOnNumberFails() { fun logicalOperatorOnNumberFails() {
assertFailsWith<KestrelException> { evalConstant(bin(BinaryOperator.And, intLit(1), trueLit)) } assertFailsWith<KestrelException> { 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<KestrelException> { evalConstant(CallExpression("UPPER", listOf(textLit("a"), textLit("b")))) }
assertFailsWith<KestrelException> { evalConstant(CallExpression("SUBSTR", listOf(textLit("a")))) }
}
@Test
fun unknownFunctionFails() {
assertFailsWith<KestrelException> { 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<KestrelException> { 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<KestrelException> {
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<KestrelException> { evalConstant(CallExpression("MOD", listOf(intLit(1), intLit(0)))) }
}
} }