Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
/**
* A mix-in interface for {@link Scan}. Data sources can implement this interface if they can
* filter initially planned {@link InputPartition}s using predicates Spark infers at runtime.
* Only one runtime filtering interface should be implemented by a data source.
* <p>
* Note that Spark will push runtime filters only if they are beneficial.
*
Expand All @@ -38,6 +39,10 @@ public interface SupportsRuntimeFiltering extends SupportsRuntimeV2Filtering {
* <p>
* 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.
*/
NamedReference[] filterAttributes();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
* filter initially planned {@link InputPartition}s using predicates Spark infers at runtime.
* This interface is very similar to {@link SupportsRuntimeFiltering} except it uses
* data source V2 {@link Predicate} instead of data source V1 {@link Filter}.
* {@link SupportsRuntimeV2Filtering} is preferred over {@link SupportsRuntimeFiltering}
* and only one of them should be implemented by the data sources.
* {@link SupportsRuntimeV2Filtering} is preferred over {@link SupportsRuntimeFiltering}.
* Only one runtime filtering interface should be implemented by a data source.
* <p>
* <b>Iterative filtering:</b> When {@link #supportsIterativePushdown()} returns true,
* {@link #filter(Predicate[])} may be called <i>multiple times</i> on the same
Expand All @@ -50,6 +50,10 @@ public interface SupportsRuntimeV2Filtering extends Scan {
* <p>
* 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.
*/
NamedReference[] filterAttributes();

Expand Down Expand Up @@ -82,6 +86,10 @@ public interface SupportsRuntimeV2Filtering extends Scan {
* Returns the predicates that are pushed to the data source via
* {@link #filter(Predicate[])}.
* <p>
* These are not fully pushed predicates: Spark may still evaluate them after the scan.
* They are predicates that fully or partially help the data source prune initially planned
* {@link InputPartition}s.
* <p>
* When iterative filtering is supported and {@link #filter(Predicate[])} was called
* multiple times, this method must return predicates from <i>all</i> calls.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import org.apache.spark.sql.connector.expressions.{FieldReference, NamedReferenc
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.internal.connector.V2StatisticsUtils
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._
Expand Down Expand Up @@ -196,14 +196,30 @@ case class DataSourceV2ScanRelation(

/**
* Resolved attributes that the scan declares for runtime filtering via
* [[SupportsRuntimeV2Filtering.filterAttributes]]. Empty when the scan
* does not implement [[SupportsRuntimeV2Filtering]] or exposes no attributes.
* [[SupportsRuntimeV2Filtering.filterAttributes]] or
* [[SupportsRuntimeCatalystFiltering.filterAttributes]]. Empty when the scan
* implements neither interface or exposes no attributes.
*/
lazy val runtimeFilterAttrs: AttributeSet = scan match {
case s: SupportsRuntimeV2Filtering =>
AttributeSet(V2ExpressionUtils.resolveRefs[Attribute](
s.filterAttributes.toImmutableArraySeq, this))
case _ => AttributeSet.empty
lazy val runtimeFilterAttrs: AttributeSet = {
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))
}

/**
* Resolved attributes for which a Catalyst runtime-filtering scan fully evaluates predicates.
*/
lazy val fullyPushedRuntimeFilterAttrs: AttributeSet = {
val filterAttrs = scan match {
case s: SupportsRuntimeCatalystFiltering => s.fullyPushedFilterAttributes()
case _ => Array.empty[NamedReference]
}
AttributeSet(V2ExpressionUtils.resolveRefs[Attribute](

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding 6. resolveRefsV2ExpressionUtils.resolveRef throws cannotResolveAttributeError when a reference doesn't resolve against the plan's output, and casts the result to Attribute — so a nested reference, which LogicalPlan.resolve hands back as an Alias(GetStructField(...)), throws a ClassCastException. fullyPushedFilterAttributes() therefore has an unwritten requirement: top-level attributes only, and only ones that survived column pruning into the scan's readSchema. Break it and the query fails at planning time.

filterAttributes carries the same requirement and is equally undocumented, but it's forced for every scan relation, so an adopter trips it on the first query. This one is only forced when a scalar-subquery runtime filter is present (scalarSubqueryFilters.filter doesn't evaluate its closure on an empty Seq), which makes it a query-shape-dependent failure. Worth a line on the trait alongside finding 2; the new fixture quietly depends on it via the scanFields.contains(name) guard at InMemoryCatalystRuntimeFilterTable.scala:267.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented on both filterAttributes() and fullyPushedFilterAttributes(): each reference must be a top-level attribute present in readSchema, since nested references and attributes pruned out of the read schema fail to resolve when Spark builds the scan relation.

Since you noted the requirement is equally undocumented on the existing interfaces, I added the same note to SupportsRuntimeFiltering.filterAttributes() and SupportsRuntimeV2Filtering.filterAttributes(). Documentation only, no behaviour change there.

filterAttrs.toImmutableArraySeq, this))
}

override def name: String = relation.name
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.spark.sql.internal.connector

import org.apache.spark.sql.catalyst.expressions.Expression
import org.apache.spark.sql.connector.expressions.NamedReference
import org.apache.spark.sql.connector.read.Scan

/**
* A mix-in interface for [[Scan]]. Data sources can implement this interface if they can
* filter initially planned [[org.apache.spark.sql.connector.read.InputPartition]]s using
* Catalyst [[Expression]]s Spark infers at runtime.
* Only one runtime filtering interface should be implemented by a data source.
*
* Spark considers a runtime predicate fully pushed when all attributes referenced by the
* predicate are returned by [[fullyPushedFilterAttributes]]. Fully pushed predicates are not
* evaluated again after the scan.
*
* Note that Spark will push runtime filters only if they are beneficial.
*/
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.
*/
def filterAttributes(): Array[NamedReference]

/**
* Returns attributes for which this scan fully evaluates runtime predicates.
*
* Any runtime predicate that references only attributes in this set is considered fully pushed
* and will not be evaluated again after the scan. These attributes must also be returned by
* [[filterAttributes]].
*
* 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.
*/
def fullyPushedFilterAttributes(): Array[NamedReference] = Array.empty

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding 2. No objection to the attribute-level shape — v2 file sources already work this way. FileScanBuilder.pushFilters (FileScanBuilder.scala:72-95) keeps every deterministic partition filter for itself and returns only dataFilters ++ nonDeterministicFilters as post-scan filters, so "any predicate over these attributes is fully evaluated by the source" is established practice. What I'd like is for the Javadoc to say what makes it sound, since nothing in the tree implements this trait yet and two things are easy to get wrong, both silently:

  1. Exactness, not just reachability. The file-source precedent holds because partition pruning is exact — every row of a surviving file carries that partition value. Nothing restricts filterAttributes to partition columns: SupportsRuntimeV2Filtering documents it as "attributes this scan can be filtered by at runtime", and a scan may prune files or row groups by min/max statistics on a data column. Statistics-based pruning is not exact, so declaring such an attribute here returns extra rows with no error.

  2. Any shape, not the shapes you recognize. The source can't refuse an individual predicate — by the time filter() runs, DataSourceV2Strategy has already removed the FilterExec. On this head, with fully-pushed-filter-attributes='part':

    SELECT * FROM t WHERE part > (SELECT max(val) FROM dim) + 1 AND CAST(part AS STRING) RLIKE '4'
    

    leaves only Filter (isnotnull(part#341) AND RLIKE(cast(part#341 as string), 4)) above the scan; part > (2 + 1) is gone, pushed as (part#341 > (2 + 1)). A source that hand-matches operators and ignores the rest — InMemoryTableWithV2Filter.filter handles only = and IN — drops it on the floor. InMemoryEnhancedRuntimePartitionFilterTable gets it right by delegating to PartitionPredicate.eval, i.e. bind and interpret (PartitionPredicateImpl.boundPredicate), which is what the file index does too.

Something along these lines:

  /**
   * Returns attributes for which this scan fully evaluates runtime predicates.
   *
   * Any runtime predicate that references only attributes in this set is considered fully pushed
   * and will not be evaluated again after the scan. These attributes must also be returned by
   * [[filterAttributes]].
   *
   * Only declare an attribute here if this scan evaluates an arbitrary deterministic Catalyst
   * predicate over it exactly, for every row it returns -- e.g. an identity partition column,
   * whose value is known for every row of a surviving partition. Do not declare an attribute
   * whose predicates only guide approximate pruning, such as file or row-group statistics.
   * Spark may push any expression that references only these attributes, so do not assume a
   * fixed set of operators: bind and evaluate the expression (see
   * [[PartitionPredicateImpl]]) instead of pattern matching it.
   */

While you're here, it would help to name the intended implementor in the PR description — it makes the contract judgeable and tells a reader why the interface is internal.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One correction here in light of #57760: that PR adds f.deterministic to the scalarSubqueryFilters routing in DataSourceV2Strategy, so a non-deterministic filter never reaches runtimeFilters and can never be a fully-pushed candidate. My "including rand() (finding 1)" above is wrong once you rebase — the set of expressions this promise has to cover is still unbounded in shape (> with arithmetic, RLIKE, a cast chain, ...) but bounded to deterministic ones, which is what the suggested wording already says. Nothing else in this finding changes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks -- both failure modes are real, and your RLIKE example is a good demonstration that the source cannot refuse an individual predicate once FilterExec is gone.

On the wording, I landed on keeping the doc shorter. "Returns attributes for which this scan fully evaluates runtime predicates", together with "will not be evaluated again after the scan", already carries the obligation: fully evaluating a predicate means evaluating it exactly, for every row returned, whatever the predicate looks like. That rules out approximate statistics pruning and hand-matching a fixed set of operators without spelling either out. The longer text reads as implementation guidance for one particular way of satisfying the contract, and it is fairly technical for a trait Javadoc, so I would rather leave it out.

I did take the top-level attribute requirement from your finding 6, since nothing in the existing wording implies it and getting it wrong fails at planning time.

On naming the implementor, the description explains the class of source this targets -- Spark-integrated sources that already bind and interpret Catalyst expressions for partition pruning, the same ones that go through PartitionPredicateImpl -- which is also why the interface is internal.

Noted on your follow-up: after rebasing onto #57760 the set of expressions is bounded to deterministic ones, so the remaining concern is shape, which is what the current wording covers.


/**
* Filters this scan using runtime Catalyst expressions.
*
* The provided expressions must be interpreted as a set of predicates that are ANDed together.
* Implementations may use the expressions to prune initially planned
* [[org.apache.spark.sql.connector.read.InputPartition]]s.
*
* If the scan also implements
* [[org.apache.spark.sql.connector.read.SupportsReportPartitioning]], it must preserve
* the originally reported partitioning during runtime filtering. While applying runtime
* predicates, the scan may detect that some
* [[org.apache.spark.sql.connector.read.InputPartition]]s have no matching data, in which
* case it can either replace the initially planned
* [[org.apache.spark.sql.connector.read.InputPartition]]s that have no matching data with
* empty [[org.apache.spark.sql.connector.read.InputPartition]]s, or report only a subset of
* the original partition values (omitting those with no data) via
* [[org.apache.spark.sql.connector.read.Batch#planInputPartitions]]. The scan must not
* report new partition values that were not present in the original partitioning.
*
* Note that Spark will call [[Scan.toBatch]] again after filtering the scan at runtime.
*/
def filter(expressions: Array[Expression]): Unit

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding 4. SupportsRuntimeV2Filtering.filter documents the partitioning-preservation contract — "If the scan also implements SupportsReportPartitioning, it must preserve the originally reported partitioning ... The scan must not report new partition values that were not present in the original partitioning" — and PushDownUtils.replanWithRuntimeFilters enforces it for whatever scan it was handed, this interface included: it calls pushRuntimeFilters, then scan.toBatch.planInputPartitions(), then the KeyedPartitioning checks that throw SparkException on a missing HasPartitionKey, a new partition key, or a grown per-key partition count. An SPJ-active adopter reading only this Javadoc finds out from "Data source must have preserved the original partitioning during runtime filtering". Please carry that paragraph over.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, carried the paragraph over. An SPJ-active adopter reading only this Javadoc should not have to learn the contract from a SparkException thrown by replanWithRuntimeFilters.

}
Original file line number Diff line number Diff line change
Expand Up @@ -521,23 +521,35 @@ abstract class InMemoryBaseTable(
private var _pushedFilters: Array[Filter] = Array.empty

override def build: Scan = {
val scan = if (InMemoryBaseTable.this.ordering.nonEmpty) {
new InMemoryBatchScanWithOrdering(
data.map(_.asInstanceOf[InputPartition]).toImmutableArraySeq, schema, tableSchema,
options)
} else {
InMemoryBatchScan(
data.map(_.asInstanceOf[InputPartition]).toImmutableArraySeq, schema, tableSchema,
options)
}
if (evaluableFilters.nonEmpty) {
scan.filter(evaluableFilters)
val scan = createScan(
data.map(_.asInstanceOf[InputPartition]).toImmutableArraySeq, schema, tableSchema, options)
scan match {
case s: InMemoryBatchScan =>
if (evaluableFilters.nonEmpty) {
s.filter(evaluableFilters)
}
s.pushedFilters = _pushedFilters
case _ =>
}
scan.pushedFilters = _pushedFilters
recordScanEvent(_pushedFilters)
scan
}

/**
* Creates the batch scan for [[build]].
*/
protected def createScan(
partitions: Seq[InputPartition],
readSchema: StructType,
tableSchema: StructType,
options: CaseInsensitiveStringMap): BatchScanBaseClass = {
if (InMemoryBaseTable.this.ordering.nonEmpty) {
new InMemoryBatchScanWithOrdering(partitions, readSchema, tableSchema, options)
} else {
InMemoryBatchScan(partitions, readSchema, tableSchema, options)
}
}

override def pruneColumns(requiredSchema: StructType): Unit = {
// The required schema could contain conflict-renamed metadata columns, so we need to match
// them by their logical (original) names, not their current names.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.spark.sql.connector.catalog

import java.util

import scala.collection.mutable.ArrayBuffer

import InMemoryCatalystRuntimeFilterTable._

import org.apache.spark.sql.catalyst.expressions.{AttributeReference, BindReferences, Expression, Predicate => CatalystPredicate}
import org.apache.spark.sql.connector.expressions.{NamedReference, Transform}
import org.apache.spark.sql.connector.read.{InputPartition, Scan, ScanBuilder}
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.internal.connector.SupportsRuntimeCatalystFiltering
import org.apache.spark.sql.types.StructType
import org.apache.spark.sql.util.CaseInsensitiveStringMap
import org.apache.spark.util.ArrayImplicits._

/**
* In-memory table whose batch scan implements
* [[SupportsRuntimeCatalystFiltering]], so runtime filters arrive as Catalyst
* [[Expression]]s rather than connector predicates.
*
* Table properties:
* - `filter-attributes` (default: all partition cols): comma-separated list of
* column names to expose from `filterAttributes`.
* - `fully-pushed-filter-attributes` (default: none): comma-separated list of
* column names to expose from `fullyPushedFilterAttributes`.
*/
class InMemoryCatalystRuntimeFilterTable(
name: String,
columns: Array[Column],
partitioning: Array[Transform],
properties: util.Map[String, String])
extends InMemoryTableWithV2Filter(name, columns, partitioning, properties) {

override def newScanBuilder(options: CaseInsensitiveStringMap): ScanBuilder = {
new InMemoryCatalystRuntimeFilterScanBuilder(schema, options)
}

class InMemoryCatalystRuntimeFilterScanBuilder(
tableSchema: StructType,
options: CaseInsensitiveStringMap)
extends InMemoryScanBuilder(tableSchema, options) {
override def build: Scan = InMemoryCatalystRuntimeFilterBatchScan(
data.map(_.asInstanceOf[InputPartition]).toImmutableArraySeq,
schema, tableSchema, options)
}

/**
* Scan that receives runtime filters as Catalyst expressions.
* Records what was pushed and evaluates expressions that reference only
* partition columns against each partition key, so fully-pushed predicates
* are enforced when Spark drops the post-scan [[org.apache.spark.sql.execution.FilterExec]].
*/
case class InMemoryCatalystRuntimeFilterBatchScan(
var _data: Seq[InputPartition],
readSchema: StructType,
tableSchema: StructType,
options: CaseInsensitiveStringMap)
extends BatchScanBaseClass(_data, readSchema, tableSchema)
with SupportsRuntimeCatalystFiltering {

private val _catalystPredicates = ArrayBuffer.empty[Expression]

private val restrictedFilterAttrs: Option[Set[String]] =
Option(InMemoryCatalystRuntimeFilterTable.this.properties.get(FilterAttributesKey))
.map(_.split(",").map(_.trim).toSet)

override def filterAttributes(): Array[NamedReference] = {
val scanFields = readSchema.fields.map(_.name).toSet
partitioning.flatMap(_.references()).filter { ref =>
val name = ref.fieldNames.mkString(".")
scanFields.contains(name) &&
restrictedFilterAttrs.forall(_.contains(name))
}
}

override def fullyPushedFilterAttributes(): Array[NamedReference] = {
val fullyPushedFilterAttrs = Option(
InMemoryCatalystRuntimeFilterTable.this.properties.get(FullyPushedFilterAttributesKey))
.map(_.split(",").map(_.trim).toSet)
.getOrElse(Set.empty)
filterAttributes().filter { ref =>
fullyPushedFilterAttrs.contains(ref.fieldNames.mkString("."))
}
}

override def filter(expressions: Array[Expression]): Unit = {
_catalystPredicates ++= expressions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A scan that declares a filter fully pushed must ensure its returned partitions satisfy that predicate. This implementation only records the expression, while the fully-pushed test uses rows that all happen to match, so checkAnswer cannot catch an incorrect post-scan-filter removal. Please make this fixture filter its partitions (or use a dedicated fully-evaluating fixture) and test with both matching and nonmatching partitions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks. The fixture now prunes rather than just recording. filter() remaps the expression's references onto the partition attributes, and when every reference is a partition column it binds the expression and evaluates it against the partition key, dropping partitions that don't match -- the same bind-and-interpret approach as PartitionPredicateImpl, rather than pattern matching operators. An incorrect post-scan filter removal now shows up as a wrong answer.

The fully-pushed test inserts ($i, $i) for i in 0..4, so four of the five partitions don't match, and it expects a single row back.

val partAttrs = partitionAttributes
if (partAttrs.isEmpty) return

val resolver = SQLConf.get.resolver
expressions.foreach { expr =>
val remapped = expr.transform {
case a: AttributeReference =>
partAttrs.find(p => resolver(p.name, a.name)).getOrElse(a)
}
// Only evaluate expressions whose refs are all partition columns, so we can bind
// against the partition key InternalRow (same approach as PartitionPredicateImpl).
if (remapped.references.forall(r => partAttrs.exists(_.exprId == r.exprId))) {
val bound = BindReferences.bindReference(remapped, partAttrs)
val pred = CatalystPredicate.createInterpreted(bound)
data = data.filter { p =>
try {
pred.eval(p.asInstanceOf[BufferedRows].partitionKey())
} catch {
// Keep the partition on eval failure, matching PartitionPredicateImpl.
case _: Exception => true
}
}
}
}
}

/** Predicates recorded by [[filter]], for test assertions only. */
def pushedCatalystPredicates: Seq[Expression] = _catalystPredicates.toSeq

/** AttributeReferences matching the partition-key InternalRow field order. */
private def partitionAttributes: Seq[AttributeReference] = {
partitioning.flatMap(_.references()).flatMap { ref =>
val name = ref.fieldNames.mkString(".")
readSchema.find(_.name == name).orElse(tableSchema.find(_.name == name)).map { f =>
AttributeReference(f.name, f.dataType, f.nullable)()
}
}.toSeq
}
}
}

object InMemoryCatalystRuntimeFilterTable {
/**
* Table property: comma-separated column names to expose from
* filterAttributes. Default: all partition columns.
*/
private[catalog] val FilterAttributesKey = "filter-attributes"

/**
* Table property: comma-separated column names to expose from
* fullyPushedFilterAttributes. Default: none.
*/
private[catalog] val FullyPushedFilterAttributesKey = "fully-pushed-filter-attributes"
}
Loading