-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-37360][SQL] Support TimestampNTZ in JSON data source #34638
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
5a71c29
f9d097c
522f7de
e867277
55f9e3f
412bb61
2cdbb18
5fd0dbe
50690e9
a626c9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2746,6 +2746,182 @@ abstract class JsonSuite | |
| } | ||
| } | ||
|
|
||
| test("SPARK-37360: Write and infer TIMESTAMP_NTZ values with a non-default pattern") { | ||
| withTempPath { path => | ||
| val exp = spark.sql("select timestamp_ntz'2020-12-12 12:12:12' as col0") | ||
|
sadikovi marked this conversation as resolved.
Outdated
|
||
| exp.write | ||
| .option("timestampNTZFormat", "yyyy-MM-dd HH:mm:ss.SSSSSS") | ||
| .json(path.getAbsolutePath) | ||
|
|
||
| withSQLConf(SQLConf.TIMESTAMP_TYPE.key -> SQLConf.TimestampTypes.TIMESTAMP_NTZ.toString) { | ||
| val res = spark.read | ||
| .option("timestampNTZFormat", "yyyy-MM-dd HH:mm:ss.SSSSSS") | ||
| .option("inferTimestamp", "true") | ||
| .json(path.getAbsolutePath) | ||
|
|
||
| assert(res.dtypes === exp.dtypes) | ||
| checkAnswer(res, exp) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| test("SPARK-37360: Write and infer TIMESTAMP_LTZ values with a non-default pattern") { | ||
| withTempPath { path => | ||
| val exp = spark.sql("select timestamp_ltz'2020-12-12 12:12:12' as col0") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you append a fraction part like
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good test case! Updated, thank you. |
||
| exp.write | ||
| .option("timestampFormat", "yyyy-MM-dd HH:mm:ss.SSSSSS") | ||
| .json(path.getAbsolutePath) | ||
|
|
||
| withSQLConf(SQLConf.TIMESTAMP_TYPE.key -> SQLConf.TimestampTypes.TIMESTAMP_LTZ.toString) { | ||
| val res = spark.read | ||
| .option("timestampFormat", "yyyy-MM-dd HH:mm:ss.SSSSSS") | ||
| .option("inferTimestamp", "true") | ||
| .json(path.getAbsolutePath) | ||
|
|
||
| assert(res.dtypes === exp.dtypes) | ||
| checkAnswer(res, exp) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| test("SPARK-37360: Roundtrip in reading and writing TIMESTAMP_NTZ values with custom schema") { | ||
| withTempPath { path => | ||
| val exp = spark.sql(""" | ||
| select | ||
| timestamp_ntz'2020-12-12 12:12:12' as col1, | ||
| timestamp_ltz'2020-12-12 12:12:12' as col2 | ||
| """) | ||
|
|
||
| exp.write.json(path.getAbsolutePath) | ||
|
|
||
| val res = spark.read | ||
| .schema("col1 TIMESTAMP_NTZ, col2 TIMESTAMP_LTZ") | ||
| .json(path.getAbsolutePath) | ||
|
|
||
| checkAnswer(res, exp) | ||
| } | ||
| } | ||
|
|
||
| test("SPARK-37360: Timestamp type inference for a column with TIMESTAMP_NTZ values") { | ||
| withTempPath { path => | ||
| val exp = spark.sql(""" | ||
| select timestamp_ntz'2020-12-12 12:12:12' as col0 union all | ||
| select timestamp_ntz'2020-12-12 12:12:12' as col0 | ||
| """) | ||
|
|
||
| exp.write.json(path.getAbsolutePath) | ||
|
|
||
| val timestampTypes = Seq( | ||
| SQLConf.TimestampTypes.TIMESTAMP_NTZ.toString, | ||
| SQLConf.TimestampTypes.TIMESTAMP_LTZ.toString) | ||
|
|
||
| for (timestampType <- timestampTypes) { | ||
| withSQLConf(SQLConf.TIMESTAMP_TYPE.key -> timestampType) { | ||
| val res = spark.read.option("inferTimestamp", "true").json(path.getAbsolutePath) | ||
|
|
||
| if (timestampType == SQLConf.TimestampTypes.TIMESTAMP_NTZ.toString) { | ||
| checkAnswer(res, exp) | ||
| } else { | ||
| checkAnswer( | ||
| res, | ||
| spark.sql(""" | ||
| select timestamp_ltz'2020-12-12 12:12:12' as col0 union all | ||
| select timestamp_ltz'2020-12-12 12:12:12' as col0 | ||
| """) | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| test("SPARK-37360: Timestamp type inference for a mix of TIMESTAMP_NTZ and TIMESTAMP_LTZ") { | ||
| withTempPath { path => | ||
| Seq( | ||
| """{"col0":"2020-12-12T12:12:12.000"}""", | ||
| """{"col0":"2020-12-12T17:12:12.000Z"}""", | ||
| """{"col0":"2020-12-12T17:12:12.000+05:00"}""", | ||
| """{"col0":"2020-12-12T12:12:12.000"}""" | ||
| ).toDF("data") | ||
| .coalesce(1) | ||
| .write.text(path.getAbsolutePath) | ||
|
|
||
| for (policy <- Seq("exception", "corrected", "legacy")) { | ||
| withSQLConf(SQLConf.LEGACY_TIME_PARSER_POLICY.key -> policy) { | ||
| val res = spark.read.option("inferTimestamp", "true").json(path.getAbsolutePath) | ||
|
|
||
| // NOTE: | ||
| // Every value is tested for all types in JSON schema inference so the sequence of | ||
| // ["timestamp_ntz", "timestamp_ltz", "timestamp_ntz"] results in "timestamp_ltz". | ||
| // This is different from CSV where inference starts from the last inferred type. | ||
| // | ||
| // This is why the similar test in CSV has a different result in "legacy" mode. | ||
|
|
||
| val exp = spark.sql(""" | ||
| select timestamp_ltz'2020-12-12T12:12:12.000' as col0 union all | ||
| select timestamp_ltz'2020-12-12T17:12:12.000Z' as col0 union all | ||
| select timestamp_ltz'2020-12-12T17:12:12.000+05:00' as col0 union all | ||
| select timestamp_ltz'2020-12-12T12:12:12.000' as col0 | ||
| """) | ||
| checkAnswer(res, exp) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| test("SPARK-37360: Malformed records when reading TIMESTAMP_LTZ as TIMESTAMP_NTZ") { | ||
| withTempPath { path => | ||
| Seq( | ||
| """{"col0": "2020-12-12T12:12:12.000"}""", | ||
| """{"col0": "2020-12-12T12:12:12.000Z"}""", | ||
| """{"col0": "2020-12-12T12:12:12.000+05:00"}""", | ||
| """{"col0": "2020-12-12T12:12:12.000"}""" | ||
| ).toDF("data") | ||
| .coalesce(1) | ||
| .write.text(path.getAbsolutePath) | ||
|
|
||
| for (timestampNTZFormat <- Seq(None, Some("yyyy-MM-dd'T'HH:mm:ss[.SSS]"))) { | ||
| val reader = spark.read.schema("col0 TIMESTAMP_NTZ") | ||
| val res = timestampNTZFormat match { | ||
| case Some(format) => | ||
| reader.option("timestampNTZFormat", format).json(path.getAbsolutePath) | ||
| case None => | ||
| reader.json(path.getAbsolutePath) | ||
| } | ||
|
|
||
| checkAnswer( | ||
| res, | ||
| Seq( | ||
| Row(LocalDateTime.of(2020, 12, 12, 12, 12, 12)), | ||
| Row(null), | ||
| Row(null), | ||
| Row(LocalDateTime.of(2020, 12, 12, 12, 12, 12)) | ||
| ) | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| test("SPARK-37360: Fail to write TIMESTAMP_NTZ if timestampNTZFormat contains zone offset") { | ||
| val patterns = Seq( | ||
| "yyyy-MM-dd HH:mm:ss XXX", | ||
| "yyyy-MM-dd HH:mm:ss Z", | ||
| "yyyy-MM-dd HH:mm:ss z") | ||
|
|
||
| val exp = spark.sql("select timestamp_ntz'2020-12-12 12:12:12' as col0") | ||
| for (pattern <- patterns) { | ||
| withTempPath { path => | ||
| val err = intercept[SparkException] { | ||
| exp.write.option("timestampNTZFormat", pattern).json(path.getAbsolutePath) | ||
| } | ||
| assert( | ||
| err.getCause.getMessage.contains("Unsupported field: OffsetSeconds") || | ||
| err.getCause.getMessage.contains("Unable to extract value") || | ||
| err.getCause.getMessage.contains("Unable to extract ZoneId")) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| test("filters push down") { | ||
| withTempPath { path => | ||
| val t = "2019-12-17 00:01:02" | ||
|
|
@@ -2996,10 +3172,6 @@ abstract class JsonSuite | |
| } | ||
|
|
||
| test("SPARK-36536: use casting when datetime pattern is not set") { | ||
| def isLegacy: Boolean = { | ||
| spark.conf.get(SQLConf.LEGACY_TIME_PARSER_POLICY).toUpperCase(Locale.ROOT) == | ||
| SQLConf.LegacyBehaviorPolicy.LEGACY.toString | ||
| } | ||
| withSQLConf( | ||
| SQLConf.DATETIME_JAVA8API_ENABLED.key -> "true", | ||
| SQLConf.SESSION_LOCAL_TIMEZONE.key -> DateTimeTestUtils.UTC.getId) { | ||
|
|
@@ -3017,13 +3189,13 @@ abstract class JsonSuite | |
| readback, | ||
| Seq( | ||
| Row(LocalDate.of(2021, 1, 1), Instant.parse("2021-01-01T00:00:00Z"), | ||
| if (isLegacy) null else LocalDateTime.of(2021, 1, 1, 0, 0, 0)), | ||
| LocalDateTime.of(2021, 1, 1, 0, 0, 0)), | ||
| Row(LocalDate.of(2021, 1, 1), Instant.parse("2021-01-01T00:00:00Z"), | ||
| if (isLegacy) null else LocalDateTime.of(2021, 1, 1, 0, 0, 0)), | ||
| LocalDateTime.of(2021, 1, 1, 0, 0, 0)), | ||
| Row(LocalDate.of(2021, 2, 1), Instant.parse("2021-03-02T00:00:00Z"), | ||
| if (isLegacy) null else LocalDateTime.of(2021, 10, 1, 0, 0, 0)), | ||
| LocalDateTime.of(2021, 10, 1, 0, 0, 0)), | ||
| Row(LocalDate.of(2021, 8, 18), Instant.parse("2021-08-18T21:44:30Z"), | ||
| if (isLegacy) null else LocalDateTime.of(2021, 8, 18, 21, 44, 30, 123000000)))) | ||
| LocalDateTime.of(2021, 8, 18, 21, 44, 30, 123000000)))) | ||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you adjust the comment for inferTimestamp and mention the ntz type:
spark/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JSONOptions.scala
Line 141 in 1235bd2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, sure. I will update, thanks for pointing it out!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated! Could you check the latest PR version? Thanks.