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

Issue:
`INTERVAL` times and divide with numerics
SQL spec defines a few more operations that are not yet implemented in partiql-lang-kotlin. These are also pointed out in https://github.com/partiql/partiql-lang-kotlin/pull/1741's description.

Operations include
- [ ] INTERVAL * NUMERIC -> INTERVAL
- [ ] INTERVAL / NUMERIC -> INTERVAL
- [ ] NUMERIC * INTERVAL -> INTERVAL

Example expected query behavior:
```
-- note: the following query currently returns `MISSING`
partiql ▶ INTERVAL '20' YEAR * 2;

=== RESULT ===
INTERVAL '40' YEAR (2)

OK!
```

Pull request:
## Relevant Issues
- Closes Issue #1779

## Description
- Add support `INTERVAL` times or divide with numerics 

## Other Information
- Updated Unreleased Section in CHANGELOG: **YES**
- 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/CHANGELOG.md b/CHANGELOG.md
index 313a64f12..fe29a0c15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -48,6 +48,7 @@ rewritten to their query representation
 - Fix alias collision problem when multiple table references need auto-generated aliases
 - Add `ABS` function support on `INTERVAL` field
 - Display of `Ion` in PartiQL CLI output
+- Support `INTERVAL` times and divide with numerics
 
 ### Removed
 
