[SPARK-33888][SQL] JDBC SQL TIME type represents incorrectly as TimestampType, it should be physical Int in millis#30902
[SPARK-33888][SQL] JDBC SQL TIME type represents incorrectly as TimestampType, it should be physical Int in millis#30902saikocat wants to merge 14 commits intoapache:masterfrom
Conversation
|
Hi @saikocat , thanks for reporting the issue. So, we can't map the time-millis/time-micros type to Timestamp type. Instead, we should map it as |
|
Hi gengliangwang, Thank you so much for your reply. I understand the issue now. Then I guess the bug should be in the Spark JDBC Utils schema conversion part then (https://github.com/apache/spark/blob/master/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/jdbc/JdbcUtils.scala#L229)? Cos it converts a |
Yes, it should be Integertype or CalendarIntervalType |
|
TIME is a standard SQL data type, which is not TIMESTAMP or INTERVAL. Unfortunately, Spark doesn't support this data type yet, and can only fail (or read as the physical type int). The JDBC mapping is wrong and we should fix it. |
|
Noted with thanks. Should I just make a change/fix to |
|
Yea, and we can also fix avro to produce better error message (or read time as int) |
|
I think I've fixed the type mapping and tests. However, I can't seem to pass this test Any helps would be much appreciated! Merry Christmas! |
|
ok to test |
|
Seems like Amplab Jenkins CI run out of space when assembly :( |
|
Test build #133380 has finished for PR 30902 at commit
|
| // Represents a time of day, with no reference to a particular calendar, | ||
| // time zone or date, with a precision of one millisecond. | ||
| // It stores the number of milliseconds after midnight, 00:00:00.000. | ||
| if (rs.getMetaData.getColumnType(columnIdx) == java.sql.Types.TIME) { |
There was a problem hiding this comment.
do we run the if check for every input row?
There was a problem hiding this comment.
Yes. You're right.
Originally, I made changes to private method makeGetters and pass in the ResultSetMetadata as additional parameter. Then we have a separate pattern matching for case IntegerType if rsMetadata.getColumnType(columnIdx) == TIME => But right now, the column index/position is not accessible at the pattern matching level, only available at the closure function :(
Any advice is deeply appreciated here.
There was a problem hiding this comment.
Actually, let me see if I can work with the metadata from getSchema function.
| val expectedTimeMillis = java.util.concurrent.TimeUnit.SECONDS.toMillis( | ||
| expectedTimeRaw.toLocalTime().toSecondOfDay() | ||
| ).toInt | ||
| assert(rows(0).getAs[java.sql.Time](0) === expectedTimeMillis) |
There was a problem hiding this comment.
what was the result before? null?
There was a problem hiding this comment.
Result before is expectedTimeMilllis for rows(0).
For row(1) then it is null
|
@saikocat can you update the PR description? It's not related to avro now. |
|
@cloud-fan I have addressed the code review comments. Took awhile cos of a bug in metadata builder not being build. I will use the metadata field to store a key |
|
Kubernetes integration test starting |
|
Kubernetes integration test status success |
|
Kubernetes integration test starting |
|
Kubernetes integration test status success |
|
Test build #133503 has finished for PR 30902 at commit
|
| val rawTime = rs.getTime(pos + 1) | ||
| if (rawTime != null) { | ||
| val rawTimeInNano = rawTime.toLocalTime().toNanoOfDay() | ||
| val timeInMillis = TimeUnit.NANOSECONDS.toMillis(rawTimeInNano).toInt |
There was a problem hiding this comment.
Can we use Math.toIntExact to avoid overflow?
There was a problem hiding this comment.
👍 Thanks for this! Good catch!
|
Kubernetes integration test starting |
|
Kubernetes integration test status success |
|
Test build #133519 has finished for PR 30902 at commit
|
| assert(rows(0).getAs[java.sql.Timestamp](2).getNanos === 543543000) | ||
| } | ||
|
|
||
| test("SPARK-33888 : test TIME types") { |
| // scalastyle:off | ||
| case java.sql.Types.NUMERIC => metadata.putLong("scale", fieldScale) | ||
| case java.sql.Types.DECIMAL => metadata.putLong("scale", fieldScale) | ||
| case java.sql.Types.TIME => metadata.putBoolean("logicaltimetype", true) |
There was a problem hiding this comment.
how about logical_time_type
| expectedTimeRaw.toLocalTime().toSecondOfDay() | ||
| ).toInt | ||
| assert(rows(0).getAs[java.sql.Time](0) === expectedTimeMillis) | ||
| assert(rows(1).getAs[java.sql.Time](0) === expectedTimeMillis) |
There was a problem hiding this comment.
are we comparing java.sql.Time and int? How does it work?
There was a problem hiding this comment.
Oops. The value is already converted to Int via makeGetter in JdbcUtils. So these should be getAs[Int]. Sorry about that.
|
Kubernetes integration test starting |
|
Kubernetes integration test status success |
|
Test build #133537 has finished for PR 30902 at commit
|
|
Anything else that is needed before this can be merged? |
|
Can one of the admins verify this patch? |
|
thanks, merging to master! (not backporting because TIME is rarely used) |
|
Hi, All. |
| // It stores the number of milliseconds after midnight, 00:00:00.000. | ||
| case IntegerType if metadata.contains("logical_time_type") => | ||
| (rs: ResultSet, row: InternalRow, pos: Int) => { | ||
| val rawTime = rs.getTime(pos + 1) |
There was a problem hiding this comment.
@sarutak do you mean what returns here is seconds (with certain precision) from a starting timestamp, while the timestamp is different between databases? I'm a bit surprised if the JDBC protocol was design this way, but if this is true, then this PR doesn't make sense...
There was a problem hiding this comment.
What returns here is java.sql.Time, and its doc says
The date components should be set to the "zero epoch"
value of January 1, 1970 and should not be accessed.
Maybe some databases don't follow the requirement, but it doesn't matter, as we call rawTime.toLocalTime which only access the hour:minute:second components.
There was a problem hiding this comment.
That said, I think reading SQL TIME type as integer has a well-define semantic in Spark (after this PR): the integer represents the milliseconds of the time from 00:00:00.
There was a problem hiding this comment.
O.K, so the test seems wrong.
Actually, I'll fix it in another PR.
…gresDialect ### What changes were proposed in this pull request? This PR fixes the regression bug brought by SPARK-33888 (#30902). After that PR merged, `PostgresDIalect#getCatalystType` throws Exception for array types. ``` [info] - Type mapping for various types *** FAILED *** (551 milliseconds) [info] java.util.NoSuchElementException: key not found: scale [info] at scala.collection.immutable.Map$EmptyMap$.apply(Map.scala:106) [info] at scala.collection.immutable.Map$EmptyMap$.apply(Map.scala:104) [info] at org.apache.spark.sql.types.Metadata.get(Metadata.scala:111) [info] at org.apache.spark.sql.types.Metadata.getLong(Metadata.scala:51) [info] at org.apache.spark.sql.jdbc.PostgresDialect$.getCatalystType(PostgresDialect.scala:43) [info] at org.apache.spark.sql.execution.datasources.jdbc.JdbcUtils$.getSchema(JdbcUtils.scala:321) ``` ### Why are the changes needed? To fix the regression bug. ### Does this PR introduce _any_ user-facing change? No. ### How was this patch tested? I confirmed the test case `SPARK-22291: Conversion error when transforming array types of uuid, inet and cidr to StingType in PostgreSQL` in `PostgresIntegrationSuite` passed. I also confirmed whether all the `v2.*IntegrationSuite` pass because this PR changed them and they passed. Closes #31262 from sarutak/fix-postgres-dialect-regression. Authored-by: Kousuke Saruta <sarutak@oss.nttdata.com> Signed-off-by: HyukjinKwon <gurwls223@apache.org>
|
|
||
| // SPARK-33888 - sql TIME type represents as physical int in millis | ||
| // Represents a time of day, with no reference to a particular calendar, | ||
| // time zone or date, with a precision of one millisecond. |
There was a problem hiding this comment.
After a second thought, why do we pick millisecond precision? Why not microsecond? Is there a standard for it?
There was a problem hiding this comment.
Since we are converting to/from java.sql.Time, and according to the javadoc https://docs.oracle.com/javase/8/docs/api/java/sql/Time.html , it supports till milliseconds for constructor.
There was a problem hiding this comment.
It may confuse Spark users, as Spark timestamp is microsecond precision.
After more thought, it's probably better to return timestamp when reading JDBC time, with a clear rule: we convert the time to timestamp by using "zero epoch" as the date part. It's also more useful as users can call hour function or similar ones to get some field values. What do you think?
There was a problem hiding this comment.
Hmm, I agree with you on the user experience part (hour function and all). I think it is hard (near impossible) to introduce new DataType (Time - HH:MM:SS.sss display) and another function to convert an int without date portion to Timestamp (most conversion is parse string) as it gonna take through multiple level of approvals and testings.
Another reason why I went with Int was that with TimestampType, it breaks compatibility with Avro logical & Spark type converter (https://github.com/apache/spark/blob/master/external/avro/src/main/scala/org/apache/spark/sql/avro/SchemaConverters.scala).
I'm not sure if a time_to_timestamp() helper function would be a better compromise or revert back the 2 MRs?
There was a problem hiding this comment.
It looks better if the avro schema converter can convert timestamp to time. After reading time column from JDBC, it becomes IntegerType and there is no context to indicate that this int comes from JDBC time and means milliseconds. What if the avro logic type is time-micros? With timestamp type, at least we know the precision is microsecond.
There was a problem hiding this comment.
Your suggestion makes sense. We can also stuff the info into the metadata field of the struct field. Let me find sometimes this weekend or over the new year to try out your suggestion? Will ping you back if I couldn't find time or manage to get a solution so we can revert the MRs. Thanks a lots for helping me out!
Should I create a new JIRA ticket and cut a new MR or do you prefer to consolidate in here?
There was a problem hiding this comment.
Let's create a new JIRA to track it. The PR is merged to master only so we have plenty of time to fix it before the 3.2 release :)
There was a problem hiding this comment.
@cloud-fan btw, I did a quick check and it seems like if we use TimestampType, the time will always be converted to the JVM system timezone. So for the JDBCSuite test, given 12:34:56 time value, when you do the .select(hour("time")) it will always point to my local timezone hour instead of 12. So I don't know if we should proceed in this case.
There was a problem hiding this comment.
We can do whatever we want. We can use JDBC API getTime to get the time value, and construct the timestamp value in a reasonable way. It's under our control.
…gresDialect ### What changes were proposed in this pull request? This PR fixes the regression bug brought by SPARK-33888 (apache#30902). After that PR merged, `PostgresDIalect#getCatalystType` throws Exception for array types. ``` [info] - Type mapping for various types *** FAILED *** (551 milliseconds) [info] java.util.NoSuchElementException: key not found: scale [info] at scala.collection.immutable.Map$EmptyMap$.apply(Map.scala:106) [info] at scala.collection.immutable.Map$EmptyMap$.apply(Map.scala:104) [info] at org.apache.spark.sql.types.Metadata.get(Metadata.scala:111) [info] at org.apache.spark.sql.types.Metadata.getLong(Metadata.scala:51) [info] at org.apache.spark.sql.jdbc.PostgresDialect$.getCatalystType(PostgresDialect.scala:43) [info] at org.apache.spark.sql.execution.datasources.jdbc.JdbcUtils$.getSchema(JdbcUtils.scala:321) ``` ### Why are the changes needed? To fix the regression bug. ### Does this PR introduce _any_ user-facing change? No. ### How was this patch tested? I confirmed the test case `SPARK-22291: Conversion error when transforming array types of uuid, inet and cidr to StingType in PostgreSQL` in `PostgresIntegrationSuite` passed. I also confirmed whether all the `v2.*IntegrationSuite` pass because this PR changed them and they passed. Closes apache#31262 from sarutak/fix-postgres-dialect-regression. Authored-by: Kousuke Saruta <sarutak@oss.nttdata.com> Signed-off-by: HyukjinKwon <gurwls223@apache.org>
…portion fixed regardless of timezone ### What changes were proposed in this pull request? Due to user-experience (confusing to Spark users - java.sql.Time using milliseconds vs Spark using microseconds; and user losing useful functions like hour(), minute(), etc on the column), we have decided to revert back to use TimestampType but this time we will enforce the hour to be consistently across system timezone (via offset manipulation) and date part fixed to zero epoch. Full Discussion with Wenchen Fan Wenchen Fan regarding this ticket is here #30902 (comment) ### Why are the changes needed? Revert and improvement to sql.Time handling ### Does this PR introduce _any_ user-facing change? No ### How was this patch tested? Unit tests and integration tests Closes #31473 from saikocat/SPARK-34357. Authored-by: Hoa <hoameomu@gmail.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com>
What changes were proposed in this pull request?
JDBC SQL TIME type represents incorrectly as TimestampType, we change it to be physical Int in millis for now.
Why are the changes needed?
Currently, for JDBC, SQL TIME type represents incorrectly as Spark TimestampType. This should be represent as physical int in millis Represents a time of day, with no reference to a particular calendar, time zone or date, with a precision of one millisecond. It stores the number of milliseconds after midnight, 00:00:00.000.
Does this PR introduce any user-facing change?
No
How was this patch tested?
Close #30902