cloud-fan commented on code in PR #59716:
URL: https://github.com/apache/spark/pull/59716#discussion_r4395624810
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/jdbc/JDBCScanBuilder.scala:
##########
@@ -66,11 +67,25 @@ case class JDBCScanBuilder(
private var sortOrders: Array[String] = Array.empty[String]
+ private def containsUnsupportedCharPad(pred: Predicate): Boolean = {
+ if
(dialect.isSupportedFunction(CharVarcharUtils.V2_CHAR_READ_SIDE_PADDING)) {
+ false
+ } else {
+ def walk(e: Expression): Boolean = e match {
+ case g: GeneralScalarExpression
+ if g.name() == CharVarcharUtils.V2_CHAR_READ_SIDE_PADDING => true
+ case _ => Option(e.children()).exists(_.exists(walk))
+ }
+ walk(pred)
+ }
+ }
+
override def pushPredicates(predicates: Array[Predicate]): Array[Predicate]
= {
if (jdbcOptions.pushDownPredicate) {
- val (pushed, unSupported) =
predicates.partition(dialect.compileExpression(_).isDefined)
+ val (unsupportedCharPad, rest) =
predicates.partition(containsUnsupportedCharPad)
Review Comment:
**Nit (P3):** This decline only runs for filters. V2ExpressionBuilder also
translates a CHAR `=`, `<=>`, `<>`, or `IN` nested inside other expressions; a
`CASE WHEN` condition, for example, goes through `generateExpression(_, true)`.
`rewriteAggregate` substitutes the read-side padding alias into aggregate and
grouping expressions, so on a MySQL catalog table `t(flag CHAR(1), amt INT)`
```sql
SELECT SUM(CASE WHEN flag = 'Y' THEN amt END) FROM mysql.t
```
reaches `pushAggregation` as `SUM(CASE WHEN CHAR_READ_SIDE_PADDING(flag, 1,
' ') = 'Y' ...)`. `pushAggregation` (lines 113 and 116), `pushTopN` (line 334),
and `pushDownJoin` (line 222) call `dialect.compileExpression` directly.
`JDBCSQLBuilder.build` throws `_LEGACY_ERROR_TEMP_3177`, and
`compileExpression` logs `Error occurs while compiling V2 expression` with the
stack trace before Spark falls back to local evaluation. That is the same
planning-failure-looking log this decline removes for filters. It now shows up
on aggregate, top-N, and join planning for MySQL, DB2, Derby, Teradata,
Snowflake, Databricks, the base dialect, and third-party dialects. Results
don't change.
Generalizing `containsUnsupportedCharPad` from `Predicate` to `Expression`
and checking it before those three compile calls would keep them quiet too.
**Verification:**
- **Regression:** The scan builder of a dialect that does not opt in
declines an aggregation whose argument contains a CHAR comparison over
CHAR_READ_SIDE_PADDING, and a top-N sort order containing one, without logging
the compile warning.
- **Behavior:** On an opt-in dialect, an aggregate such as SUM(CASE WHEN
flag = 'Y' THEN 1 ELSE 0 END) over a CHAR column is pushed and returns the same
value as Spark-local evaluation.
- **Compatibility:** On dialects that do not opt in, CHAR comparisons in
filters still stay post-scan and IS [NOT] NULL is still pushed on the raw
column, and aggregates, top-N, and joins without the function push exactly as
before.
##########
sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala:
##########
@@ -1656,6 +1658,8 @@ class JDBCSuite extends SharedSparkSession {
Some(ArrayType(DecimalType.SYSTEM_DEFAULT)))
assert(Postgres.getCatalystType(java.sql.Types.ARRAY, "_bpchar", 64, md) ==
Some(ArrayType(CharType(64))))
+ assert(Postgres.getCatalystType(java.sql.Types.CHAR, "char", 1, md) ===
Some(StringType))
Review Comment:
**Nit (P3):** This pins the scalar internal `"char"` mapping, but the PR
also changes its array element path. `toCatalystType` now has `case "char" =>
Some(StringType)`, so a `"char"[]` column (JDBC type name `_char`) reads as
`ARRAY<STRING>` instead of `ARRAY<CHAR(n)>`. Nothing calls
`getCatalystType(Types.ARRAY, "_char", ...)`, and no docker suite declares a
`"char"[]` column. Re-merging the arm with `bpchar` would leave every test
green, and empty elements would read as `' '` again. An assertion next to the
`_bpchar` one, such as `Postgres.getCatalystType(java.sql.Types.ARRAY, "_char",
1, md) == Some(ArrayType(StringType))`, would pin it.
**Verification:**
- **Regression:** Reverting the internal "char" array element mapping to
CHAR(n) makes the PostgreSQL type-mapping test fail.
- **Compatibility:** The _bpchar array mapping and the scalar char and
bpchar mappings keep their current expectations.
##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2StrategySuite.scala:
##########
@@ -1882,6 +1883,115 @@ 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)
+ .build()
+ assert(nseq.isDefined)
Review Comment:
**Nit (P3):** Unlike the `=`, `<>`, and `InSet` cases here, this only checks
that some predicate comes back. The JDBCV2Suite `flag <=> 'Y'` query doesn't
pin the operator either. A `WHERE` clause drops the NULL row whether the pushed
predicate is `<=>` or `=`, so the answer is the same. The difference shows up
under `NOT`: `NOT (flag <=> 'Y')` must return the NULL row, and JDBC keeps it
only while the operator stays `<=>`, which it renders with the NULL-aware `IS
NULL` form. If this arm emitted `=`, the remote would drop that row with
Spark's filter already removed, and every current assertion would still pass.
Asserting the `<=>` name, the wrapper child, and the padded literal, as the `=`
case does, plus an H2 `NOT (flag <=> 'Y')` query with `checkPushedInfo` and
`checkAnswer`, would pin it.
**Verification:**
- **Regression:** Translating EqualNullSafe over the padded CHAR column with
= instead of <=> fails both the translation-shape assertion and the end-to-end
NOT (flag <=> 'Y') answer.
- **Behavior:** The pushed NOT (flag <=> 'Y') filter leaves no post-scan
filter and returns the rows whose flag is not 'Y' together with the NULL row.
##########
docs/sql-migration-guide.md:
##########
@@ -40,6 +40,7 @@ license: |
- Since Spark 4.4, when an already-analyzed Data Source V2 query is refreshed
after a compatible schema change, connectors can return more data columns from
the current table schema in `Scan.readSchema()` than requested by
`SupportsPushDownRequiredColumns.pruneColumns`. Previously, this partial
pruning could fail planning because the scan reported columns absent from the
analyzed relation output.
- Since Spark 4.4, when an already-analyzed Data Source V2 query is refreshed,
if rebinding an expanded struct used as a map key causes distinct current keys
to collide under the analyzed schema, the query fails with `DUPLICATED_MAP_KEY`
by default instead of returning duplicate keys. With
`spark.sql.mapKeyDedupPolicy=LAST_WIN`, the last value is kept.
- Since Spark 4.4, the Oracle JDBC datasource maps bare `NUMBER` columns
(declared without explicit precision or scale) to `DecimalType(38, 18)` instead
of `DecimalType(38, 10)`. This uses `DecimalType.DEFAULT_SCALE` and preserves
up to 18 fractional digits instead of 10, reducing silent precision loss.
However, the integer range shrinks from 28 to 20 digits; bare `NUMBER` values
with more than 20 integer digits that previously read correctly will raise
`NUMERIC_VALUE_OUT_OF_RANGE`. The same change applies to Oracle `FLOAT`
columns, which JDBC reports as `NUMERIC` with `scale = -127`. Columns with
explicit `NUMBER(p,s)` are not affected. To restore the previous
`DecimalType(38, 10)` mapping, set
`spark.sql.legacy.oracle.numberMapping.enabled` to `true`.
+- Since Spark 4.4, JDBC Data Source V2 catalog tables on H2, PostgreSQL, SQL
Server, and Oracle push CHAR equality, inequality, null-safe equality,
`IN`/`NOT IN`, and `IS [NOT] NULL` again when `spark.sql.readSideCharPadding`
is true (the default). In 4.3 the default read-side padding wrap kept those
filters in Spark. Pushed predicates use the remote collation, so a
case-insensitive SQL Server collation can match a stored `'ABC'` with `flag =
'abc'` while a Spark-local comparison would not. The V1 path (`spark.read.jdbc`
and `USING jdbc`) still evaluates these CHAR filters in Spark. PostgreSQL's
internal `"char"` type (JDBC type name `char`) is mapped to `STRING` instead of
`CHAR(1)`, so Spark no longer blank-pads its empty value.
Review Comment:
**Nit (P3):** Two scope statements here don't match the code. First, line 32
of this section says later CHAR/VARCHAR bullets that don't name
`preserveCharVarcharTypeInfo` apply when
`spark.sql.charVarchar.standardSemantics.enabled` is true. This pushdown
happens only when that flag is false. Under standard semantics the scan gets
`charTypeReadSideCheck`, which isn't translated, and JDBCV2Suite asserts that
the equality stays in Spark. Read under that rule, this bullet describes the
wrong mode. Second, the `IS [NOT] NULL` unwrap in `V2ExpressionBuilder` has no
dialect gate. Every JDBC dialect and every DSv2 source gets the raw-column null
check, not only H2, PostgreSQL, SQL Server, and Oracle, and JDBCSuite asserts
that Derby pushes it. Naming the standard-semantics condition and moving `IS
[NOT] NULL` out of the four-dialect list would fix both.
**Verification:**
- **Inspection:** The bullet names the standard-semantics condition and
separates the IS [NOT] NULL change from the four-dialect equality and IN
change, consistent with the translation and JDBC tests.
--
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]