This is an automated email from the ASF dual-hosted git repository.

gengliang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/master by this push:
     new 6af121b79f5 [SPARK-38829][SQL] Add a configuration flag to enable 
TIMESTAMP_NTZ support in Parquet data source
6af121b79f5 is described below

commit 6af121b79f5e571f10be28c1011196b56dc5d5d9
Author: Ivan Sadikov <[email protected]>
AuthorDate: Wed Apr 27 13:48:58 2022 +0800

    [SPARK-38829][SQL] Add a configuration flag to enable TIMESTAMP_NTZ support 
in Parquet data source
    
    ### What changes were proposed in this pull request?
    
    This PR adds the configuration flag 
`spark.sql.parquet.timestampNTZ.enabled` which allows to read and write 
`TIMESTAMP_NTZ` values in Parquet and is enabled by default.
    
    Previously the usage of `TIMESTAMP_NTZ` was hardcoded so there was no way 
to restore the original behaviour of reading values as `TIMESTAMP_LTZ`. This PR 
addresses the issue - when the config is disabled, all `TIMESTAMP_NTZ` values 
will be read as `TIMESTAMP_LTZ` and writes would fail with an unsupported type 
error (and will have to be cast to a different type).
    
    ### Why are the changes needed?
    
    Allows users to fall back to reading Parquet types TIMESTAMP_MICROS and 
TIMESTAMP_MILLIS with UTC offset disabled as `TIMESTAMP_LTZ`. Note that the 
users must explicitly disable the config - by default we would read such 
Parquet types as `TIMESTAMP_NTZ`.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, users have the ability to disable `TIMESTAMP_NTZ` support in Parquet 
data source.
    
    ### How was this patch tested?
    
    I updated a few unit tests to check the behaviour of the config flag.
    
    Closes #36158 from sadikovi/SPARK-38829-add-ntz-config-flag.
    
    Authored-by: Ivan Sadikov <[email protected]>
    Signed-off-by: Gengliang Wang <[email protected]>
---
 .../org/apache/spark/sql/internal/SQLConf.scala    | 13 ++++
 .../parquet/SpecificParquetRecordReaderBase.java   |  2 +
 .../datasources/parquet/ParquetFileFormat.scala    | 14 +++-
 .../datasources/parquet/ParquetReadSupport.scala   | 76 +++++++++++--------
 .../datasources/parquet/ParquetRowConverter.scala  |  3 +-
 .../parquet/ParquetSchemaConverter.scala           | 32 +++++---
 .../datasources/v2/parquet/ParquetScan.scala       |  3 +
 .../parquet/ParquetFieldIdSchemaSuite.scala        |  6 +-
 .../datasources/parquet/ParquetIOSuite.scala       | 85 ++++++++++++++++------
 .../datasources/parquet/ParquetSchemaSuite.scala   | 73 +++++++++++++++++--
 10 files changed, 233 insertions(+), 74 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
