From 224453801831eb9085ed5e0f4a6153ad6d8d4ca6 Mon Sep 17 00:00:00 2001 From: Benjamin Gaidioz Date: Mon, 13 May 2024 17:38:28 +0200 Subject: [PATCH] Fixed RD-10923: Milliseconds lost in TIME/TIMESTAMP --- .../credentials/MySQLPackageTest.scala | 8 +-- .../credentials/PostgreSQLPackageTest.scala | 8 +-- .../credentials/SnowflakePackageTest.scala | 8 +-- .../credentials/SqlServerPackageTest.scala | 8 +-- .../truffle/ast/io/jdbc/JdbcQuery.java | 4 +- .../NamedParametersPreparedStatement.scala | 5 +- .../sql/writers/TypedResultSetCsvWriter.scala | 4 +- .../writers/TypedResultSetJsonWriter.scala | 4 +- .../sql/TestSqlCompilerServiceAirports.scala | 69 +++++++++++++++++++ 9 files changed, 97 insertions(+), 21 deletions(-) diff --git a/snapi-client/src/test/scala/raw/compiler/rql2/tests/builtin/credentials/MySQLPackageTest.scala b/snapi-client/src/test/scala/raw/compiler/rql2/tests/builtin/credentials/MySQLPackageTest.scala index d1c42c1cc..6a689dfea 100644 --- a/snapi-client/src/test/scala/raw/compiler/rql2/tests/builtin/credentials/MySQLPackageTest.scala +++ b/snapi-client/src/test/scala/raw/compiler/rql2/tests/builtin/credentials/MySQLPackageTest.scala @@ -35,9 +35,9 @@ trait MySQLPackageTest extends Rql2CompilerTestContext with CredentialsTestConte | CAST(3.14 AS DOUBLE) AS doublecol, | CAST(1200000000 AS DECIMAL) AS decimalcol, | '120' AS stringcol, - | CAST('12:23:34' AS TIME) AS timecol, + | TIME('12:23:34.123') AS timecol, | CAST('2020-01-01' AS DATE) AS datecol, - | CAST('2020-01-01 12:23:34' AS DATETIME) AS timestampcol, + | TIMESTAMP('2020-01-01 12:23:34.123') AS timestampcol, | 1 = 0 AS boolcol, | convert('Olala!' using utf8) AS binarycol$ttt, type collection( | record( @@ -65,9 +65,9 @@ trait MySQLPackageTest extends Rql2CompilerTestContext with CredentialsTestConte | doublecol: 3.14, | decimalcol: Decimal.From(1200000000), | stringcol: "120", - | timecol: Time.Build(12, 23, seconds=34), + | timecol: Time.Build(12, 23, seconds=34, millis=123), | datecol: Date.Build(2020, 1, 1), - | timestampcol: Timestamp.Build(2020, 1, 1, 12, 23, seconds=34), + | timestampcol: Timestamp.Build(2020, 1, 1, 12, 23, seconds=34, millis=123), | boolcol: false, | binarycol: Binary.FromString("Olala!") |}]""".stripMargin) diff --git a/snapi-client/src/test/scala/raw/compiler/rql2/tests/builtin/credentials/PostgreSQLPackageTest.scala b/snapi-client/src/test/scala/raw/compiler/rql2/tests/builtin/credentials/PostgreSQLPackageTest.scala index 8b43b90af..f3d12beae 100644 --- a/snapi-client/src/test/scala/raw/compiler/rql2/tests/builtin/credentials/PostgreSQLPackageTest.scala +++ b/snapi-client/src/test/scala/raw/compiler/rql2/tests/builtin/credentials/PostgreSQLPackageTest.scala @@ -35,9 +35,9 @@ trait PostgreSQLPackageTest extends Rql2CompilerTestContext with CredentialsTest | CAST('3.14' AS DOUBLE PRECISION) AS doublecol, | CAST('12000000' AS DECIMAL) AS decimalcol, | CAST('120' AS VARCHAR) AS stringcol, - | CAST('12:23:34' AS TIME) AS timecol, + | CAST('12:23:34.123' AS TIME) AS timecol, | CAST('2020-01-01' AS DATE) AS datecol, - | CAST('2020-01-01 12:23:34' AS TIMESTAMP) AS timestampcol, + | CAST('2020-01-01 12:23:34.123' AS TIMESTAMP) AS timestampcol, | CAST('false' AS BOOL) AS boolcol, | decode('T2xhbGEh', 'base64') as binarycol$ttt, type collection( | record( @@ -65,9 +65,9 @@ trait PostgreSQLPackageTest extends Rql2CompilerTestContext with CredentialsTest | doublecol: 3.14, | decimalcol: Decimal.From(12000000), | stringcol: "120", - | timecol: Time.Build(12, 23, seconds=34), + | timecol: Time.Build(12, 23, seconds=34, millis=123), | datecol: Date.Build(2020, 1, 1), - | timestampcol: Timestamp.Build(2020, 1, 1, 12, 23, seconds=34), + | timestampcol: Timestamp.Build(2020, 1, 1, 12, 23, seconds=34, millis=123), | boolcol: false, | binarycol: Binary.FromString("Olala!") |}]""".stripMargin) diff --git a/snapi-client/src/test/scala/raw/compiler/rql2/tests/builtin/credentials/SnowflakePackageTest.scala b/snapi-client/src/test/scala/raw/compiler/rql2/tests/builtin/credentials/SnowflakePackageTest.scala index 44ce94fe6..f5418f778 100644 --- a/snapi-client/src/test/scala/raw/compiler/rql2/tests/builtin/credentials/SnowflakePackageTest.scala +++ b/snapi-client/src/test/scala/raw/compiler/rql2/tests/builtin/credentials/SnowflakePackageTest.scala @@ -37,9 +37,9 @@ trait SnowflakePackageTest extends Rql2CompilerTestContext with CredentialsTestC | CAST('3.14' AS FLOAT8) AS "doublecol", | CAST('12000000' AS DECIMAL) AS "decimalcol", | CAST('120' AS VARCHAR) AS "stringcol", - | CAST('12:23:34' AS TIME) AS "timecol", + | CAST('12:23:34.123' AS TIME) AS "timecol", | CAST('2020-01-01' AS DATE) AS "datecol", - | CAST('2020-01-01 12:23:34' AS DATETIME) AS "timestampcol", + | CAST('2020-01-01 12:23:34.123' AS DATETIME) AS "timestampcol", | 1 = 0 AS "boolcol", | to_binary('tralala', 'utf-8') AS "binarycol" $ttt, type collection( | record( @@ -67,9 +67,9 @@ trait SnowflakePackageTest extends Rql2CompilerTestContext with CredentialsTestC | doublecol: 3.14, | decimalcol: Decimal.From(12000000), | stringcol: "120", - | timecol: Time.Build(12, 23, seconds=34), + | timecol: Time.Build(12, 23, seconds=34, millis=123), | datecol: Date.Build(2020, 1, 1), - | timestampcol: Timestamp.Build(2020, 1, 1, 12, 23, seconds=34), + | timestampcol: Timestamp.Build(2020, 1, 1, 12, 23, seconds=34, millis=123), | boolcol: false, | binarycol: String.Encode("tralala", "utf-8") |}]""".stripMargin) diff --git a/snapi-client/src/test/scala/raw/compiler/rql2/tests/builtin/credentials/SqlServerPackageTest.scala b/snapi-client/src/test/scala/raw/compiler/rql2/tests/builtin/credentials/SqlServerPackageTest.scala index 6db6ff6c7..9bbd9b6b6 100644 --- a/snapi-client/src/test/scala/raw/compiler/rql2/tests/builtin/credentials/SqlServerPackageTest.scala +++ b/snapi-client/src/test/scala/raw/compiler/rql2/tests/builtin/credentials/SqlServerPackageTest.scala @@ -35,9 +35,9 @@ trait SqlServerPackageTest extends Rql2CompilerTestContext with CredentialsTestC | CAST('3.14' AS DOUBLE PRECISION) AS doublecol, | CAST('12000000' AS DECIMAL) AS decimalcol, | CAST('120' AS VARCHAR) AS stringcol, - | CAST('12:23:34' AS TIME) AS timecol, + | CAST('12:23:34.123' AS TIME) AS timecol, | CAST('2020-01-01' AS DATE) AS datecol, - | CAST('2020-01-01 12:23:34' AS DATETIME) AS timestampcol, + | CAST('2020-01-01 12:23:34.123' AS DATETIME) AS timestampcol, | CAST('Olala!' AS VARBINARY(MAX)) AS binarycol $ttt)""".stripMargin) { it => it should typeAs("""collection( | record( @@ -64,9 +64,9 @@ trait SqlServerPackageTest extends Rql2CompilerTestContext with CredentialsTestC | doublecol: 3.14, | decimalcol: Decimal.From(12000000), | stringcol: "120", - | timecol: Time.Build(12, 23, seconds=34), + | timecol: Time.Build(12, 23, seconds=34, millis=123), | datecol: Date.Build(2020, 1, 1), - | timestampcol: Timestamp.Build(2020, 1, 1, 12, 23, seconds=34), + | timestampcol: Timestamp.Build(2020, 1, 1, 12, 23, seconds=34, millis=123), | binarycol: Binary.FromString("Olala!") |}]""".stripMargin) } diff --git a/snapi-truffle/src/main/java/raw/runtime/truffle/ast/io/jdbc/JdbcQuery.java b/snapi-truffle/src/main/java/raw/runtime/truffle/ast/io/jdbc/JdbcQuery.java index 09a55ba0c..44ae1c9f2 100644 --- a/snapi-truffle/src/main/java/raw/runtime/truffle/ast/io/jdbc/JdbcQuery.java +++ b/snapi-truffle/src/main/java/raw/runtime/truffle/ast/io/jdbc/JdbcQuery.java @@ -18,6 +18,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.time.LocalTime; import raw.client.api.LocationDescription; import raw.runtime.truffle.runtime.exceptions.rdbms.JdbcExceptionHandler; import raw.runtime.truffle.runtime.exceptions.rdbms.JdbcReaderRawTruffleException; @@ -177,7 +178,8 @@ DateObject getDate(String colName, Node node) { TimeObject getTime(String colName, Node node) { try { java.sql.Time sqlTime = rs.getTime(colName); - return new TimeObject(sqlTime.toLocalTime()); + LocalTime localTime = LocalTime.ofNanoOfDay(sqlTime.getTime() * 1000000); + return new TimeObject(localTime); } catch (SQLException e) { throw exceptionHandler.columnParseError(e, colName, node); } diff --git a/sql-client/src/main/scala/raw/client/sql/NamedParametersPreparedStatement.scala b/sql-client/src/main/scala/raw/client/sql/NamedParametersPreparedStatement.scala index 21889933e..9d5e159b0 100644 --- a/sql-client/src/main/scala/raw/client/sql/NamedParametersPreparedStatement.scala +++ b/sql-client/src/main/scala/raw/client/sql/NamedParametersPreparedStatement.scala @@ -19,6 +19,7 @@ import raw.client.api._ import raw.client.sql.antlr4._ import java.sql.{Connection, ResultSet, ResultSetMetaData} +import java.time.LocalTime import scala.collection.mutable /* This class is wrapping the PreparedStatement class from the JDBC API. @@ -463,7 +464,7 @@ class NamedParametersPreparedStatement( case RawString(v) => setString(paramName, v) case RawDecimal(v) => setBigDecimal(paramName, v) case RawDate(v) => setDate(paramName, java.sql.Date.valueOf(v)) - case RawTime(v) => setTime(paramName, java.sql.Time.valueOf(v)) + case RawTime(v) => setTime(paramName, new java.sql.Time(v.toNanoOfDay / 1000000)) case RawTimestamp(v) => setTimestamp(paramName, java.sql.Timestamp.valueOf(v)) case RawInterval(years, months, weeks, days, hours, minutes, seconds, millis) => ??? case RawBinary(v) => setBytes(paramName, v) @@ -594,7 +595,7 @@ class NamedParametersPreparedStatement( case java.sql.Types.FLOAT => RawFloat(rs.getFloat(1)) case java.sql.Types.DOUBLE => RawDouble(rs.getDouble(1)) case java.sql.Types.DATE => RawDate(rs.getDate(1).toLocalDate) - case java.sql.Types.TIME => RawTime(rs.getTime(1).toLocalTime) + case java.sql.Types.TIME => RawTime(LocalTime.ofNanoOfDay(rs.getTime(1).getTime * 1000000)) case java.sql.Types.TIMESTAMP => RawTimestamp(rs.getTimestamp(1).toLocalDateTime) case java.sql.Types.BOOLEAN => RawBool(rs.getBoolean(1)) case java.sql.Types.VARCHAR => RawString(rs.getString(1)) diff --git a/sql-client/src/main/scala/raw/client/sql/writers/TypedResultSetCsvWriter.scala b/sql-client/src/main/scala/raw/client/sql/writers/TypedResultSetCsvWriter.scala index b9d40efaf..e73c41f91 100644 --- a/sql-client/src/main/scala/raw/client/sql/writers/TypedResultSetCsvWriter.scala +++ b/sql-client/src/main/scala/raw/client/sql/writers/TypedResultSetCsvWriter.scala @@ -20,6 +20,7 @@ import raw.client.utils.RecordFieldsNaming import java.io.{IOException, OutputStream} import java.sql.ResultSet +import java.time.LocalTime import java.time.format.DateTimeFormatter import scala.annotation.tailrec @@ -123,7 +124,8 @@ class TypedResultSetCsvWriter(os: OutputStream, lineSeparator: String, maxRows: val date = v.getDate(i).toLocalDate gen.writeString(dateFormatter.format(date)) case _: RawTimeType => - val time = v.getTime(i).toLocalTime + val sqlTime = v.getTime(i) + val time = LocalTime.ofNanoOfDay(sqlTime.getTime * 1000000) val formatter = if (time.getNano > 0) timeFormatter else timeFormatterNoMs val formatted = formatter.format(time) gen.writeString(formatted) diff --git a/sql-client/src/main/scala/raw/client/sql/writers/TypedResultSetJsonWriter.scala b/sql-client/src/main/scala/raw/client/sql/writers/TypedResultSetJsonWriter.scala index 7b9da6744..8a3b680e2 100644 --- a/sql-client/src/main/scala/raw/client/sql/writers/TypedResultSetJsonWriter.scala +++ b/sql-client/src/main/scala/raw/client/sql/writers/TypedResultSetJsonWriter.scala @@ -20,6 +20,7 @@ import raw.client.utils.RecordFieldsNaming import java.io.{IOException, OutputStream} import java.sql.ResultSet +import java.time.LocalTime import java.time.format.DateTimeFormatter import scala.annotation.tailrec @@ -112,7 +113,8 @@ class TypedResultSetJsonWriter(os: OutputStream, maxRows: Option[Long]) { val date = v.getDate(i).toLocalDate gen.writeString(dateFormatter.format(date)) case _: RawTimeType => - val time = v.getTime(i).toLocalTime + val sqlTime = v.getTime(i) + val time = LocalTime.ofNanoOfDay(sqlTime.getTime * 1000000) val formatted = timeFormatter.format(time) gen.writeString(formatted) case _: RawTimestampType => diff --git a/sql-client/src/test/scala/raw/client/sql/TestSqlCompilerServiceAirports.scala b/sql-client/src/test/scala/raw/client/sql/TestSqlCompilerServiceAirports.scala index c06bf8929..559e5f887 100644 --- a/sql-client/src/test/scala/raw/client/sql/TestSqlCompilerServiceAirports.scala +++ b/sql-client/src/test/scala/raw/client/sql/TestSqlCompilerServiceAirports.scala @@ -956,4 +956,73 @@ class TestSqlCompilerServiceAirports assert(v.messages.size == 1) assert(v.messages(0).message == "non-executable code") } + + test("""SELECT pg_typeof(NOW())""".stripMargin) { t => + val ValidateResponse(errors) = compilerService.validate(t.q, asJson()) + assert(errors.isEmpty) + val GetProgramDescriptionFailure(errors2) = compilerService.getProgramDescription(t.q, asJson()) + errors2.map(_.message).contains("unsupported type: regtype") + } + + test("""SELECT CAST(pg_typeof(NOW()) AS VARCHAR)""".stripMargin) { t => + val ValidateResponse(errors) = compilerService.validate(t.q, asJson()) + assert(errors.isEmpty) + val GetProgramDescriptionSuccess(_) = compilerService.getProgramDescription(t.q, asJson()) + val baos = new ByteArrayOutputStream() + baos.reset() + assert(compilerService.execute(t.q, asJson(), None, baos) == ExecutionSuccess(true)) + assert(baos.toString() == """[{"pg_typeof":"timestamp with time zone"}]""") + + } + + test("""SELECT NOW()""".stripMargin) { t => + // NOW() is a timestamp with timezone. The one of the SQL connection. This test is to make sure + // it works (we cannot assert on the result). + val ValidateResponse(errors) = compilerService.validate(t.q, asJson()) + assert(errors.isEmpty) + val GetProgramDescriptionSuccess(description) = compilerService.getProgramDescription(t.q, asJson()) + val baos = new ByteArrayOutputStream() + baos.reset() + assert(compilerService.execute(t.q, asJson(), None, baos) == ExecutionSuccess(true)) + } + + test("""SELECT TIMESTAMP '2001-07-01 12:13:14.567' AS t""".stripMargin) { t => + val ValidateResponse(errors) = compilerService.validate(t.q, asJson()) + assert(errors.isEmpty) + val GetProgramDescriptionSuccess(_) = compilerService.getProgramDescription(t.q, asJson()) + val baos = new ByteArrayOutputStream() + for (fmt <- Seq("json", "csv")) { + baos.reset() + assert(compilerService.execute(t.q, asJson(), None, baos) == ExecutionSuccess(true)) + assert(baos.toString().contains("14.567")) + } + } + + test("""SELECT TIME '12:13:14.567' AS t""".stripMargin) { t => + val ValidateResponse(errors) = compilerService.validate(t.q, asJson()) + assert(errors.isEmpty) + val GetProgramDescriptionSuccess(_) = compilerService.getProgramDescription(t.q, asJson()) + val baos = new ByteArrayOutputStream() + baos.reset() + for (fmt <- Seq("json", "csv")) { + baos.reset() + assert(compilerService.execute(t.q, asJson(), None, baos) == ExecutionSuccess(true)) + assert(baos.toString().contains("14.567")) + } + } + + test("""-- @default t TIME '12:13:14.567' + |SELECT :t AS t""".stripMargin) { t => + val ValidateResponse(errors) = compilerService.validate(t.q, asJson()) + assert(errors.isEmpty) + val GetProgramDescriptionSuccess(_) = compilerService.getProgramDescription(t.q, asJson()) + val baos = new ByteArrayOutputStream() + baos.reset() + for (fmt <- Seq("json", "csv")) { + baos.reset() + assert(compilerService.execute(t.q, asJson(), None, baos) == ExecutionSuccess(true)) + assert(baos.toString().contains("14.567")) + } + } + }