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

cloud-fan pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new 3964a48b049b [SPARK-57364][SQL] Fix Oracle TRUNC pushdown to map Spark 
format strings to Oracle format
3964a48b049b is described below

commit 3964a48b049b91f391350b8992dd054b2fc217a5
Author: Shrirang Mhalgi <[email protected]>
AuthorDate: Wed Jun 17 21:31:37 2026 -0700

    [SPARK-57364][SQL] Fix Oracle TRUNC pushdown to map Spark format strings to 
Oracle format
    
    ### What changes were proposed in this pull request?
    Fix the Oracle TRUNC pushdown to correctly map Spark's date truncation 
format strings to their Oracle equivalents, instead of hardcoding `'IW'` (ISO 
week) for all levels.
    
    ### Why are the changes needed?
    `OracleDialect.visitSQLFunction` hardcodes `'IW'` for all TRUNC pushdowns 
regardless of the requested truncation level. When a user writes `trunc(col, 
'MONTH')`, `trunc(col, 'YEAR')`, or `trunc(col, 'QUARTER')`, the generated SQL 
sent to Oracle is always `TRUNC(col, 'IW')` - silently returning week-truncated 
dates instead of the correct truncation. Introduced in SPARK-51585.
    
    ### Does this PR introduce _any_ user-facing change?
    Yes. `trunc()` predicates pushed down to Oracle V2 data sources now produce 
correct results for MONTH, YEAR, and QUARTER truncation levels (previously all 
returned week-truncated dates).
    
    ### How was this patch tested?
    Added new unit test in `JDBCSuite` that constructs TRUNC V2 expressions for 
each format level (WEEK, MONTH, YEAR, QUARTER), compiles them via 
`OracleDialect.compileExpression`, and asserts the correct Oracle format string 
appears in the output. The test fails without the fix (produces `'IW'` for all 
levels) and passes with the fix.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Yes. Authored using Claude-Opus 4.6
    
    Closes #56425 from shrirangmhalgi/SPARK-57364-oracle-trunc-format.
    
    Authored-by: Shrirang Mhalgi <[email protected]>
    Signed-off-by: Wenchen Fan <[email protected]>
    (cherry picked from commit 80ac691b713f7a58f81f9fcf7201e1cb28337452)
    Signed-off-by: Wenchen Fan <[email protected]>
---
 .../spark/sql/jdbc/v2/OracleIntegrationSuite.scala |  8 ++++
 .../org/apache/spark/sql/jdbc/OracleDialect.scala  | 17 +++++++-
 .../org/apache/spark/sql/jdbc/JDBCSuite.scala      | 46 +++++++++++++++++++++-
 3 files changed, 69 insertions(+), 2 deletions(-)

diff --git 
a/connector/docker-integration-tests/src/test/scala/org/apache/spark/sql/jdbc/v2/OracleIntegrationSuite.scala
 
b/connector/docker-integration-tests/src/test/scala/org/apache/spark/sql/jdbc/v2/OracleIntegrationSuite.scala
index c71f9ae7688f..594819689e6f 100644
--- 
a/connector/docker-integration-tests/src/test/scala/org/apache/spark/sql/jdbc/v2/OracleIntegrationSuite.scala
+++ 
b/connector/docker-integration-tests/src/test/scala/org/apache/spark/sql/jdbc/v2/OracleIntegrationSuite.scala
@@ -340,5 +340,13 @@ class OracleIntegrationSuite extends 
DockerJDBCIntegrationV2Suite with V2JDBCTes
     assert(rows10.length === 2)
     assert(rows10(0).getString(0) === "amy")
     assert(rows10(1).getString(0) === "alex")
+
+    // SPARK-57364: verify MONTH truncation pushes down correctly (not as 'IW')
+    val df11 = sql(s"SELECT name FROM $tbl WHERE trunc(date1, 'MONTH') = 
date'2022-05-01'")
+    checkFilterPushed(df11)
+    val rows11 = df11.collect()
+    assert(rows11.length === 2)
+    assert(rows11(0).getString(0) === "amy")
+    assert(rows11(1).getString(0) === "alex")
   }
 }
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/OracleDialect.scala 
b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/OracleDialect.scala
index 6caf1f4b1ff6..d3ef79fdf3f9 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/OracleDialect.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/OracleDialect.scala
@@ -75,7 +75,22 @@ private case class OracleDialect() extends JdbcDialect with 
SQLConfHelper with N
     override def visitSQLFunction(funcName: String, inputs: Array[String]): 