index 49cd23851ec..0ba870d10e9 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
@@ -1071,6 +1071,17 @@ object SQLConf {
       .booleanConf
       .createWithDefault(false)
 
+  val PARQUET_TIMESTAMP_NTZ_ENABLED =
+    buildConf("spark.sql.parquet.timestampNTZ.enabled")
+      .doc(s"Enables ${TimestampTypes.TIMESTAMP_NTZ} support for Parquet reads 
and writes. " +
+        s"When enabled, ${TimestampTypes.TIMESTAMP_NTZ} values are written as 
Parquet timestamp " +
+        "columns with annotation isAdjustedToUTC = false and are inferred in a 
similar way. " +
+        s"When disabled, such values are read as 
${TimestampTypes.TIMESTAMP_LTZ} and have to be " +
+        s"converted to ${TimestampTypes.TIMESTAMP_LTZ} for writes.")
+      .version("3.4.0")
+      .booleanConf
+      .createWithDefault(true)
+
   val ORC_COMPRESSION = buildConf("spark.sql.orc.compression.codec")
     .doc("Sets the compression codec used when writing ORC files. If either 
`compression` or " +
       "`orc.compress` is specified in the table-specific options/properties, 
the precedence " +
@@ -4536,6 +4547,8 @@ class SQLConf extends Serializable with Logging {
 
   def ignoreMissingParquetFieldId: Boolean = 
getConf(SQLConf.IGNORE_MISSING_PARQUET_FIELD_ID)
 
+  def parquetTimestampNTZEnabled: Boolean = 
getConf(PARQUET_TIMESTAMP_NTZ_ENABLED)
+
   def useV1Command: Boolean = getConf(SQLConf.LEGACY_USE_V1_COMMAND)
 
   def histogramNumericPropagateInputType: Boolean =
diff --git 
a/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/SpecificParquetRecordReaderBase.java
 
b/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/SpecificParquetRecordReaderBase.java
index 292a0f98af1..61aab6c5398 100644
--- 
a/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/SpecificParquetRecordReaderBase.java
+++ 
b/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/SpecificParquetRecordReaderBase.java
@@ -148,6 +148,7 @@ public abstract class SpecificParquetRecordReaderBase<T> 
extends RecordReader<Vo
     config.setBoolean(SQLConf.PARQUET_BINARY_AS_STRING().key() , false);
     config.setBoolean(SQLConf.PARQUET_INT96_AS_TIMESTAMP().key(), false);
     config.setBoolean(SQLConf.CASE_SENSITIVE().key(), false);
+    config.setBoolean(SQLConf.PARQUET_TIMESTAMP_NTZ_ENABLED().key(), false);
 
     this.file = new Path(path);
     long length = 
this.file.getFileSystem(config).getFileStatus(this.file).getLen();
@@ -198,6 +199,7 @@ public abstract class SpecificParquetRecordReaderBase<T> 
extends RecordReader<Vo
     config.setBoolean(SQLConf.PARQUET_BINARY_AS_STRING().key() , false);
     config.setBoolean(SQLConf.PARQUET_INT96_AS_TIMESTAMP().key(), false);
     config.setBoolean(SQLConf.CASE_SENSITIVE().key(), false);
+    config.setBoolean(SQLConf.PARQUET_TIMESTAMP_NTZ_ENABLED().key(), false);
     this.parquetColumn = new ParquetToSparkSchemaConverter(config)
       .convertParquetColumn(requestedSchema, Option.empty());
     this.sparkSchema = (StructType) parquetColumn.sparkType();
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetFileFormat.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetFileFormat.scala
index de0759979d5..d9b24565ccc 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetFileFormat.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetFileFormat.scala
@@ -124,6 +124,10 @@ class ParquetFileFormat
       SQLConf.PARQUET_FIELD_ID_WRITE_ENABLED.key,
       sparkSession.sessionState.conf.parquetFieldIdWriteEnabled.toString)
 
+    conf.set(
+      SQLConf.PARQUET_TIMESTAMP_NTZ_ENABLED.key,
+      sparkSession.sessionState.conf.parquetTimestampNTZEnabled.toString)
+
     // Sets compression scheme
     conf.set(ParquetOutputFormat.COMPRESSION, 
parquetOptions.compressionCodecClassName)
 
@@ -230,6 +234,9 @@ class ParquetFileFormat
     hadoopConf.setBoolean(
       SQLConf.PARQUET_INT96_AS_TIMESTAMP.key,
       sparkSession.sessionState.conf.isParquetINT96AsTimestamp)
+    hadoopConf.setBoolean(
+      SQLConf.PARQUET_TIMESTAMP_NTZ_ENABLED.key,
+      sparkSession.sessionState.conf.parquetTimestampNTZEnabled)
 
     val broadcastedHadoopConf =
       sparkSession.sparkContext.broadcast(new 
SerializableConfiguration(hadoopConf))
@@ -417,7 +424,8 @@ object ParquetFileFormat extends Logging {
 
     val converter = new ParquetToSparkSchemaConverter(
       sparkSession.sessionState.conf.isParquetBinaryAsString,
-      sparkSession.sessionState.conf.isParquetINT96AsTimestamp)
+      sparkSession.sessionState.conf.isParquetINT96AsTimestamp,
+      timestampNTZEnabled = 
sparkSession.sessionState.conf.parquetTimestampNTZEnabled)
 
     val seen = mutable.HashSet[String]()
     val finalSchemas: Seq[StructType] = footers.flatMap { footer =>
@@ -513,12 +521,14 @@ object ParquetFileFormat extends Logging {
       sparkSession: SparkSession): Option[StructType] = {
     val assumeBinaryIsString = 
sparkSession.sessionState.conf.isParquetBinaryAsString
     val assumeInt96IsTimestamp = 
sparkSession.sessionState.conf.isParquetINT96AsTimestamp
+    val timestampNTZEnabled = 
sparkSession.sessionState.conf.parquetTimestampNTZEnabled
 
     val reader = (files: Seq[FileStatus], conf: Configuration, 
ignoreCorruptFiles: Boolean) => {
       // Converter used to convert Parquet `MessageType` to Spark SQL 
`StructType`
       val converter = new ParquetToSparkSchemaConverter(
         assumeBinaryIsString = assumeBinaryIsString,
-        assumeInt96IsTimestamp = assumeInt96IsTimestamp)
+        assumeInt96IsTimestamp = assumeInt96IsTimestamp,
+        timestampNTZEnabled = timestampNTZEnabled)
 
       readParquetFootersInParallel(conf, files, ignoreCorruptFiles)
         .map(ParquetFileFormat.readSchemaFromFooter(_, converter))
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetReadSupport.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetReadSupport.scala
index 69684f9466f..1d35e9ea049 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetReadSupport.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetReadSupport.scala
@@ -130,6 +130,8 @@ object ParquetReadSupport extends Logging {
       SQLConf.NESTED_SCHEMA_PRUNING_ENABLED.defaultValue.get)
     val useFieldId = conf.getBoolean(SQLConf.PARQUET_FIELD_ID_READ_ENABLED.key,
       SQLConf.PARQUET_FIELD_ID_READ_ENABLED.defaultValue.get)
+    val timestampNTZEnabled = 
conf.getBoolean(SQLConf.PARQUET_TIMESTAMP_NTZ_ENABLED.key,
+      SQLConf.PARQUET_TIMESTAMP_NTZ_ENABLED.defaultValue.get)
     val ignoreMissingIds = 
conf.getBoolean(SQLConf.IGNORE_MISSING_PARQUET_FIELD_ID.key,
       SQLConf.IGNORE_MISSING_PARQUET_FIELD_ID.defaultValue.get)
 
@@ -150,7 +152,7 @@ object ParquetReadSupport extends Logging {
            |""".stripMargin)
     }
     val parquetClippedSchema = 
ParquetReadSupport.clipParquetSchema(parquetFileSchema,
-      catalystRequestedSchema, caseSensitive, useFieldId)
+      catalystRequestedSchema, caseSensitive, useFieldId, timestampNTZEnabled)
 
     // We pass two schema to ParquetRecordMaterializer:
     // - parquetRequestedSchema: the schema of the file data we want to read
@@ -184,17 +186,6 @@ object ParquetReadSupport extends Logging {
     parquetRequestedSchema
   }
 
-  /**
-   * Overloaded method for backward compatibility with
-   * `caseSensitive` default to `true` and `useFieldId` default to `false`
-   */
-  def clipParquetSchema(
-      parquetSchema: MessageType,
-      catalystSchema: StructType,
-      caseSensitive: Boolean = true): MessageType = {
-    clipParquetSchema(parquetSchema, catalystSchema, caseSensitive, useFieldId 
= false)
-  }
-
   /**
    * Tailors `parquetSchema` according to `catalystSchema` by removing column 
paths don't exist
    * in `catalystSchema`, and adding those only exist in `catalystSchema`.
@@ -203,9 +194,10 @@ object ParquetReadSupport extends Logging {
       parquetSchema: MessageType,
       catalystSchema: StructType,
       caseSensitive: Boolean,
-      useFieldId: Boolean): MessageType = {
+      useFieldId: Boolean,
+      timestampNTZEnabled: Boolean): MessageType = {
     val clippedParquetFields = clipParquetGroupFields(
-      parquetSchema.asGroupType(), catalystSchema, caseSensitive, useFieldId)
+      parquetSchema.asGroupType(), catalystSchema, caseSensitive, useFieldId, 
timestampNTZEnabled)
     if (clippedParquetFields.isEmpty) {
       ParquetSchemaConverter.EMPTY_MESSAGE
     } else {
@@ -220,21 +212,25 @@ object ParquetReadSupport extends Logging {
       parquetType: Type,
       catalystType: DataType,
       caseSensitive: Boolean,
-      useFieldId: Boolean): Type = {
+      useFieldId: Boolean,
+      timestampNTZEnabled: Boolean): Type = {
     val newParquetType = catalystType match {
       case t: ArrayType if !isPrimitiveCatalystType(t.elementType) =>
         // Only clips array types with nested type as element type.
-        clipParquetListType(parquetType.asGroupType(), t.elementType, 
caseSensitive, useFieldId)
+        clipParquetListType(parquetType.asGroupType(), t.elementType, 
caseSensitive, useFieldId,
+          timestampNTZEnabled)
 
       case t: MapType
         if !isPrimitiveCatalystType(t.keyType) ||
            !isPrimitiveCatalystType(t.valueType) =>
         // Only clips map types with nested key type or value type
         clipParquetMapType(
-          parquetType.asGroupType(), t.keyType, t.valueType, caseSensitive, 
useFieldId)
+          parquetType.asGroupType(), t.keyType, t.valueType, caseSensitive, 
useFieldId,
+          timestampNTZEnabled)
 
       case t: StructType =>
-        clipParquetGroup(parquetType.asGroupType(), t, caseSensitive, 
useFieldId)
+        clipParquetGroup(
+          parquetType.asGroupType(), t, caseSensitive, useFieldId, 
timestampNTZEnabled)
 
       case _ =>
         // UDTs and primitive types are not clipped.  For UDTs, a clipped 
version might not be able
@@ -270,7 +266,8 @@ object ParquetReadSupport extends Logging {
       parquetList: GroupType,
       elementType: DataType,
       caseSensitive: Boolean,
-      useFieldId: Boolean): Type = {
+      useFieldId: Boolean,
+      timestampNTZEnabled: Boolean): Type = {
     // Precondition of this method, should only be called for lists with 
nested element types.
     assert(!isPrimitiveCatalystType(elementType))
 
@@ -278,7 +275,7 @@ object ParquetReadSupport extends Logging {
     // list element type is just the group itself.  Clip it.
     if (parquetList.getLogicalTypeAnnotation == null &&
       parquetList.isRepetition(Repetition.REPEATED)) {
-      clipParquetType(parquetList, elementType, caseSensitive, useFieldId)
+      clipParquetType(parquetList, elementType, caseSensitive, useFieldId, 
timestampNTZEnabled)
     } else {
       assert(
         
parquetList.getLogicalTypeAnnotation.isInstanceOf[ListLogicalTypeAnnotation],
@@ -310,14 +307,17 @@ object ParquetReadSupport extends Logging {
         Types
           .buildGroup(parquetList.getRepetition)
           .as(LogicalTypeAnnotation.listType())
-          .addField(clipParquetType(repeatedGroup, elementType, caseSensitive, 
useFieldId))
+          .addField(
+            clipParquetType(
+              repeatedGroup, elementType, caseSensitive, useFieldId, 
timestampNTZEnabled))
           .named(parquetList.getName)
       } else {
         val newRepeatedGroup = Types
           .repeatedGroup()
           .addField(
             clipParquetType(
-              repeatedGroup.getType(0), elementType, caseSensitive, 
useFieldId))
+              repeatedGroup.getType(0), elementType, caseSensitive, useFieldId,
+              timestampNTZEnabled))
           .named(repeatedGroup.getName)
 
         val newElementType = if (useFieldId && repeatedGroup.getId != null) {
@@ -347,7 +347,8 @@ object ParquetReadSupport extends Logging {
       keyType: DataType,
       valueType: DataType,
       caseSensitive: Boolean,
-      useFieldId: Boolean): GroupType = {
+      useFieldId: Boolean,
+      timestampNTZEnabled: Boolean): GroupType = {
     // Precondition of this method, only handles maps with nested key types or 
value types.
     assert(!isPrimitiveCatalystType(keyType) || 
!isPrimitiveCatalystType(valueType))
 
@@ -359,8 +360,12 @@ object ParquetReadSupport extends Logging {
       val newRepeatedGroup = Types
         .repeatedGroup()
         .as(repeatedGroup.getLogicalTypeAnnotation)
-        .addField(clipParquetType(parquetKeyType, keyType, caseSensitive, 
useFieldId))
-        .addField(clipParquetType(parquetValueType, valueType, caseSensitive, 
useFieldId))
+        .addField(
+          clipParquetType(
+            parquetKeyType, keyType, caseSensitive, useFieldId, 
timestampNTZEnabled))
+        .addField(
+          clipParquetType(
+            parquetValueType, valueType, caseSensitive, useFieldId, 
timestampNTZEnabled))
         .named(repeatedGroup.getName)
       if (useFieldId && repeatedGroup.getId != null) {
         newRepeatedGroup.withId(repeatedGroup.getId.intValue())
@@ -388,9 +393,11 @@ object ParquetReadSupport extends Logging {
       parquetRecord: GroupType,
       structType: StructType,
       caseSensitive: Boolean,
-      useFieldId: Boolean): GroupType = {
+      useFieldId: Boolean,
+      timestampNTZEnabled: Boolean): GroupType = {
     val clippedParquetFields =
-      clipParquetGroupFields(parquetRecord, structType, caseSensitive, 
useFieldId)
+      clipParquetGroupFields(parquetRecord, structType, caseSensitive, 
useFieldId,
+        timestampNTZEnabled)
     Types
       .buildGroup(parquetRecord.getRepetition)
       .as(parquetRecord.getLogicalTypeAnnotation)
@@ -407,9 +414,12 @@ object ParquetReadSupport extends Logging {
       parquetRecord: GroupType,
       structType: StructType,
       caseSensitive: Boolean,
-      useFieldId: Boolean): Seq[Type] = {
+      useFieldId: Boolean,
+      timestampNTZEnabled: Boolean): Seq[Type] = {
     val toParquet = new SparkToParquetSchemaConverter(
-      writeLegacyParquetFormat = false, useFieldId = useFieldId)
+      writeLegacyParquetFormat = false,
+      useFieldId = useFieldId,
+      timestampNTZEnabled = timestampNTZEnabled)
     lazy val caseSensitiveParquetFieldMap =
         parquetRecord.getFields.asScala.map(f => f.getName -> f).toMap
     lazy val caseInsensitiveParquetFieldMap =
@@ -420,7 +430,7 @@ object ParquetReadSupport extends Logging {
     def matchCaseSensitiveField(f: StructField): Type = {
       caseSensitiveParquetFieldMap
           .get(f.name)
-          .map(clipParquetType(_, f.dataType, caseSensitive, useFieldId))
+          .map(clipParquetType(_, f.dataType, caseSensitive, useFieldId, 
timestampNTZEnabled))
           .getOrElse(toParquet.convertField(f))
     }
 
@@ -435,7 +445,8 @@ object ParquetReadSupport extends Logging {
               throw 
QueryExecutionErrors.foundDuplicateFieldInCaseInsensitiveModeError(
                 f.name, parquetTypesString)
             } else {
-              clipParquetType(parquetTypes.head, f.dataType, caseSensitive, 
useFieldId)
+              clipParquetType(
+                parquetTypes.head, f.dataType, caseSensitive, useFieldId, 
timestampNTZEnabled)
             }
           }.getOrElse(toParquet.convertField(f))
     }
@@ -451,7 +462,8 @@ object ParquetReadSupport extends Logging {
             throw 
QueryExecutionErrors.foundDuplicateFieldInFieldIdLookupModeError(
               fieldId, parquetTypesString)
           } else {
-            clipParquetType(parquetTypes.head, f.dataType, caseSensitive, 
useFieldId)
+            clipParquetType(
+              parquetTypes.head, f.dataType, caseSensitive, useFieldId, 
timestampNTZEnabled)
           }
         }.getOrElse {
           // When there is no ID match, we use a fake name to avoid a name 
match by accident
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetRowConverter.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetRowConverter.scala
index a955dd6fc76..7bfc294a2d4 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetRowConverter.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetRowConverter.scala
@@ -484,7 +484,8 @@ private[parquet] class ParquetRowConverter(
   // can be read as Spark's TimestampNTZ type. This is to avoid mistakes in 
reading the timestamp
   // values.
   private def canReadAsTimestampNTZ(parquetType: Type): Boolean =
-    parquetType.asPrimitiveType().getPrimitiveTypeName == INT64 &&
+    schemaConverter.isTimestampNTZEnabled() &&
+      parquetType.asPrimitiveType().getPrimitiveTypeName == INT64 &&
       
parquetType.getLogicalTypeAnnotation.isInstanceOf[TimestampLogicalTypeAnnotation]
 &&
       !parquetType.getLogicalTypeAnnotation
         .asInstanceOf[TimestampLogicalTypeAnnotation].isAdjustedToUTC
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetSchemaConverter.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetSchemaConverter.scala
index 0e065f19a88..2e4041ee1c1 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetSchemaConverter.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetSchemaConverter.scala
@@ -47,23 +47,33 @@ import org.apache.spark.sql.types._
  * @param assumeInt96IsTimestamp Whether unannotated INT96 fields should be 
assumed to be Spark SQL
  *        [[TimestampType]] fields.
  * @param caseSensitive Whether use case sensitive analysis when comparing 
Spark catalyst read
- *                      schema with Parquet schema
+ *                      schema with Parquet schema.
+ * @param timestampNTZEnabled Whether TimestampNTZType type is enabled.
  */
 class ParquetToSparkSchemaConverter(
     assumeBinaryIsString: Boolean = 
SQLConf.PARQUET_BINARY_AS_STRING.defaultValue.get,
     assumeInt96IsTimestamp: Boolean = 
SQLConf.PARQUET_INT96_AS_TIMESTAMP.defaultValue.get,
-    caseSensitive: Boolean = SQLConf.CASE_SENSITIVE.defaultValue.get) {
+    caseSensitive: Boolean = SQLConf.CASE_SENSITIVE.defaultValue.get,
+    timestampNTZEnabled: Boolean = 
SQLConf.PARQUET_TIMESTAMP_NTZ_ENABLED.defaultValue.get) {
 
   def this(conf: SQLConf) = this(
     assumeBinaryIsString = conf.isParquetBinaryAsString,
     assumeInt96IsTimestamp = conf.isParquetINT96AsTimestamp,
-    caseSensitive = conf.caseSensitiveAnalysis)
+    caseSensitive = conf.caseSensitiveAnalysis,
+    timestampNTZEnabled = conf.parquetTimestampNTZEnabled)
 
   def this(conf: Configuration) = this(
     assumeBinaryIsString = 
conf.get(SQLConf.PARQUET_BINARY_AS_STRING.key).toBoolean,
     assumeInt96IsTimestamp = 
conf.get(SQLConf.PARQUET_INT96_AS_TIMESTAMP.key).toBoolean,
-    caseSensitive = conf.get(SQLConf.CASE_SENSITIVE.key).toBoolean)
+    caseSensitive = conf.get(SQLConf.CASE_SENSITIVE.key).toBoolean,
+    timestampNTZEnabled = 
conf.get(SQLConf.PARQUET_TIMESTAMP_NTZ_ENABLED.key).toBoolean)
 
+  /**
+   * Returns true if TIMESTAMP_NTZ type is enabled in this 
ParquetToSparkSchemaConverter.
+   */
+  def isTimestampNTZEnabled(): Boolean = {
+    timestampNTZEnabled
+  }
 
   /**
    * Converts Parquet [[MessageType]] `parquetSchema` to a Spark SQL 
[[StructType]].
@@ -250,7 +260,7 @@ class ParquetToSparkSchemaConverter(
             }
           case timestamp: TimestampLogicalTypeAnnotation
             if timestamp.getUnit == TimeUnit.MICROS || timestamp.getUnit == 
TimeUnit.MILLIS =>
-            if (timestamp.isAdjustedToUTC) {
+            if (timestamp.isAdjustedToUTC || !timestampNTZEnabled) {
               TimestampType
             } else {
               TimestampNTZType
@@ -443,23 +453,27 @@ class ParquetToSparkSchemaConverter(
  * @param outputTimestampType which parquet timestamp type to use when writing.
  * @param useFieldId whether we should include write field id to Parquet 
schema. Set this to false
  *        via `spark.sql.parquet.fieldId.write.enabled = false` to disable 
writing field ids.
+ * @param timestampNTZEnabled whether TIMESTAMP_NTZ type support is enabled.
  */
 class SparkToParquetSchemaConverter(
     writeLegacyParquetFormat: Boolean = 
SQLConf.PARQUET_WRITE_LEGACY_FORMAT.defaultValue.get,
     outputTimestampType: SQLConf.ParquetOutputTimestampType.Value =
       SQLConf.ParquetOutputTimestampType.INT96,
-    useFieldId: Boolean = 
SQLConf.PARQUET_FIELD_ID_WRITE_ENABLED.defaultValue.get) {
+    useFieldId: Boolean = 
SQLConf.PARQUET_FIELD_ID_WRITE_ENABLED.defaultValue.get,
+    timestampNTZEnabled: Boolean = 
SQLConf.PARQUET_TIMESTAMP_NTZ_ENABLED.defaultValue.get) {
 
   def this(conf: SQLConf) = this(
     writeLegacyParquetFormat = conf.writeLegacyParquetFormat,
     outputTimestampType = conf.parquetOutputTimestampType,
-    useFieldId = conf.parquetFieldIdWriteEnabled)
+    useFieldId = conf.parquetFieldIdWriteEnabled,
+    timestampNTZEnabled = conf.parquetTimestampNTZEnabled)
 
   def this(conf: Configuration) = this(
     writeLegacyParquetFormat = 
conf.get(SQLConf.PARQUET_WRITE_LEGACY_FORMAT.key).toBoolean,
     outputTimestampType = SQLConf.ParquetOutputTimestampType.withName(
       conf.get(SQLConf.PARQUET_OUTPUT_TIMESTAMP_TYPE.key)),
-    useFieldId = 
conf.get(SQLConf.PARQUET_FIELD_ID_WRITE_ENABLED.key).toBoolean)
+    useFieldId = 
conf.get(SQLConf.PARQUET_FIELD_ID_WRITE_ENABLED.key).toBoolean,
+    timestampNTZEnabled = 
conf.get(SQLConf.PARQUET_TIMESTAMP_NTZ_ENABLED.key).toBoolean)
 
   /**
    * Converts a Spark SQL [[StructType]] to a Parquet [[MessageType]].
@@ -547,7 +561,7 @@ class SparkToParquetSchemaConverter(
               .as(LogicalTypeAnnotation.timestampType(true, 
TimeUnit.MILLIS)).named(field.name)
         }
 
-      case TimestampNTZType =>
+      case TimestampNTZType if timestampNTZEnabled =>
         Types.primitive(INT64, repetition)
           .as(LogicalTypeAnnotation.timestampType(false, 
TimeUnit.MICROS)).named(field.name)
       case BinaryType =>
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/parquet/ParquetScan.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/parquet/ParquetScan.scala
index 6b35f2406a8..ae55159c07d 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/parquet/ParquetScan.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/parquet/ParquetScan.scala
@@ -85,6 +85,9 @@ case class ParquetScan(
     hadoopConf.setBoolean(
       SQLConf.PARQUET_INT96_AS_TIMESTAMP.key,
       sparkSession.sessionState.conf.isParquetINT96AsTimestamp)
+    hadoopConf.setBoolean(
+      SQLConf.PARQUET_TIMESTAMP_NTZ_ENABLED.key,
+      sparkSession.sessionState.conf.parquetTimestampNTZEnabled)
 
     val broadcastedConf = sparkSession.sparkContext.broadcast(
       new SerializableConfiguration(hadoopConf))
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetFieldIdSchemaSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetFieldIdSchemaSuite.scala
index b3babdd3a0c..400e0ce28ea 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetFieldIdSchemaSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetFieldIdSchemaSuite.scala
@@ -39,14 +39,16 @@ class ParquetFieldIdSchemaSuite extends ParquetSchemaTest {
       catalystSchema: StructType,
       expectedSchema: String,
       caseSensitive: Boolean = true,
-      useFieldId: Boolean = true): Unit = {
+      useFieldId: Boolean = true,
+      timestampNTZEnabled: Boolean = true): Unit = {
     test(s"Clipping with field id - $testName") {
       val fileSchema = MessageTypeParser.parseMessageType(parquetSchema)
       val actual = ParquetReadSupport.clipParquetSchema(
         fileSchema,
         catalystSchema,
         caseSensitive = caseSensitive,
-        useFieldId = useFieldId)
+        useFieldId = useFieldId,
+        timestampNTZEnabled = timestampNTZEnabled)
 
       // each fake name should be uniquely generated
       val fakeColumnNames = 
actual.getPaths.asScala.flatten.filter(_.startsWith(FAKE_COLUMN_NAME))
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetIOSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetIOSuite.scala
index 9e3be31423a..6560db4cb17 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetIOSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetIOSuite.scala
@@ -120,10 +120,12 @@ class ParquetIOSuite extends QueryTest with ParquetTest 
with SharedSparkSession
   }
 
   test("SPARK-36182: TimestampNTZ") {
-    val data = Seq("2021-01-01T00:00:00", "1970-07-15T01:02:03.456789")
-      .map(ts => Tuple1(LocalDateTime.parse(ts)))
-    withAllParquetReaders {
-      checkParquetFile(data)
+    withSQLConf(SQLConf.PARQUET_TIMESTAMP_NTZ_ENABLED.key -> "true") {
+      val data = Seq("2021-01-01T00:00:00", "1970-07-15T01:02:03.456789")
+        .map(ts => Tuple1(LocalDateTime.parse(ts)))
+      withAllParquetReaders {
+        checkParquetFile(data)
+      }
     }
   }
 
@@ -155,27 +157,66 @@ class ParquetIOSuite extends QueryTest with ParquetTest 
with SharedSparkSession
         }
         writer.close
 
-        withAllParquetReaders {
-          val df = spark.read.parquet(tablePath.toString)
-          assertResult(df.schema) {
-            StructType(
-              StructField("timestamp_ltz_millis_depr", TimestampType, nullable 
= true) ::
-              StructField("timestamp_ltz_micros_depr", TimestampType, nullable 
= true) ::
-              StructField("timestamp_ltz_millis", TimestampType, nullable = 
true) ::
-              StructField("timestamp_ltz_micros", TimestampType, nullable = 
true) ::
-              StructField("timestamp_ntz_millis", TimestampNTZType, nullable = 
true) ::
-              StructField("timestamp_ntz_micros", TimestampNTZType, nullable = 
true) ::
-              Nil
-            )
+        for (timestampNTZEnabled <- Seq(true, false)) {
+          withSQLConf(SQLConf.PARQUET_TIMESTAMP_NTZ_ENABLED.key -> 
s"$timestampNTZEnabled") {
+            val timestampNTZType = if (timestampNTZEnabled) TimestampNTZType 
else TimestampType
+
+            withAllParquetReaders {
+              val df = spark.read.parquet(tablePath.toString)
+              assertResult(df.schema) {
+                StructType(
+                  StructField("timestamp_ltz_millis_depr", TimestampType, 
nullable = true) ::
+                  StructField("timestamp_ltz_micros_depr", TimestampType, 
nullable = true) ::
+                  StructField("timestamp_ltz_millis", TimestampType, nullable 
= true) ::
+                  StructField("timestamp_ltz_micros", TimestampType, nullable 
= true) ::
+                  StructField("timestamp_ntz_millis", timestampNTZType, 
nullable = true) ::
+                  StructField("timestamp_ntz_micros", timestampNTZType, 
nullable = true) ::
+                  Nil
+                )
+              }
+
+              val ltz_value = new java.sql.Timestamp(1000L)
+              val ntz_value = LocalDateTime.of(1970, 1, 1, 0, 0, 1)
+
+              val exp = if (timestampNTZEnabled) {
+                (0 until numRecords).map { _ =>
+                  (ltz_value, ltz_value, ltz_value, ltz_value, ntz_value, 
ntz_value)
+                }.toDF()
+              } else {
+                (0 until numRecords).map { _ =>
+                  (ltz_value, ltz_value, ltz_value, ltz_value, ltz_value, 
ltz_value)
+                }.toDF()
+              }
+
+              checkAnswer(df, exp)
+            }
           }
+        }
+      }
+    }
+  }
 
-          val exp = (0 until numRecords).map { _ =>
-            val ltz_value = new java.sql.Timestamp(1000L)
-            val ntz_value = LocalDateTime.of(1970, 1, 1, 0, 0, 1)
-            (ltz_value, ltz_value, ltz_value, ltz_value, ntz_value, ntz_value)
-          }.toDF()
+  test("Write TimestampNTZ type") {
+    // Writes should fail if timestamp_ntz support is disabled.
+    withSQLConf(SQLConf.PARQUET_TIMESTAMP_NTZ_ENABLED.key -> "false") {
+      withTempPath { dir =>
+        val data = Seq(LocalDateTime.parse("2021-01-01T00:00:00")).toDF("col")
+        val err = intercept[Exception] {
+          data.write.parquet(dir.getCanonicalPath)
+        }.getCause
+        assert(err.getMessage.contains("Unsupported data type timestamp_ntz"))
+      }
+    }
 
-          checkAnswer(df, exp)
+    withSQLConf(SQLConf.PARQUET_TIMESTAMP_NTZ_ENABLED.key -> "true") {
+      withTempPath { dir =>
+        val data = Seq(LocalDateTime.parse("2021-01-01T00:00:00")).toDF("col")
+        data.write.parquet(dir.getCanonicalPath)
+        assertResult(spark.read.parquet(dir.getCanonicalPath).schema) {
+          StructType(
+            StructField("col", TimestampNTZType, nullable = true) ::
+            Nil
+          )
         }
       }
     }
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetSchemaSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetSchemaSuite.scala
index 4579d06b26c..1bef75ef0d9 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetSchemaSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetSchemaSuite.scala
@@ -64,12 +64,14 @@ abstract class ParquetSchemaTest extends ParquetTest with 
SharedSparkSession {
       binaryAsString: Boolean,
       int96AsTimestamp: Boolean,
       caseSensitive: Boolean = false,
+      timestampNTZEnabled: Boolean = true,
       sparkReadSchema: Option[StructType] = None,
       expectedParquetColumn: Option[ParquetColumn] = None): Unit = {
     val converter = new ParquetToSparkSchemaConverter(
       assumeBinaryIsString = binaryAsString,
       assumeInt96IsTimestamp = int96AsTimestamp,
-      caseSensitive = caseSensitive)
+      caseSensitive = caseSensitive,
+      timestampNTZEnabled = timestampNTZEnabled)
 
     test(s"sql <= parquet: $testName") {
       val actualParquetColumn = converter.convertParquetColumn(
@@ -95,10 +97,12 @@ abstract class ParquetSchemaTest extends ParquetTest with 
SharedSparkSession {
       parquetSchema: String,
       writeLegacyParquetFormat: Boolean,
       outputTimestampType: SQLConf.ParquetOutputTimestampType.Value =
-        SQLConf.ParquetOutputTimestampType.INT96): Unit = {
+        SQLConf.ParquetOutputTimestampType.INT96,
+      timestampNTZEnabled: Boolean = true): Unit = {
     val converter = new SparkToParquetSchemaConverter(
       writeLegacyParquetFormat = writeLegacyParquetFormat,
-      outputTimestampType = outputTimestampType)
+      outputTimestampType = outputTimestampType,
+      timestampNTZEnabled = timestampNTZEnabled)
 
     test(s"sql => parquet: $testName") {
       val actual = converter.convert(sqlSchema)
@@ -2237,7 +2241,62 @@ class ParquetSchemaSuite extends ParquetSchemaTest {
         |}
         """.stripMargin,
       binaryAsString = true,
-      int96AsTimestamp = int96AsTimestamp)
+      int96AsTimestamp = int96AsTimestamp,
+      timestampNTZEnabled = true)
+  }
+
+  testCatalystToParquet(
+    "TimestampNTZ Spark to Parquet conversion for complex types",
+    StructType(
+      Seq(
+        StructField("f1", TimestampNTZType),
+        StructField("f2", ArrayType(TimestampNTZType)),
+        StructField("f3", StructType(Seq(StructField("f4", TimestampNTZType))))
+      )
+    ),
+    """message spark_schema {
+      |  optional int64 f1 (TIMESTAMP(MICROS,false));
+      |  optional group f2 (LIST) {
+      |    repeated group list {
+      |      optional int64 element (TIMESTAMP(MICROS,false));
+      |    }
+      |  }
+      |  optional group f3 {
+      |    optional int64 f4 (TIMESTAMP(MICROS,false));
+      |  }
+      |}
+      """.stripMargin,
+    writeLegacyParquetFormat = false,
+    timestampNTZEnabled = true)
+
+  for (timestampNTZEnabled <- Seq(true, false)) {
+    val dataType = if (timestampNTZEnabled) TimestampNTZType else TimestampType
+
+    testParquetToCatalyst(
+      "TimestampNTZ Parquet to Spark conversion for complex types, " +
+        s"timestampNTZEnabled: $timestampNTZEnabled",
+      StructType(
+        Seq(
+          StructField("f1", dataType),
+          StructField("f2", ArrayType(dataType)),
+          StructField("f3", StructType(Seq(StructField("f4", dataType))))
+        )
+      ),
+      """message spark_schema {
+        |  optional int64 f1 (TIMESTAMP(MICROS,false));
+        |  optional group f2 (LIST) {
+        |    repeated group list {
+        |      optional int64 element (TIMESTAMP(MICROS,false));
+        |    }
+        |  }
+        |  optional group f3 {
+        |    optional int64 f4 (TIMESTAMP(MICROS,false));
+        |  }
+        |}
+        """.stripMargin,
+      binaryAsString = true,
+      int96AsTimestamp = false,
+      timestampNTZEnabled = timestampNTZEnabled)
   }
 
   private def testSchemaClipping(
@@ -2261,7 +2320,8 @@ class ParquetSchemaSuite extends ParquetSchemaTest {
         MessageTypeParser.parseMessageType(parquetSchema),
         catalystSchema,
         caseSensitive,
-        useFieldId = false)
+        useFieldId = false,
+        timestampNTZEnabled = true)
 
       try {
         expectedSchema.checkContains(actual)
@@ -2828,7 +2888,8 @@ class ParquetSchemaSuite extends ParquetSchemaTest {
          MessageTypeParser.parseMessageType(parquetSchema),
           catalystSchema,
           caseSensitive = false,
-          useFieldId = false)
+          useFieldId = false,
+          timestampNTZEnabled = false)
       }
     }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to