From 433c609d8d3bea9bda74cb4a259ed9a2abcb48c7 Mon Sep 17 00:00:00 2001 From: John Ed Quinn <40360967+johnedquinn@users.noreply.github.com> Date: Tue, 1 Oct 2024 21:24:51 -0700 Subject: [PATCH] Fixes small evaluation bugs related to casts, types, etc (#1577) --- .../src/main/kotlin/org/partiql/cli/Main.kt | 2 +- .../kotlin/org/partiql/cli/shell/Shell.kt | 2 +- partiql-eval/api/partiql-eval.api | 10 +- .../kotlin/org/partiql/eval/PartiQLResult.kt | 6 +- .../eval/internal/operator/rex/CastTable.kt | 138 ++++++++++++++---- .../eval/internal/statement/QueryStatement.kt | 4 +- .../eval/internal/PartiQLEngineDefaultTest.kt | 104 +++++++++---- .../planner/internal/casts/CastTable.kt | 114 +++++++++++---- .../internal/transforms/RexConverter.kt | 96 +++++++++--- partiql-spi/api/partiql-spi.api | 5 + .../java/org/partiql/spi/value/Datum.java | 81 +++++++++- .../org/partiql/spi/value/ion/IonDatum.kt | 28 +++- .../partiql/runner/executor/EvalExecutor.kt | 8 +- 13 files changed, 473 insertions(+), 125 deletions(-) diff --git a/partiql-cli/src/main/kotlin/org/partiql/cli/Main.kt b/partiql-cli/src/main/kotlin/org/partiql/cli/Main.kt index 2f1d7fdf89..6a4d785492 100644 --- a/partiql-cli/src/main/kotlin/org/partiql/cli/Main.kt +++ b/partiql-cli/src/main/kotlin/org/partiql/cli/Main.kt @@ -178,7 +178,7 @@ internal class MainCommand : Runnable { } is PartiQLResult.Value -> { val writer = PartiQLValueTextWriter(System.out) - writer.append(result.value) + writer.append(result.value.toPartiQLValue()) // TODO: Create a Datum writer println() } } diff --git a/partiql-cli/src/main/kotlin/org/partiql/cli/shell/Shell.kt b/partiql-cli/src/main/kotlin/org/partiql/cli/shell/Shell.kt index 5629a5008d..c990290ca5 100644 --- a/partiql-cli/src/main/kotlin/org/partiql/cli/shell/Shell.kt +++ b/partiql-cli/src/main/kotlin/org/partiql/cli/shell/Shell.kt @@ -268,7 +268,7 @@ internal class Shell( is PartiQLResult.Error -> throw result.cause is PartiQLResult.Value -> { val writer = PartiQLValueTextWriter(out) - writer.append(result.value) + writer.append(result.value.toPartiQLValue()) // TODO: Create a Datum writer out.appendLine() out.appendLine() out.info("OK!") diff --git a/partiql-eval/api/partiql-eval.api b/partiql-eval/api/partiql-eval.api index 9fdde2a2e9..f9b32ff2b7 100644 --- a/partiql-eval/api/partiql-eval.api +++ b/partiql-eval/api/partiql-eval.api @@ -33,12 +33,12 @@ public final class org/partiql/eval/PartiQLResult$Error : org/partiql/eval/Parti } public final class org/partiql/eval/PartiQLResult$Value : org/partiql/eval/PartiQLResult { - public fun (Lorg/partiql/value/PartiQLValue;)V - public final fun component1 ()Lorg/partiql/value/PartiQLValue; - public final fun copy (Lorg/partiql/value/PartiQLValue;)Lorg/partiql/eval/PartiQLResult$Value; - public static synthetic fun copy$default (Lorg/partiql/eval/PartiQLResult$Value;Lorg/partiql/value/PartiQLValue;ILjava/lang/Object;)Lorg/partiql/eval/PartiQLResult$Value; + public fun (Lorg/partiql/spi/value/Datum;)V + public final fun component1 ()Lorg/partiql/spi/value/Datum; + public final fun copy (Lorg/partiql/spi/value/Datum;)Lorg/partiql/eval/PartiQLResult$Value; + public static synthetic fun copy$default (Lorg/partiql/eval/PartiQLResult$Value;Lorg/partiql/spi/value/Datum;ILjava/lang/Object;)Lorg/partiql/eval/PartiQLResult$Value; public fun equals (Ljava/lang/Object;)Z - public final fun getValue ()Lorg/partiql/value/PartiQLValue; + public final fun getValue ()Lorg/partiql/spi/value/Datum; public fun hashCode ()I public fun toString ()Ljava/lang/String; } diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/PartiQLResult.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/PartiQLResult.kt index 3a1f6c1a30..5dcf941525 100644 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/PartiQLResult.kt +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/PartiQLResult.kt @@ -1,12 +1,10 @@ package org.partiql.eval -import org.partiql.value.PartiQLValue -import org.partiql.value.PartiQLValueExperimental +import org.partiql.spi.value.Datum public sealed interface PartiQLResult { - @OptIn(PartiQLValueExperimental::class) - public data class Value(public val value: PartiQLValue) : PartiQLResult + public data class Value(public val value: Datum) : PartiQLResult public data class Error(public val cause: Throwable) : PartiQLResult } 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 043ec2552d..9bcb81f805 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 @@ -11,6 +11,9 @@ import org.partiql.types.PType.Kind.ARRAY import org.partiql.types.PType.Kind.BAG import org.partiql.types.PType.Kind.BIGINT import org.partiql.types.PType.Kind.BOOL +import org.partiql.types.PType.Kind.CHAR +import org.partiql.types.PType.Kind.CLOB +import org.partiql.types.PType.Kind.DATE import org.partiql.types.PType.Kind.DECIMAL import org.partiql.types.PType.Kind.DECIMAL_ARBITRARY import org.partiql.types.PType.Kind.DOUBLE @@ -23,9 +26,13 @@ import org.partiql.types.PType.Kind.SMALLINT import org.partiql.types.PType.Kind.STRING import org.partiql.types.PType.Kind.STRUCT import org.partiql.types.PType.Kind.SYMBOL +import org.partiql.types.PType.Kind.TIME import org.partiql.types.PType.Kind.TIMESTAMP import org.partiql.types.PType.Kind.TIMESTAMPZ +import org.partiql.types.PType.Kind.TIMEZ import org.partiql.types.PType.Kind.TINYINT +import org.partiql.types.PType.Kind.VARCHAR +import org.partiql.value.datetime.DateTimeValue import java.math.BigDecimal import java.math.BigInteger import java.math.RoundingMode @@ -102,6 +109,8 @@ internal object CastTable { registerList() registerSexp() registerTimestamp() + registerDate() + registerTime() } private fun PType.Kind.pad(): String { @@ -151,7 +160,6 @@ internal object CastTable { /** * CAST( AS ) - * TODO: CHAR, VARCHAR, SYMBOL */ private fun registerBool() { register(BOOL, BOOL) { x, _ -> x } @@ -177,12 +185,14 @@ internal object CastTable { DOUBLE ) { x, _ -> Datum.doublePrecision(if (x.boolean) 1.0 else 0.0) } register(BOOL, STRING) { x, _ -> Datum.string(if (x.boolean) "true" else "false") } - register(BOOL, SYMBOL) { x, _ -> Datum.string(if (x.boolean) "true" else "false") } + register(BOOL, SYMBOL) { x, _ -> Datum.symbol(if (x.boolean) "true" else "false") } + register(BOOL, VARCHAR) { x, t -> Datum.varchar(if (x.boolean) "true" else "false", t.length) } + register(BOOL, CHAR) { x, t -> Datum.character(if (x.boolean) "true" else "false", t.length) } + register(BOOL, CLOB) { x, t -> Datum.clob((if (x.boolean) "true" else "false").toByteArray(), t.length) } } /** * CAST( AS ) - * TODO: CHAR, VARCHAR, SYMBOL */ private fun registerTinyInt() { register(TINYINT, BOOL) { x, _ -> Datum.bool(x.byte.toInt() != 0) } @@ -210,12 +220,14 @@ internal object CastTable { register(TINYINT, REAL) { x, _ -> Datum.real(x.byte.toFloat()) } register(TINYINT, DOUBLE) { x, _ -> Datum.doublePrecision(x.byte.toDouble()) } register(TINYINT, STRING) { x, _ -> Datum.string(x.byte.toString()) } - register(TINYINT, SYMBOL) { x, _ -> Datum.string(x.byte.toString()) } + register(TINYINT, SYMBOL) { x, _ -> Datum.symbol(x.byte.toString()) } + register(TINYINT, VARCHAR) { x, t -> Datum.varchar(x.byte.toString(), t.length) } + register(TINYINT, CHAR) { x, t -> Datum.character(x.byte.toString(), t.length) } + register(TINYINT, CLOB) { x, t -> Datum.clob(x.byte.toString().toByteArray(), t.length) } } /** * CAST( AS ) - * TODO: CHAR, VARCHAR, SYMBOL */ private fun registerSmallInt() { register(SMALLINT, BOOL) { x, _ -> Datum.bool(x.short.toInt() != 0) } @@ -243,12 +255,14 @@ internal object CastTable { register(SMALLINT, REAL) { x, _ -> Datum.real(x.short.toFloat()) } register(SMALLINT, DOUBLE) { x, _ -> Datum.doublePrecision(x.short.toDouble()) } register(SMALLINT, STRING) { x, _ -> Datum.string(x.short.toString()) } - register(SMALLINT, SYMBOL) { x, _ -> Datum.string(x.short.toString()) } + register(SMALLINT, SYMBOL) { x, _ -> Datum.symbol(x.short.toString()) } + register(SMALLINT, VARCHAR) { x, t -> Datum.varchar(x.short.toString(), t.length) } + register(SMALLINT, CHAR) { x, t -> Datum.character(x.short.toString(), t.length) } + register(SMALLINT, CLOB) { x, t -> Datum.clob(x.short.toString().toByteArray(), t.length) } } /** * CAST( AS ) - * TODO: CHAR, VARCHAR, SYMBOL */ private fun registerInt() { register(INTEGER, BOOL) { x, _ -> Datum.bool(x.int != 0) } @@ -268,12 +282,14 @@ internal object CastTable { register(INTEGER, REAL) { x, _ -> Datum.real(x.int.toFloat()) } register(INTEGER, DOUBLE) { x, _ -> Datum.doublePrecision(x.int.toDouble()) } register(INTEGER, STRING) { x, _ -> Datum.string(x.int.toString()) } - register(INTEGER, SYMBOL) { x, _ -> Datum.string(x.int.toString()) } + register(INTEGER, SYMBOL) { x, _ -> Datum.symbol(x.int.toString()) } + register(INTEGER, VARCHAR) { x, t -> Datum.varchar(x.int.toString(), t.length) } + register(INTEGER, CHAR) { x, t -> Datum.character(x.int.toString(), t.length) } + register(INTEGER, CLOB) { x, t -> Datum.clob(x.int.toString().toByteArray(), t.length) } } /** * CAST( AS ) - * TODO: CHAR, VARCHAR, SYMBOL */ private fun registerBigInt() { register(BIGINT, BOOL) { x, _ -> Datum.bool(x.long != 0L) } @@ -296,12 +312,14 @@ internal object CastTable { register(BIGINT, REAL) { x, _ -> Datum.real(x.long.toFloat()) } register(BIGINT, DOUBLE) { x, _ -> Datum.doublePrecision(x.long.toDouble()) } register(BIGINT, STRING) { x, _ -> Datum.string(x.long.toString()) } - register(BIGINT, SYMBOL) { x, _ -> Datum.string(x.long.toString()) } + register(BIGINT, SYMBOL) { x, _ -> Datum.symbol(x.long.toString()) } + register(BIGINT, VARCHAR) { x, t -> Datum.varchar(x.long.toString(), t.length) } + register(BIGINT, CHAR) { x, t -> Datum.character(x.long.toString(), t.length) } + register(BIGINT, CLOB) { x, t -> Datum.clob(x.long.toString().toByteArray(), t.length) } } /** * CAST( AS ) - * TODO: CHAR, VARCHAR, SYMBOL */ private fun registerIntArbitrary() { register(NUMERIC, BOOL) { x, _ -> Datum.bool(x.bigInteger != BigInteger.ZERO) } @@ -327,12 +345,14 @@ internal object CastTable { DOUBLE ) { x, _ -> datumDoublePrecision(x.bigInteger) } register(NUMERIC, STRING) { x, _ -> Datum.string(x.bigInteger.toString()) } - register(NUMERIC, SYMBOL) { x, _ -> Datum.string(x.bigInteger.toString()) } + register(NUMERIC, SYMBOL) { x, _ -> Datum.symbol(x.bigInteger.toString()) } + register(NUMERIC, VARCHAR) { x, t -> Datum.varchar(x.bigInteger.toString(), t.length) } + register(NUMERIC, CHAR) { x, t -> Datum.character(x.bigInteger.toString(), t.length) } + register(NUMERIC, CLOB) { x, t -> Datum.clob(x.bigInteger.toString().toByteArray(), t.length) } } /** * CAST( AS ) - * TODO: CHAR, VARCHAR, SYMBOL */ private fun registerDecimal() { register(DECIMAL, BOOL) { x, _ -> Datum.bool(x.bigDecimal != BigDecimal.ZERO) } @@ -352,12 +372,14 @@ internal object CastTable { DOUBLE ) { x, _ -> datumDoublePrecision(x.bigDecimal) } register(DECIMAL, STRING) { x, _ -> Datum.string(x.bigDecimal.toString()) } - register(DECIMAL, SYMBOL) { x, _ -> Datum.string(x.bigDecimal.toString()) } + register(DECIMAL, SYMBOL) { x, _ -> Datum.symbol(x.bigDecimal.toString()) } + register(DECIMAL, VARCHAR) { x, t -> Datum.varchar(x.bigDecimal.toString(), t.length) } + register(DECIMAL, CHAR) { x, t -> Datum.character(x.bigDecimal.toString(), t.length) } + register(DECIMAL, CLOB) { x, t -> Datum.clob(x.bigDecimal.toString().toByteArray(), t.length) } } /** * CAST( AS ) - * TODO: CHAR, VARCHAR, SYMBOL */ private fun registerDecimalArbitrary() { register( @@ -386,12 +408,14 @@ internal object CastTable { DOUBLE ) { x, _ -> datumDoublePrecision(x.bigDecimal) } register(DECIMAL_ARBITRARY, STRING) { x, _ -> Datum.string(x.bigDecimal.toString()) } - register(DECIMAL_ARBITRARY, SYMBOL) { x, _ -> Datum.string(x.bigDecimal.toString()) } + register(DECIMAL_ARBITRARY, SYMBOL) { x, _ -> Datum.symbol(x.bigDecimal.toString()) } + register(DECIMAL_ARBITRARY, VARCHAR) { x, t -> Datum.varchar(x.bigDecimal.toString(), t.length) } + register(DECIMAL_ARBITRARY, CHAR) { x, t -> Datum.character(x.bigDecimal.toString(), t.length) } + register(DECIMAL_ARBITRARY, CLOB) { x, t -> Datum.clob(x.bigDecimal.toString().toByteArray(), t.length) } } /** * CAST( AS ) - * TODO: CHAR, VARCHAR, SYMBOL */ private fun registerReal() { register(REAL, BOOL) { x, _ -> Datum.bool(x.float != 0F) } @@ -418,12 +442,14 @@ internal object CastTable { register(REAL, REAL) { x, _ -> x } register(REAL, DOUBLE) { x, _ -> Datum.doublePrecision(x.float.toDouble()) } register(REAL, STRING) { x, _ -> Datum.string(x.float.toString()) } - register(REAL, SYMBOL) { x, _ -> Datum.string(x.float.toString()) } + register(REAL, SYMBOL) { x, _ -> Datum.symbol(x.float.toString()) } + register(REAL, VARCHAR) { x, t -> Datum.varchar(x.float.toString(), t.length) } + register(REAL, CHAR) { x, t -> Datum.character(x.float.toString(), t.length) } + register(REAL, CLOB) { x, t -> Datum.clob(x.float.toString().toByteArray(), t.length) } } /** * CAST( AS ) - * TODO: CHAR, VARCHAR, SYMBOL */ private fun registerDoublePrecision() { register(DOUBLE, BOOL) { x, _ -> Datum.bool(x.double != 0.0) } @@ -452,7 +478,10 @@ internal object CastTable { register(DOUBLE, REAL) { x, _ -> datumReal(x.double) } register(DOUBLE, DOUBLE) { x, _ -> x } register(DOUBLE, STRING) { x, _ -> Datum.string(x.double.toString()) } - register(DOUBLE, SYMBOL) { x, _ -> Datum.string(x.double.toString()) } + register(DOUBLE, SYMBOL) { x, _ -> Datum.symbol(x.double.toString()) } + register(DOUBLE, VARCHAR) { x, t -> Datum.varchar(x.double.toString(), t.length) } + register(DOUBLE, CHAR) { x, t -> Datum.character(x.double.toString(), t.length) } + register(DOUBLE, CLOB) { x, t -> Datum.clob(x.double.toString().toByteArray(), t.length) } } /** @@ -484,7 +513,10 @@ internal object CastTable { register(STRING, REAL) { x, t -> cast(numberFromString(x.string), t) } register(STRING, DOUBLE) { x, t -> cast(numberFromString(x.string), t) } register(STRING, STRING) { x, _ -> x } - register(STRING, SYMBOL) { x, _ -> Datum.string(x.string) } + register(STRING, SYMBOL) { x, _ -> Datum.symbol(x.string) } + register(STRING, VARCHAR) { x, t -> Datum.varchar(x.string, t.length) } + register(STRING, CHAR) { x, t -> Datum.character(x.string, t.length) } + register(STRING, CLOB) { x, t -> Datum.clob(x.string.toByteArray(), t.length) } } /** * CAST( AS ) @@ -509,6 +541,9 @@ internal object CastTable { register(SYMBOL, DOUBLE) { x, t -> cast(numberFromString(x.string), t) } register(SYMBOL, STRING) { x, _ -> Datum.string(x.string) } register(SYMBOL, SYMBOL) { x, _ -> x } + register(SYMBOL, VARCHAR) { x, t -> Datum.varchar(x.string, t.length) } + register(SYMBOL, CHAR) { x, t -> Datum.character(x.string, t.length) } + register(SYMBOL, CLOB) { x, t -> Datum.clob(x.string.toByteArray(), t.length) } } private fun registerBag() { @@ -533,7 +568,58 @@ internal object CastTable { * TODO: Flush this out. */ private fun registerTimestamp() { + // WITHOUT TZ + register(TIMESTAMP, VARCHAR) { x, t -> Datum.varchar(x.timestamp.toString(), t.length) } + register(TIMESTAMP, CHAR) { x, t -> Datum.character(x.timestamp.toString(), t.length) } + register(TIMESTAMP, CLOB) { x, t -> Datum.clob(x.timestamp.toString().toByteArray(), t.length) } + register(TIMESTAMP, STRING) { x, _ -> Datum.string(x.timestamp.toString()) } + register(TIMESTAMP, SYMBOL) { x, _ -> Datum.symbol(x.timestamp.toString()) } + register(TIMESTAMP, TIMESTAMP) { x, _ -> Datum.timestamp(x.timestamp) } + register(TIMESTAMP, TIMESTAMPZ) { x, _ -> Datum.timestamp(x.timestamp) } + register(TIMESTAMP, TIME) { x, _ -> Datum.time(x.timestamp.toTime()) } + register(TIMESTAMP, DATE) { x, _ -> Datum.date(x.timestamp.toDate()) } + // WITH TZ + register(TIMESTAMPZ, VARCHAR) { x, t -> Datum.varchar(x.timestamp.toString(), t.length) } + register(TIMESTAMPZ, CHAR) { x, t -> Datum.character(x.timestamp.toString(), t.length) } + register(TIMESTAMPZ, CLOB) { x, t -> Datum.clob(x.timestamp.toString().toByteArray(), t.length) } + register(TIMESTAMPZ, STRING) { x, _ -> Datum.string(x.timestamp.toString()) } + register(TIMESTAMPZ, SYMBOL) { x, _ -> Datum.symbol(x.timestamp.toString()) } register(TIMESTAMPZ, TIMESTAMP) { x, _ -> Datum.timestamp(x.timestamp) } + register(TIMESTAMPZ, TIMESTAMPZ) { x, _ -> Datum.timestamp(x.timestamp) } + register(TIMESTAMPZ, TIME) { x, _ -> Datum.time(x.timestamp.toTime()) } + register(TIMESTAMPZ, DATE) { x, _ -> Datum.date(x.timestamp.toDate()) } + } + private fun registerDate() { + register(DATE, VARCHAR) { x, t -> Datum.varchar(x.date.toString(), t.length) } + register(DATE, CHAR) { x, t -> Datum.character(x.date.toString(), t.length) } + register(DATE, CLOB) { x, t -> Datum.clob(x.date.toString().toByteArray(), t.length) } + register(DATE, STRING) { x, _ -> Datum.string(x.date.toString()) } + register(DATE, SYMBOL) { x, _ -> Datum.symbol(x.date.toString()) } + register(DATE, DATE) { x, _ -> Datum.date(x.date) } + register(DATE, TIMESTAMP) { x, _ -> Datum.timestamp(DateTimeValue.timestamp(x.date, DateTimeValue.time(0, 0, 0))) } + register(DATE, TIMESTAMPZ) { x, _ -> Datum.timestamp(DateTimeValue.timestamp(x.date, DateTimeValue.time(0, 0, 0))) } + } + + private fun registerTime() { + // WITHOUT TZ + register(TIME, VARCHAR) { x, t -> Datum.varchar(x.time.toString(), t.length) } + register(TIME, CHAR) { x, t -> Datum.character(x.time.toString(), t.length) } + register(TIME, CLOB) { x, t -> Datum.clob(x.time.toString().toByteArray(), t.length) } + register(TIME, STRING) { x, _ -> Datum.string(x.time.toString()) } + register(TIME, SYMBOL) { x, _ -> Datum.symbol(x.time.toString()) } + register(TIME, TIME) { x, _ -> Datum.time(x.time) } + register(TIME, TIMESTAMP) { x, _ -> Datum.timestamp(DateTimeValue.timestamp(DateTimeValue.date(1970, 1, 1), x.time)) } + register(TIME, TIMESTAMPZ) { x, _ -> Datum.timestamp(DateTimeValue.timestamp(DateTimeValue.date(1970, 1, 1), x.time)) } + + // WITH TZ + register(TIMEZ, VARCHAR) { x, t -> Datum.varchar(x.time.toString(), t.length) } + register(TIMEZ, CHAR) { x, t -> Datum.character(x.time.toString(), t.length) } + register(TIMEZ, CLOB) { x, t -> Datum.clob(x.time.toString().toByteArray(), t.length) } + register(TIMEZ, STRING) { x, _ -> Datum.string(x.time.toString()) } + register(TIMEZ, SYMBOL) { x, _ -> Datum.symbol(x.time.toString()) } + register(TIMEZ, TIME) { x, _ -> Datum.time(x.time) } + register(TIMEZ, TIMESTAMP) { x, _ -> Datum.timestamp(DateTimeValue.timestamp(DateTimeValue.date(1970, 1, 1), x.time)) } + register(TIMEZ, TIMESTAMPZ) { x, _ -> Datum.timestamp(DateTimeValue.timestamp(DateTimeValue.date(1970, 1, 1), x.time)) } } private fun register(source: PType.Kind, target: PType.Kind, cast: (Datum, PType) -> Datum) { @@ -614,7 +700,7 @@ internal object CastTable { private fun datumInt(value: BigDecimal): Datum { val int = try { - value.setScale(0, RoundingMode.DOWN).intValueExact() + value.setScale(0, RoundingMode.HALF_EVEN).intValueExact() } catch (e: ArithmeticException) { throw DataException("Overflow when casting $value to INT") } @@ -659,7 +745,7 @@ internal object CastTable { private fun datumTinyInt(value: BigDecimal): Datum { val byte = try { - value.setScale(0, RoundingMode.DOWN).byteValueExact() + value.setScale(0, RoundingMode.HALF_EVEN).byteValueExact() } catch (e: ArithmeticException) { throw DataException("Overflow when casting $value to TINYINT") } @@ -703,7 +789,7 @@ internal object CastTable { } private fun datumSmallInt(value: BigDecimal): Datum { val short = try { - value.setScale(0, RoundingMode.DOWN).shortValueExact() + value.setScale(0, RoundingMode.HALF_EVEN).shortValueExact() } catch (e: ArithmeticException) { throw DataException("Overflow when casting $value to SMALLINT") } @@ -741,7 +827,7 @@ internal object CastTable { } private fun datumIntArbitrary(value: BigDecimal): Datum { - return Datum.numeric(value.setScale(0, RoundingMode.DOWN).toBigInteger()) + return Datum.numeric(value.setScale(0, RoundingMode.HALF_EVEN).toBigInteger()) } private fun datumIntArbitrary(value: Double): Datum { @@ -753,7 +839,7 @@ internal object CastTable { } private fun datumBigInt(value: BigDecimal): Datum { - return Datum.bigint(value.setScale(0, RoundingMode.DOWN).longValueExact()) + return Datum.bigint(value.setScale(0, RoundingMode.HALF_EVEN).longValueExact()) } private fun datumBigInt(value: Double): Datum { diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/statement/QueryStatement.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/statement/QueryStatement.kt index e130928c48..9b713eb632 100644 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/statement/QueryStatement.kt +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/statement/QueryStatement.kt @@ -5,17 +5,15 @@ import org.partiql.eval.PartiQLStatement import org.partiql.eval.internal.Environment import org.partiql.eval.internal.operator.Operator import org.partiql.spi.catalog.Session -import org.partiql.value.PartiQLValueExperimental internal class QueryStatement(root: Operator.Expr) : PartiQLStatement { // DO NOT USE FINAL private var _root = root - @OptIn(PartiQLValueExperimental::class) override fun execute(session: Session): PartiQLResult { val datum = _root.eval(Environment.empty) - val value = PartiQLResult.Value(datum.toPartiQLValue()) + val value = PartiQLResult.Value(datum) return value } } diff --git a/partiql-eval/src/test/kotlin/org/partiql/eval/internal/PartiQLEngineDefaultTest.kt b/partiql-eval/src/test/kotlin/org/partiql/eval/internal/PartiQLEngineDefaultTest.kt index 923f04cc1f..ad0827bea6 100644 --- a/partiql-eval/src/test/kotlin/org/partiql/eval/internal/PartiQLEngineDefaultTest.kt +++ b/partiql-eval/src/test/kotlin/org/partiql/eval/internal/PartiQLEngineDefaultTest.kt @@ -1,6 +1,5 @@ package org.partiql.eval.internal -import com.amazon.ionelement.api.createIonElementLoader import com.amazon.ionelement.api.loadSingleElement import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test @@ -12,16 +11,15 @@ import org.partiql.eval.PartiQLEngine import org.partiql.eval.PartiQLResult import org.partiql.parser.PartiQLParser import org.partiql.plan.v1.PartiQLPlan -import org.partiql.planner.builder.PartiQLPlannerBuilder import org.partiql.planner.internal.SqlPlannerV1 import org.partiql.plugins.memory.MemoryCatalog import org.partiql.plugins.memory.MemoryTable import org.partiql.spi.catalog.Name import org.partiql.spi.catalog.Session +import org.partiql.spi.value.Datum import org.partiql.spi.value.ion.IonDatum import org.partiql.types.PType import org.partiql.types.StaticType -import org.partiql.value.CollectionValue import org.partiql.value.PartiQLValue import org.partiql.value.PartiQLValueExperimental import org.partiql.value.bagValue @@ -36,6 +34,7 @@ import org.partiql.value.missingValue import org.partiql.value.nullValue import org.partiql.value.stringValue import org.partiql.value.structValue +import org.partiql.value.symbolValue import java.io.ByteArrayOutputStream import java.math.BigDecimal import java.math.BigInteger @@ -77,8 +76,70 @@ class PartiQLEngineDefaultTest { @Execution(ExecutionMode.CONCURRENT) fun globalsTests(tc: SuccessTestCase) = tc.assert() + @ParameterizedTest + @MethodSource("castTestCases") + @Execution(ExecutionMode.CONCURRENT) + fun castTests(tc: SuccessTestCase) = tc.assert() + companion object { + @JvmStatic + fun castTestCases() = listOf( + SuccessTestCase( + input = """ + CAST(20 AS DECIMAL(10, 5)); + """.trimIndent(), + expected = decimalValue(BigDecimal.valueOf(2000000, 5)) + ), + SuccessTestCase( + input = """ + CAST(20 AS DECIMAL(10, 3)); + """.trimIndent(), + expected = decimalValue(BigDecimal.valueOf(20000, 3)) + ), + SuccessTestCase( + input = """ + CAST(20 AS DECIMAL(2, 0)); + """.trimIndent(), + expected = decimalValue(BigDecimal.valueOf(20, 0)) + ), + SuccessTestCase( + input = """ + CAST(20 AS DECIMAL(1, 0)); + """.trimIndent(), + expected = missingValue(), + mode = PartiQLEngine.Mode.PERMISSIVE + ), + SuccessTestCase( + input = """ + 1 + 2.0 + """.trimIndent(), + expected = decimalValue(BigDecimal.valueOf(30, 1)) + ), + SuccessTestCase( + input = "SELECT DISTINCT VALUE t * 100 FROM <<0, 1, 2.0, 3.0>> AS t;", + expected = bagValue( + int32Value(0), + int32Value(100), + decimalValue(BigDecimal.valueOf(2000, 1)), + decimalValue(BigDecimal.valueOf(3000, 1)), + ) + ), + SuccessTestCase( + input = """ + CAST(20 AS SYMBOL); + """.trimIndent(), + expected = symbolValue("20"), + ), + // TODO: Use Datum for assertions. Currently, PartiQLValue doesn't support parameterized CHAR/VARCHAR +// SuccessTestCase( +// input = """ +// CAST(20 AS CHAR(2)); +// """.trimIndent(), +// expected = charValue("20"), +// ), + ) + @JvmStatic fun globalsTestCases() = listOf( SuccessTestCase( @@ -1246,9 +1307,7 @@ class PartiQLEngineDefaultTest { ) { private val engine = PartiQLEngine.builder().build() - private val planner = PartiQLPlannerBuilder().build() private val parser = PartiQLParser.default() - private val loader = createIonElementLoader() /** * @property value is a serialized Ion value. @@ -1288,7 +1347,7 @@ class PartiQLEngineDefaultTest { throw returned.cause } } - val output = result.value + val output = result.value.toPartiQLValue() // TODO: Assert directly on Datum assert(expected == output) { comparisonString(expected, output, plan) } @@ -1321,29 +1380,30 @@ class PartiQLEngineDefaultTest { ) { private val engine = PartiQLEngine.builder().build() - private val planner = PartiQLPlannerBuilder().build() private val parser = PartiQLParser.default() internal fun assert() { - val permissiveResult = run(mode = PartiQLEngine.Mode.PERMISSIVE) + val (permissiveResult, plan) = run(mode = PartiQLEngine.Mode.PERMISSIVE) + val permissiveResultPValue = permissiveResult.toPartiQLValue() val assertionCondition = try { - expectedPermissive == permissiveResult.first + expectedPermissive == permissiveResultPValue // TODO: Assert using Datum } catch (t: Throwable) { val str = buildString { appendLine("Test Name: $name") // TODO pretty-print V1 plans! - appendLine(permissiveResult.second) + appendLine(plan) } throw RuntimeException(str, t) } assert(assertionCondition) { - comparisonString(expectedPermissive, permissiveResult.first, permissiveResult.second) + comparisonString(expectedPermissive, permissiveResultPValue, plan) } var error: Throwable? = null try { - when (val result = run(mode = PartiQLEngine.Mode.STRICT).first) { - is CollectionValue<*> -> result.toList() - else -> result + val (strictResult, _) = run(mode = PartiQLEngine.Mode.STRICT) + when (strictResult.type.kind) { + PType.Kind.BAG, PType.Kind.ARRAY, PType.Kind.SEXP -> strictResult.toList() + else -> strictResult } } catch (e: Throwable) { error = e @@ -1351,7 +1411,7 @@ class PartiQLEngineDefaultTest { assertNotNull(error) } - private fun run(mode: PartiQLEngine.Mode): Pair { + private fun run(mode: PartiQLEngine.Mode): Pair { val statement = parser.parse(input).root val catalog = MemoryCatalog.builder().name("memory").build() val session = Session.builder() @@ -1419,20 +1479,6 @@ class PartiQLEngineDefaultTest { tc.assert() } - @Test - @Disabled("CASTS have not yet been implemented.") - fun testCast1() = SuccessTestCase( - input = "1 + 2.0", - expected = int32Value(3), - ).assert() - - @Test - @Disabled("CASTS have not yet been implemented.") - fun testCasts() = SuccessTestCase( - input = "SELECT DISTINCT VALUE t * 100 FROM <<0, 1, 2.0, 3.0>> AS t;", - expected = bagValue(int32Value(0), int32Value(100), int32Value(200), int32Value(300)) - ).assert() - @Test @Disabled("We need to support section 5.1") fun testTypingOfPositionVariable() = TypingTestCase( diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/casts/CastTable.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/casts/CastTable.kt index e68a175037..f7ced8bf82 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/casts/CastTable.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/casts/CastTable.kt @@ -59,6 +59,11 @@ internal class CastTable private constructor( // initialize all with empty relationships graph[type.ordinal] = Array(N) { Status.NO } } + graph[Kind.UNKNOWN.ordinal] = relationships { + PType.Kind.values().map { + cast(it) + } + } graph[Kind.DYNAMIC.ordinal] = relationships { cast(Kind.DYNAMIC) Kind.entries.filterNot { it == Kind.DYNAMIC }.forEach { @@ -94,6 +99,7 @@ internal class CastTable private constructor( cast(Kind.DOUBLE) cast(Kind.STRING) cast(Kind.VARCHAR) + cast(Kind.CHAR) cast(Kind.SYMBOL) } graph[Kind.SMALLINT.ordinal] = relationships { @@ -108,6 +114,7 @@ internal class CastTable private constructor( cast(Kind.REAL) cast(Kind.DOUBLE) cast(Kind.STRING) + cast(Kind.CHAR) cast(Kind.VARCHAR) cast(Kind.SYMBOL) } @@ -123,6 +130,7 @@ internal class CastTable private constructor( cast(Kind.REAL) cast(Kind.DOUBLE) cast(Kind.STRING) + cast(Kind.CHAR) cast(Kind.VARCHAR) cast(Kind.SYMBOL) } @@ -138,6 +146,7 @@ internal class CastTable private constructor( cast(Kind.REAL) cast(Kind.DOUBLE) cast(Kind.STRING) + cast(Kind.CHAR) cast(Kind.VARCHAR) cast(Kind.SYMBOL) } @@ -153,6 +162,7 @@ internal class CastTable private constructor( cast(Kind.REAL) cast(Kind.DOUBLE) cast(Kind.STRING) + cast(Kind.CHAR) cast(Kind.VARCHAR) cast(Kind.SYMBOL) } @@ -168,6 +178,7 @@ internal class CastTable private constructor( cast(Kind.REAL) cast(Kind.DOUBLE) cast(Kind.STRING) + cast(Kind.CHAR) cast(Kind.VARCHAR) cast(Kind.SYMBOL) } @@ -183,6 +194,7 @@ internal class CastTable private constructor( cast(Kind.REAL) cast(Kind.DOUBLE) cast(Kind.STRING) + cast(Kind.CHAR) cast(Kind.VARCHAR) cast(Kind.SYMBOL) } @@ -198,6 +210,7 @@ internal class CastTable private constructor( cast(Kind.REAL) cast(Kind.DOUBLE) cast(Kind.STRING) + cast(Kind.CHAR) cast(Kind.VARCHAR) cast(Kind.SYMBOL) } @@ -213,60 +226,107 @@ internal class CastTable private constructor( cast(Kind.REAL) cast(Kind.DOUBLE) cast(Kind.STRING) + cast(Kind.CHAR) cast(Kind.VARCHAR) cast(Kind.SYMBOL) } graph[Kind.CHAR.ordinal] = relationships { - cast(Kind.BOOL) - cast(Kind.CHAR) + Kind.values().filterNot { + it in setOf(Kind.BLOB, Kind.UNKNOWN, Kind.DYNAMIC, Kind.ARRAY, Kind.BAG, Kind.SEXP, Kind.ROW) + }.forEach { + cast(it) + } + } + graph[Kind.STRING.ordinal] = relationships { + Kind.values().filterNot { + it in setOf(Kind.BLOB, Kind.UNKNOWN, Kind.DYNAMIC, Kind.ARRAY, Kind.BAG, Kind.SEXP, Kind.ROW) + }.forEach { + cast(it) + } + } + graph[Kind.VARCHAR.ordinal] = relationships { + Kind.values().filterNot { + it in setOf(Kind.BLOB, Kind.UNKNOWN, Kind.DYNAMIC, Kind.ARRAY, Kind.BAG, Kind.SEXP, Kind.ROW) + }.forEach { + cast(it) + } + } + graph[Kind.SYMBOL.ordinal] = relationships { + Kind.values().filterNot { + it in setOf(Kind.BLOB, Kind.UNKNOWN, Kind.DYNAMIC, Kind.ARRAY, Kind.BAG, Kind.SEXP, Kind.ROW) + }.forEach { + cast(it) + } + } + graph[Kind.CLOB.ordinal] = relationships { + Kind.values().filterNot { + it in setOf(Kind.BLOB, Kind.UNKNOWN, Kind.DYNAMIC, Kind.ARRAY, Kind.BAG, Kind.SEXP, Kind.ROW) + }.forEach { + cast(it) + } + } + graph[Kind.BLOB.ordinal] = Array(N) { Status.NO } + graph[Kind.DATE.ordinal] = relationships { + cast(Kind.TIMESTAMPZ) + cast(Kind.TIMESTAMP) + cast(Kind.TIMEZ) + cast(Kind.DATE) cast(Kind.STRING) cast(Kind.VARCHAR) + cast(Kind.CHAR) cast(Kind.SYMBOL) + cast(Kind.CLOB) } - graph[Kind.STRING.ordinal] = relationships { - cast(Kind.BOOL) - cast(Kind.TINYINT) - cast(Kind.SMALLINT) - cast(Kind.INTEGER) - cast(Kind.BIGINT) - cast(Kind.NUMERIC) - cast(Kind.DECIMAL) - cast(Kind.DECIMAL_ARBITRARY) + graph[Kind.TIMEZ.ordinal] = relationships { + cast(Kind.TIMESTAMPZ) + cast(Kind.TIMESTAMP) + cast(Kind.TIMEZ) + cast(Kind.TIME) cast(Kind.STRING) cast(Kind.VARCHAR) + cast(Kind.CHAR) cast(Kind.SYMBOL) cast(Kind.CLOB) } - graph[Kind.VARCHAR.ordinal] = relationships { - cast(Kind.BOOL) - cast(Kind.TINYINT) - cast(Kind.SMALLINT) - cast(Kind.INTEGER) - cast(Kind.BIGINT) - cast(Kind.NUMERIC) + graph[Kind.TIME.ordinal] = relationships { + cast(Kind.TIMESTAMPZ) + cast(Kind.TIMESTAMP) + cast(Kind.TIMEZ) + cast(Kind.TIME) cast(Kind.STRING) cast(Kind.VARCHAR) + cast(Kind.CHAR) cast(Kind.SYMBOL) cast(Kind.CLOB) } - graph[Kind.SYMBOL.ordinal] = relationships { - cast(Kind.BOOL) + graph[Kind.TIMESTAMPZ.ordinal] = relationships { + cast(Kind.TIMESTAMPZ) + cast(Kind.TIMESTAMP) + cast(Kind.DATE) + cast(Kind.TIMEZ) + cast(Kind.TIME) cast(Kind.STRING) cast(Kind.VARCHAR) + cast(Kind.CHAR) cast(Kind.SYMBOL) cast(Kind.CLOB) } - graph[Kind.CLOB.ordinal] = relationships { + graph[Kind.TIMESTAMP.ordinal] = relationships { + cast(Kind.TIMESTAMPZ) + cast(Kind.TIMESTAMP) + cast(Kind.DATE) + cast(Kind.TIMEZ) + cast(Kind.TIME) + cast(Kind.STRING) + cast(Kind.VARCHAR) + cast(Kind.CHAR) + cast(Kind.SYMBOL) cast(Kind.CLOB) } - graph[Kind.BLOB.ordinal] = Array(N) { Status.NO } - graph[Kind.DATE.ordinal] = Array(N) { Status.NO } - graph[Kind.TIMEZ.ordinal] = Array(N) { Status.NO } - graph[Kind.TIME.ordinal] = Array(N) { Status.NO } - graph[Kind.TIMESTAMPZ.ordinal] = Array(N) { Status.NO } - graph[Kind.TIMESTAMP.ordinal] = Array(N) { Status.NO } graph[Kind.BAG.ordinal] = relationships { cast(Kind.BAG) + cast(Kind.ARRAY) + cast(Kind.SEXP) } graph[Kind.ARRAY.ordinal] = relationships { cast(Kind.BAG) diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/RexConverter.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/RexConverter.kt index 977f19d22e..b458587a54 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/RexConverter.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/RexConverter.kt @@ -25,6 +25,7 @@ import org.partiql.ast.Select import org.partiql.ast.SetQuantifier import org.partiql.ast.Type import org.partiql.ast.visitor.AstBaseVisitor +import org.partiql.errors.TypeCheckException import org.partiql.planner.internal.Env import org.partiql.planner.internal.ir.Rel import org.partiql.planner.internal.ir.Rex @@ -875,34 +876,66 @@ internal object RexConverter { is Type.Real -> PType.real() is Type.Float32 -> PType.real() is Type.Float64 -> PType.doublePrecision() - is Type.Decimal -> when { - type.precision == null && type.scale == null -> PType.decimal() - type.precision != null && type.scale != null -> PType.decimal(type.precision!!, type.scale!!) - type.precision != null && type.scale == null -> PType.decimal(type.precision!!, 0) - else -> error("Precision can never be null while scale is specified.") + is Type.Decimal -> { + val p = type.precision + val s = type.scale + when { + p == null && s == null -> PType.decimal() + p != null && s != null -> { + assertParamCompToZero(PType.Kind.DECIMAL, "precision", p, false) + assertParamCompToZero(PType.Kind.DECIMAL, "scale", s, true) + if (s > p) { + throw TypeCheckException("Decimal scale cannot be greater than precision.") + } + PType.decimal(p, s) + } + p != null && s == null -> { + assertParamCompToZero(PType.Kind.DECIMAL, "precision", p, false) + PType.decimal(p, 0) + } + else -> error("Precision can never be null while scale is specified.") + } } - - is Type.Numeric -> when { - type.precision == null && type.scale == null -> PType.decimal() - type.precision != null && type.scale != null -> PType.decimal(type.precision!!, type.scale!!) - type.precision != null && type.scale == null -> PType.decimal(type.precision!!, 0) - else -> error("Precision can never be null while scale is specified.") + is Type.Numeric -> { + val p = type.precision + val s = type.scale + when { + p == null && s == null -> PType.decimal() + p != null && s != null -> { + assertParamCompToZero(PType.Kind.NUMERIC, "precision", p, false) + assertParamCompToZero(PType.Kind.NUMERIC, "scale", s, true) + if (s > p) { + throw TypeCheckException("Numeric scale cannot be greater than precision.") + } + PType.decimal(type.precision!!, type.scale!!) + } + p != null && s == null -> { + assertParamCompToZero(PType.Kind.NUMERIC, "precision", p, false) + PType.decimal(p, 0) + } + else -> error("Precision can never be null while scale is specified.") + } + } + is Type.Char -> { + val length = type.length ?: 1 + assertGtZeroAndCreate(PType.Kind.CHAR, "length", length, PType::character) + } + is Type.Varchar -> { + val length = type.length ?: 1 + assertGtZeroAndCreate(PType.Kind.VARCHAR, "length", length, PType::varchar) } - - is Type.Char -> PType.character(type.length ?: 255) // TODO: What is default? - is Type.Varchar -> error("VARCHAR is not supported yet.") is Type.String -> PType.string() is Type.Symbol -> PType.symbol() is Type.Bit -> error("BIT is not supported yet.") is Type.BitVarying -> error("BIT VARYING is not supported yet.") is Type.ByteString -> error("BINARY is not supported yet.") - is Type.Blob -> PType.blob(type.length ?: Int.MAX_VALUE) - is Type.Clob -> PType.clob(type.length ?: Int.MAX_VALUE) + is Type.Blob -> assertGtZeroAndCreate(PType.Kind.BLOB, "length", type.length ?: Int.MAX_VALUE, PType::blob) + is Type.Clob -> assertGtZeroAndCreate(PType.Kind.CLOB, "length", type.length ?: Int.MAX_VALUE, PType::clob) is Type.Date -> PType.date() - is Type.Time -> PType.time(type.precision ?: 6) - is Type.TimeWithTz -> PType.timez(type.precision ?: 6) - is Type.Timestamp -> PType.timestamp(type.precision ?: 6) - is Type.TimestampWithTz -> PType.timestampz(type.precision ?: 6) + is Type.Time -> assertGtEqZeroAndCreate(PType.Kind.TIME, "precision", type.precision ?: 0, PType::time) + is Type.TimeWithTz -> assertGtEqZeroAndCreate(PType.Kind.TIMEZ, "precision", type.precision ?: 0, PType::timez) + is Type.Timestamp -> assertGtEqZeroAndCreate(PType.Kind.TIMESTAMP, "precision", type.precision ?: 6, PType::timestamp) + is Type.TimestampWithTz -> assertGtEqZeroAndCreate(PType.Kind.TIMESTAMPZ, "precision", type.precision ?: 6, PType::timestampz) is Type.Interval -> error("INTERVAL is not supported yet.") is Type.Bag -> PType.bag() is Type.Sexp -> PType.sexp() @@ -914,6 +947,29 @@ internal object RexConverter { }.toCType() } + private fun assertGtZeroAndCreate(type: PType.Kind, param: String, value: Int, create: (Int) -> PType): PType { + assertParamCompToZero(type, param, value, false) + return create.invoke(value) + } + + private fun assertGtEqZeroAndCreate(type: PType.Kind, param: String, value: Int, create: (Int) -> PType): PType { + assertParamCompToZero(type, param, value, true) + return create.invoke(value) + } + + /** + * @param allowZero when FALSE, this asserts that [value] > 0. If TRUE, this asserts that [value] >= 0. + */ + private fun assertParamCompToZero(type: PType.Kind, param: String, value: Int, allowZero: Boolean) { + val (result, compString) = when (allowZero) { + true -> (value >= 0) to "greater than" + false -> (value > 0) to "greater than or equal to" + } + if (!result) { + throw TypeCheckException("$type $param must be an integer value $compString 0.") + } + } + override fun visitExprDateAdd(node: Expr.DateAdd, ctx: Env): Rex { val type = TIMESTAMP // Args diff --git a/partiql-spi/api/partiql-spi.api b/partiql-spi/api/partiql-spi.api index fcebd234cb..4191c899af 100644 --- a/partiql-spi/api/partiql-spi.api +++ b/partiql-spi/api/partiql-spi.api @@ -590,7 +590,10 @@ public abstract interface class org/partiql/spi/value/Datum : java/lang/Iterable public static fun bigint (J)Lorg/partiql/spi/value/Datum; public static fun blob ([B)Lorg/partiql/spi/value/Datum; public static fun bool (Z)Lorg/partiql/spi/value/Datum; + public static fun character (Ljava/lang/String;)Lorg/partiql/spi/value/Datum; + public static fun character (Ljava/lang/String;I)Lorg/partiql/spi/value/Datum; public static fun clob ([B)Lorg/partiql/spi/value/Datum; + public static fun clob ([BI)Lorg/partiql/spi/value/Datum; public static fun comparator ()Ljava/util/Comparator; public static fun comparator (Z)Ljava/util/Comparator; public static fun date (Lorg/partiql/value/datetime/Date;)Lorg/partiql/spi/value/Datum; @@ -637,6 +640,8 @@ public abstract interface class org/partiql/spi/value/Datum : java/lang/Iterable public static fun timestamp (Lorg/partiql/value/datetime/Timestamp;)Lorg/partiql/spi/value/Datum; public static fun tinyint (B)Lorg/partiql/spi/value/Datum; public fun toPartiQLValue ()Lorg/partiql/value/PartiQLValue; + public static fun varchar (Ljava/lang/String;)Lorg/partiql/spi/value/Datum; + public static fun varchar (Ljava/lang/String;I)Lorg/partiql/spi/value/Datum; } public abstract interface class org/partiql/spi/value/Field { diff --git a/partiql-spi/src/main/java/org/partiql/spi/value/Datum.java b/partiql-spi/src/main/java/org/partiql/spi/value/Datum.java index 718287545b..5efa3a0fca 100644 --- a/partiql-spi/src/main/java/org/partiql/spi/value/Datum.java +++ b/partiql-spi/src/main/java/org/partiql/spi/value/Datum.java @@ -3,6 +3,7 @@ import kotlin.NotImplementedError; import kotlin.Pair; import org.jetbrains.annotations.NotNull; +import org.partiql.errors.DataException; import org.partiql.types.PType; import org.partiql.value.PartiQL; import org.partiql.value.PartiQLValue; @@ -13,6 +14,8 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.math.MathContext; +import java.math.RoundingMode; import java.util.Comparator; import java.util.Iterator; import java.util.Objects; @@ -585,8 +588,12 @@ static Datum decimal(@NotNull BigDecimal value) { } @NotNull - static Datum decimal(@NotNull BigDecimal value, int precision, int scale) { - return new DatumDecimal(value, PType.decimal(precision, scale)); + static Datum decimal(@NotNull BigDecimal value, int precision, int scale) throws DataException { + BigDecimal d = value.round(new MathContext(precision)).setScale(scale, RoundingMode.HALF_UP); + if (d.precision() > precision) { + throw new DataException("Value " + d + " could not fit into decimal with precision/scale."); + } + return new DatumDecimal(d, PType.decimal(precision, scale)); } // CHARACTER STRINGS @@ -596,6 +603,69 @@ static Datum string(@NotNull String value) { return new DatumString(value, PType.string()); } + /** + * + * @param value the string to place in the varchar + * @return a varchar value with a default length of 255 + */ + @NotNull + static Datum varchar(@NotNull String value) { + return varchar(value, 255); + } + + /** + * + * @param value the string to place in the varchar + * @return a varchar value + * TODO: Error or coerce here? Right now coerce, though I think this should likely error. + */ + @NotNull + static Datum varchar(@NotNull String value, int length) { + String newValue; + if (length <= 0) { + throw new DataException("VARCHAR of length " + length + " not allowed."); + } + if (value.length() < length) { + newValue = String.format("%-" + length + "." + length + "s", value); + } else if (value.length() == length) { + newValue = value; + } else { + newValue = value.substring(0, length); + } + return new DatumString(newValue, PType.varchar(length)); + } + + /** + * + * @param value the string to place in the char + * @return a char value with a default length of 255 + */ + @NotNull + static Datum character(@NotNull String value) { + return character(value, 255); + } + + /** + * + * @param value the string to place in the char + * @return a char value + */ + @NotNull + static Datum character(@NotNull String value, int length) { + String newValue; + if (length <= 0) { + throw new DataException("CHAR of length " + length + " not allowed."); + } + if (value.length() < length) { + newValue = String.format("%-" + length + "." + length + "s", value); + } else if (value.length() == length) { + newValue = value; + } else { + newValue = value.substring(0, length); + } + return new DatumString(newValue, PType.character(length)); + } + @NotNull static Datum symbol(@NotNull String value) { return new DatumString(value, PType.symbol()); @@ -603,7 +673,12 @@ static Datum symbol(@NotNull String value) { @NotNull static Datum clob(@NotNull byte[] value) { - return new DatumBytes(value, PType.clob(Integer.MAX_VALUE)); + return clob(value, Integer.MAX_VALUE); + } + + @NotNull + static Datum clob(@NotNull byte[] value, int length) { + return new DatumBytes(value, PType.clob(length)); } // BYTE STRINGS diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/value/ion/IonDatum.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/value/ion/IonDatum.kt index 525a48a067..201fa2d64f 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/value/ion/IonDatum.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/value/ion/IonDatum.kt @@ -18,7 +18,9 @@ import org.partiql.spi.value.Datum import org.partiql.spi.value.Field import org.partiql.types.PType import org.partiql.value.datetime.Date +import org.partiql.value.datetime.DateTimeValue import org.partiql.value.datetime.Time +import org.partiql.value.datetime.TimeZone import org.partiql.value.datetime.Timestamp import java.math.BigDecimal import java.math.BigInteger @@ -162,15 +164,35 @@ public class IonDatum private constructor(value: AnyElement, type: PType) : // } override fun getDate(): Date { - TODO("IonDatum does not support DATE") + return when (_kind) { + TIMESTAMP -> { + val ts = _value.timestampValue + DateTimeValue.date(ts.year, ts.month, ts.day) + } + else -> super.getDate() + } } override fun getTime(): Time { - TODO("IonDatum does not support TIME") + return when (_kind) { + TIMESTAMP -> { + val ts = _value.timestampValue + val tz = when (ts.localOffset) { + null -> TimeZone.UnknownTimeZone + else -> TimeZone.UtcOffset.of(ts.zHour, ts.zMinute) + } + DateTimeValue.time(ts.hour, ts.minute, ts.second, tz) + } + else -> super.getTime() + } } + // TODO: Handle struct notation override fun getTimestamp(): Timestamp { - TODO("IonDatum does not support TIMESTAMP") + return when (_kind) { + TIMESTAMP -> DateTimeValue.timestamp(_value.timestampValue) + else -> super.getTimestamp() + } } override fun getBigInteger(): BigInteger = when (_kind) { diff --git a/test/partiql-tests-runner/src/test/kotlin/org/partiql/runner/executor/EvalExecutor.kt b/test/partiql-tests-runner/src/test/kotlin/org/partiql/runner/executor/EvalExecutor.kt index e7c660ba22..beaf5e1bfe 100644 --- a/test/partiql-tests-runner/src/test/kotlin/org/partiql/runner/executor/EvalExecutor.kt +++ b/test/partiql-tests-runner/src/test/kotlin/org/partiql/runner/executor/EvalExecutor.kt @@ -48,20 +48,22 @@ class EvalExecutor( override fun fromIon(value: IonValue): PartiQLResult { val partiql = PartiQLValueIonReaderBuilder.standard().build(value.toIonElement()).read() + val datum = Datum.of(partiql) - return PartiQLResult.Value(partiql) + return PartiQLResult.Value(datum) } override fun toIon(value: PartiQLResult): IonValue { if (value is PartiQLResult.Value) { - return value.value.toIon().toIonValue(ION) + return value.value.toPartiQLValue().toIon().toIonValue(ION) } error("PartiQLResult cannot be converted to Ion") } + // TODO: Use DATUM override fun compare(actual: PartiQLResult, expect: PartiQLResult): Boolean { if (actual is PartiQLResult.Value && expect is PartiQLResult.Value) { - return valueComparison(actual.value, expect.value) + return valueComparison(actual.value.toPartiQLValue(), expect.value.toPartiQLValue()) } if (actual is PartiQLResult.Error) { throw actual.cause