diff --git a/partiql-spi/src/main/java/org/partiql/spi/types/PType.java b/partiql-spi/src/main/java/org/partiql/spi/types/PType.java
index 9ba8ad200..4ff6b283b 100644
--- a/partiql-spi/src/main/java/org/partiql/spi/types/PType.java
+++ b/partiql-spi/src/main/java/org/partiql/spi/types/PType.java
@@ -326,7 +326,7 @@ public static int[] codes() {
     public static final int BIGINT = 5;
 
     /**
-     * PartiQL's big integer type.
+     * SQL's numeric type.
      * <br>
      * <br>
      * <b>Type Syntax</b>: <code>NUMERIC</code>
diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/DiadicOperator.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/DiadicOperator.kt
index 5531e8d99..7b89ae61b 100644
--- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/DiadicOperator.kt
+++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/DiadicOperator.kt
@@ -287,6 +287,24 @@ internal abstract class DiadicOperator(
         return null
     }
 
+    /**
+     * @param lhs a type of number
+     * @param rhs a type of interval
+     * @return an instance of a function
+     */
+    open fun getNumberIntervalInstance(lhs: PType, rhs: PType): Fn? {
+        return null
+    }
+
+    /**
+     * @param lhs a type of interval
+     * @param rhs a type of number
+     * @return an instance of a function
+     */
+    open fun getIntervalNumberInstance(lhs: PType, rhs: PType): Fn? {
+        return null
+    }
+
     /**
      * This is used when all operands are NULL/MISSING.
      * @return an instance of a function
@@ -386,6 +404,56 @@ internal abstract class DiadicOperator(
         fillTable(PType.TIMESTAMPZ, PType.INTERVAL_DT) { lhs, rhs -> instance(lhs, rhs) }
     }
 
+    private fun fillIntervalTable() {
+        // TinyInt
+        fillTable(PType.INTERVAL_YM, PType.TINYINT) { lhs, rhs -> getIntervalNumberInstance(lhs, rhs) }
+        fillTable(PType.INTERVAL_DT, PType.TINYINT) { lhs, rhs -> getIntervalNumberInstance(lhs, rhs) }
+        fillTable(PType.TINYINT, PType.INTERVAL_YM) { lhs, rhs -> getNumberIntervalInstance(lhs, rhs) }
+        fillTable(PType.TINYINT, PType.INTERVAL_DT) { lhs, rhs -> getNumberIntervalInstance(lhs, rhs) }
+
+        // SmallInt
+        fillTable(PType.INTERVAL_YM, PType.SMALLINT) { lhs, rhs -> getIntervalNumberInstance(lhs, rhs) }
+        fillTable(PType.INTERVAL_DT, PType.SMALLINT) { lhs, rhs -> getIntervalNumberInstance(lhs, rhs) }
+        fillTable(PType.SMALLINT, PType.INTERVAL_YM) { lhs, rhs -> getNumberIntervalInstance(lhs, rhs) }
+        fillTable(PType.SMALLINT, PType.INTERVAL_DT) { lhs, rhs -> getNumberIntervalInstance(lhs, rhs) }
+
+        // Integer
+        fillTable(PType.INTERVAL_YM, PType.INTEGER) { lhs, rhs -> getIntervalNumberInstance(lhs, rhs) }
+        fillTable(PType.INTERVAL_DT, PType.INTEGER) { lhs, rhs -> getIntervalNumberInstance(lhs, rhs) }
+        fillTable(PType.INTEGER, PType.INTERVAL_YM) { lhs, rhs -> getNumberIntervalInstance(lhs, rhs) }
+        fillTable(PType.INTEGER, PType.INTERVAL_DT) { lhs, rhs -> getNumberIntervalInstance(lhs, rhs) }
+
+        // BigInt
+        fillTable(PType.INTERVAL_YM, PType.BIGINT) { lhs, rhs -> getIntervalNumberInstance(lhs, rhs) }
+        fillTable(PType.INTERVAL_DT, PType.BIGINT) { lhs, rhs -> getIntervalNumberInstance(lhs, rhs) }
+        fillTable(PType.BIGINT, PType.INTERVAL_YM) { lhs, rhs -> getNumberIntervalInstance(lhs, rhs) }
+        fillTable(PType.BIGINT, PType.INTERVAL_DT) { lhs, rhs -> getNumberIntervalInstance(lhs, rhs) }
+
+        // Numeric
+        fillTable(PType.INTERVAL_YM, PType.NUMERIC) { lhs, rhs -> getIntervalNumberInstance(lhs, rhs) }
+        fillTable(PType.INTERVAL_DT, PType.NUMERIC) { lhs, rhs -> getIntervalNumberInstance(lhs, rhs) }
+        fillTable(PType.NUMERIC, PType.INTERVAL_YM) { lhs, rhs -> getNumberIntervalInstance(lhs, rhs) }
+        fillTable(PType.NUMERIC, PType.INTERVAL_DT) { lhs, rhs -> getNumberIntervalInstance(lhs, rhs) }
+
+        // Decimal
+        fillTable(PType.INTERVAL_YM, PType.DECIMAL) { lhs, rhs -> getIntervalNumberInstance(lhs, rhs) }
+        fillTable(PType.INTERVAL_DT, PType.DECIMAL) { lhs, rhs -> getIntervalNumberInstance(lhs, rhs) }
+        fillTable(PType.DECIMAL, PType.INTERVAL_YM) { lhs, rhs -> getNumberIntervalInstance(lhs, rhs) }
+        fillTable(PType.DECIMAL, PType.INTERVAL_DT) { lhs, rhs -> getNumberIntervalInstance(lhs, rhs) }
+
+        // REAL
+        fillTable(PType.INTERVAL_YM, PType.REAL) { lhs, rhs -> getIntervalNumberInstance(lhs, rhs) }
+        fillTable(PType.INTERVAL_DT, PType.REAL) { lhs, rhs -> getIntervalNumberInstance(lhs, rhs) }
+        fillTable(PType.REAL, PType.INTERVAL_YM) { lhs, rhs -> getNumberIntervalInstance(lhs, rhs) }
+        fillTable(PType.REAL, PType.INTERVAL_DT) { lhs, rhs -> getNumberIntervalInstance(lhs, rhs) }
+
+        // DOUBLE
+        fillTable(PType.INTERVAL_YM, PType.DOUBLE) { lhs, rhs -> getIntervalNumberInstance(lhs, rhs) }
+        fillTable(PType.INTERVAL_DT, PType.DOUBLE) { lhs, rhs -> getIntervalNumberInstance(lhs, rhs) }
+        fillTable(PType.DOUBLE, PType.INTERVAL_YM) { lhs, rhs -> getNumberIntervalInstance(lhs, rhs) }
+        fillTable(PType.DOUBLE, PType.INTERVAL_DT) { lhs, rhs -> getNumberIntervalInstance(lhs, rhs) }
+    }
+
     private fun fillBlobTable(instance: (PType, PType) -> Fn?) {
         fillTable(PType.BLOB, PType.BLOB) { lhs, rhs -> instance(lhs, rhs) }
         fillTable(PType.BLOB, PType.UNKNOWN) { lhs, _ -> instance(lhs, lhs) }
@@ -458,6 +526,7 @@ internal abstract class DiadicOperator(
         fillNumberTable(PType.DOUBLE, ::getDoubleInstance)
         fillTimeTable(::getTimeInstance)
         fillDateTable(::getDateInstance)
+        fillIntervalTable()
         fillIntervalTable(::getIntervalInstance)
         fillTable(PType.DATE, PType.INTERVAL_YM, ::getDateIntervalInstance)
         fillTable(PType.DATE, PType.INTERVAL_DT, ::getDateIntervalInstance)
@@ -476,6 +545,7 @@ internal abstract class DiadicOperator(
         fillTable(PType.INTERVAL_DT, PType.TIMESTAMP, ::getIntervalTimestampInstance)
         fillTable(PType.INTERVAL_YM, PType.TIMESTAMPZ, ::getIntervalTimestampInstance) // TODO: Create a specific timezone impl
         fillTable(PType.INTERVAL_DT, PType.TIMESTAMPZ, ::getIntervalTimestampInstance) // TODO: Create a specific timezone impl
+
         fillBlobTable(::getBlobInstance)
         fillTimestampTable(::getTimestampInstance)
         fillCharacterStringTable(PType.STRING, ::getStringInstance)
diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnDivide.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnDivide.kt
index 63b2ad25c..a169d26a9 100644
--- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnDivide.kt
+++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnDivide.kt
@@ -6,7 +6,9 @@ package org.partiql.spi.function.builtins
 import org.partiql.spi.function.Fn
 import org.partiql.spi.function.builtins.internal.PErrors
 import org.partiql.spi.types.PType
+import org.partiql.spi.utils.IntervalUtils
 import org.partiql.spi.utils.NumberUtils.isZero
+import org.partiql.spi.utils.getNumber
 import org.partiql.spi.value.Datum
 import java.math.RoundingMode
 
@@ -129,4 +131,17 @@ internal object FnDivide : DiadicArithmeticOperator("divide") {
             Datum.doublePrecision(arg0 / arg1)
         }
     }
+
+    override fun getIntervalNumberInstance(lhs: PType, rhs: PType): Fn {
+        val op = IntervalUtils.intervalDivide(lhs)
+        return basic(lhs, lhs, rhs) { args ->
+            val interval = args[0]
+            val number = args[1].getNumber()
+            if (number.isZero()) {
+                throw PErrors.divisionByZeroException(interval, lhs)
+            }
+
+            op(interval, number)
+        }
+    }
 }
diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTimes.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTimes.kt
index 731a430db..5abc164c3 100644
--- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTimes.kt
+++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTimes.kt
@@ -6,8 +6,10 @@ package org.partiql.spi.function.builtins
 import org.partiql.spi.function.Fn
 import org.partiql.spi.function.builtins.internal.PErrors
 import org.partiql.spi.types.PType
+import org.partiql.spi.utils.IntervalUtils
 import org.partiql.spi.utils.NumberUtils.byteOverflows
 import org.partiql.spi.utils.NumberUtils.shortOverflows
+import org.partiql.spi.utils.getNumber
 import org.partiql.spi.value.Datum
 
 internal object FnTimes : DiadicArithmeticOperator("times") {
@@ -116,4 +118,22 @@ internal object FnTimes : DiadicArithmeticOperator("times") {
             Datum.doublePrecision(arg0 * arg1)
         }
     }
+
+    override fun getIntervalNumberInstance(lhs: PType, rhs: PType): Fn {
+        val op = IntervalUtils.intervalMultiply(lhs)
+        return basic(lhs, lhs, rhs) { args ->
+            val interval = args[0]
+            val number = args[1].getNumber()
+            op(interval, number)
+        }
+    }
+
+    override fun getNumberIntervalInstance(lhs: PType, rhs: PType): Fn {
+        val op = IntervalUtils.intervalMultiply(rhs)
+        return basic(rhs, lhs, rhs) { args ->
+            val number = args[0].getNumber()
+            val interval = args[1]
+            op(interval, number)
+        }
+    }
 }
diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/utils/IntervalUtils.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/utils/IntervalUtils.kt
new file mode 100644
index 000000000..19139218f
--- /dev/null
+++ b/partiql-spi/src/main/kotlin/org/partiql/spi/utils/IntervalUtils.kt
@@ -0,0 +1,174 @@
+package org.partiql.spi.utils
+
+import org.partiql.spi.types.IntervalCode
+import org.partiql.spi.types.PType
+import org.partiql.spi.value.Datum
+import java.math.BigDecimal
+import java.math.BigInteger
+import java.math.RoundingMode
+
+internal object IntervalUtils {
+    // According to SQL1992, whether to truncate or round in the least significant
+    // field of the result is implementation-defined. We implemented as truncation for Interval as most database systems do.
+    private val INTERVAL_ROUNDING_MODE = RoundingMode.DOWN
+
+    private const val INTERVAL_DEFAULT_FRACTIONAL_PRECISION = 6
+    private const val NANO_MAX_PRECISION = 9
+    private const val MONTHS_PER_YEAR = 12L
+    private const val SECONDS_PER_MINUTE = 60L
+    private const val MINUTES_PER_HOUR = 60L
+    private const val HOURS_PER_DAY = 24L
+    private const val SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR
+    private const val SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY
+    private const val NANOS_PER_SECOND = 1_000_000_000L
+
+    /**
+     * Divides an interval by a numeric value.
+     *
+     * @param interval The interval type to divide
+     * @return A function that takes an interval Datum and BigDecimal divisor, returns the divided interval
+     */
+    fun intervalDivide(interval: PType): ((Datum, Number) -> Datum) {
+        return when (interval.intervalCode) {
+            IntervalCode.YEAR,
+            IntervalCode.MONTH,
+            IntervalCode.YEAR_MONTH -> { i, number ->
+                // get total months from the interval as Long Type as it can hold the interval with max precision = 9.
+                val totalMonths: Long = i.years * MONTHS_PER_YEAR + i.months
+                // For approximate number type, it will introduce inaccuracy when converting to BigDecimal,
+                // so we keep it as approximate type for the calculation.
+                // Intermediate result is converted to Long type, which will lead to fractional part truncated
+                val resultTotalMonths: Long = when (number) {
+                    is Float,
+                    is Double -> (totalMonths / number.toDouble()).toLong()
+                    else -> NumberUtils.bigDecimalOf(totalMonths).divide(NumberUtils.bigDecimalOf(number), INTERVAL_ROUNDING_MODE).toLong()
+                }
+
+                val years = (resultTotalMonths / MONTHS_PER_YEAR).toInt()
+                val months = (resultTotalMonths % MONTHS_PER_YEAR).toInt()
+                Datum.intervalYearMonth(years, months, interval.precision)
+            }
+
+            IntervalCode.DAY,
+            IntervalCode.DAY_HOUR,
+            IntervalCode.DAY_MINUTE,
+            IntervalCode.DAY_SECOND,
+            IntervalCode.HOUR,
+            IntervalCode.HOUR_MINUTE,
+            IntervalCode.HOUR_SECOND,
+            IntervalCode.MINUTE,
+            IntervalCode.MINUTE_SECOND,
+            IntervalCode.SECOND -> { i, number ->
+                // When float/double is converted to BigDecimal, NumberUtils.bigDecimalOf will introduce inaccuracy for double due to the nature of approximate type
+                // For example, 0.1 could be 0.100000000000000111 which leads to computing inaccuracy.
+                val numberInBigDecimal = NumberUtils.bigDecimalOf(number)
+                val totalSecondsInBigDecimal = toSeconds(i)
+                val resultInBigDecimal = totalSecondsInBigDecimal.divide(numberInBigDecimal, NANO_MAX_PRECISION, INTERVAL_ROUNDING_MODE)
+                fromSecond(resultInBigDecimal, interval.precision, getFractionPrecision(interval))
+            }
+
+            else -> throw IllegalArgumentException("Unable to calculate division for INTERVAL expression")
+        }
+    }
+
+    /**
+     * Multiplies an interval by a numeric value.
+     *
+     * @param interval The interval type to multiply
+     * @return A function that takes an interval Datum and BigDecimal multiplier, returns the multiplied interval
+     */
+    fun intervalMultiply(interval: PType): ((Datum, Number) -> Datum) {
+        return when (interval.intervalCode) {
+            IntervalCode.YEAR,
+            IntervalCode.MONTH,
+            IntervalCode.YEAR_MONTH -> { i, number ->
+                // get total months from the interval as Long Type as it can hold the interval with max precision = 9.
+                val totalMonths: Long = i.years * MONTHS_PER_YEAR + i.months
+                // For approximate number type, it will introduce inaccuracy when converting to BigDecimal,
+                // so we keep it as approximate type for the calculation.
+                // Intermediate result is converted to Long type, which will lead to fractional part truncated
+                val resultTotalMonths: Long = when (number) {
+                    is Float,
+                    is Double -> (totalMonths * number.toDouble()).toLong()
+                    else -> NumberUtils.bigDecimalOf(totalMonths).multiply(NumberUtils.bigDecimalOf(number)).toLong()
+                }
+                val years = (resultTotalMonths / MONTHS_PER_YEAR).toInt()
+                val months = (resultTotalMonths % MONTHS_PER_YEAR).toInt()
+                Datum.intervalYearMonth(years, months, interval.precision)
+            }
+
+            IntervalCode.DAY,
+            IntervalCode.DAY_HOUR,
+            IntervalCode.DAY_MINUTE,
+            IntervalCode.DAY_SECOND,
+            IntervalCode.HOUR,
+            IntervalCode.HOUR_MINUTE,
+            IntervalCode.HOUR_SECOND,
+            IntervalCode.MINUTE,
+            IntervalCode.MINUTE_SECOND,
+            IntervalCode.SECOND -> { i, number ->
+                // When float/double is converted to BigDecimal, NumberUtils.bigDecimalOf will introduce inaccuracy for double due to the nature of approximate type
+                // For example, 0.1 could be 0.100000000000000111 which leads to computing inaccuracy.
+                val numberInBigDecimal = BigDecimal(number.toString())
+                val totalSecondsInBigDecimal = toSeconds(i)
+                val resultInBigDecimal = totalSecondsInBigDecimal.multiply(numberInBigDecimal)
+                fromSecond(resultInBigDecimal, interval.precision, getFractionPrecision(interval))
+            }
+
+            else -> throw IllegalArgumentException("Unable to calculate multiplication for INTERVAL expression")
+        }
+    }
+
+    private fun toSeconds(i: Datum): BigDecimal {
+        if (i.type.code() == PType.INTERVAL_DT) {
+            val daysInSeconds = i.days * SECONDS_PER_DAY
+            val hoursInSeconds = i.hours * SECONDS_PER_HOUR
+            val minutesInSeconds = i.minutes * SECONDS_PER_MINUTE
+            val totalSeconds = daysInSeconds + hoursInSeconds + minutesInSeconds + i.seconds
+            return BigDecimal.valueOf(totalSeconds).add(BigDecimal.valueOf(i.nanos.toLong(), NANO_MAX_PRECISION))
+        } else {
+            throw UnsupportedOperationException("Unable to convert non-INTERVAL_DT type to seconds")
+        }
+    }
+
+    private fun fromSecond(totalSeconds: BigDecimal, precision: Int, fractionalPrecision: Int): Datum {
+        val resultTotalNanos: BigInteger = totalSeconds.movePointRight(NANO_MAX_PRECISION).toBigIntegerExact()
+        val divRem = resultTotalNanos.divideAndRemainder(BigInteger.valueOf(NANOS_PER_SECOND))
+        val resultTotalSeconds = divRem[0].toLong()
+
+        val days = (resultTotalSeconds / SECONDS_PER_DAY).toInt()
+        val remainingSeconds = resultTotalSeconds % SECONDS_PER_DAY
+
+        val hours = (remainingSeconds / SECONDS_PER_HOUR).toInt()
+        val remainingSecondsAfterHours = remainingSeconds % SECONDS_PER_HOUR
+
+        val minutes = (remainingSecondsAfterHours / SECONDS_PER_MINUTE).toInt()
+        val seconds = remainingSecondsAfterHours % SECONDS_PER_MINUTE
+
+        return Datum.intervalDaySecond(
+            days,
+            hours,
+            minutes,
+            seconds.toInt(),
+            divRem[1].toInt(),
+            precision,
+            fractionalPrecision
+        )
+    }
+
+    private fun getFractionPrecision(interval: PType): Int {
+        return when (interval.intervalCode) {
+            IntervalCode.SECOND,
+            IntervalCode.MINUTE_SECOND,
+            IntervalCode.HOUR_SECOND,
+            IntervalCode.DAY_SECOND -> interval.fractionalPrecision
+            IntervalCode.DAY,
+            IntervalCode.DAY_HOUR,
+            IntervalCode.DAY_MINUTE,
+            IntervalCode.HOUR,
+            IntervalCode.HOUR_MINUTE,
+            IntervalCode.MINUTE -> INTERVAL_DEFAULT_FRACTIONAL_PRECISION
+            else -> throw IllegalArgumentException("Cannot get fractional precision for non-INTERVAL_DT type")
+        }
+    }
+}


