diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dbdafbbde..f644b23357 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Thank you to all who have contributed! ## [Unreleased] ### Added +- Adds `PartiQLValueTextWriter` implementation of date, time, and timestamp values ### Changed - **Behavioral change**: The planner now does NOT support the NullType and MissingType variants of StaticType. The logic diff --git a/partiql-ast/src/test/kotlin/org/partiql/ast/sql/SqlDialectTest.kt b/partiql-ast/src/test/kotlin/org/partiql/ast/sql/SqlDialectTest.kt index 039fbd4112..6276adb43d 100644 --- a/partiql-ast/src/test/kotlin/org/partiql/ast/sql/SqlDialectTest.kt +++ b/partiql-ast/src/test/kotlin/org/partiql/ast/sql/SqlDialectTest.kt @@ -27,6 +27,9 @@ import org.partiql.ast.builder.ast import org.partiql.ast.exprLit import org.partiql.value.PartiQLValueExperimental import org.partiql.value.boolValue +import org.partiql.value.dateValue +import org.partiql.value.datetime.DateTimeValue +import org.partiql.value.datetime.TimeZone import org.partiql.value.decimalValue import org.partiql.value.float32Value import org.partiql.value.float64Value @@ -39,6 +42,8 @@ import org.partiql.value.missingValue import org.partiql.value.nullValue import org.partiql.value.stringValue import org.partiql.value.symbolValue +import org.partiql.value.timeValue +import org.partiql.value.timestampValue import java.math.BigDecimal import java.math.BigInteger import kotlin.test.assertFails @@ -406,6 +411,16 @@ class SqlDialectTest { expect("""hello""") { exprLit(symbolValue("hello")) }, + expect("DATE '0001-02-03'") { + exprLit(dateValue(DateTimeValue.date(1, 2, 3))) + }, + expect("TIME '01:02:03.456-00:30'") { + exprLit(timeValue(DateTimeValue.time(1, 2, BigDecimal.valueOf(3.456), TimeZone.UtcOffset.of(-30)))) + }, + expect("TIMESTAMP '0001-02-03 04:05:06.78-00:30'") { + exprLit(timestampValue(DateTimeValue.timestamp(1, 2, 3, 4, 5, BigDecimal.valueOf(6.78), TimeZone.UtcOffset.of(-30)))) + }, + // expect("""{{ '''Hello''' '''World''' }}""") { // exprLit(clobValue("HelloWorld".toByteArray())) // }, diff --git a/partiql-types/src/main/kotlin/org/partiql/value/io/PartiQLValueTextWriter.kt b/partiql-types/src/main/kotlin/org/partiql/value/io/PartiQLValueTextWriter.kt index e0d1ca63bb..9ae8e49d07 100644 --- a/partiql-types/src/main/kotlin/org/partiql/value/io/PartiQLValueTextWriter.kt +++ b/partiql-types/src/main/kotlin/org/partiql/value/io/PartiQLValueTextWriter.kt @@ -18,6 +18,7 @@ import org.partiql.value.BagValue import org.partiql.value.BoolValue import org.partiql.value.CharValue import org.partiql.value.CollectionValue +import org.partiql.value.DateValue import org.partiql.value.DecimalValue import org.partiql.value.Float32Value import org.partiql.value.Float64Value @@ -35,9 +36,17 @@ import org.partiql.value.SexpValue import org.partiql.value.StringValue import org.partiql.value.StructValue import org.partiql.value.SymbolValue +import org.partiql.value.TimeValue +import org.partiql.value.TimestampValue +import org.partiql.value.datetime.Date +import org.partiql.value.datetime.Time +import org.partiql.value.datetime.TimeZone +import org.partiql.value.datetime.Timestamp import org.partiql.value.util.PartiQLValueBaseVisitor import java.io.OutputStream import java.io.PrintStream +import java.math.BigDecimal +import kotlin.math.abs /** * [PartiQLValueWriter] which outputs PartiQL text. @@ -192,6 +201,74 @@ public class PartiQLValueTextWriter( } } + override fun visitDate(v: DateValue, format: Format?) = v.toString(format) { + when (val value = v.value) { + null -> "null" // null.date + else -> sqlString(value) + } + } + + private fun padZeros(v: Int, totalDigits: Int): String = String.format("%0${totalDigits}d", v) + + private fun sqlString(d: Date): String { + val yyyy = padZeros(d.year, 4) + val mm = padZeros(d.month, 2) + val dd = padZeros(d.day, 2) + return "DATE '$yyyy-$mm-$dd'" + } + + override fun visitTime(v: TimeValue, format: Format?) = v.toString(format) { + when (val value = v.value) { + null -> "null" // null.time + else -> sqlString(value) + } + } + + private fun sqlString(tz: TimeZone?): String { + return when (tz) { + null -> "" + is TimeZone.UnknownTimeZone -> "-00:00" + is TimeZone.UtcOffset -> { + val sign = if (tz.totalOffsetMinutes < 0) { + "-" + } else { + "+" + } + val hh = padZeros(abs(tz.tzHour), 2) + val mm = padZeros(abs(tz.tzMinute), 2) + "$sign$hh:$mm" + } + } + } + + private fun sqlString(t: Time): String { + val hh = padZeros(t.hour, 2) + val mm = padZeros(t.minute, 2) + val ss = padZeros(t.decimalSecond.toInt(), 2) + val frac = t.decimalSecond.remainder(BigDecimal.ONE).toString().substring(1) // drop leading 0 + val timeZone = sqlString(t.timeZone) + return "TIME '$hh:$mm:$ss$frac$timeZone'" + } + + override fun visitTimestamp(v: TimestampValue, format: Format?) = v.toString(format) { + when (val value = v.value) { + null -> "null" // null.timestamp + else -> sqlString(value) + } + } + + private fun sqlString(t: Timestamp): String { + val yyyy = padZeros(t.year, 4) + val mon = padZeros(t.month, 2) + val dd = padZeros(t.day, 2) + val hh = padZeros(t.hour, 2) + val min = padZeros(t.minute, 2) + val ss = padZeros(t.decimalSecond.toInt(), 2) + val frac = t.decimalSecond.remainder(BigDecimal.ONE).toString().substring(1) // drop leading 0 + val timeZone = sqlString(t.timeZone) + return "TIMESTAMP '$yyyy-$mon-$dd $hh:$min:$ss$frac$timeZone'" + } + override fun visitBag(v: BagValue<*>, format: Format?) = collection(v, format, "<<" to ">>") override fun visitList(v: ListValue<*>, format: Format?) = collection(v, format, "[" to "]") diff --git a/partiql-types/src/test/kotlin/org/partiql/value/io/PartiQLValueTextWriterTest.kt b/partiql-types/src/test/kotlin/org/partiql/value/io/PartiQLValueTextWriterTest.kt index 309d832a64..1b5d40495b 100644 --- a/partiql-types/src/test/kotlin/org/partiql/value/io/PartiQLValueTextWriterTest.kt +++ b/partiql-types/src/test/kotlin/org/partiql/value/io/PartiQLValueTextWriterTest.kt @@ -13,6 +13,9 @@ import org.partiql.value.PartiQLValueExperimental import org.partiql.value.bagValue import org.partiql.value.boolValue import org.partiql.value.charValue +import org.partiql.value.dateValue +import org.partiql.value.datetime.DateTimeValue +import org.partiql.value.datetime.TimeZone import org.partiql.value.decimalValue import org.partiql.value.float32Value import org.partiql.value.float64Value @@ -28,6 +31,8 @@ import org.partiql.value.sexpValue import org.partiql.value.stringValue import org.partiql.value.structValue import org.partiql.value.symbolValue +import org.partiql.value.timeValue +import org.partiql.value.timestampValue import java.io.ByteArrayOutputStream import java.io.PrintStream import java.math.BigDecimal @@ -37,7 +42,6 @@ import java.math.BigInteger * Basic text writing test. * * TODOs - * - Dates and times * - String/Symbol escapes */ class PartiQLValueTextWriterTest { @@ -173,13 +177,62 @@ class PartiQLValueTextWriterTest { value = symbolValue("f.x"), expected = "f.x", ), + case( + value = dateValue(DateTimeValue.date(1, 2, 3)), + expected = "DATE '0001-02-03'", + ), + case( + value = dateValue(DateTimeValue.date(2020, 2, 3)), + expected = "DATE '2020-02-03'", + ), + case( + value = timeValue(DateTimeValue.time(1, 2, 3)), + expected = "TIME '01:02:03'", + ), + case( + value = timeValue(DateTimeValue.time(1, 2, BigDecimal.valueOf(3.456))), + expected = "TIME '01:02:03.456'", + ), + case( + value = timeValue(DateTimeValue.time(1, 2, BigDecimal.valueOf(3.456), TimeZone.UnknownTimeZone)), + expected = "TIME '01:02:03.456-00:00'", + ), + case( + value = timeValue(DateTimeValue.time(1, 2, BigDecimal.valueOf(3.456), TimeZone.UtcOffset.of(0))), + expected = "TIME '01:02:03.456+00:00'", + ), + case( + value = timeValue(DateTimeValue.time(1, 2, BigDecimal.valueOf(3.456), TimeZone.UtcOffset.of(+30))), + expected = "TIME '01:02:03.456+00:30'", + ), + case( + value = timeValue(DateTimeValue.time(1, 2, BigDecimal.valueOf(3.456), TimeZone.UtcOffset.of(-30))), + expected = "TIME '01:02:03.456-00:30'", + ), + case( + value = timestampValue(DateTimeValue.timestamp(1, 2, 3, 4, 5, BigDecimal.valueOf(6.78))), + expected = "TIMESTAMP '0001-02-03 04:05:06.78'", + ), + case( + value = timestampValue(DateTimeValue.timestamp(1, 2, 3, 4, 5, BigDecimal.valueOf(6.78), TimeZone.UnknownTimeZone)), + expected = "TIMESTAMP '0001-02-03 04:05:06.78-00:00'", + ), + case( + value = timestampValue(DateTimeValue.timestamp(1, 2, 3, 4, 5, BigDecimal.valueOf(6.78), TimeZone.UtcOffset.of(0))), + expected = "TIMESTAMP '0001-02-03 04:05:06.78+00:00'", + ), + case( + value = timestampValue(DateTimeValue.timestamp(1, 2, 3, 4, 5, BigDecimal.valueOf(6.78), TimeZone.UtcOffset.of(+30))), + expected = "TIMESTAMP '0001-02-03 04:05:06.78+00:30'", + ), + case( + value = timestampValue(DateTimeValue.timestamp(1, 2, 3, 4, 5, BigDecimal.valueOf(6.78), TimeZone.UtcOffset.of(-30))), + expected = "TIMESTAMP '0001-02-03 04:05:06.78-00:30'", + ), // TODO CLOB // TODO BINARY // TODO BYTE // TODO BLOB - // TODO DATE - // TODO TIME - // TODO TIMESTAMP // TODO INTERVAL )