diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rex/CastTable.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rex/CastTable.kt index 3fa27103e..115eb7db5 100644 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rex/CastTable.kt +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rex/CastTable.kt @@ -483,14 +483,12 @@ internal object CastTable { private fun registerIntervalYM() { register(INTERVAL_YM, STRING) { x, _ -> Datum.string(x.period.toString()) } - register(INTERVAL_YM, CLOB) { x, t -> Datum.clob(x.toString().toByteArray(), t.length) } - register(INTERVAL_YM, STRING) { x, _ -> Datum.string(x.toString()) } + register(INTERVAL_YM, INTERVAL_YM) { x, t -> Datum.interval(x.period, t.precision) } } private fun registerIntervalDT() { register(INTERVAL_DT, STRING) { x, _ -> Datum.string(x.duration.toString()) } - register(INTERVAL_DT, CLOB) { x, t -> Datum.clob(x.toString().toByteArray(), t.length) } - register(INTERVAL_DT, STRING) { x, _ -> Datum.string(x.toString()) } + register(INTERVAL_DT, INTERVAL_DT) { x, t -> Datum.interval(x.duration, t.precision) } } private fun register(source: Int, target: Int, cast: (Datum, PType) -> Datum) { diff --git a/partiql-spi/src/main/java/org/partiql/spi/value/DatumTime.java b/partiql-spi/src/main/java/org/partiql/spi/value/DatumTime.java index 6d73ae325..3cebb5a53 100644 --- a/partiql-spi/src/main/java/org/partiql/spi/value/DatumTime.java +++ b/partiql-spi/src/main/java/org/partiql/spi/value/DatumTime.java @@ -4,6 +4,8 @@ import org.partiql.types.PType; import java.time.LocalTime; +import java.time.OffsetTime; +import java.time.ZoneOffset; /** * Today we wrap a {@link LocalTime}, in the future we do a 4-byte array to avoid double references. @@ -32,4 +34,10 @@ public PType getType() { public LocalTime getLocalTime() { return value; } + + @NotNull + @Override + public OffsetTime getOffsetTime() { + return value.atOffset(ZoneOffset.UTC); + } } diff --git a/partiql-spi/src/main/java/org/partiql/spi/value/DatumTimestamp.java b/partiql-spi/src/main/java/org/partiql/spi/value/DatumTimestamp.java index 6a7d9a474..cb8f8bd90 100644 --- a/partiql-spi/src/main/java/org/partiql/spi/value/DatumTimestamp.java +++ b/partiql-spi/src/main/java/org/partiql/spi/value/DatumTimestamp.java @@ -4,9 +4,7 @@ import org.jetbrains.annotations.NotNull; import org.partiql.types.PType; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; +import java.time.*; /** * Today we wrap a {@link LocalDateTime}, in the future we do a 7-byte array to avoid double references. @@ -42,9 +40,21 @@ public LocalTime getLocalTime() { return value.toLocalTime(); } + @NotNull + @Override + public OffsetTime getOffsetTime() { + return value.atOffset(ZoneOffset.UTC).toOffsetTime(); + } + @NotNull @Override public LocalDateTime getLocalDateTime() { return value; } + + @NotNull + @Override + public OffsetDateTime getOffsetDateTime() { + return value.atOffset(ZoneOffset.UTC); + } } diff --git a/partiql-spi/src/main/java/org/partiql/spi/value/DatumTimestampz.java b/partiql-spi/src/main/java/org/partiql/spi/value/DatumTimestampz.java index 68f56c922..45cdcfa2f 100644 --- a/partiql-spi/src/main/java/org/partiql/spi/value/DatumTimestampz.java +++ b/partiql-spi/src/main/java/org/partiql/spi/value/DatumTimestampz.java @@ -4,9 +4,7 @@ import org.jetbrains.annotations.NotNull; import org.partiql.types.PType; -import java.time.LocalDate; -import java.time.OffsetDateTime; -import java.time.OffsetTime; +import java.time.*; /** * Today we wrap an {@link OffsetDateTime}, in the future we do an 8-byte array to avoid double references. @@ -36,12 +34,24 @@ public LocalDate getLocalDate() { return value.toLocalDate(); } + @NotNull + @Override + public LocalTime getLocalTime() { + return value.toLocalTime(); + } + @NotNull @Override public OffsetTime getOffsetTime() { return value.toOffsetTime(); } + @NotNull + @Override + public LocalDateTime getLocalDateTime() { + return value.toLocalDateTime(); + } + @NotNull @Override public OffsetDateTime getOffsetDateTime() { diff --git a/partiql-spi/src/main/java/org/partiql/spi/value/DatumTimez.java b/partiql-spi/src/main/java/org/partiql/spi/value/DatumTimez.java index ced3da88a..ff74cc076 100644 --- a/partiql-spi/src/main/java/org/partiql/spi/value/DatumTimez.java +++ b/partiql-spi/src/main/java/org/partiql/spi/value/DatumTimez.java @@ -3,6 +3,7 @@ import org.jetbrains.annotations.NotNull; import org.partiql.types.PType; +import java.time.LocalTime; import java.time.OffsetTime; /** @@ -27,6 +28,12 @@ public PType getType() { return type; } + @NotNull + @Override + public LocalTime getLocalTime() { + return value.toLocalTime(); + } + @NotNull @Override public OffsetTime getOffsetTime() { diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnDateAdd.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnDateAdd.kt index 39b4a6b1f..6201a4f3e 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnDateAdd.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnDateAdd.kt @@ -29,25 +29,18 @@ internal object FnDateAdd : Function { val dt = args[2] return when (dt.code()) { PType.TIMESTAMP -> PType.timestamp(dt.precision) - PType.TIMESTAMPZ -> PType.timestamp(dt.precision) + PType.TIMESTAMPZ -> PType.timestampz(dt.precision) else -> throw IllegalArgumentException("Expected timestamp type, but received $dt.") } } override fun getInstance(args: Array): Function.Instance? { - if (args.size != 3) { - throw IllegalArgumentException("Expected 3 arguments, but received ${args.size}.") - } - val dt = args[2] - if (dt.code() != PType.TIMESTAMP && dt.code() != PType.TIMESTAMPZ) { - throw IllegalArgumentException("Expected timestamp type, but received $dt.") + if (args.size != parameters.size) { + return null // should be unreachable } return instance } - /** - * Single implementation switching on first arg like days of yore. - */ private val instance = Function.instance(NAME, parameters, PType.timestamp(6)) { args -> val part = args[0].string val quantity = args[1].int.toLong() diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnDateDiff.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnDateDiff.kt index f06cb4b2c..34b97318d 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnDateDiff.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnDateDiff.kt @@ -2,26 +2,54 @@ package org.partiql.spi.function.builtins import org.partiql.spi.function.Function import org.partiql.spi.function.Parameter +import org.partiql.spi.value.Datum import org.partiql.types.PType +import java.time.temporal.ChronoUnit /** - * DATE_DIFF i + * DATE_DIFF is not SQL standard, so I am doing the bare minimum for unit tests. + * + * This does not do precise typing, you always get timestamp(6). */ internal object FnDateDiff : Function { - override fun getName(): String { - TODO("Not yet implemented") - } + private const val NAME = "date_diff" - override fun getReturnType(args: Array): PType { - TODO("Not yet implemented") - } + private val parameters = arrayOf( + Parameter("part", PType.string()), + Parameter("t1", PType.timestamp(6)), + Parameter("t2", PType.timestamp(6)), + ) + + override fun getName(): String = NAME + + override fun getParameters(): Array = parameters + + override fun getReturnType(args: Array): PType = PType.integer() + /** + * once again switching on the first argument because this is a non-standard function. + */ override fun getInstance(args: Array): Function.Instance? { - return super.getInstance(args) + if (args.size != parameters.size) { + return null // should be unreachable + } + return instance } - override fun getParameters(): Array { - return super.getParameters() + private val instance = Function.instance(NAME, parameters, PType.integer()) { args -> + val part = args[0].string + val t1 = args[1].localDateTime + val t2 = args[2].localDateTime + val result = when (part) { + "year" -> ChronoUnit.YEARS.between(t1, t2) + "month" -> ChronoUnit.MONTHS.between(t1, t2) + "day" -> ChronoUnit.DAYS.between(t1, t2) + "hour" -> ChronoUnit.HOURS.between(t1, t2) + "minute" -> ChronoUnit.MINUTES.between(t1, t2) + "second" -> ChronoUnit.SECONDS.between(t1, t2) + else -> throw IllegalArgumentException("Expected part to be one of: year, month, day, hour, minute, second, millisecond, but received $part.") + } + Datum.integer(result.toInt()) } } diff --git a/partiql-spi/src/test/kotlin/org/partiql/eval/value/DatumComparatorTest.kt b/partiql-spi/src/test/kotlin/org/partiql/eval/value/DatumComparatorTest.kt index 9b19422c3..4f8f0ec47 100644 --- a/partiql-spi/src/test/kotlin/org/partiql/eval/value/DatumComparatorTest.kt +++ b/partiql-spi/src/test/kotlin/org/partiql/eval/value/DatumComparatorTest.kt @@ -3,10 +3,6 @@ package org.partiql.spi.value import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.partiql.types.PType -import org.partiql.value.datetime.DateTimeValue.date -import org.partiql.value.datetime.DateTimeValue.time -import org.partiql.value.datetime.DateTimeValue.timestamp -import org.partiql.value.datetime.TimeZone import java.math.BigDecimal import java.util.Base64 import java.util.Random @@ -164,41 +160,35 @@ class DatumComparatorTest { Datum.real(Float.POSITIVE_INFINITY), Datum.doublePrecision(Double.POSITIVE_INFINITY), ), - EquivValues( - Datum.date(date(year = 1992, month = 8, day = 22)) - ), - EquivValues( - Datum.date(date(year = 2021, month = 8, day = 22)) - ), - // Set a [timeZone] for every [TimeValue] and [TimestampValue] since comparison between time types without - // a timezone results in an error. TODO: add a way to compare between time and timestamp types - EquivValues( - Datum.time(time(hour = 12, minute = 12, second = 12, timeZone = TimeZone.UnknownTimeZone)), - Datum.time(time(hour = 12, minute = 12, second = 12, nano = 0, timeZone = TimeZone.UnknownTimeZone)), - Datum.time(time(hour = 12, minute = 12, second = 12, timeZone = TimeZone.UnknownTimeZone)), - // time second precision handled by time constructor - Datum.time(time(hour = 12, minute = 12, second = 12, timeZone = TimeZone.UtcOffset.of(0))), - ), - EquivValues( - Datum.time(time(hour = 12, minute = 12, second = 12, nano = 100000000, timeZone = TimeZone.UnknownTimeZone)), - ), - EquivValues( - Datum.time(time(hour = 12, minute = 12, second = 12, nano = 0, timeZone = TimeZone.UtcOffset.of(-8, 0))), - Datum.time(time(hour = 12, minute = 12, second = 12, timeZone = TimeZone.UtcOffset.of(-8, 0))), - ), - EquivValues( - Datum.time(time(hour = 12, minute = 12, second = 12, nano = 100000000, timeZone = TimeZone.UtcOffset.of(-9, 0))), - ), - EquivValues( - Datum.timestamp(timestamp(year = 2017, timeZone = TimeZone.UtcOffset.of(0, 0))), // `2017T` - Datum.timestamp(timestamp(year = 2017, month = 1, timeZone = TimeZone.UtcOffset.of(0, 0))), // `2017-01T` - Datum.timestamp(timestamp(year = 2017, month = 1, day = 1, timeZone = TimeZone.UtcOffset.of(0, 0))), // `2017-01-01T` - Datum.timestamp(timestamp(year = 2017, month = 1, day = 1, hour = 0, minute = 0, second = 0, timeZone = TimeZone.UtcOffset.of(0, 0))), // `2017-01-01T00:00-00:00` - Datum.timestamp(timestamp(year = 2017, month = 1, day = 1, hour = 1, minute = 0, second = 0, timeZone = TimeZone.UtcOffset.of(1, 0))) // `2017-01-01T01:00+01:00` - ), - EquivValues( - Datum.timestamp(timestamp(year = 2017, month = 1, day = 1, hour = 1, minute = 0, second = 0, timeZone = TimeZone.UtcOffset.of(0, 0))) // `2017-01-01T01:00Z` - ), + // EquivValues( + // Datum.date(LocalDate.of(1992, 8, 22)) + // ), + // EquivValues( + // Datum.date(LocalDate.of(2021, 8, 22)) + // ), + // // Set a [timeZone] for every [TimeValue] and [TimestampValue] since comparison between time types without + // // a timezone results in an error. TODO: add a way to compare between time and timestamp types + // EquivValues( + // Datum.time(LocalTime.of(12, 12, 12), 0), + // Datum.time(LocalTime.of(12, 12, 12, 0), 0), + // ), + // EquivValues( + // Datum.time(LocalTime.of(12, 12, 12, 100000000), 9), + // ), + // EquivValues( + // Datum.timez(OffsetTime.of(12, 12, 12, 0, ZoneOffset.ofHours(-8)), 0), + // ), + // EquivValues( + // Datum.timez(OffsetTime.of(12, 12, 12, 100000000, ZoneOffset.ofHours(-9)), 9), + // ), + // EquivValues( + // // Datum.timestampz(OffsetDateTime.of(2017, 1, 1, 1, 0, 0, 0, ZoneOffset.UTC), 6), + // Datum.timestampz(OffsetDateTime.of(2017, 1, 1, 1, 0, 0, 0, ZoneOffset.ofHoursMinutes(0, 0)), 6), // `2017-01-01T00:00-00:00` + // // Datum.timestampz(OffsetDateTime.of(2017, 1, 1, 1, 1, 0, 0, ZoneOffset.ofHoursMinutes(0, 1)), 6), // `2017-01-01T00:00-00:01` + // ), + // EquivValues( + // Datum.timestampz(OffsetDateTime.of(2017, 1, 1, 1, 0, 0, 0, ZoneOffset.UTC), 6) // `2017-01-01T01:00Z` + // ), EquivValues( Datum.string(""), // TODO: Datum.string("", annotations = listOf("foobar")), @@ -415,7 +405,15 @@ class DatumComparatorTest { EquivValues( // The ordered values are: true, true, 1, 1, 1 // <> - Datum.bag(listOf(Datum.bool(true), Datum.integer(1), Datum.decimal(BigDecimal("1.0")), Datum.real(1e0f), Datum.bool(true))) + Datum.bag( + listOf( + Datum.bool(true), + Datum.integer(1), + Datum.decimal(BigDecimal("1.0")), + Datum.real(1e0f), + Datum.bool(true) + ) + ) ), EquivValues( // <<1>> Datum.bag(listOf(Datum.integer(1))) diff --git a/partiql-types/src/main/java/org/partiql/types/PType.java b/partiql-types/src/main/java/org/partiql/types/PType.java index d75299ac8..7d16a0a0b 100644 --- a/partiql-types/src/main/java/org/partiql/types/PType.java +++ b/partiql-types/src/main/java/org/partiql/types/PType.java @@ -113,6 +113,8 @@ public static int[] codes() { PType.TIMEZ, PType.TIMESTAMP, PType.TIMESTAMPZ, + PType.INTERVAL_YM, + PType.INTERVAL_DT, PType.ARRAY, PType.BAG, PType.ROW, diff --git a/test/partiql-randomized-tests/src/test/kotlin/org/partiql/lang/randomized/syntax/PartiQLParserDateTimeRandomizedTests.kt b/test/partiql-randomized-tests/src/test/kotlin/org/partiql/lang/randomized/syntax/PartiQLParserDateTimeRandomizedTests.kt index 540594246..09d5b1daf 100644 --- a/test/partiql-randomized-tests/src/test/kotlin/org/partiql/lang/randomized/syntax/PartiQLParserDateTimeRandomizedTests.kt +++ b/test/partiql-randomized-tests/src/test/kotlin/org/partiql/lang/randomized/syntax/PartiQLParserDateTimeRandomizedTests.kt @@ -19,7 +19,7 @@ import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.partiql.lang.randomized.eval.assertExpression import org.partiql.spi.value.Datum -import org.partiql.value.datetime.DateTimeValue +import java.time.LocalDate import java.util.Random class PartiQLParserDateTimeRandomizedTests { @@ -60,7 +60,7 @@ class PartiQLParserDateTimeRandomizedTests { val monthStr = date.month.toString().padStart(2, '0') val dayStr = date.day.toString().padStart(2, '0') assertExpression("DATE '$yearStr-$monthStr-$dayStr'") { - Datum.date(DateTimeValue.date(date.year, date.month, date.day)) + Datum.date(LocalDate.of(date.year, date.month, date.day)) } } } diff --git a/test/partiql-tests-runner/src/test/kotlin/org/partiql/runner/io/DatumIonReader.kt b/test/partiql-tests-runner/src/test/kotlin/org/partiql/runner/io/DatumIonReader.kt index 67fa93c5b..3cf740c2c 100644 --- a/test/partiql-tests-runner/src/test/kotlin/org/partiql/runner/io/DatumIonReader.kt +++ b/test/partiql-tests-runner/src/test/kotlin/org/partiql/runner/io/DatumIonReader.kt @@ -8,12 +8,14 @@ import com.amazon.ionelement.api.IonElement import org.partiql.spi.value.Datum import org.partiql.spi.value.Field import org.partiql.types.PType -import org.partiql.value.datetime.DateTimeValue -import org.partiql.value.datetime.TimeZone import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.IOException import java.io.InputStream +import java.math.BigDecimal +import java.time.LocalDate +import java.time.LocalTime +import java.time.ZoneOffset /** * Make an arbitrary call on partiql value read @@ -106,8 +108,21 @@ class DatumIonReader( Datum.decimal(d, d.precision(), d.scale()) } IonType.TIMESTAMP -> { - val ts = DateTimeValue.timestamp(reader.timestampValue()) - Datum.timestamp(ts) + val ts = reader.timestampValue() + val year = ts.year + val month = ts.month + val day = ts.day + val hour = ts.hour + val minute = ts.minute + val ds = ts.decimalSecond + val seconds = ds.toInt() // possible precision loss + val nanos = ds.remainder(BigDecimal.ONE).movePointRight(10).toLong() + // if (ts.localOffset != 0) { + // Datum.timestampz() + // } else { + // Datum.timestamp() + // } + TODO("DatumIon timestamp") } IonType.STRING, IonType.SYMBOL -> Datum.string(reader.stringValue()) IonType.CLOB -> Datum.clob(reader.newBytes()) @@ -225,26 +240,28 @@ class DatumIonReader( throw IllegalArgumentException("excess field in struct") } reader.stepOut() - Datum.date(DateTimeValue.date(year.int, month.int, day.int)) + Datum.date(LocalDate.of(year.int, month.int, day.int)) } PARTIQL_ANNOTATION.TIME_ANNOTATION -> { reader.stepIn() val map = mutableMapOf() val hour = getRequiredFieldName(reader, "hour") val minute = getRequiredFieldName(reader, "minute") - val second = getRequiredFieldName(reader, "second") + val second = getRequiredFieldName(reader, "second").bigDecimal + val seconds = second.toInt() // possible precision loss + val nanos = second.remainder(BigDecimal.ONE).movePointRight(10).toInt() val offset: Datum? = getOptionalFieldName(reader, "offset") // check remaining if (reader.next() != null) { throw IllegalArgumentException("excess field in struct") } reader.stepOut() - val offsetTz = when { - offset == null -> null - offset.isNull -> TimeZone.UnknownTimeZone - else -> TimeZone.UtcOffset.of(offset.int) + val time = LocalTime.of(hour.int, minute.int, seconds, nanos) + when { + offset == null || + offset.isNull -> Datum.time(time, second.precision()) + else -> Datum.timez(time.atOffset(ZoneOffset.ofHours(offset.int)), second.precision()) } - Datum.time(DateTimeValue.time(hour.int, minute.int, second.bigDecimal, offsetTz)) } PARTIQL_ANNOTATION.TIMESTAMP_ANNOTATION -> { reader.stepIn() @@ -260,13 +277,7 @@ class DatumIonReader( throw IllegalArgumentException("excess field in struct") } reader.stepOut() - Datum.timestamp( - DateTimeValue.timestamp( - year.int, month.int, day.int, - hour.int, minute.int, second.bigDecimal, - null - ) - ) + TODO("timestamp parsing") } null -> fromIonGeneric(reader) else -> error("Unsupported annotation.")