Test patch:
diff --git a/partiql-eval/src/test/kotlin/org/partiql/eval/internal/IntervalDivideTests.kt b/partiql-eval/src/test/kotlin/org/partiql/eval/internal/IntervalDivideTests.kt
new file mode 100644
index 000000000..afaec156f
--- /dev/null
+++ b/partiql-eval/src/test/kotlin/org/partiql/eval/internal/IntervalDivideTests.kt
@@ -0,0 +1,191 @@
+package org.partiql.eval.internal
+
+import org.junit.jupiter.api.parallel.Execution
+import org.junit.jupiter.api.parallel.ExecutionMode
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.MethodSource
+import org.partiql.spi.value.Datum
+
+/**
+ * Test cases for interval division operations.
+ * Tests division of intervals by numeric values including integers, decimals, doubles,
+ * negative numbers, null, missing values, and edge cases like division by zero.
+ *
+ * Related Github issues:
+ * https://github.com/partiql/partiql-lang/issues/100
+ * https://github.com/partiql/partiql-lang-kotlin/issues/1784
+ */
+class IntervalDivideTests {
+
+    @ParameterizedTest
+    @MethodSource("intervalDivideNumberCases")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervalDivideNumber(tc: SuccessTestCase) = tc.run()
+
+    @ParameterizedTest
+    @MethodSource("intervalDivideNegativeCases")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervalDivideNegative(tc: SuccessTestCase) = tc.run()
+
+    @ParameterizedTest
+    @MethodSource("intervalDivideZeroCases")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervalDivideZero(tc: FailureTestCase) = tc.run()
+
+    @ParameterizedTest
+    @MethodSource("intervalDivideNullCases")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervalDivideNull(tc: SuccessTestCase) = tc.run()
+
+    @ParameterizedTest
+    @MethodSource("intervalDivideMissingCases")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervalDivideMissing(tc: SuccessTestCase) = tc.run()
+
+    @ParameterizedTest
+    @MethodSource("intervalDivideDecimalCases")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervalDivideDecimal(tc: SuccessTestCase) = tc.run()
+
+    @ParameterizedTest
+    @MethodSource("intervalDivideDoubleCases")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervalDivideDouble(tc: SuccessTestCase) = tc.run()
+
+    @ParameterizedTest
+    @MethodSource("intervalDivideWithPrecisionOverflowCases")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervalDivideWithPrecisionOverflowCases(tc: FailureTestCase) = tc.run()
+
+    companion object {
+        private const val INTERVAL_Y = "INTERVAL '3' YEAR"
+        private const val INTERVAL_M = "INTERVAL '7' MONTH"
+        private const val INTERVAL_YM = "INTERVAL '1-5' YEAR TO MONTH"
+
+        private const val INTERVAL_D = "INTERVAL '2' DAY"
+        private const val INTERVAL_H = "INTERVAL '4' HOUR"
+        private const val INTERVAL_MIN = "INTERVAL '5' MINUTE"
+        private const val INTERVAL_S = "INTERVAL '10.5' SECOND"
+        private const val INTERVAL_DTS = "INTERVAL '2 4:5:10.5' DAY TO SECOND"
+        private const val NANOS_PER_SECOND = 1000000000
+
+        private class Input(
+            val arg0: String,
+            val arg1: String,
+            val expected: Datum
+        )
+
+        @JvmStatic
+        fun intervalDivideNumberCases() = listOf(
+            Input(INTERVAL_Y, "3", Datum.intervalYearMonth(1, 0, 2)),
+            Input(INTERVAL_M, "3", Datum.intervalYearMonth(0, 2, 2)),
+            Input(INTERVAL_YM, "3", Datum.intervalYearMonth(0, 5, 2)),
+
+            Input(INTERVAL_D, "3", Datum.intervalDaySecond(0, 16, 0, 0, 0, 2, 6)),
+            Input(INTERVAL_H, "3", Datum.intervalDaySecond(0, 1, 20, 0, 0, 2, 6)),
+            Input(INTERVAL_MIN, "3", Datum.intervalDaySecond(0, 0, 1, 40, 0, 2, 6)),
+            Input(INTERVAL_S, "3", Datum.intervalDaySecond(0, 0, 0, 3, (0.5 * NANOS_PER_SECOND).toInt(), 2, 6)),
+            Input(INTERVAL_DTS, "3", Datum.intervalDaySecond(0, 17, 21, 43, (0.5 * NANOS_PER_SECOND).toInt(), 2, 6))
+        ).map { case ->
+            SuccessTestCase("${case.arg0} / ${case.arg1}", case.expected)
+        }
+
+        @JvmStatic
+        fun intervalDivideNegativeCases() = listOf(
+            Input(INTERVAL_Y, "-3", Datum.intervalYearMonth(-1, 0, 2)),
+            Input(INTERVAL_M, "-3", Datum.intervalYearMonth(0, -2, 2)),
+            Input(INTERVAL_YM, "-3", Datum.intervalYearMonth(0, -5, 2)),
+
+            Input(INTERVAL_D, "-3", Datum.intervalDaySecond(0, -16, 0, 0, 0, 2, 6)),
+            Input(INTERVAL_H, "-3", Datum.intervalDaySecond(0, -1, -20, 0, 0, 2, 6)),
+            Input(INTERVAL_MIN, "-3", Datum.intervalDaySecond(0, 0, -1, -40, 0, 2, 6)),
+            Input(INTERVAL_S, "-3", Datum.intervalDaySecond(0, 0, 0, -3, (-0.5 * NANOS_PER_SECOND).toInt(), 2, 6)),
+            Input(INTERVAL_DTS, "-3", Datum.intervalDaySecond(0, -17, -21, -43, (-0.5 * NANOS_PER_SECOND).toInt(), 2, 6))
+        ).map { case ->
+            SuccessTestCase("${case.arg0} / ${case.arg1}", case.expected)
+        }
+
+        @JvmStatic
+        fun intervalDivideDecimalCases() = listOf(
+            Input(INTERVAL_Y, "2.5", Datum.intervalYearMonth(1, 2, 2)),
+            Input(INTERVAL_M, "2.5", Datum.intervalYearMonth(0, 2, 2)),
+            Input(INTERVAL_YM, "2.5", Datum.intervalYearMonth(0, 6, 2)),
+
+            Input(INTERVAL_D, "2.5", Datum.intervalDaySecond(0, 19, 12, 0, 0, 2, 6)),
+            Input(INTERVAL_H, "2.5", Datum.intervalDaySecond(0, 1, 36, 0, 0, 2, 6)),
+            Input(INTERVAL_MIN, "2.5", Datum.intervalDaySecond(0, 0, 2, 0, 0, 2, 6)),
+            Input(INTERVAL_S, "2.5", Datum.intervalDaySecond(0, 0, 0, 4, 200000000, 2, 6)),
+            Input(INTERVAL_DTS, "2.5", Datum.intervalDaySecond(0, 20, 50, 4, 200000000, 2, 6))
+        ).map { case ->
+            SuccessTestCase("${case.arg0} / ${case.arg1}", case.expected)
+        }
+
+        @JvmStatic
+        fun intervalDivideDoubleCases() = listOf(
+            Input(INTERVAL_Y, "2.25e-1", Datum.intervalYearMonth(13, 4, 2)),
+            Input(INTERVAL_M, "2.25e-1", Datum.intervalYearMonth(2, 7, 2)),
+            Input(INTERVAL_YM, "2.25e-1", Datum.intervalYearMonth(6, 3, 2)),
+
+            // DT interval computation is using BigDecimal internally. When double is converted to BigDecimal, some inaccuracy is introduced.
+            // For example, 4 hours / 0.225 = 64,000 seconds, which is 17h46m40s. But what you get is 639999,99999999999 and then converted 17h46m39.999999s
+            Input(INTERVAL_D, "2.25e-1", Datum.intervalDaySecond(8, 21, 19, 59, 999999999, 2, 6)),
+            Input(INTERVAL_H, "2.25e-1", Datum.intervalDaySecond(0, 17, 46, 39, 999999999, 2, 6)),
+            Input(INTERVAL_MIN, "2.25e-1", Datum.intervalDaySecond(0, 0, 22, 13, 333333333, 2, 6)),
+            Input(INTERVAL_S, "2.25e-1", Datum.intervalDaySecond(0, 0, 0, 46, 666666666, 2, 6)),
+            Input(INTERVAL_DTS, "2.25e-1", Datum.intervalDaySecond(9, 15, 29, 39, 999999999, 2, 6))
+        ).map { case ->
+            SuccessTestCase("${case.arg0} / ${case.arg1}", case.expected)
+        }
+        @JvmStatic
+        fun intervalDivideWithPrecisionOverflowCases() = listOf(
+            "$INTERVAL_Y / 0.03", // default year precision is 2. And 3 / 0.03 will exceed the year precision and fail
+            "$INTERVAL_D / 0.02" // default day precision is 2. And 2 / 0.02 will exceed the year precision and fail
+        ).map { exp ->
+            FailureTestCase(exp)
+        }
+
+        @JvmStatic
+        fun intervalDivideNullCases() = listOf(
+            Input(INTERVAL_Y, "NULL", Datum.nullValue()),
+            Input(INTERVAL_M, "NULL", Datum.nullValue()),
+            Input(INTERVAL_YM, "NULL", Datum.nullValue()),
+
+            Input(INTERVAL_D, "NULL", Datum.nullValue()),
+            Input(INTERVAL_H, "NULL", Datum.nullValue()),
+            Input(INTERVAL_MIN, "NULL", Datum.nullValue()),
+            Input(INTERVAL_S, "NULL", Datum.nullValue()),
+            Input(INTERVAL_DTS, "NULL", Datum.nullValue())
+        ).map { case ->
+            SuccessTestCase("${case.arg0} / ${case.arg1}", case.expected)
+        }
+
+        @JvmStatic
+        fun intervalDivideMissingCases() = listOf(
+            Input(INTERVAL_Y, "MISSING", Datum.missing()),
+            Input(INTERVAL_M, "MISSING", Datum.missing()),
+            Input(INTERVAL_YM, "MISSING", Datum.missing()),
+
+            Input(INTERVAL_D, "MISSING", Datum.missing()),
+            Input(INTERVAL_H, "MISSING", Datum.missing()),
+            Input(INTERVAL_MIN, "MISSING", Datum.missing()),
+            Input(INTERVAL_S, "MISSING", Datum.missing()),
+            Input(INTERVAL_DTS, "MISSING", Datum.missing())
+        ).map { case ->
+            SuccessTestCase("${case.arg0} / ${case.arg1}", case.expected)
+        }
+
+        @JvmStatic
+        fun intervalDivideZeroCases() = listOf(
+            INTERVAL_Y,
+            INTERVAL_M,
+            INTERVAL_YM,
+            INTERVAL_D,
+            INTERVAL_H,
+            INTERVAL_MIN,
+            INTERVAL_S,
+            INTERVAL_DTS
+        ).map { interval ->
+            FailureTestCase("$interval / 0")
+        }
+    }
+}
diff --git a/partiql-eval/src/test/kotlin/org/partiql/eval/internal/IntervalTimesTests.kt b/partiql-eval/src/test/kotlin/org/partiql/eval/internal/IntervalTimesTests.kt
new file mode 100644
index 000000000..bad6ab2e1
--- /dev/null
+++ b/partiql-eval/src/test/kotlin/org/partiql/eval/internal/IntervalTimesTests.kt
@@ -0,0 +1,214 @@
+package org.partiql.eval.internal
+
+import org.junit.jupiter.api.parallel.Execution
+import org.junit.jupiter.api.parallel.ExecutionMode
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.MethodSource
+import org.partiql.spi.value.Datum
+
+/**
+ * Test cases for interval multiplication operations.
+ * Tests multiplication of intervals by numeric values including integers, decimals, floats,
+ * negative numbers, zero, null, missing values, and precision overflow scenarios.
+ *
+ * Related Github issues:
+ * https://github.com/partiql/partiql-lang/issues/100
+ * https://github.com/partiql/partiql-lang-kotlin/issues/1784
+ */
+class IntervalTimesTests {
+
+    @ParameterizedTest
+    @MethodSource("intervalTimesNumberCases")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervalTimesNumber(tc: SuccessTestCase) = tc.run()
+
+    @ParameterizedTest
+    @MethodSource("intervalTimesZeroCases")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervalTimesZero(tc: SuccessTestCase) = tc.run()
+
+    @ParameterizedTest
+    @MethodSource("intervalTimesNegativeCases")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervalTimesNegative(tc: SuccessTestCase) = tc.run()
+
+    @ParameterizedTest
+    @MethodSource("intervalTimesDecimalCases")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervalTimesDecimal(tc: SuccessTestCase) = tc.run()
+
+    @ParameterizedTest
+    @MethodSource("intervalTimesDoubleCases")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervalTimesDouble(tc: SuccessTestCase) = tc.run()
+
+    @ParameterizedTest
+    @MethodSource("intervalTimesCalculationOverflow")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervalTimesCalculationOverflow(tc: SuccessTestCase) = tc.run()
+
+    @ParameterizedTest
+    @MethodSource("intervaltimesWithPrecisionOverflowCases")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervaltimesWithPrecisionOverflowCases(tc: FailureTestCase) = tc.run()
+
+    @ParameterizedTest
+    @MethodSource("intervalTimesNullCases")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervalTimesNull(tc: SuccessTestCase) = tc.run()
+
+    @ParameterizedTest
+    @MethodSource("intervalTimesMissingCases")
+    @Execution(ExecutionMode.CONCURRENT)
+    fun intervalTimesMissing(tc: SuccessTestCase) = tc.run()
+
+    companion object {
+
+        private const val INTERVAL_Y = "INTERVAL '3' YEAR"
+        private const val INTERVAL_M = "INTERVAL '7' MONTH"
+        private const val INTERVAL_YM = "INTERVAL '1-5' YEAR TO MONTH"
+        private const val INTERVAL_YM_LARGE = "INTERVAL '111111-1' YEAR(6) TO MONTH"
+        private const val INTERVAL_D = "INTERVAL '2' DAY"
+        private const val INTERVAL_H = "INTERVAL '4' HOUR"
+        private const val INTERVAL_MIN = "INTERVAL '5' MINUTE"
+        private const val INTERVAL_S = "INTERVAL '10.5' SECOND"
+        private const val INTERVAL_DTS = "INTERVAL '2 4:5:10.5' DAY TO SECOND"
+        private const val INTERVAL_DTS_LARGE = "INTERVAL '111111 1:1:1.111111' DAY(6) TO SECOND(6)"
+        private const val NANOS_PER_SECOND = 1_000_000_000
+
+        private class Input(
+            val arg0: String,
+            val arg1: String,
+            val expected: Datum
+        )
+
+        @JvmStatic
+        fun intervalTimesNumberCases() = listOf(
+            Input(INTERVAL_Y, "2", Datum.intervalYearMonth(6, 0, 2)),
+            Input(INTERVAL_M, "2", Datum.intervalYearMonth(1, 2, 2)),
+            Input(INTERVAL_YM, "2", Datum.intervalYearMonth(2, 10, 2)),
+
+            Input(INTERVAL_D, "20", Datum.intervalDaySecond(40, 0, 0, 0, 0, 2, 6)),
+            Input(INTERVAL_H, "20", Datum.intervalDaySecond(3, 8, 0, 0, 0, 2, 6)),
+            Input(INTERVAL_MIN, "20", Datum.intervalDaySecond(0, 1, 40, 0, 0, 2, 6)),
+            Input(INTERVAL_S, "20", Datum.intervalDaySecond(0, 0, 3, 30, 0, 2, 6)),
+            Input(INTERVAL_DTS, "20", Datum.intervalDaySecond(43, 9, 43, 30, 0, 2, 6)),
+        ).map { case ->
+            SuccessTestCase("${case.arg0} * ${case.arg1}", case.expected)
+            SuccessTestCase("${case.arg1} * ${case.arg0}", case.expected)
+        }
+
+        @JvmStatic
+        fun intervalTimesZeroCases() = listOf(
+            Input(INTERVAL_Y, "0", Datum.intervalYearMonth(0, 0, 2)),
+            Input(INTERVAL_M, "0", Datum.intervalYearMonth(0, 0, 2)),
+            Input(INTERVAL_YM, "0", Datum.intervalYearMonth(0, 0, 2)),
+
+            Input(INTERVAL_D, "0", Datum.intervalDaySecond(0, 0, 0, 0, 0, 2, 6)),
+            Input(INTERVAL_H, "0", Datum.intervalDaySecond(0, 0, 0, 0, 0, 2, 6)),
+            Input(INTERVAL_MIN, "0", Datum.intervalDaySecond(0, 0, 0, 0, 0, 2, 6)),
+            Input(INTERVAL_S, "0", Datum.intervalDaySecond(0, 0, 0, 0, 0, 2, 6)),
+            Input(INTERVAL_DTS, "0", Datum.intervalDaySecond(0, 0, 0, 0, 0, 2, 6))
+        ).map { case ->
+            SuccessTestCase("${case.arg0} * ${case.arg1}", case.expected)
+            SuccessTestCase("${case.arg1} * ${case.arg0}", case.expected)
+        }
+
+        @JvmStatic
+        fun intervalTimesNegativeCases() = listOf(
+            Input(INTERVAL_Y, "-2", Datum.intervalYearMonth(-6, 0, 2)),
+            Input(INTERVAL_M, "-2", Datum.intervalYearMonth(-1, -2, 2)),
+            Input(INTERVAL_YM, "-2", Datum.intervalYearMonth(-2, -10, 2)),
+
+            Input(INTERVAL_D, "-20", Datum.intervalDaySecond(-40, 0, 0, 0, 0, 2, 6)),
+            Input(INTERVAL_H, "-20", Datum.intervalDaySecond(-3, -8, 0, 0, 0, 2, 6)),
+            Input(INTERVAL_MIN, "-20", Datum.intervalDaySecond(0, -1, -40, 0, 0, 2, 6)),
+            Input(INTERVAL_S, "-20", Datum.intervalDaySecond(0, 0, -3, -30, 0, 2, 6)),
+            Input(INTERVAL_DTS, "-20", Datum.intervalDaySecond(-43, -9, -43, -30, 0, 2, 6)),
+        ).map { case ->
+            SuccessTestCase("${case.arg0} * ${case.arg1}", case.expected)
+            SuccessTestCase("${case.arg1} * ${case.arg0}", case.expected)
+        }
+
+        @JvmStatic
+        fun intervalTimesDecimalCases() = listOf(
+            Input(INTERVAL_Y, "2.1", Datum.intervalYearMonth(6, 3, 2)),
+            Input(INTERVAL_M, "2.1", Datum.intervalYearMonth(1, 2, 2)),
+            Input(INTERVAL_YM, "2.1", Datum.intervalYearMonth(2, 11, 2)),
+
+            Input(INTERVAL_D, "2.1", Datum.intervalDaySecond(4, 4, 48, 0, 0, 2, 6)),
+            Input(INTERVAL_H, "2.1", Datum.intervalDaySecond(0, 8, 24, 0, 0, 2, 6)),
+            Input(INTERVAL_MIN, "2.1", Datum.intervalDaySecond(0, 0, 10, 30, 0, 2, 6)),
+            Input(INTERVAL_S, "2.1", Datum.intervalDaySecond(0, 0, 0, 22, (0.05 * NANOS_PER_SECOND).toInt(), 2, 6)),
+            Input(INTERVAL_DTS, "2.1", Datum.intervalDaySecond(4, 13, 22, 52, (0.05 * NANOS_PER_SECOND).toInt(), 2, 6)),
+        ).map { case ->
+            SuccessTestCase("${case.arg0} * ${case.arg1}", case.expected)
+            SuccessTestCase("${case.arg1} * ${case.arg0}", case.expected)
+        }
+
+        @JvmStatic
+        fun intervalTimesDoubleCases() = listOf(
+            Input(INTERVAL_Y, "2.25e-1", Datum.intervalYearMonth(0, 8, 2)),
+            Input(INTERVAL_M, "2.25e-1", Datum.intervalYearMonth(0, 1, 2)),
+            Input(INTERVAL_YM, "2.25e-1", Datum.intervalYearMonth(0, 3, 2)),
+
+            Input(INTERVAL_D, "2.25e-1", Datum.intervalDaySecond(0, 10, 48, 0, 0, 2, 6)),
+            Input(INTERVAL_H, "2.25e-1", Datum.intervalDaySecond(0, 0, 54, 0, 0, 2, 6)),
+            Input(INTERVAL_MIN, "2.25e-1", Datum.intervalDaySecond(0, 0, 1, 7, 500000000, 2, 6)),
+            Input(INTERVAL_S, "2.25e-1", Datum.intervalDaySecond(0, 0, 0, 2, 362500000, 2, 6)),
+            Input(INTERVAL_DTS, "2.25e-1", Datum.intervalDaySecond(0, 11, 43, 9, 862500000, 2, 6))
+        ).map { case ->
+            SuccessTestCase("${case.arg0} * ${case.arg1}", case.expected)
+            SuccessTestCase("${case.arg1} * ${case.arg0}", case.expected)
+        }
+
+        @JvmStatic
+        fun intervalTimesCalculationOverflow() = listOf(
+            Input(INTERVAL_YM_LARGE, "9", Datum.intervalYearMonth(999999, 9, 6)),
+            Input(INTERVAL_DTS_LARGE, "9", Datum.intervalDaySecond(999999, 9, 9, 9, 999999000, 6, 6)),
+        ).map { case ->
+            SuccessTestCase("${case.arg0} * ${case.arg1}", case.expected)
+            SuccessTestCase("${case.arg1} * ${case.arg0}", case.expected)
+        }
+
+        @JvmStatic
+        fun intervaltimesWithPrecisionOverflowCases() = listOf(
+            "$INTERVAL_Y * 50", // default year precision is 2. And 3 * 50 will exceed the year precision and fail
+            "$INTERVAL_D * 50" // default day precision is 2. And 3 * 50 will exceed the year precision and fail
+        ).map { exp ->
+            FailureTestCase(exp)
+        }
+
+        @JvmStatic
+        fun intervalTimesNullCases() = listOf(
+            Input(INTERVAL_Y, "NULL", Datum.nullValue()),
+            Input(INTERVAL_M, "NULL", Datum.nullValue()),
+            Input(INTERVAL_YM, "NULL", Datum.nullValue()),
+
+            Input(INTERVAL_D, "NULL", Datum.nullValue()),
+            Input(INTERVAL_H, "NULL", Datum.nullValue()),
+            Input(INTERVAL_MIN, "NULL", Datum.nullValue()),
+            Input(INTERVAL_S, "NULL", Datum.nullValue()),
+            Input(INTERVAL_DTS, "NULL", Datum.nullValue())
+        ).map { case ->
+            SuccessTestCase("${case.arg0} / ${case.arg1}", case.expected)
+            SuccessTestCase("${case.arg0} / ${case.arg1}", case.expected)
+        }
+
+        @JvmStatic
+        fun intervalTimesMissingCases() = listOf(
+            Input(INTERVAL_Y, "MISSING", Datum.missing()),
+            Input(INTERVAL_M, "MISSING", Datum.missing()),
+            Input(INTERVAL_YM, "MISSING", Datum.missing()),
+
+            Input(INTERVAL_D, "MISSING", Datum.missing()),
+            Input(INTERVAL_H, "MISSING", Datum.missing()),
+            Input(INTERVAL_MIN, "MISSING", Datum.missing()),
+            Input(INTERVAL_S, "MISSING", Datum.missing()),
+            Input(INTERVAL_DTS, "MISSING", Datum.missing())
+        ).map { case ->
+            SuccessTestCase("${case.arg0} * ${case.arg1}", case.expected)
+            SuccessTestCase("${case.arg1} * ${case.arg0}", case.expected)
+        }
+    }
+}
diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/operator/OpArithmeticTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/operator/OpArithmeticTest.kt
index fdb2f2822..4e677d016 100644
--- a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/operator/OpArithmeticTest.kt
+++ b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/operator/OpArithmeticTest.kt
@@ -20,11 +20,9 @@ import java.util.stream.Stream
 
 class OpArithmeticTest : PartiQLTyperTestBase() {
     @TestFactory
-    fun arithmetic(): Stream<DynamicContainer> {
+    fun mod(): Stream<DynamicContainer> {
         val tests = listOf(
-            "expr-39",
-            "expr-40",
-            "expr-41",
+            "expr-39"
         ).map { inputs.get("basics", it)!! }
 
         val argsMap: Map<TestResult, Set<List<PType>>> = buildMap {
@@ -63,7 +61,17 @@ class OpArithmeticTest : PartiQLTyperTestBase() {
         ).map { inputs.get("basics", it)!! }
 
         val argsMap: Map<TestResult, Set<List<PType>>> = buildMap {
-            val successArgs = allNumberPType.let { cartesianProduct(it, it) } + cartesianProduct(allDatePType, allIntervalType) + cartesianProduct(allIntervalType, allDatePType) + cartesianProduct(allTimePType, allIntervalDTType) + cartesianProduct(allIntervalDTType, allTimePType) + cartesianProduct(allTimeStampPType, allIntervalType) + cartesianProduct(allIntervalType, allTimeStampPType) + cartesianProduct(allIntervalYMType, allIntervalYMType) + cartesianProduct(allIntervalDTType, allIntervalDTType)
+            val successArgs = (
+                allNumberPType.let { cartesianProduct(it, it) } +
+                    cartesianProduct(allDatePType, allIntervalType) +
+                    cartesianProduct(allIntervalType, allDatePType) +
+                    cartesianProduct(allTimePType, allIntervalDTType) +
+                    cartesianProduct(allIntervalDTType, allTimePType) +
+                    cartesianProduct(allTimeStampPType, allIntervalType) +
+                    cartesianProduct(allIntervalType, allTimeStampPType) +
+                    cartesianProduct(allIntervalYMType, allIntervalYMType) +
+                    cartesianProduct(allIntervalDTType, allIntervalDTType)
+                )
             val failureArgs = cartesianProduct(
                 allSupportedPType,
                 allSupportedPType
@@ -108,7 +116,17 @@ class OpArithmeticTest : PartiQLTyperTestBase() {
         ).map { inputs.get("basics", it)!! }
 
         val argsMap: Map<TestResult, Set<List<PType>>> = buildMap {
-            val successArgs = allNumberPType.let { cartesianProduct(it, it) } + allDatePType.let { cartesianProduct(it, it) } + allTimePType.let { cartesianProduct(it, it) } + allTimeStampPType.let { cartesianProduct(it, it) } + cartesianProduct(allDatePType, allIntervalType) + cartesianProduct(allTimePType, allIntervalDTType) + cartesianProduct(allTimeStampPType, allIntervalType) + cartesianProduct(allIntervalYMType, allIntervalYMType) + cartesianProduct(allIntervalDTType, allIntervalDTType)
+            val successArgs = (
+                allNumberPType.let { cartesianProduct(it, it) } +
+                    allDatePType.let { cartesianProduct(it, it) } +
+                    allTimePType.let { cartesianProduct(it, it) } +
+                    allTimeStampPType.let { cartesianProduct(it, it) } +
+                    cartesianProduct(allDatePType, allIntervalType) +
+                    cartesianProduct(allTimePType, allIntervalDTType) +
+                    cartesianProduct(allTimeStampPType, allIntervalType) +
+                    cartesianProduct(allIntervalYMType, allIntervalYMType) +
+                    cartesianProduct(allIntervalDTType, allIntervalDTType)
+                )
             val failureArgs = cartesianProduct(
                 allSupportedPType,
                 allSupportedPType
@@ -142,4 +160,83 @@ class OpArithmeticTest : PartiQLTyperTestBase() {
         }
         return super.testGenPType("minus", tests, argsMap)
     }
+
+    @TestFactory
+    fun times(): Stream<DynamicContainer> {
+        val tests = listOf(
+            "expr-40"
+        ).map { inputs.get("basics", it)!! }
+
+        val argsMap: Map<TestResult, Set<List<PType>>> = buildMap {
+            val successArgs = (
+                allNumberPType.let { cartesianProduct(it, it) } +
+                    cartesianProduct(allNumberPType, allIntervalType) +
+                    cartesianProduct(allIntervalType, allNumberPType)
+                )
+            val failureArgs = cartesianProduct(
+                allSupportedPType,
+                allSupportedPType
+            ).filterNot {
+                successArgs.contains(it)
+            }.toSet()
+
+            successArgs.forEach { args: List<PType> ->
+                val arg0 = args.first()
+                val arg1 = args[1]
+                val output = when {
+                    arg0 in allNumberPType && arg1 in allIntervalYMType -> PType.intervalYearMonth(9)
+                    arg0 in allNumberPType && arg1 in allIntervalDTType -> PType.intervalDaySecond(9, 6)
+
+                    arg0 in allIntervalYMType && arg1 in allNumberPType -> PType.intervalYearMonth(9)
+                    arg0 in allIntervalDTType && arg1 in allNumberPType -> PType.intervalDaySecond(9, 6)
+                    arg0 == arg1 -> arg1
+                    castTablePType(arg1, arg0) == CastType.COERCION -> arg0
+                    castTablePType(arg0, arg1) == CastType.COERCION -> arg1
+                    else -> error("Arguments do not conform to parameters. Args: $args")
+                }
+                accumulateSuccess(output, args)
+            }
+
+            put(TestResult.Failure, failureArgs)
+        }
+
+        return super.testGenPType("arithmetic", tests, argsMap)
+    }
+
+    @TestFactory
+    fun divide(): Stream<DynamicContainer> {
+        val tests = listOf(
+            "expr-41",
+        ).map { inputs.get("basics", it)!! }
+
+        val argsMap: Map<TestResult, Set<List<PType>>> = buildMap {
+            val successArgs = (
+                allNumberPType.let { cartesianProduct(it, it) } +
+                    cartesianProduct(allIntervalType, allNumberPType)
+                )
+            val failureArgs = cartesianProduct(
+                allSupportedPType,
+                allSupportedPType
+            ).filterNot {
+                successArgs.contains(it)
+            }.toSet()
+
+            successArgs.forEach { args: List<PType> ->
+                val arg0 = args.first()
+                val arg1 = args[1]
+                val output = when {
+                    arg0 in allIntervalYMType && arg1 in allNumberPType -> PType.intervalYearMonth(9)
+                    arg0 in allIntervalDTType && arg1 in allNumberPType -> PType.intervalDaySecond(9, 6)
+                    arg0 == arg1 -> arg1
+                    castTablePType(arg1, arg0) == CastType.COERCION -> arg0
+                    castTablePType(arg0, arg1) == CastType.COERCION -> arg1
+                    else -> error("Arguments do not conform to parameters. Args: $args")
+                }
+                accumulateSuccess(output, args)
+            }
+
+            put(TestResult.Failure, failureArgs)
+        }
+        return super.testGenPType("plus", tests, argsMap)
+    }
 }