-
Notifications
You must be signed in to change notification settings - Fork 29.4k
[SPARK-59410][SQL] Derive PartitionPredicate from identity fields of a mixed partitioning #58702
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,14 +20,19 @@ package org.apache.spark.sql.internal.connector | |
| import org.apache.spark.sql.catalyst.expressions.AttributeReference | ||
|
|
||
| /** | ||
| * Metadata for one partition field. | ||
| * Metadata for one field of `Table.partitioning()`. A partition predicate is built over the | ||
| * fields in partitioning order, so their ordinals match the partition key a connector passes to | ||
| * `PartitionPredicate.eval`. | ||
| * | ||
| * @param fieldNames the multi-part field name from the table's partitioning | ||
| * (e.g. `Seq("s", "tz")`). | ||
| * @param attrRef the [[AttributeReference]] for the partition field. | ||
| * Created from the resolved partition field so it carries the | ||
| * flattened dotted name (e.g. `"s.tz"`) for nested fields. | ||
| * (e.g. `Seq("s", "tz")`) for an identity transform, or the transform's | ||
| * description (e.g. `Seq("bucket(4, id)")`) otherwise. | ||
| * @param attrRef the [[AttributeReference]] a filter can reference, for an identity transform. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: we can say 'for now Spark doesnt support'. it was in the plan but never implemented yet
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in d0d6b52. |
||
| * Created from the resolved partition field so it carries the flattened dotted | ||
| * name (e.g. `"s.tz"`) for nested fields. None for any other transform: for now | ||
| * Spark does not evaluate a filter against its partition value, so no filter | ||
| * references it, but the field keeps its ordinal. | ||
| */ | ||
| case class PartitionPredicateField( | ||
| fieldNames: Seq[String], | ||
| attrRef: AttributeReference) | ||
| attrRef: Option[AttributeReference]) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,17 +19,23 @@ package org.apache.spark.sql.connector.catalog | |
|
|
||
| import java.util | ||
|
|
||
| import scala.collection.mutable.ArrayBuffer | ||
|
|
||
| import org.apache.spark.sql.catalyst.InternalRow | ||
| import org.apache.spark.sql.catalyst.analysis.TableAlreadyExistsException | ||
| import org.apache.spark.sql.catalyst.expressions.MetadataStructFieldWithLogicalName | ||
| import org.apache.spark.sql.connector.catalog.CatalogV2Implicits.MultipartIdentifierHelper | ||
| import org.apache.spark.sql.connector.expressions.Transform | ||
| import org.apache.spark.sql.connector.expressions.filter.{PartitionPredicate, Predicate} | ||
| import org.apache.spark.sql.connector.read.{InputPartition, Scan, ScanBuilder, SupportsPushDownRequiredColumns, SupportsPushDownV2Filters} | ||
| import org.apache.spark.sql.types.StructType | ||
| import org.apache.spark.sql.util.CaseInsensitiveStringMap | ||
| import org.apache.spark.util.ArrayImplicits._ | ||
|
|
||
| /** | ||
| * In-memory table that supports row-level operations and accepts [[PartitionPredicate]]s | ||
| * in V2 [[canDeleteWhere]]/[[deleteWhere]] for metadata-only deletes. | ||
| * in V2 [[canDeleteWhere]]/[[deleteWhere]] for metadata-only deletes, and in the scan of a | ||
| * group-based UPDATE, MERGE or DELETE, which pushes V2 predicates iteratively. | ||
| * | ||
| * Contains some knobs to control acceptance of various partition and data predicates. | ||
| */ | ||
|
|
@@ -107,6 +113,72 @@ class InMemoryPartitionPredicateDeleteTable( | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Row-level scans push V2 predicates iteratively, so a group-based operation receives a | ||
| * second-pass [[PartitionPredicate]] the same way a metadata-only DELETE does. Only partition | ||
| * predicates prune, by partition key; a data predicate is always returned since the scan | ||
| * cannot filter rows. | ||
| */ | ||
| override protected def newRowLevelScanBuilder( | ||
| options: CaseInsensitiveStringMap)( | ||
| onBuild: BatchScanBaseClass => Unit): ScanBuilder = { | ||
| new PartitionPredicateRowLevelScanBuilder(onBuild) | ||
| } | ||
|
|
||
| class PartitionPredicateRowLevelScanBuilder(onBuild: BatchScanBaseClass => Unit) | ||
| extends ScanBuilder with SupportsPushDownV2Filters with SupportsPushDownRequiredColumns { | ||
|
|
||
| private var readSchema: StructType = schema | ||
| private val pushed = ArrayBuffer.empty[Predicate] | ||
|
|
||
| override def supportsIterativePushdown(): Boolean = true | ||
|
|
||
| override def pushPredicates(predicates: Array[Predicate]): Array[Predicate] = { | ||
| val (accepted, returned) = predicates.partition { | ||
| case _: PartitionPredicate => acceptPartitionPredicates | ||
| case p => refsOnlyPartCols(p) && InMemoryTableWithV2Filter.supportsPredicates(Array(p)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test-only follow-up: |
||
| } | ||
| pushed ++= accepted | ||
| returned | ||
| } | ||
|
|
||
| override def pushedPredicates(): Array[Predicate] = pushed.toArray | ||
|
|
||
| override def pruneColumns(requiredSchema: StructType): Unit = { | ||
| val metadataNames = metadataColumns.map(_.name).toSet | ||
| val schemaNames = schema.map(_.name).toSet | ||
| readSchema = StructType(requiredSchema.filter { | ||
| case MetadataStructFieldWithLogicalName(_, name) => metadataNames.contains(name) | ||
| case f => schemaNames.contains(f.name) | ||
| }) | ||
| } | ||
|
|
||
| override def build(): Scan = { | ||
| val (partPreds, stdPreds) = pushed.toArray.partition(_.isInstanceOf[PartitionPredicate]) | ||
| val partitionPredicates = partPreds.map(_.asInstanceOf[PartitionPredicate]) | ||
| val keys = InMemoryTableWithV2Filter.filtersToKeys( | ||
| data.map(_.key).toImmutableArraySeq, | ||
| partCols.map(_.toSeq.quoted).toImmutableArraySeq, | ||
| stdPreds).toSet | ||
| val partitions = data.filter { p => | ||
| keys.contains(p.key) && partitionPredicates.forall(_.eval(p.partitionKey())) | ||
| } | ||
| val scan = PartitionPredicateRowLevelBatchScan( | ||
| partitions.map(_.asInstanceOf[InputPartition]).toImmutableArraySeq, | ||
| readSchema, schema, partitionPredicates.toImmutableArraySeq) | ||
| onBuild(scan) | ||
| scan | ||
| } | ||
| } | ||
|
|
||
| /** Row-level batch scan that records the [[PartitionPredicate]]s it was pruned by. */ | ||
| case class PartitionPredicateRowLevelBatchScan( | ||
| _data: Seq[InputPartition], | ||
| readSchema: StructType, | ||
| tableSchema: StructType, | ||
| pushedPartitionPredicates: Seq[PartitionPredicate]) | ||
| extends BatchScanBaseClass(_data, readSchema, tableSchema) | ||
|
|
||
| private def rowMatchesAll( | ||
| row: InternalRow, | ||
| preds: Array[Predicate], | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -203,14 +203,19 @@ object InMemoryTableWithV2Filter { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Whether every predicate has a shape [[evalPredicate]] can evaluate: a plain column, or a | ||
| * column and a literal. A predicate over an expression, e.g. a cast, is not supported and | ||
| * returned to Spark, as a real connector without expression support would do. | ||
| */ | ||
| def supportsPredicates(predicates: Array[Predicate]): Boolean = { | ||
| predicates.flatMap(splitAnd).forall { | ||
| case p: Predicate if p.name().equals("=") => true | ||
| case p: Predicate if p.name().equals("<=>") => true | ||
| case p: Predicate if p.name().equals("IS_NULL") => true | ||
| case p: Predicate if p.name().equals("IS_NOT_NULL") => true | ||
| case p: Predicate if p.name().equals("ALWAYS_TRUE") => true | ||
| case _ => false | ||
| predicates.flatMap(splitAnd).forall { p => | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Applied as suggested in d0d6b52. |
||
| (p.name(), p.children().toSeq) match { | ||
| case ("=" | "<=>", Seq(_: NamedReference, _: LiteralValue[_])) => true | ||
| case ("IS_NULL" | "IS_NOT_NULL", Seq(_: NamedReference)) => true | ||
| case ("ALWAYS_TRUE", _) => true | ||
| case _ => false | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Row-level rewrites replace the original relation table with this wrapper, so without this delegation
getPartitionPredicateSchemasees the default empty partitioning and disables second-passPartitionPredicatepushdown. This enables UPDATE/MERGE/DELETE, including mixed partitioning.