Repository: partiql/partiql-lang-kotlin (kotlin)

Issue:
Add support for `INTERVAL` in conformance test runner
`partiql-lang-kotlin` issue to track eventually adding `INTERVAL` in the conformance test runner. Need https://github.com/partiql/partiql-tests/issues/145 to be implemented first and for us to remove `PartiQLValue` from the testing (https://github.com/partiql/partiql-lang-kotlin/issues/1782).

D.o.D

- [x] Define partiql-tests representation of INTERVAL in Ion
- [x] Add INTERVAL to evaluation test runner in partiql-tests
- [x] [Out of scope/can be tracked separately] port existing INTERVAL tests to partiql-tests

- [Literal tests](https://github.com/partiql/partiql-lang-kotlin/blob/b0941184b750e38056ad43b826e69b9b834eec78/partiql-eval/src/test/kotlin/org/partiql/eval/internal/LiteralIntervalTests.kt)
- [Parsing sign](https://github.com/partiql/partiql-lang-kotlin/blob/b0941184b750e38056ad43b826e69b9b834eec78/partiql-eval/src/test/kotlin/org/partiql/eval/internal/IntervalParseSignedTests.kt)
- [Arithmetic: plus](https://github.com/partiql/partiql-lang-kotlin/blob/b0941184b750e38056ad43b826e69b9b834eec78/partiql-eval/src/test/kotlin/org/partiql/eval/internal/IntervalPlusTests.kt)
- [Arithmetic: minus](https://github.com/partiql/partiql-lang-kotlin/blob/b0941184b750e38056ad43b826e69b9b834eec78/partiql-eval/src/test/kotlin/org/partiql/eval/internal/IntervalMinusTests.kt)
- [Arithmetic: times](https://github.com/partiql/partiql-lang-kotlin/blob/b0941184b750e38056ad43b826e69b9b834eec78/partiql-eval/src/test/kotlin/org/partiql/eval/internal/IntervalTimesTests.kt)
- [Arithmetic: divide](https://github.com/partiql/partiql-lang-kotlin/blob/b0941184b750e38056ad43b826e69b9b834eec78/partiql-eval/src/test/kotlin/org/partiql/eval/internal/IntervalDivideTests.kt)
- [Abs](https://github.com/partiql/partiql-lang-kotlin/blob/b0941184b750e38056ad43b826e69b9b834eec78/partiql-eval/src/test/kotlin/org/partiql/eval/internal/PartiQLEvaluatorTest.kt#L1489-L1649)

- [x] Add additional evaluation tests. See https://github.com/partiql/partiql-tests/issues/144 to track.

Pull request description:
## Relevant Issues
- Closes Issue #1783 

## Description
- Define `Ion` representation for `INTERVAL` in conformance tests
- Add support for `INTERVAL` in `partiql-tests-runner`

## Other Information
- Updated Unreleased Section in CHANGELOG: **[NO]**
- Any backward-incompatible changes? **[NO]**
- Any new external dependencies? **[NO]**
- Do your changes comply with the [contributing][cg] and [code style][csg] guidelines? **[YES]**

## License Information

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

<!-- DO NOT DELETE BELOW -->

[cg]: https://github.com/partiql/partiql-lang-kotlin/blob/main/CONTRIBUTING.md
[csg]: https://github.com/partiql/partiql-lang-kotlin/blob/main/CODE_STYLE.md

Patch:
diff --git a/partiql-spi/src/main/java/org/partiql/spi/value/DatumComparator.java b/partiql-spi/src/main/java/org/partiql/spi/value/DatumComparator.java
index b56948108..d8785df21 100644
--- a/partiql-spi/src/main/java/org/partiql/spi/value/DatumComparator.java
+++ b/partiql-spi/src/main/java/org/partiql/spi/value/DatumComparator.java
@@ -561,7 +561,7 @@ private static DatumComparison[] fillTimeComparator(DatumComparison[] comps) {
 
     @SuppressWarnings({"UnusedReturnValue"})
     private static DatumComparison[] fillIntervalComparator(DatumComparison[] comps) {
-        comps[INTERVAL_YM] = (self, other, comp) -> Long.compare(self.getTotalMonths(), self.getTotalMonths());
+        comps[INTERVAL_YM] = (self, other, comp) -> Long.compare(self.getTotalMonths(), other.getTotalMonths());
         comps[INTERVAL_DT] = (self, other, comp) -> {
             int comparison = Long.compare(self.getTotalSeconds(), other.getTotalSeconds());
             if (comparison != 0) {


Test patch:
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 efce97dc2..ad50a0a73 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
@@ -31,12 +31,16 @@ class DatumIonReader(
     private val ionReader: IonReader,
     private val sourceDataFormat: DatumIonReaderBuilder.SourceDataFormat
 ) : AutoCloseable {
+    private val INTERVAL_MAX_PRECISION = 9
+    private val INTERVAL_MAX_FRACTIONAL_PRECISION = 9
     private enum class PARTIQL_ANNOTATION(val annotation: String) {
         MISSING_ANNOTATION("\$missing"),
         BAG_ANNOTATION("\$bag"),
         DATE_ANNOTATION("\$date"),
         TIME_ANNOTATION("\$time"),
         TIMESTAMP_ANNOTATION("\$timestamp"),
+        INTERVAL_YM_ANNOTATION("\$interval_ym"),
+        INTERVAL_DT_ANNOTATION("\$interval_dt"),
         GRAPH_ANNOTATION("\$graph")
     }
 
@@ -158,6 +162,8 @@ class DatumIonReader(
             PARTIQL_ANNOTATION.DATE_ANNOTATION -> assert(type == IonType.STRUCT)
             PARTIQL_ANNOTATION.TIME_ANNOTATION -> assert(type == IonType.STRUCT)
             PARTIQL_ANNOTATION.TIMESTAMP_ANNOTATION -> assert(type == IonType.STRUCT)
+            PARTIQL_ANNOTATION.INTERVAL_YM_ANNOTATION -> assert(type == IonType.STRUCT)
+            PARTIQL_ANNOTATION.INTERVAL_DT_ANNOTATION -> assert(type == IonType.STRUCT)
             PARTIQL_ANNOTATION.GRAPH_ANNOTATION -> assert(type == IonType.STRUCT)
         }
     }
@@ -178,6 +184,8 @@ class DatumIonReader(
                 PARTIQL_ANNOTATION.DATE_ANNOTATION -> Datum.nullValue(PType.date())
                 PARTIQL_ANNOTATION.TIME_ANNOTATION -> Datum.nullValue(PType.time(6))
                 PARTIQL_ANNOTATION.TIMESTAMP_ANNOTATION -> Datum.nullValue(PType.timestamp(6))
+                PARTIQL_ANNOTATION.INTERVAL_YM_ANNOTATION -> Datum.nullValue(PType.intervalYearMonth(INTERVAL_MAX_PRECISION))
+                PARTIQL_ANNOTATION.INTERVAL_DT_ANNOTATION -> Datum.nullValue(PType.intervalDaySecond(INTERVAL_MAX_PRECISION, INTERVAL_MAX_FRACTIONAL_PRECISION))
                 PARTIQL_ANNOTATION.GRAPH_ANNOTATION -> TODO("Graph not yet implemented")
                 null -> Datum.nullValue()
             }
@@ -291,6 +299,65 @@ class DatumIonReader(
                         val time = LocalTime.of(hour.int, minute.int, seconds, nanoOfSecond)
                         Datum.timestampz(OffsetDateTime.of(date, time, offsetTz), 9)
                     }
+                    PARTIQL_ANNOTATION.INTERVAL_YM_ANNOTATION -> {
+                        reader.stepIn()
+                        var sign: String? = null
+                        var years = 0
+                        var months = 0
+                        while (reader.next() != null) {
+                            when (reader.fieldName) {
+                                "sign" -> sign = reader.stringValue()
+                                "years" -> years = reader.intValue()
+                                "months" -> months = reader.intValue()
+                                else -> throw IllegalArgumentException("unexpected field: ${reader.fieldName}")
+                            }
+                        }
+                        reader.stepOut()
+                        val signMultiplier = if (sign == "-") -1 else 1
+                        when {
+                            years < 0 -> throw IllegalArgumentException("years must be non-negative, got: $years")
+                            months < 0 -> throw IllegalArgumentException("months must be non-negative, got: $months")
+                        }
+                        Datum.intervalYearMonth(signMultiplier * years, signMultiplier * months, INTERVAL_MAX_PRECISION)
+                    }
+                    PARTIQL_ANNOTATION.INTERVAL_DT_ANNOTATION -> {
+                        reader.stepIn()
+                        var sign: String? = null
+                        var days = 0
+                        var hours = 0
+                        var minutes = 0
+                        var seconds = 0
+                        var nanos = 0
+                        while (reader.next() != null) {
+                            when (reader.fieldName) {
+                                "sign" -> sign = reader.stringValue()
+                                "days" -> days = reader.intValue()
+                                "hours" -> hours = reader.intValue()
+                                "minutes" -> minutes = reader.intValue()
+                                "seconds" -> seconds = reader.intValue()
+                                "nanos" -> nanos = reader.intValue()
+                                else -> throw IllegalArgumentException("unexpected field: ${reader.fieldName}")
+                            }
+                        }
+                        reader.stepOut()
+                        val signMultiplier = if (sign == "-") -1 else 1
+                        when {
+                            days < 0 -> throw IllegalArgumentException("days must be non-negative, got: $days")
+                            hours < 0 -> throw IllegalArgumentException("hours must be non-negative, got: $hours")
+                            minutes < 0 -> throw IllegalArgumentException("minutes must be non-negative, got: $minutes")
+                            seconds < 0 -> throw IllegalArgumentException("seconds must be non-negative, got: $seconds")
+                            nanos < 0 -> throw IllegalArgumentException("nanos must be non-negative, got: $nanos")
+                        }
+                        Datum.intervalDaySecond(
+                            signMultiplier * days,
+                            signMultiplier * hours,
+                            signMultiplier * minutes,
+                            signMultiplier * seconds,
+                            signMultiplier * nanos,
+                            INTERVAL_MAX_PRECISION,
+                            INTERVAL_MAX_FRACTIONAL_PRECISION,
+                        )
+                    }
                     null -> fromIonGeneric(reader)
                     else -> error("Unsupported annotation.")
                 }
diff --git a/test/partiql-tests-runner/src/test/kotlin/org/partiql/runner/util/DatumUtil.kt b/test/partiql-tests-runner/src/test/kotlin/org/partiql/runner/util/DatumUtil.kt
index c23fca0be..b3d339b75 100644
--- a/test/partiql-tests-runner/src/test/kotlin/org/partiql/runner/util/DatumUtil.kt
+++ b/test/partiql-tests-runner/src/test/kotlin/org/partiql/runner/util/DatumUtil.kt
@@ -15,6 +15,7 @@ import com.amazon.ionelement.api.ionStructOf
 import com.amazon.ionelement.api.ionTimestamp
 import org.partiql.spi.types.PType
 import org.partiql.spi.value.Datum
+import kotlin.math.abs
 
 // For error display only
 internal fun Datum.toIonElement(): IonElement {
@@ -54,6 +55,33 @@ internal fun Datum.toIonElement(): IonElement {
                     fields.add(field("timezone_minute", ionInt(timezoneMinutes.toLong())))
                     ionStructOf(fields).withAnnotations("\$time")
                 }
+                PType.INTERVAL_YM -> {
+                    val fields = mutableListOf<StructField>()
+                    val totalMonths = this.totalMonths
+                    if (totalMonths < 0) {
+                        fields.add(field("sign", ionString("-")))
+                    } else {
+                        fields.add(field("sign", ionString("+")))
+                    }
+                    fields.add(field("years", ionInt(abs(this.years).toLong())))
+                    fields.add(field("months", ionInt(abs(this.months).toLong())))
+                    ionStructOf(fields).withAnnotations("\$interval_ym")
+                }
+                PType.INTERVAL_DT -> {
+                    val fields = mutableListOf<StructField>()
+                    val totalSeconds = this.totalSeconds
+                    if (totalSeconds < 0 || this.nanos < 0) {
+                        fields.add(field("sign", ionString("-")))
+                    } else {
+                        fields.add(field("sign", ionString("+")))
+                    }
+                    fields.add(field("days", ionInt(abs(this.days).toLong())))
+                    fields.add(field("hours", ionInt(abs(this.hours).toLong())))
+                    fields.add(field("minutes", ionInt(abs(this.minutes).toLong())))
+                    fields.add(field("seconds", ionInt(abs(this.seconds).toLong())))
+                    fields.add(field("nanos", ionInt(abs(this.nanos).toLong())))
+                    ionStructOf(fields).withAnnotations("\$interval_dt")
+                }
                 PType.ARRAY -> {
                     val elements = mutableListOf<IonElement>()
                     for (element in this) {