Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions common/utils/src/main/resources/error/error-conditions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2212,6 +2212,24 @@
],
"sqlState" : "KD010"
},
"DATA_SOURCE_INVALID_RUNTIME_FILTER_ATTRIBUTE" : {
"message" : [
"The runtime filter attribute <attribute> reported by `<method>` in data source scan <scanClass> is invalid for the scan relation output <relationOutput>."
],
"subClass" : {
"CANNOT_RESOLVE" : {
"message" : [
"The attribute cannot be resolved."
]
},
"NOT_TOP_LEVEL" : {
"message" : [
"The attribute must be top-level, but it is a nested reference."
]
}
},
"sqlState" : "KD000"
},
"DATA_SOURCE_METADATA_SCHEMA_NOT_IMPLEMENTED" : {
"message" : [
"<class> does not implement metadataSchema."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,8 @@ public interface SupportsRuntimeFiltering extends SupportsRuntimeV2Filtering {
* Spark will call {@link #filter(Filter[])} if it can derive a runtime
* predicate for any of the filter attributes.
* <p>
* Each reference must be a top-level attribute present in {@link Scan#readSchema()}.
* Nested references and attributes pruned out of the read schema fail to resolve when
* Spark builds the scan relation.
* Each reference must resolve against the scan relation output when Spark builds it. Attributes
* pruned out of {@link Scan#readSchema()} fail to resolve.
*/
NamedReference[] filterAttributes();

Expand All @@ -51,6 +50,13 @@ public interface SupportsRuntimeFiltering extends SupportsRuntimeV2Filtering {
* The provided expressions must be interpreted as a set of filters that are ANDed together.
* Implementations may use the filters to prune initially planned {@link InputPartition}s.
* <p>
* Spark tracks runtime-filter eligibility by root attribute. If {@link #filterAttributes()}
* returns a nested reference, this method may receive a filter on another nested field under
* the same root. Implementations must inspect each filter and use only filters they can apply.
* Nested paths are encoded in a V1 {@link Filter} as unquoted dot-separated names such as
* {@code parent.child}. A top-level column whose name contains a dot remains quoted, such as
* {@code `parent.child`}.
* <p>
* If the scan also implements {@link SupportsReportPartitioning}, it must preserve
* the originally reported partitioning during runtime filtering. While applying runtime filters,
* the scan may detect that some {@link InputPartition}s have no matching data, in which case
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,8 @@ public interface SupportsRuntimeV2Filtering extends Scan {
* Spark will call {@link #filter(Predicate[])} if it can derive a runtime
* predicate for any of the filter attributes.
* <p>
* Each reference must be a top-level attribute present in {@link Scan#readSchema()}.
* Nested references and attributes pruned out of the read schema fail to resolve when
* Spark builds the scan relation.
* Each reference must resolve against the scan relation output when Spark builds it. Attributes
* pruned out of {@link Scan#readSchema()} fail to resolve.
*/
NamedReference[] filterAttributes();

Expand All @@ -64,6 +63,11 @@ public interface SupportsRuntimeV2Filtering extends Scan {
* The provided expressions must be interpreted as a set of predicates that are ANDed together.
* Implementations may use the predicates to prune initially planned {@link InputPartition}s.
* <p>
* Spark tracks runtime-filter eligibility by root attribute. If {@link #filterAttributes()}
* returns a nested reference, this method may receive a predicate on another nested field under
* the same root. Implementations must inspect each predicate and use only predicates they can
* apply.
* <p>
* If the scan also implements {@link SupportsReportPartitioning}, it must preserve
* the originally reported partitioning during runtime filtering. While applying runtime
* predicates, the scan may detect that some {@link InputPartition}s have no matching data, in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ object V2ExpressionUtils extends SQLConfHelper with Logging {
refs: Array[NamedReference],
output: Seq[Attribute]): AttributeSet = {
val plan = LocalRelation(output)
AttributeSet(resolveRefs[Attribute](refs.toImmutableArraySeq, plan))
AttributeSet(resolveRefs[NamedExpression](refs.toImmutableArraySeq, plan))
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4640,6 +4640,46 @@ private[sql] object QueryCompilationErrors extends QueryErrorsBase with Compilat
)
}

def cannotResolveDataSourceRuntimeFilterAttributeError(
attribute: Array[String],
method: String,
scanClass: String,
relationOutput: StructType,
cause: AnalysisException): AnalysisException = {
invalidDataSourceRuntimeFilterAttributeError(
attribute, method, scanClass, relationOutput, "CANNOT_RESOLVE", Some(cause))
}

def nestedDataSourceFullyPushedRuntimeFilterAttributeError(
attribute: Array[String],
scanClass: String,
relationOutput: StructType): AnalysisException = {
invalidDataSourceRuntimeFilterAttributeError(
attribute,
"fullyPushedFilterAttributes()",
scanClass,
relationOutput,
"NOT_TOP_LEVEL",
None)
}

private def invalidDataSourceRuntimeFilterAttributeError(
attribute: Array[String],
method: String,
scanClass: String,
relationOutput: StructType,
errorSubClass: String,
cause: Option[AnalysisException]): AnalysisException = {
new AnalysisException(
errorClass = s"DATA_SOURCE_INVALID_RUNTIME_FILTER_ATTRIBUTE.$errorSubClass",
messageParameters = Map(
"attribute" -> toSQLId(attribute.toImmutableArraySeq),
"method" -> method,
"scanClass" -> scanClass,
"relationOutput" -> toSQLType(relationOutput)),
cause = cause)
}

def foundMultipleXMLDataSourceError(provider: String,
sourceNames: Seq[String],
externalSource: String): Throwable = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,26 @@ package org.apache.spark.sql.execution.datasources.v2
import java.util.{Collections, Optional, OptionalLong}

import org.apache.spark.SparkException
import org.apache.spark.sql.AnalysisException
import org.apache.spark.sql.catalyst.analysis.{MultiInstanceRelation, NamedRelation, TimeTravelSpec}
import org.apache.spark.sql.catalyst.catalog.{CatalogColumnStat, CatalogStatistics}
import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeMap, AttributeReference, AttributeSet, Expression, SortOrder, V2ExpressionUtils}
import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeMap, AttributeReference, AttributeSet, Expression, NamedExpression, SortOrder, V2ExpressionUtils}
import org.apache.spark.sql.catalyst.plans.QueryPlan
import org.apache.spark.sql.catalyst.plans.logical.{ColumnStat, ExposesMetadataColumns, Histogram, HistogramBin, LeafNode, LogicalPlan, Statistics}
import org.apache.spark.sql.catalyst.plans.logical.statsEstimation.EstimationUtils
import org.apache.spark.sql.catalyst.streaming.{StreamingSourceIdentifyingName, Unassigned}
import org.apache.spark.sql.catalyst.types.DataTypeUtils.toAttributes
import org.apache.spark.sql.catalyst.types.DataTypeUtils.{fromAttributes, toAttributes}
import org.apache.spark.sql.catalyst.util.{removeInternalMetadata, truncatedString, CharVarcharUtils}
import org.apache.spark.sql.connector.catalog.{CatalogPlugin, FunctionCatalog, Identifier, SupportsMetadataColumns, Table, TableCapability, TableCatalog, V2TableUtil}
import org.apache.spark.sql.connector.catalog.CatalogV2Implicits.CatalogHelper
import org.apache.spark.sql.connector.expressions.{FieldReference, NamedReference}
import org.apache.spark.sql.connector.read.{Scan, Statistics => V2Statistics, SupportsReportStatistics, SupportsRuntimeV2Filtering}
import org.apache.spark.sql.connector.read.colstats.{ColumnStatistics, Histogram => V2Histogram, HistogramBin => V2HistogramBin}
import org.apache.spark.sql.connector.read.streaming.{Offset, SparkDataStream}
import org.apache.spark.sql.errors.QueryCompilationErrors
import org.apache.spark.sql.internal.connector.{SupportsRuntimeCatalystFiltering, V2StatisticsUtils}
import org.apache.spark.sql.types.{DataType, StructType}
import org.apache.spark.sql.util.CaseInsensitiveStringMap
import org.apache.spark.util.ArrayImplicits._
import org.apache.spark.util.Utils

/**
Expand Down Expand Up @@ -198,17 +199,23 @@ case class DataSourceV2ScanRelation(
* Resolved attributes that the scan declares for runtime filtering via
* [[SupportsRuntimeV2Filtering.filterAttributes]] or
* [[SupportsRuntimeCatalystFiltering.filterAttributes]]. Empty when the scan
* implements neither interface or exposes no attributes.
* implements neither interface or exposes no attributes. Accessing this value also validates
* attributes returned by [[SupportsRuntimeCatalystFiltering.fullyPushedFilterAttributes]].
*/
lazy val runtimeFilterAttrs: AttributeSet = {
checkRuntimeFilteringInterfaces()
checkFullyPushedFilterAttrs()
val filterAttrs = scan match {
case s: SupportsRuntimeV2Filtering => s.filterAttributes
case s: SupportsRuntimeCatalystFiltering => s.filterAttributes()
case _ => Array.empty[NamedReference]
}
AttributeSet(V2ExpressionUtils.resolveRefs[Attribute](
filterAttrs.toImmutableArraySeq, this))
resolveFilterAttrs(filterAttrs, "filterAttributes()")
}

private lazy val declaredFullyPushedRuntimeFilterAttrs: Array[NamedReference] = scan match {
case s: SupportsRuntimeCatalystFiltering => s.fullyPushedFilterAttributes()
case _ => Array.empty
}

/**
Expand All @@ -217,12 +224,34 @@ case class DataSourceV2ScanRelation(
*/
lazy val fullyPushedRuntimeFilterAttrs: AttributeSet = {
checkRuntimeFilteringInterfaces()
val filterAttrs = scan match {
case s: SupportsRuntimeCatalystFiltering => s.fullyPushedFilterAttributes()
case _ => Array.empty[NamedReference]
checkFullyPushedFilterAttrs()
resolveFilterAttrs(
declaredFullyPushedRuntimeFilterAttrs, "fullyPushedFilterAttributes()")
}

/**
* Resolves runtime-filter references against this relation's output.
*
* [[AttributeSet]] reduces nested references to their root attributes. This is sufficient for
* ordinary runtime-filter eligibility because Spark retains the post-scan predicate.
*/
private def resolveFilterAttrs(
filterAttrs: Array[NamedReference],
method: String): AttributeSet = {
val resolvedAttrs = filterAttrs.map { ref =>
try {
V2ExpressionUtils.resolveRef[NamedExpression](ref, this)
} catch {
case e: AnalysisException =>
throw QueryCompilationErrors.cannotResolveDataSourceRuntimeFilterAttributeError(
attribute = ref.fieldNames,
method = method,
scanClass = scan.getClass.getName,
relationOutput = fromAttributes(output),
cause = e)
}
}
AttributeSet(V2ExpressionUtils.resolveRefs[Attribute](
filterAttrs.toImmutableArraySeq, this))
AttributeSet(resolvedAttrs)
}

override def name: String = relation.name
Expand Down Expand Up @@ -271,12 +300,23 @@ case class DataSourceV2ScanRelation(
Statistics(sizeInBytes = conf.defaultSizeInBytes)
}

private def checkRuntimeFilteringInterfaces(): Unit = scan match {
case _: SupportsRuntimeV2Filtering with SupportsRuntimeCatalystFiltering =>
throw SparkException.internalError(
"A scan must not implement both SupportsRuntimeV2Filtering and " +
s"SupportsRuntimeCatalystFiltering, but ${scan.getClass.getName} implements both.")
case _ =>
private def checkRuntimeFilteringInterfaces(): Unit = {
scan match {
case _: SupportsRuntimeV2Filtering with SupportsRuntimeCatalystFiltering =>
throw SparkException.internalError(
"A scan must not implement both SupportsRuntimeV2Filtering and " +
s"SupportsRuntimeCatalystFiltering, but ${scan.getClass.getName} implements both.")
case _ =>
}
}

private def checkFullyPushedFilterAttrs(): Unit = {
declaredFullyPushedRuntimeFilterAttrs.find(_.fieldNames.length > 1).foreach { ref =>
throw QueryCompilationErrors.nestedDataSourceFullyPushedRuntimeFilterAttributeError(
attribute = ref.fieldNames,
scanClass = scan.getClass.getName,
relationOutput = fromAttributes(output))
}
}

override def doCanonicalize(): DataSourceV2ScanRelation = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,8 @@ trait SupportsRuntimeCatalystFiltering extends Scan {
* Returns attributes this scan can be filtered by at runtime.
*
* Spark will call [[filter]] if it can derive a runtime filter for any of these attributes.
* Each reference must be a top-level attribute present in [[Scan.readSchema]]. Nested
* references and attributes pruned out of the read schema fail to resolve when Spark builds
* the scan relation.
* Each reference must resolve against the scan relation output when Spark builds it. Attributes
* pruned out of [[Scan.readSchema]] fail to resolve.
*/
def filterAttributes(): Array[NamedReference]

Expand All @@ -64,8 +63,11 @@ trait SupportsRuntimeCatalystFiltering extends Scan {
* predicate over it.
*
* Each reference must be a top-level attribute present in [[Scan.readSchema]]. Nested
* references and attributes pruned out of the read schema fail to resolve when Spark builds
* the scan relation.
* references are rejected, and attributes pruned out of the read schema fail to resolve, when
* Spark builds the scan relation. Spark cannot currently represent an individual fully pushed
* nested path. A scan must not return the root struct as a substitute unless it can fully
* evaluate predicates over every nested field, since Spark would remove their post-scan
* evaluation as well.
*/
def fullyPushedFilterAttributes(): Array[NamedReference] = Array.empty

Expand All @@ -76,9 +78,9 @@ trait SupportsRuntimeCatalystFiltering extends Scan {
* Implementations may use the expressions to prune initially planned
* [[org.apache.spark.sql.connector.read.InputPartition]]s.
*
* An expression may access nested fields of an attribute returned by [[filterAttributes]], as
* that attribute is required to be top-level. The scan is responsible for matching such
* accesses against its own partition layout.
* Spark tracks runtime-filter eligibility by root attribute. If [[filterAttributes]] returns a
* nested reference, an expression may access another nested field under the same root. The scan
* must match each access against its own partition layout and use only expressions it can apply.
*
* Spark may call this method more than once for the same scan instance: a plan can hold several
* scan nodes sharing one scan (e.g. the two branches of a group-based UPDATE), and each pushes
Expand Down
Loading