String = {
       funcName match {
         case "TRUNC" =>
-          s"TRUNC(${inputs(0)}, 'IW')"
+          // Map Spark's trunc format strings to Oracle equivalents.
+          // inputs(1) arrives quoted, e.g. "'MONTH'" (see 
JDBCSQLBuilder.visitLiteral).
+          // Case-insensitive: Spark's parseTruncLevel uppercases before 
matching.
+          val fmt = inputs(1).toUpperCase(Locale.ROOT)
+          val oracleFormat = fmt match {
+            case "'WEEK'" => "'IW'"
+            case "'MONTH'" | "'MM'" | "'MON'" => "'MM'"
+            case "'QUARTER'" => "'Q'"
+            case "'YEAR'" | "'YYYY'" | "'YY'" => "'YYYY'"
+            case _ =>
+              // Unmapped formats: don't push down. compileExpression catches 
the
+              // exception and returns None, so Spark evaluates trunc locally.
+              throw new IllegalArgumentException(
+                s"Unsupported Oracle TRUNC format: ${inputs(1)}")
+          }
+          s"TRUNC(${inputs(0)}, $oracleFormat)"
         case _ => super.visitSQLFunction(funcName, inputs)
       }
     }
diff --git a/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala 
b/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala
index 3c04c517a670..bcd4bd81321a 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala
@@ -37,7 +37,7 @@ import org.apache.spark.sql.catalyst.parser.CatalystSqlParser
 import org.apache.spark.sql.catalyst.plans.logical.ShowCreateTable
 import org.apache.spark.sql.catalyst.util.{CaseInsensitiveMap, 
CharVarcharUtils, DateTimeTestUtils}
 import org.apache.spark.sql.connector.catalog.Identifier
-import org.apache.spark.sql.connector.expressions.{Expression => V2Expression, 
FieldReference}
+import org.apache.spark.sql.connector.expressions.{Expression => V2Expression, 
FieldReference, GeneralScalarExpression, LiteralValue}
 import org.apache.spark.sql.connector.expressions.filter.{AlwaysFalse, 
AlwaysTrue, Predicate}
 import org.apache.spark.sql.execution.{DataSourceScanExec, ExtendedMode, 
ProjectExec}
 import org.apache.spark.sql.execution.command.{ExplainCommand, 
ShowCreateTableCommand}
@@ -1532,6 +1532,50 @@ class JDBCSuite extends SharedSparkSession {
     assert(getJdbcType(oracleDialect, TimestampNTZType) == "TIMESTAMP")
   }
 
+  test("Oracle TRUNC pushdown should map Spark format strings to Oracle 
format") {
+    val oracleDialect = JdbcDialects.get("jdbc:oracle://127.0.0.1/db")
+    val dateRef = FieldReference("d")
+
+    // LiteralValue for StringType must use UTF8String (Spark's internal 
string type)
+    // to match what V2ExpressionBuilder produces in the real pushdown path.
+    import org.apache.spark.unsafe.types.UTF8String
+    def truncExpr(fmt: String): GeneralScalarExpression = new 
GeneralScalarExpression("TRUNC",
+      Array[V2Expression](dateRef, LiteralValue(UTF8String.fromString(fmt), 
StringType)))
+
+    val monthSql = oracleDialect.compileExpression(truncExpr("MONTH")).get
+    assert(monthSql.contains("'MM'"),
+      s"trunc(d, 'MONTH') should produce Oracle 'MM', got: $monthSql")
+    assert(!monthSql.contains("'IW'"),
+      s"trunc(d, 'MONTH') should NOT produce 'IW', got: $monthSql")
+
+    val weekSql = oracleDialect.compileExpression(truncExpr("WEEK")).get
+    assert(weekSql.contains("'IW'"),
+      s"trunc(d, 'WEEK') should produce Oracle 'IW', got: $weekSql")
+
+    val yearSql = oracleDialect.compileExpression(truncExpr("YEAR")).get
+    assert(yearSql.contains("'YYYY'"),
+      s"trunc(d, 'YEAR') should produce Oracle 'YYYY', got: $yearSql")
+
+    val quarterSql = oracleDialect.compileExpression(truncExpr("QUARTER")).get
+    assert(quarterSql.contains("'Q'"),
+      s"trunc(d, 'QUARTER') should produce Oracle 'Q', got: $quarterSql")
+
+    // Case-insensitive: lowercase formats must also map correctly
+    val weekLowerSql = oracleDialect.compileExpression(truncExpr("week")).get
+    assert(weekLowerSql.contains("'IW'"),
+      s"trunc(d, 'week') (lowercase) should produce Oracle 'IW', got: 
$weekLowerSql")
+
+    // Unmapped formats should NOT be pushed down (compileExpression returns 
None)
+    assert(oracleDialect.compileExpression(truncExpr("DAY")).isEmpty,
+      "Unmapped format 'DAY' should not be pushed down (compileExpression 
should return None)")
+
+    // Alias formats (MM, MON, YYYY, YY) should also map correctly
+    val mmSql = oracleDialect.compileExpression(truncExpr("MM")).get
+    assert(mmSql.contains("'MM'"), s"trunc(d, 'MM') should produce Oracle 
'MM', got: $mmSql")
+    val yySql = oracleDialect.compileExpression(truncExpr("YY")).get
+    assert(yySql.contains("'YYYY'"), s"trunc(d, 'YY') should produce Oracle 
'YYYY', got: $yySql")
+  }
+
   private def assertEmptyQuery(sqlString: String): Unit = {
     assert(sql(sqlString).collect().isEmpty)
   }


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

Reply via email to