dongjoon-hyun commented on code in PR #58979:
URL: https://github.com/apache/spark/pull/58979#discussion_r4146972849
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2ScanExecBase.scala:
##########
@@ -135,7 +135,8 @@ trait DataSourceV2ScanExecBase
/**
* Returns the output ordering for this scan. When the source reports
ordering via
- * `SupportsReportOrdering`, that ordering is returned as-is. Otherwise,
when the output
+ * `SupportsReportOrdering` and `V2ScanPartitioningAndOrdering` keeps it,
that ordering is
Review Comment:
nit. The same condition is described in the doc of
`spark.sql.sources.v2.bucketing.partitionKeyOrdering.enabled` in `SQLConf` and
in `docs/sql-performance-tuning.md`, e.g., `... reports a KeyedPartitioning but
does not report explicit ordering via SupportsReportOrdering`. After this PR, a
source whose reported ordering is ignored also gets the key-derived ordering,
as the new test asserts. They are incomplete rather than wrong, so this is
optional. If you update them, could you update both? For example, `..., or
reports one that Spark ignores because it references a column that cannot be
resolved`.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/V2ScanPartitioningAndOrdering.scala:
##########
@@ -75,8 +89,43 @@ object V2ScanPartitioningAndOrdering extends
Rule[LogicalPlan] with Logging {
private def ordering(plan: LogicalPlan) = plan.transformDownWithPruning(
_.containsPattern(DATA_SOURCE_V2_SCAN_RELATION)) {
case d @ ExtractV2ScanInfo(relation, scan: SupportsReportOrdering, _) =>
Review Comment:
FYI, this is also pre-existing and not for this PR. Unlike the partitioning
pass (`if d.keyGroupedPartitioning.isEmpty`), the ordering pass has no guard
and recomputes the ordering on every application. For a resolvable nested sort
key like `s.x`, `plan.resolve` returns a new `Alias(GetStructField(...))` with
a fresh `ExprId` each time, so the rule is not idempotent for such a report.
It looks like the `Once` batch idempotence check of `RuleExecutor` would
fail in testing mode. In production, the reported `SortOrder(Alias(...))` would
not match a required ordering because the `Alias` is not stripped. No existing
test reports a nested sort key, but this PR adds the struct column `s` and
`reportedOrdering` to the fixture, so it is easy to hit by adding such a case.
This is from reading the code, not from a run.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/V2ScanPartitioningAndOrdering.scala:
##########
@@ -75,8 +89,43 @@ object V2ScanPartitioningAndOrdering extends
Rule[LogicalPlan] with Logging {
private def ordering(plan: LogicalPlan) = plan.transformDownWithPruning(
_.containsPattern(DATA_SOURCE_V2_SCAN_RELATION)) {
case d @ ExtractV2ScanInfo(relation, scan: SupportsReportOrdering, _) =>
- val ordering =
- V2ExpressionUtils.toCatalystOrdering(scan.outputOrdering(), relation,
relation.funCatalog)
- d.copy(ordering = Some(ordering))
+ val reportedOrdering = scan.outputOrdering()
+ val unresolvedColumns =
unresolvableColumns(reportedOrdering.toImmutableArraySeq, relation)
+ if (unresolvedColumns.nonEmpty) {
+ logWarning(
+ log"Spark ignores the ordering reported by ${MDC(RELATION_NAME,
relation.name)} " +
+ log"(scan ${MDC(CLASS_NAME, scan.getClass.getName)}) because the
ordering columns " +
+ log"cannot be resolved: ${MDC(COLUMN_NAMES,
unresolvedColumns.mkString(", "))}.")
+ // With no reported ordering, DataSourceV2ScanExecBase.outputOrdering
may still derive one
+ // from a kept key-grouped partitioning, which does not depend on the
dropped report.
+ d.copy(ordering = None)
+ } else {
+ val ordering =
+ V2ExpressionUtils.toCatalystOrdering(reportedOrdering, relation,
relation.funCatalog)
+ d.copy(ordering = Some(ordering))
+ }
+ }
+
+ private def unresolvableColumns(
+ exprs: Seq[V2Expression],
+ relation: LogicalPlan): Seq[String] = {
+ // This is not `V2Expression.references()` because `ApplyTransform` and
`BucketTransform`
+ // override it to return only their top-level arguments, which would miss
`f(g(missing))`.
Review Comment:
nit. The wording which I suggested earlier is imprecise. `BucketTransform`
takes `Seq[NamedReference]`, so its override cannot miss anything. Only
`ApplyTransform` truncates. The more durable reason is that `references()` can
be overridden by connectors (e.g., `MyIdentityTransform.references()` in
`DataSourceV2Suite` returns an empty array), while the conversion resolves
structurally through `arguments()` and `expression()`. Could you revise it? For
example,
```scala
// Walk `children()` instead of calling `V2Expression.references()`: the
conversion resolves
// every reference reachable through `arguments()`/`expression()`, while
`references()` can
// return fewer. `ApplyTransform` returns only its top-level arguments,
which would miss
// `f(g(missing))`, and a connector-defined `Transform` can return
anything.
```
In addition, the comment in the `catch` below and the scaladoc of
`resolveRefOpt` mention only a missing nested field and an ambiguous reference,
but `AMBIGUOUS_REFERENCE_TO_FIELDS` and `INVALID_EXTRACT_BASE_FIELD_TYPE`
(e.g., `id.x` on an INT column) reach this `catch` too.
##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -3486,6 +3492,139 @@ class KeyGroupedPartitioningSuite
}
}
+ private val customReportingCatalogName = "custom_reporting_cat"
+ private val customReportingTableName =
s"$customReportingCatalogName.ns.$table"
+
+ private def withCustomReportingTable(f: CustomReportingCatalog => Unit):
Unit = {
+ spark.conf.set(s"spark.sql.catalog.$customReportingCatalogName",
+ classOf[CustomReportingCatalog].getName)
+ val reportingCatalog =
spark.sessionState.catalogManager.catalog(customReportingCatalogName)
+ .asInstanceOf[CustomReportingCatalog]
+ try {
+ val reportingColumns = Array(
+ Column.create("id", IntegerType),
+ Column.create("data", StringType),
+ Column.create("s", new StructType().add("x", IntegerType)))
+ createTable(table, reportingColumns, Array(identity("id")), catalog =
reportingCatalog)
+ sql(s"INSERT INTO $customReportingCatalogName.ns.$table VALUES " +
+ "(1, 'aa', named_struct('x', 10)), (2, 'bb', named_struct('x', 20))")
+ createTable(table, columns, Array(identity("id")))
+ sql(s"INSERT INTO testcat.ns.$table VALUES " +
+ "(1, 'cc', cast('2020-01-03' as timestamp)), " +
+ "(2, 'dd', cast('2020-01-04' as timestamp))")
+ withSQLConf(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false") {
+ f(reportingCatalog)
+ }
+ } finally {
+ reportingCatalog.reportedKeys = Seq.empty
+ reportingCatalog.reportedOrdering = Seq.empty
+ reportingCatalog.clearTables()
+ spark.conf.unset(s"spark.sql.catalog.$customReportingCatalogName")
+ }
+ }
+
+ private def customReportingJoinDf(): DataFrame = sql(
+ s"""SELECT t1.data, t2.data FROM $customReportingCatalogName.ns.$table t1
+ |JOIN testcat.ns.$table t2 ON t1.id = t2.id
+ |""".stripMargin)
+
+ private def customReportingJoin(warningText: String): (SparkPlan,
BatchScanExec, Seq[String]) = {
+ val df = customReportingJoinDf()
+ val logAppender = new LogAppender("custom reported partitioning or
ordering")
+ withLogAppender(logAppender, level = Some(Level.WARN)) {
+ checkAnswer(df, Seq(Row("aa", "cc"), Row("bb", "dd")))
+ }
+ val warnings =
logAppender.loggingEvents.map(_.getMessage.getFormattedMessage)
+ .filter(_.contains(warningText)).toSeq
+ val plan = df.queryExecution.executedPlan
+ val reportingScans =
collectScans(plan).filter(_.scan.isInstanceOf[CustomReportingScan])
+ assert(reportingScans.length == 1)
+ (plan, reportingScans.head, warnings)
+ }
+
+ test("SPARK-59721: an unresolvable reported partition key falls back to
unknown partitioning") {
+ val nestedTransform =
+ ApplyTransform("f", Seq(ApplyTransform("g",
Seq(FieldReference("missing")))))
+ // CustomReportingCatalog is not a FunctionCatalog, so a `bucket` key
would be dropped even with
+ // a resolvable column. The warning assertion shows the unresolvable
column is what drops it.
+ val cases = Seq[(Seq[Expression], Option[String])](
+ (Seq(identity("id")), None),
+ (Seq(bucket(4, "missing")), Some("missing")),
+ (Seq(nestedTransform), Some("missing")),
+ (Seq(identity("id"), identity("missing")), Some("missing")),
+ (Seq(identity("missing"), bucket(4, "missing")), Some("missing")),
+ (Seq(identity("s.missing")), Some("s.missing (FIELD_NOT_FOUND)")))
+ withCustomReportingTable { reportingCatalog =>
+ cases.foreach { case (keys, unresolvedColumns) =>
+ reportingCatalog.reportedKeys = keys
+ val (plan, reportingScan, warnings) =
+ customReportingJoin("partition key columns cannot be resolved")
+ val keysString = keys.map(_.describe()).mkString(", ")
+ unresolvedColumns match {
+ case Some(columnsString) =>
+ assert(warnings.nonEmpty, s"no warning for keys [$keysString]")
+ assert(warnings.forall(_.contains(s"reported by
$customReportingTableName ")), warnings)
+
assert(warnings.forall(_.contains(classOf[CustomReportingScan].getName)),
warnings)
+ assert(warnings.forall(_.contains(s"resolved: $columnsString.")),
warnings)
+ assert(reportingScan.keyGroupedPartitioning.isEmpty, keysString)
+ assert(collectShuffles(plan).nonEmpty, keysString)
+ case None =>
+ assert(warnings.isEmpty, warnings)
+ assert(reportingScan.keyGroupedPartitioning.isDefined, keysString)
+ assert(collectShuffles(plan).isEmpty, keysString)
+ }
+ }
+ }
+ }
+
+ test("SPARK-59721: an unresolvable reported ordering column is ignored") {
+ withCustomReportingTable { reportingCatalog =>
+ withSQLConf(SQLConf.V2_BUCKETING_PARTITION_KEY_ORDERING_ENABLED.key ->
"true") {
+ reportingCatalog.reportedKeys = Seq(identity("id"))
+ reportingCatalog.reportedOrdering = Seq(
+ sort(FieldReference("id"), SortDirection.ASCENDING,
NullOrdering.NULLS_FIRST))
+ val (_, keptScan, keptWarnings) =
+ customReportingJoin("ordering columns cannot be resolved")
+ assert(keptWarnings.isEmpty, keptWarnings)
+
assert(keptScan.ordering.map(_.map(_.child.references.map(_.name).toSeq)) ==
+ Some(Seq(Seq("id"))))
+
+ reportingCatalog.reportedOrdering = Seq(
+ sort(FieldReference("missing"), SortDirection.ASCENDING,
NullOrdering.NULLS_FIRST))
+ val (_, reportingScan, warnings) =
+ customReportingJoin("ordering columns cannot be resolved")
+ assert(warnings.nonEmpty)
+ assert(warnings.forall(_.contains(s"reported by
$customReportingTableName ")), warnings)
+
assert(warnings.forall(_.contains(classOf[CustomReportingScan].getName)),
warnings)
+ assert(warnings.forall(_.contains("resolved: missing.")), warnings)
+ assert(reportingScan.ordering.isEmpty)
+ assert(reportingScan.keyGroupedPartitioning.isDefined)
+ val derivedOrdering = reportingScan.outputOrdering.map { o =>
+ (o.child.references.map(_.name).toSeq, o.direction)
+ }
+ assert(derivedOrdering == Seq((Seq("id"), Ascending)))
+ }
+ }
+ }
+
+ test("SPARK-59721: a reported partition key that cannot be converted still
fails") {
Review Comment:
This pins `id + 1` and `f(g(id))`, where every column resolves. Note that
the pre-check runs over the whole report before the conversion, so a report
which mixes an unconvertible key with an unresolvable column (e.g., `[id + 1,
identity(missing)]`, or a single key `missing + 1`) does not fail anymore. It
succeeds with a warning which mentions only `missing`, and it starts to fail
with `_LEGACY_ERROR_TEMP_3054` once the column becomes resolvable.
It is consistent with the PR description (`Keys whose columns all resolve
take the existing conversion path unchanged`), so no code change is needed.
Could you add one mixed case to the fallback test above, e.g., `(Seq(<id + 1>,
identity("missing")), Some("missing"))`, so that this boundary is explicit?
##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -3486,6 +3492,139 @@ class KeyGroupedPartitioningSuite
}
}
+ private val customReportingCatalogName = "custom_reporting_cat"
+ private val customReportingTableName =
s"$customReportingCatalogName.ns.$table"
+
+ private def withCustomReportingTable(f: CustomReportingCatalog => Unit):
Unit = {
+ spark.conf.set(s"spark.sql.catalog.$customReportingCatalogName",
+ classOf[CustomReportingCatalog].getName)
+ val reportingCatalog =
spark.sessionState.catalogManager.catalog(customReportingCatalogName)
+ .asInstanceOf[CustomReportingCatalog]
+ try {
+ val reportingColumns = Array(
+ Column.create("id", IntegerType),
+ Column.create("data", StringType),
+ Column.create("s", new StructType().add("x", IntegerType)))
+ createTable(table, reportingColumns, Array(identity("id")), catalog =
reportingCatalog)
+ sql(s"INSERT INTO $customReportingCatalogName.ns.$table VALUES " +
+ "(1, 'aa', named_struct('x', 10)), (2, 'bb', named_struct('x', 20))")
+ createTable(table, columns, Array(identity("id")))
+ sql(s"INSERT INTO testcat.ns.$table VALUES " +
+ "(1, 'cc', cast('2020-01-03' as timestamp)), " +
+ "(2, 'dd', cast('2020-01-04' as timestamp))")
+ withSQLConf(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false") {
+ f(reportingCatalog)
+ }
+ } finally {
+ reportingCatalog.reportedKeys = Seq.empty
+ reportingCatalog.reportedOrdering = Seq.empty
+ reportingCatalog.clearTables()
+ spark.conf.unset(s"spark.sql.catalog.$customReportingCatalogName")
+ }
+ }
+
+ private def customReportingJoinDf(): DataFrame = sql(
+ s"""SELECT t1.data, t2.data FROM $customReportingCatalogName.ns.$table t1
+ |JOIN testcat.ns.$table t2 ON t1.id = t2.id
+ |""".stripMargin)
+
+ private def customReportingJoin(warningText: String): (SparkPlan,
BatchScanExec, Seq[String]) = {
+ val df = customReportingJoinDf()
+ val logAppender = new LogAppender("custom reported partitioning or
ordering")
+ withLogAppender(logAppender, level = Some(Level.WARN)) {
+ checkAnswer(df, Seq(Row("aa", "cc"), Row("bb", "dd")))
+ }
+ val warnings =
logAppender.loggingEvents.map(_.getMessage.getFormattedMessage)
+ .filter(_.contains(warningText)).toSeq
+ val plan = df.queryExecution.executedPlan
+ val reportingScans =
collectScans(plan).filter(_.scan.isInstanceOf[CustomReportingScan])
+ assert(reportingScans.length == 1)
+ (plan, reportingScans.head, warnings)
+ }
+
+ test("SPARK-59721: an unresolvable reported partition key falls back to
unknown partitioning") {
+ val nestedTransform =
+ ApplyTransform("f", Seq(ApplyTransform("g",
Seq(FieldReference("missing")))))
+ // CustomReportingCatalog is not a FunctionCatalog, so a `bucket` key
would be dropped even with
+ // a resolvable column. The warning assertion shows the unresolvable
column is what drops it.
+ val cases = Seq[(Seq[Expression], Option[String])](
Review Comment:
All the new cases use the built-in expression classes (`FieldReference`,
`ApplyTransform`, `BucketTransform`, `SortValue`), so the unresolvable path is
not covered for connector-defined `Transform`/`SortOrder`/`NamedReference`
classes. That is where walking `children()` matters most. For example,
`MyIdentityTransform.references()` in `DataSourceV2Suite` returns an empty
array.
Could you add one case which reads the existing
`OrderAndPartitionAwareDataSource` (and its Java twin,
`JavaOrderAndPartitionAwareDataSource`) with `partitionKeys=i` and
`orderKeys=missing`, and verify that the query succeeds, the reported ordering
is dropped, and the warning names `missing`? It also covers my `describe()`
comment in `V2ScanPartitioningAndOrdering`.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/V2ExpressionUtils.scala:
##########
@@ -46,14 +46,20 @@ import org.apache.spark.util.ArrayImplicits._
object V2ExpressionUtils extends SQLConfHelper with Logging {
import
org.apache.spark.sql.connector.catalog.CatalogV2Implicits.MultipartIdentifierHelper
+ /**
+ * Variant of `resolveRef` that returns `None` if no attribute matches the
reference. It still
+ * throws for a missing nested field or an ambiguous reference.
+ */
+ private[sql] def resolveRefOpt[T <: NamedExpression](
Review Comment:
nit. The type parameter looks unnecessary now. All callers other than
`resolveRef` pass `[NamedExpression]`, and none of them uses the typed result.
How about making this non-generic and keeping the cast in `resolveRef` like
before?
```scala
private[sql] def resolveRefOpt(
ref: NamedReference, plan: LogicalPlan): Option[NamedExpression] = {
plan.resolve(ref.fieldNames.toImmutableArraySeq, conf.resolver)
}
def resolveRef[T <: NamedExpression](ref: NamedReference, plan:
LogicalPlan): T = {
resolveRefOpt(ref, plan).getOrElse {
...
}.asInstanceOf[T]
}
```
Then, the explicit `[NamedExpression]` at the call sites and the
`NamedExpression` import in `V2ScanPartitioningAndOrdering` can be removed.
##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -9259,3 +9398,51 @@ class KeyGroupedPartitioningCatalystRuntimeFilterSuite
catalog.clearTables()
}
}
+
+/**
+ * An in-memory catalog whose scans report the partitioning and ordering set
on the catalog instead
+ * of the table's own, so a test can report keys on columns the table does not
have.
+ */
+class CustomReportingCatalog extends InMemoryTableCatalog {
+ @volatile var reportedKeys: Seq[Expression] = Seq.empty
Review Comment:
nit. `@volatile` looks unnecessary. These fields are written by the test
thread and read only in `build()`, which the optimizer invokes on the same
thread. The other in-memory test catalogs use plain `var` for such knobs.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/V2ScanPartitioningAndOrdering.scala:
##########
@@ -75,8 +89,43 @@ object V2ScanPartitioningAndOrdering extends
Rule[LogicalPlan] with Logging {
private def ordering(plan: LogicalPlan) = plan.transformDownWithPruning(
_.containsPattern(DATA_SOURCE_V2_SCAN_RELATION)) {
case d @ ExtractV2ScanInfo(relation, scan: SupportsReportOrdering, _) =>
- val ordering =
- V2ExpressionUtils.toCatalystOrdering(scan.outputOrdering(), relation,
relation.funCatalog)
- d.copy(ordering = Some(ordering))
+ val reportedOrdering = scan.outputOrdering()
+ val unresolvedColumns =
unresolvableColumns(reportedOrdering.toImmutableArraySeq, relation)
+ if (unresolvedColumns.nonEmpty) {
+ logWarning(
+ log"Spark ignores the ordering reported by ${MDC(RELATION_NAME,
relation.name)} " +
+ log"(scan ${MDC(CLASS_NAME, scan.getClass.getName)}) because the
ordering columns " +
+ log"cannot be resolved: ${MDC(COLUMN_NAMES,
unresolvedColumns.mkString(", "))}.")
+ // With no reported ordering, DataSourceV2ScanExecBase.outputOrdering
may still derive one
+ // from a kept key-grouped partitioning, which does not depend on the
dropped report.
+ d.copy(ordering = None)
+ } else {
+ val ordering =
+ V2ExpressionUtils.toCatalystOrdering(reportedOrdering, relation,
relation.funCatalog)
+ d.copy(ordering = Some(ordering))
+ }
+ }
+
+ private def unresolvableColumns(
+ exprs: Seq[V2Expression],
+ relation: LogicalPlan): Seq[String] = {
+ // This is not `V2Expression.references()` because `ApplyTransform` and
`BucketTransform`
+ // override it to return only their top-level arguments, which would miss
`f(g(missing))`.
+ def references(expr: V2Expression): Seq[NamedReference] = expr match {
+ case ref: NamedReference => Seq(ref)
+ case other => other.children().toImmutableArraySeq.flatMap(references)
+ }
+ exprs.flatMap(references).distinct.flatMap { ref =>
+ try {
+ if (V2ExpressionUtils.resolveRefOpt[NamedExpression](ref,
relation).isDefined) {
+ None
+ } else {
+ Some(ref.describe())
Review Comment:
`Expression.describe()` defaults to `toString()`, and `NamedReference`
requires only `fieldNames()`. So, for a connector-defined `NamedReference`
without `toString`/`equals`, this prints `Class@hash` instead of the column
name, and `.distinct` above does not de-duplicate it.
For example, when `OrderAndPartitionAwareDataSource` in `DataSourceV2Suite`
(`MySortOrder` -> `MyIdentityTransform` -> `MyNamedReference`) is read with
`orderKeys=missing`, the query failed with `Unable to resolve missing given
[...]` before this PR. Now, it succeeds, and the only diagnostic is a warning
which ends with `...OrderAndPartitionAwareDataSource$MyNamedReference@<hash>`.
(This is from reading the code, not from a run.)
Could you build the name from `fieldNames` like `resolveRef` does, and
de-duplicate on the string? For example,
```scala
exprs.flatMap(references).flatMap { ref =>
val name = ref.fieldNames.toImmutableArraySeq.quoted
try {
if (V2ExpressionUtils.resolveRefOpt[NamedExpression](ref,
relation).isDefined) {
None
} else {
Some(name)
}
} catch {
// A missing nested field or an ambiguous name throws instead of
returning None.
case e: AnalysisException => Some(s"$name (${e.getCondition})")
}
}.distinct
```
`quoted` comes from `CatalogV2Implicits.MultipartIdentifierHelper`.
`FieldReference.toString` is `parts.quoted`, so the expected strings in the new
tests (`missing` and `s.missing (FIELD_NOT_FOUND)`) stay the same.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/V2ScanPartitioningAndOrdering.scala:
##########
@@ -47,9 +49,21 @@ object V2ScanPartitioningAndOrdering extends
Rule[LogicalPlan] with Logging {
if d.keyGroupedPartitioning.isEmpty =>
val catalystPartitioning = scan.outputPartitioning() match {
case kgp: KeyGroupedPartitioning =>
- val partitioning = sequenceToOption(
- kgp.keys().map(V2ExpressionUtils.toCatalystOpt(_, relation,
relation.funCatalog))
- .toImmutableArraySeq)
+ val keys = kgp.keys()
+ val unresolvedColumns =
unresolvableColumns(keys.toImmutableArraySeq, relation)
+ val partitioning = if (unresolvedColumns.nonEmpty) {
+ logWarning(
+ log"Spark ignores the KeyGroupedPartitioning reported by " +
+ log"${MDC(RELATION_NAME, relation.name)} (scan " +
+ log"${MDC(CLASS_NAME, scan.getClass.getName)}) because the
partition key columns " +
+ log"cannot be resolved: ${MDC(COLUMN_NAMES,
unresolvedColumns.mkString(", "))}. " +
+ log"Storage-partitioned join will not be applied for this
scan.")
Review Comment:
nit. This sentence is narrower than what happens. Without the reported
partitioning, storage-partitioned aggregates and windows and the
partition-key-derived ordering are lost too (cf. `storage-partitioned
operations (joins, aggregates, and windows)` in `SQLConf`). In addition, when
`spark.sql.sources.v2.bucketing.shuffle.enabled` is true and the other side is
keyed, a storage-partitioned join can still be planned by shuffling only this
side. Could you remove this sentence like the ordering warning below, or
generalize it?
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2ScanExecBase.scala:
##########
@@ -135,7 +135,8 @@ trait DataSourceV2ScanExecBase
/**
* Returns the output ordering for this scan. When the source reports
ordering via
- * `SupportsReportOrdering`, that ordering is returned as-is. Otherwise,
when the output
+ * `SupportsReportOrdering` and `V2ScanPartitioningAndOrdering` keeps it,
that ordering is
+ * returned as-is. Otherwise, when the output
Review Comment:
FYI, this is pre-existing and not for this PR. `returned as-is` also applies
to a sort order on a column which is pruned out of the scan output, because the
ordering pass resolves against the full `relation`.
`DataSourceV2ScanRelation.doCanonicalize` handles it with
`takeWhile(_.references.subsetOf(outputSet))`, but `outputOrdering` does not.
A row-based scan is safe because the `ProjectExec` above it truncates such a
sort order. For a columnar scan without `ProjectExec`, it looks like the k-way
merge of `GroupPartitionsExec` (`new LazyCodeGenOrdering(kWayMergeOrdering,
child.output)`) can fail to bind it when
`spark.sql.sources.v2.bucketing.preserveOrderingOnCoalesce.enabled` is true
(false by default). This is from reading the code, and I have not reproduced it
yet. I am checking it separately.
##########
sql/core/src/test/scala/org/apache/spark/sql/execution/planmerging/MergeSubplansSuite.scala:
##########
@@ -2927,9 +2927,10 @@ class MergeSubplansSuite extends PlanTest {
test("SPARK-40259: merge DSv2 scans that report empty key-grouped
partitioning or ordering") {
// A source implementing SupportsReportPartitioning/SupportsReportOrdering
but reporting nothing
- // yields Some(Nil), not None (V2ScanPartitioningAndOrdering sets the
field unconditionally). An
- // empty report carries no partitioning/ordering to drop, so the merge
should still proceed --
- // the gate tests the inner Seq, not the Option. The fused plan is the
plain column union.
+ // yields Some(Nil), not None (V2ScanPartitioningAndOrdering sets the
field unless a reported
Review Comment:
nit. Since this comment is updated, could you also update the sibling
comment above `mergeDegradesReporting` in `PlanMerger.scala`? It says
`partitioning pass keeps the report whenever any key survives`, which is no
longer literally true. For example, `[identity(id), identity(missing)]` drops
the whole report although `id` survives in the scan output (the new test pins
it). The merge logic and the conclusion of that comment still hold because the
new check depends only on the reported columns and `relation`, so adding
`unless a reported column cannot be resolved` there would be enough.
##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -3486,6 +3492,139 @@ class KeyGroupedPartitioningSuite
}
}
+ private val customReportingCatalogName = "custom_reporting_cat"
+ private val customReportingTableName =
s"$customReportingCatalogName.ns.$table"
+
+ private def withCustomReportingTable(f: CustomReportingCatalog => Unit):
Unit = {
+ spark.conf.set(s"spark.sql.catalog.$customReportingCatalogName",
+ classOf[CustomReportingCatalog].getName)
+ val reportingCatalog =
spark.sessionState.catalogManager.catalog(customReportingCatalogName)
+ .asInstanceOf[CustomReportingCatalog]
+ try {
+ val reportingColumns = Array(
+ Column.create("id", IntegerType),
+ Column.create("data", StringType),
+ Column.create("s", new StructType().add("x", IntegerType)))
+ createTable(table, reportingColumns, Array(identity("id")), catalog =
reportingCatalog)
+ sql(s"INSERT INTO $customReportingCatalogName.ns.$table VALUES " +
Review Comment:
nit. `customReportingTableName` is defined above, but this line and the
`SELECT` in `customReportingJoinDf` assemble the same string again. Could you
use the constant?
--
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]