dtenedor commented on code in PR #59716:
URL: https://github.com/apache/spark/pull/59716#discussion_r4354620388


##########
connector/docker-integration-tests/src/test/scala/org/apache/spark/sql/jdbc/v2/OracleIntegrationSuite.scala:
##########
@@ -428,4 +428,35 @@ class OracleIntegrationSuite extends 
DockerJDBCIntegrationV2Suite with V2JDBCTes
     assert(rows11(0).getString(0) === "amy")
     assert(rows11(1).getString(0) === "alex")
   }
+
+  test("SPARK-60479: CHAR equality is pushed, including CJK") {
+    val table = s"$catalogName.char_pad"

Review Comment:
   This is why the Oracle Docker job [failed on 
`3d24a8c`](https://github.com/srielau/spark/actions/runs/39847088203/job/120074532366):
 `TABLE_OR_VIEW_NOT_FOUND` for `oracle`.`char_pad` at the `INSERT`. The 
unquoted `CREATE TABLE char_pad` over JDBC creates `CHAR_PAD`, and this suite 
overrides `caseConvert` to upper-case names for exactly that reason. 
`s"$catalogName.${caseConvert("char_pad")}"` should fix it. Please make sure 
this passes before merge, since it's the only test that exercises the Oracle 
path, including the CJK case.
   



##########
sql/catalyst/src/main/java/org/apache/spark/sql/connector/expressions/GeneralScalarExpression.java:
##########
@@ -428,6 +428,16 @@
  *    <li>Since version: 4.0.0</li>
  *   </ul>
  *  </li>
+ *  <li>Name: <code>CHAR_READ_SIDE_PADDING</code>
+ *   <ul>
+ *    <li>SQL semantic: <code>RPAD(str, len, ' ')</code> on a CHAR scan column 
when
+ *        read-side padding is on. Spark only emits this in <code>=</code>,
+ *        <code>&lt;=&gt;</code>, <code>&lt;&gt;</code>, and <code>IN</code> 
against
+ *        a string of length <code>len</code>. JDBC sources that store CHAR 
already
+ *        padded may compile it as the raw column.</li>

Review Comment:
   `RPAD` truncates values longer than `len`, but read-side padding returns 
them unchanged (`CharVarcharCodegenUtils.readSidePadding` is pad-or-keep). 
Since connector authors implement against this text, could it say something 
like "right-pads `str` with spaces to `len` characters; values of `len` or more 
characters are returned unchanged"?
   



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2StrategySuite.scala:
##########
@@ -1882,6 +1883,71 @@ class DataSourceV2StrategySuite extends 
SharedSparkSession {
     }
   }
 
+  test("SPARK-60479: read-side CHAR padding StaticInvoke translates") {
+    val md = new MetadataBuilder()
+      .putString(CharVarcharUtils.CHAR_VARCHAR_TYPE_STRING_METADATA_KEY, 
"char(1)")
+      .build()
+    val col = AttributeReference("flag", StringType, metadata = md)()
+    val padded = CharVarcharUtils.addPaddingForScan(col)
+    val eq = EqualTo(padded, Literal("Y"))
+    val result = new V2ExpressionBuilder(eq, isPredicate = true).build()
+    assert(result.isDefined, "readSidePadding should translate to a V2 
predicate")
+    result.get match {
+      case p: Predicate if p.name() == "=" =>
+        p.children()(0) match {
+          case g: GeneralScalarExpression
+              if g.name() == CharVarcharUtils.V2_CHAR_READ_SIDE_PADDING =>
+            assert(g.children()(0) === FieldReference("flag"))
+            assert(g.children()(1) === LiteralValue(1, IntegerType))
+            assert(g.children()(2) ===
+              LiteralValue(UTF8String.fromString(" "), StringType))
+          case other =>
+            fail(s"expected CHAR_READ_SIDE_PADDING, got ${other.describe()}")
+        }
+        assert(p.children()(1) === LiteralValue(UTF8String.fromString("Y"), 
StringType))
+      case other =>
+        fail(s"expected '=' predicate, got ${other.describe()}")
+    }
+
+    val isNull = new V2ExpressionBuilder(IsNull(padded), isPredicate = 
true).build()
+    assert(isNull.isDefined)
+    isNull.get match {
+      case p: Predicate if p.name() == "IS_NULL" =>
+        assert(p.children()(0) === FieldReference("flag"))
+      case other => fail(s"expected IS_NULL on the raw column, got 
${other.describe()}")
+    }
+
+    val nseq = new V2ExpressionBuilder(EqualNullSafe(padded, Literal("Y")), 
isPredicate = true)

Review Comment:
   nit: it would be worth also covering `Not(EqualTo(...))` (`<>`), `InSet`, an 
`IN` list containing a NULL literal (it should stay local without throwing), 
and the literal on the left side (`EqualTo(Literal("Y"), padded)`).
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/V2ExpressionBuilder.scala:
##########
@@ -458,6 +478,91 @@ class V2ExpressionBuilder(e: Expression, isPredicate: 
Boolean = false) extends L
     }
   }
 
+  private def isCharReadSidePaddingInvoke(invoke: StaticInvoke): Boolean = {
+    invoke.scalarFunction.isEmpty &&
+      invoke.staticObject == classOf[CharVarcharCodegenUtils] &&
+      invoke.functionName == CharVarcharUtils.READ_SIDE_PADDING &&
+      invoke.arguments.length == 2
+  }
+
+  // Identity compile is only valid vs a literal Spark already padded to the 
CHAR length.
+  private def charPadLength(invoke: StaticInvoke): Option[Int] = {
+    val len = invoke.arguments(1)
+    if (len.foldable) {
+      len.eval() match {
+        case n: Int => Some(n)
+        case _ => None
+      }
+    } else {
+      None
+    }
+  }
+
+  private def isFoldableStringOfLen(expr: Expression, n: Int): Boolean = {
+    expr.foldable && (expr.eval() match {

Review Comment:
   nit: this calls `eval()` on any foldable expression. After constant folding 
the padded literal is already a `Literal`, but `ConstantFolding` deliberately 
leaves an expression unfolded when it fails inside a conditional branch, and 
evaluating one here would throw during planning. Matching `Literal(s: 
UTF8String, _)` instead would avoid that.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/jdbc/JdbcDialects.scala:
##########
@@ -467,7 +479,12 @@ abstract class JdbcDialect extends Serializable with 
Logging {
     }
 
     override def visitSQLFunction(funcName: String, inputs: Array[String]): 
String = {
-      s"""${dialectFunctionName(funcName)}(${inputs.mkString(", ")})"""
+      funcName match {
+        // JDBC CHAR is stored padded. readSidePadding wrapped the column and 
blocked
+        // pushdown; compiling it as the column restores col = 'Y    ' 
(literal padded).
+        case CharVarcharUtils.V2_CHAR_READ_SIDE_PADDING => inputs(0)

Review Comment:
   nit: this branch looks unreachable now. `build` handles 
`CHAR_READ_SIDE_PADDING` before it gets here, and `V2ExpressionSQLBuilder` no 
longer routes the name to `visitSQLFunction`. I'd drop it and its comment.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to