fix(expr): add spark.auron.enableNativeDateTimeFunctions
diff --git a/spark-extension-shims-spark/src/main/scala/org/apache/spark/sql/auron/ShimsImpl.scala b/spark-extension-shims-spark/src/main/scala/org/apache/spark/sql/auron/ShimsImpl.scala
index dff3628..b4dca22 100644
--- a/spark-extension-shims-spark/src/main/scala/org/apache/spark/sql/auron/ShimsImpl.scala
+++ b/spark-extension-shims-spark/src/main/scala/org/apache/spark/sql/auron/ShimsImpl.scala
@@ -691,7 +691,7 @@
             .build()
         }
 
-      case e if isUnixDateConversion(e) =>
+      case e if NativeConverters.nativeDateTimeFunctionsEnabled && isUnixDateConversion(e) =>
         // Date32 and Int32 share the same epoch-day representation.
         Some(
           pb.PhysicalExprNode
diff --git a/spark-extension-shims-spark/src/test/scala/org/apache/auron/AuronFunctionSuite.scala b/spark-extension-shims-spark/src/test/scala/org/apache/auron/AuronFunctionSuite.scala
index 8a06374..8d3e9af 100644
--- a/spark-extension-shims-spark/src/test/scala/org/apache/auron/AuronFunctionSuite.scala
+++ b/spark-extension-shims-spark/src/test/scala/org/apache/auron/AuronFunctionSuite.scala
@@ -18,6 +18,7 @@
 
 import java.text.SimpleDateFormat
 
+import org.apache.spark.SparkConf
 import org.apache.spark.sql.{AuronQueryTest, Row}
 import org.apache.spark.sql.auron.NativeConverters
 import org.apache.spark.sql.catalyst.expressions.Alias
@@ -30,62 +31,8 @@
 
 class AuronFunctionSuite extends AuronQueryTest with BaseAuronSQLSuite {
 
-  test("float comparisons follow Spark NaN and signed zero ordering") {
-    withTable("t_float_ordering") {
-      sql(
-        "create table t_float_ordering (d1 double, d2 double, f1 float, f2 float) using parquet")
-      sql("""insert into t_float_ordering values
-          |  (0.0, -0.0, cast(0.0 as float), cast(-0.0 as float)),
-          |  (cast('NaN' as double), cast('NaN' as double),
-          |   cast('NaN' as float), cast('NaN' as float)),
-          |  (cast('NaN' as double), cast('Infinity' as double),
-          |   cast('NaN' as float), cast('Infinity' as float)),
-          |  (null, 1.0, null, cast(1.0 as float))
-          |""".stripMargin)
-
-      checkSparkAnswerAndOperator("""select
-          |  d1 = d2, not (d1 = d2), d1 < d2, d1 <= d2, d1 > d2, d1 >= d2, d1 <=> d2,
-          |  f1 = f2, not (f1 = f2), f1 < f2, f1 <= f2, f1 > f2, f1 >= f2, f1 <=> f2
-          |from t_float_ordering
-          |""".stripMargin)
-    }
-  }
-
-  test("float scan filters preserve NaN and signed zero results") {
-    Seq("parquet", "orc").foreach { format =>
-      withTable("t_float_scan") {
-        sql(s"create table t_float_scan (d double) using $format")
-        sql("""insert into t_float_scan values
-            |  (0.0), (-0.0), (cast('NaN' as double))
-            |""".stripMargin)
-
-        // Spark 3.0/3.1 ORC pushdown can drop NaN before evaluating this filter.
-        val greaterThan = sql("select d from t_float_scan where d > 100.0")
-        checkAnswer(greaterThan, Seq(Row(Double.NaN)))
-        assertPlanIsNative(greaterThan)
-        checkSparkAnswerAndOperator("select d from t_float_scan where d = 0.0")
-
-        val notIn = sql("select count(*) from t_float_scan where d not in (100.0, 200.0)")
-        checkAnswer(notIn, Seq(Row(3L)))
-        assertPlanIsNative(notIn)
-      }
-    }
-  }
-
-  test("float min and max follow Spark NaN ordering") {
-    withTable("t_float_ordering") {
-      sql("create table t_float_ordering (d double, f float) using parquet")
-      sql("""insert into t_float_ordering values
-          |  (cast('Infinity' as double), cast('Infinity' as float)),
-          |  (cast('NaN' as double), cast('NaN' as float)),
-          |  (-1.0, cast(-1.0 as float)),
-          |  (null, null)
-          |""".stripMargin)
-
-      checkSparkAnswerAndOperator(
-        "select isnan(max(d)), min(d), isnan(max(f)), min(f) from t_float_ordering")
-    }
-  }
+  override protected def sparkConf: SparkConf =
+    super.sparkConf.set("spark.auron.enableNativeDateTimeFunctions", "true")
 
   test("sum function with float input") {
     if (AuronTestUtils.isSparkV31OrGreater) {
diff --git a/spark-extension-shims-spark/src/test/scala/org/apache/auron/AuronQuerySuite.scala b/spark-extension-shims-spark/src/test/scala/org/apache/auron/AuronQuerySuite.scala
index c961283..475ebd4 100644
--- a/spark-extension-shims-spark/src/test/scala/org/apache/auron/AuronQuerySuite.scala
+++ b/spark-extension-shims-spark/src/test/scala/org/apache/auron/AuronQuerySuite.scala
@@ -163,7 +163,9 @@
   }
 
   test("test select multiple spark ext functions with the same signature") {
-    withSQLConf("spark.auron.udf.singleChildFallback.enabled" -> "true") {
+    withSQLConf(
+      "spark.auron.udf.singleChildFallback.enabled" -> "true",
+      "spark.auron.enableNativeDateTimeFunctions" -> "true") {
       withTable("t1") {
         sql("create table t1 using parquet as select '2024-12-18' as event_time")
         checkSparkAnswerAndOperator("select year(event_time), month(event_time) from t1")
diff --git a/spark-extension-shims-spark/src/test/scala/org/apache/auron/NativeConvertersSuite.scala b/spark-extension-shims-spark/src/test/scala/org/apache/auron/NativeConvertersSuite.scala
index 7aac711..5c4bc22 100644
--- a/spark-extension-shims-spark/src/test/scala/org/apache/auron/NativeConvertersSuite.scala
+++ b/spark-extension-shims-spark/src/test/scala/org/apache/auron/NativeConvertersSuite.scala
@@ -18,16 +18,105 @@
 
 import org.apache.spark.sql.AuronQueryTest
 import org.apache.spark.sql.auron.NativeConverters
-import org.apache.spark.sql.catalyst.expressions.{AttributeReference, Cast, GreaterThan, Literal}
-import org.apache.spark.sql.types.{BooleanType, DataType, DoubleType, FloatType, IntegerType, StringType}
+import org.apache.spark.sql.catalyst.expressions.{Cast, Expression, Literal}
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.types.{BooleanType, DataType, IntegerType, StringType}
 
 import org.apache.auron.protobuf.ScalarFunction
+import org.apache.auron.spark.configuration.SparkAuronConfiguration
+import org.apache.auron.util.AuronTestUtils
 
 class NativeConvertersSuite
     extends AuronQueryTest
     with BaseAuronSQLSuite
     with AuronSQLTestHelper {
 
+  private def dateTimeExpressions: Seq[Expression] = {
+    val functions = Seq(
+      "year(date'2024-03-15')",
+      "month(date'2024-03-15')",
+      "dayofmonth(date'2024-03-15')",
+      "dayofyear(date'2024-03-15')",
+      "dayofweek(date'2024-03-15')",
+      "weekofyear(date'2024-03-15')",
+      "quarter(date'2024-03-15')",
+      "hour(timestamp'2024-03-15 14:30:45')",
+      "minute(timestamp'2024-03-15 14:30:45')",
+      "second(timestamp'2024-03-15 14:30:45')",
+      "last_day(date'2024-03-15')",
+      "next_day(date'2024-03-15', 'MON')",
+      "datediff(date'2024-03-15', date'2024-03-01')",
+      "date_add(date'2024-03-15', 1)",
+      "date_sub(date'2024-03-15', 1)",
+      "add_months(date'2024-03-15', 1)",
+      "months_between(timestamp'2024-03-15 14:30:45', timestamp'2024-02-15 14:30:45')",
+      "date_trunc('day', timestamp'2024-03-15 14:30:45')") ++
+      (if (AuronTestUtils.isSparkV30OrGreater) Seq("make_date(2024, 3, 15)") else Nil) ++
+      (if (AuronTestUtils.isSparkV31OrGreater) {
+         Seq("unix_date(date'2024-03-15')", "date_from_unix_date(1)")
+       } else Nil)
+    functions.map { function =>
+      sql(s"select $function").queryExecution.analyzed.expressions.head.children.head
+    }
+  }
+
+  test("日期时间 native 翻译默认关闭") {
+    assert(!SparkAuronConfiguration.ENABLE_NATIVE_DATETIME_FUNCTIONS.defaultValue())
+    assert(!NativeConverters.nativeDateTimeFunctionsEnabled)
+    withSQLConf(
+      "spark.auron.udf.singleChildFallback.enabled" -> "true",
+      "spark.auron.datetime.extract.enabled" -> "true") {
+      dateTimeExpressions.foreach { expr =>
+        assert(NativeConverters.convertExpr(expr).hasSparkUdfWrapperExpr)
+      }
+    }
+  }
+
+  test("关闭日期时间 native 翻译时普通表达式和裁剪表达式均回退") {
+    withSQLConf(
+      "spark.auron.enableNativeDateTimeFunctions" -> "false",
+      "spark.auron.datetime.extract.enabled" -> "true") {
+      for (expr <- dateTimeExpressions; isPruningExpr <- Seq(false, true)) {
+        var fallbackCalled = false
+        NativeConverters.convertExprWithFallback(
+          expr,
+          isPruningExpr,
+          e => {
+            assert(e == expr)
+            fallbackCalled = true
+            org.apache.auron.protobuf.PhysicalExprNode.getDefaultInstance
+          })
+        assert(fallbackCalled)
+      }
+      assert(NativeConverters.convertExpr(Literal(1)).hasLiteral)
+    }
+  }
+
+  test("开启日期时间 native 翻译后恢复现有转换") {
+    withSQLConf(
+      "spark.auron.enableNativeDateTimeFunctions" -> "true",
+      "spark.auron.datetime.extract.enabled" -> "true",
+      SQLConf.SESSION_LOCAL_TIMEZONE.key -> "UTC") {
+      dateTimeExpressions.foreach { expr =>
+        val converted = NativeConverters.convertExpr(expr)
+        assert(converted.hasScalarFunction || converted.hasTryCast)
+      }
+    }
+  }
+
+  test("日期时间总开关不覆盖时分秒提取开关") {
+    withSQLConf(
+      "spark.auron.enableNativeDateTimeFunctions" -> "true",
+      "spark.auron.datetime.extract.enabled" -> "false",
+      "spark.auron.udf.singleChildFallback.enabled" -> "true") {
+      dateTimeExpressions
+        .filter(e => Seq("Hour", "Minute", "Second").contains(e.getClass.getSimpleName))
+        .foreach { expr =>
+          assert(NativeConverters.convertExpr(expr).hasSparkUdfWrapperExpr)
+        }
+    }
+  }
+
   private def assertTrimmedCast(rawValue: String, targetType: DataType): Unit = {
     val expr = Cast(Literal.create(rawValue, StringType), targetType)
     val nativeExpr = NativeConverters.convertExpr(expr)
diff --git a/spark-extension/src/main/java/org/apache/auron/spark/configuration/SparkAuronConfiguration.java b/spark-extension/src/main/java/org/apache/auron/spark/configuration/SparkAuronConfiguration.java
index ac4ba38..14550c3 100644
--- a/spark-extension/src/main/java/org/apache/auron/spark/configuration/SparkAuronConfiguration.java
+++ b/spark-extension/src/main/java/org/apache/auron/spark/configuration/SparkAuronConfiguration.java
@@ -577,6 +577,12 @@
             .withDescription("Enable decimal arithmetic operations conversion to native Auron implementations.")
             .withDefaultValue(false);
 
+    public static final ConfigOption<Boolean> ENABLE_NATIVE_DATETIME_FUNCTIONS = new SQLConfOption<>(Boolean.class)
+            .withKey("auron.enableNativeDateTimeFunctions")
+            .withCategory("Expression/Function Supports")
+            .withDescription("启用日期时间函数到 native Auron 实现的转换,默认回退到 Spark。")
+            .withDefaultValue(false);
+
     public static final ConfigOption<Boolean> DATETIME_EXTRACT_ENABLED = new SQLConfOption<>(Boolean.class)
             .withKey("auron.datetime.extract.enabled")
             .withCategory("Expression/Function Supports")
diff --git a/spark-extension/src/main/scala/org/apache/spark/sql/auron/NativeConverters.scala b/spark-extension/src/main/scala/org/apache/spark/sql/auron/NativeConverters.scala
index 2975499..d9b8434 100644
--- a/spark-extension/src/main/scala/org/apache/spark/sql/auron/NativeConverters.scala
+++ b/spark-extension/src/main/scala/org/apache/spark/sql/auron/NativeConverters.scala
@@ -89,6 +89,8 @@
   def udfJsonEnabled: Boolean = SparkAuronConfiguration.UDF_JSON_ENABLED.get()
   def udfBrickHouseEnabled: Boolean = SparkAuronConfiguration.UDF_BRICKHOUSE_ENABLED.get()
   def decimalArithOpEnabled: Boolean = SparkAuronConfiguration.DECIMAL_ARITH_OP_ENABLED.get()
+  def nativeDateTimeFunctionsEnabled: Boolean =
+    SparkAuronConfiguration.ENABLE_NATIVE_DATETIME_FUNCTIONS.get()
   def datetimeExtractEnabled: Boolean = SparkAuronConfiguration.DATETIME_EXTRACT_ENABLED.get()
   def castTrimStringEnabled: Boolean = SparkAuronConfiguration.CAST_STRING_TRIM_ENABLE.get()
   def singleChildFallbackEnabled: Boolean =
@@ -415,6 +417,11 @@
       this.buildExtScalarFunctionNode(_, _, _, isPruningExpr, fallback)
 
     sparkExpr match {
+      case e @ (_: Year | _: Month | _: DayOfMonth | _: DayOfYear | _: DayOfWeek |
+          _: WeekOfYear | _: Quarter | _: Hour | _: Minute | _: Second | _: LastDay |
+          _: NextDay | _: DateDiff | _: DateAdd | _: DateSub | _: AddMonths |
+          _: MonthsBetween | _: TruncTimestamp) if !nativeDateTimeFunctionsEnabled =>
+        fallback(e)
       case e: NativeExprWrapperBase => e.wrapped
       case e: Literal =>
         val schema = StructType(Seq(StructField("", e.dataType, e.nullable)))
@@ -842,7 +849,8 @@
       case e: Acosh => buildScalarFunction(pb.ScalarFunction.Acosh, e.children, e.dataType)
       case e: Atan => buildScalarFunction(pb.ScalarFunction.Atan, e.children, e.dataType)
       case e: Exp => buildScalarFunction(pb.ScalarFunction.Exp, e.children, e.dataType)
-      case e: MakeDate =>
+      case e: MakeDate
+        if nativeDateTimeFunctionsEnabled =>
         buildExtScalarFunction(
           "Spark_MakeDate",
           e.children :+ Literal(Shims.get.getMakeDateFailOnError(e)),