This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 636643eb8f [core][spark] Use format partition row counts in scan
statistics (#9351)
636643eb8f is described below
commit 636643eb8f16b9ca698f22f5289e132a9886ae36
Author: Jingsong Lee <[email protected]>
AuthorDate: Sat Aug 22 17:29:07 2026 +0800
[core][spark] Use format partition row counts in scan statistics (#9351)
---
.../table/format/CatalogSplitEnumerator.java | 65 ++++++++--
.../paimon/table/format/FormatTableScan.java | 39 ++++--
.../paimon/table/format/SplitEnumerator.java | 38 ++++++
.../format/CatalogManagedPartitionScanTest.java | 23 ++++
.../paimon/spark/PaimonFormatTableScan.scala | 26 ++--
.../paimon/spark/read/PaimonStatistics.scala | 7 +-
.../sql/CatalogManagedPartitionAnalyzeTest.scala | 132 +++++++++++++++++++++
7 files changed, 306 insertions(+), 24 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/format/CatalogSplitEnumerator.java
b/paimon-core/src/main/java/org/apache/paimon/table/format/CatalogSplitEnumerator.java
index eaa43e3d69..bbfad3e836 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/format/CatalogSplitEnumerator.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/format/CatalogSplitEnumerator.java
@@ -26,6 +26,7 @@ import org.apache.paimon.fs.Path;
import org.apache.paimon.manifest.PartitionEntry;
import org.apache.paimon.partition.Partition;
import org.apache.paimon.partition.PartitionPredicate;
+import org.apache.paimon.partition.PartitionStatistics;
import org.apache.paimon.predicate.Predicate;
import org.apache.paimon.table.FormatTable;
import org.apache.paimon.table.source.Split;
@@ -49,6 +50,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.OptionalLong;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
@@ -88,8 +90,25 @@ final class CatalogSplitEnumerator extends SplitEnumerator {
@Override
List<Split> enumeratePartitions(@Nullable PartitionPredicate
partitionFilter)
throws IOException {
+ return enumeratePartitions(findCatalogPartitions(partitionFilter),
partitionFilter);
+ }
+
+ @Override
+ ScanPlan plan(@Nullable PartitionPredicate partitionFilter) throws
IOException {
+ if (table.partitionKeys().isEmpty()) {
+ return super.plan(partitionFilter);
+ }
+ List<Partition> partitions = findCatalogPartitions(partitionFilter);
+ List<PartitionEntry> entries = toPartitionEntries(partitions,
partitionFilter);
+ return new ScanPlan(enumeratePartitions(partitions, partitionFilter),
rowCount(entries));
+ }
+
+ private List<Split> enumeratePartitions(
+ List<Partition> catalogPartitions, @Nullable PartitionPredicate
partitionFilter)
+ throws IOException {
List<Pair<LinkedHashMap<String, String>, Path>> partitions =
- findPartitions(partitionFilter);
+ toSpecsAndPaths(
+ catalogPartitions,
coreOptions.formatTablePartitionOnlyValueInPath());
List<Split> splits = new ArrayList<>();
if (partitions.isEmpty()) {
return splits;
@@ -128,6 +147,12 @@ final class CatalogSplitEnumerator extends SplitEnumerator
{
@Override
List<Pair<LinkedHashMap<String, String>, Path>> findPartitions(
@Nullable PartitionPredicate partitionFilter) {
+ return toSpecsAndPaths(
+ findCatalogPartitions(partitionFilter),
+ coreOptions.formatTablePartitionOnlyValueInPath());
+ }
+
+ private List<Partition> findCatalogPartitions(@Nullable PartitionPredicate
partitionFilter) {
Optional<Predicate> extracted =
FormatTableScan.extractPartitionPredicate(partitionFilter);
Map<String, String> prefix = leadingEqualityPrefix(extracted);
Predicate catalogFilter = extracted.orElse(null);
@@ -135,15 +160,21 @@ final class CatalogSplitEnumerator extends
SplitEnumerator {
if (partitions.isEmpty() && prefix.isEmpty() && catalogFilter == null)
{
warnIfFilesystemPartitionsExist();
}
- return toSpecsAndPaths(partitions,
coreOptions.formatTablePartitionOnlyValueInPath());
+ return partitions;
}
@Override
List<PartitionEntry> listPartitionEntries() {
- List<Partition> partitions =
partitionManager.listPartitions(Collections.emptyMap(), null);
- if (partitions.isEmpty()) {
- warnIfFilesystemPartitionsExist();
- }
+ return listPartitionEntries(null);
+ }
+
+ @Override
+ List<PartitionEntry> listPartitionEntries(@Nullable PartitionPredicate
partitionFilter) {
+ return toPartitionEntries(findCatalogPartitions(partitionFilter),
partitionFilter);
+ }
+
+ private List<PartitionEntry> toPartitionEntries(
+ List<Partition> partitions, @Nullable PartitionPredicate
partitionFilter) {
boolean onlyValueInPath =
coreOptions.formatTablePartitionOnlyValueInPath();
List<PartitionEntry> entries = new ArrayList<>(partitions.size());
Set<Map<String, String>> seen = new HashSet<>(partitions.size());
@@ -151,9 +182,14 @@ final class CatalogSplitEnumerator extends SplitEnumerator
{
if (!seen.add(partition.spec())) {
continue;
}
+ BinaryRow partitionRow =
+ toPartitionRow(normalizeSpec(partition.spec(),
onlyValueInPath));
+ if (partitionFilter != null &&
!partitionFilter.test(partitionRow)) {
+ continue;
+ }
entries.add(
new PartitionEntry(
- toPartitionRow(normalizeSpec(partition.spec(),
onlyValueInPath)),
+ partitionRow,
partition.recordCount(),
partition.fileSizeInBytes(),
partition.fileCount(),
@@ -163,6 +199,21 @@ final class CatalogSplitEnumerator extends SplitEnumerator
{
return entries;
}
+ private OptionalLong rowCount(List<PartitionEntry> entries) {
+ long rowCount = 0L;
+ for (PartitionEntry entry : entries) {
+ if (!PartitionStatistics.isKnown(entry.recordCount())) {
+ return OptionalLong.empty();
+ }
+ try {
+ rowCount = Math.addExact(rowCount, entry.recordCount());
+ } catch (ArithmeticException e) {
+ return OptionalLong.empty();
+ }
+ }
+ return OptionalLong.of(rowCount);
+ }
+
private List<Pair<LinkedHashMap<String, String>, Path>> toSpecsAndPaths(
List<Partition> partitions, boolean onlyValueInPath) {
List<Pair<LinkedHashMap<String, String>, Path>> result = new
ArrayList<>(partitions.size());
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableScan.java
b/paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableScan.java
index 3f059a0e59..127535773a 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableScan.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableScan.java
@@ -58,11 +58,19 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.OptionalLong;
import java.util.Set;
/** {@link TableScan} for {@link FormatTable}. */
public class FormatTableScan implements InnerTableScan {
+ /** A format-table scan plan with statistics collected during split
planning. */
+ public interface Plan extends TableScan.Plan {
+
+ /** Returns the row count of the planned partitions, or empty if any
count is unknown. */
+ OptionalLong rowCount();
+ }
+
final FormatTable table;
final CoreOptions coreOptions;
@Nullable private PartitionPredicate partitionFilter;
@@ -93,7 +101,7 @@ public class FormatTableScan implements InnerTableScan {
@Override
public List<PartitionEntry> listPartitionEntries() {
- return splitEnumerator.listPartitionEntries();
+ return splitEnumerator.listPartitionEntries(partitionFilter);
}
@Override
@@ -204,19 +212,34 @@ public class FormatTableScan implements InnerTableScan {
}
private class FormatTableScanPlan implements Plan {
+
+ @Nullable private SplitEnumerator.ScanPlan scanPlan;
+
@Override
public List<Split> splits() {
- List<Split> splits = new ArrayList<>();
+ List<Split> splits = new ArrayList<>(scanPlan().splits());
+ // Keep all splits for a positive limit because FormatDataSplit
has no row count.
+ if (limit != null && limit <= 0) {
+ return new ArrayList<>();
+ }
+ return splits;
+ }
+
+ @Override
+ public OptionalLong rowCount() {
+ return scanPlan().rowCount();
+ }
+
+ private synchronized SplitEnumerator.ScanPlan scanPlan() {
+ if (scanPlan != null) {
+ return scanPlan;
+ }
try {
- splits.addAll(splitEnumerator.enumerate(partitionFilter));
- // Keep all splits for a positive limit because
FormatDataSplit has no row count.
- if (limit != null && limit <= 0) {
- return new ArrayList<>();
- }
+ scanPlan = splitEnumerator.plan(partitionFilter);
} catch (IOException e) {
throw new RuntimeException("Failed to scan files", e);
}
- return splits;
+ return scanPlan;
}
}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/format/SplitEnumerator.java
b/paimon-core/src/main/java/org/apache/paimon/table/format/SplitEnumerator.java
index 64c799dddf..c46919576d 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/format/SplitEnumerator.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/format/SplitEnumerator.java
@@ -44,6 +44,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.OptionalLong;
import static
org.apache.paimon.format.text.HadoopCompressionUtils.isCompressed;
import static org.apache.paimon.format.text.TextLineReader.isDefaultDelimiter;
@@ -89,6 +90,10 @@ abstract class SplitEnumerator {
return enumeratePartitions(partitionFilter);
}
+ ScanPlan plan(@Nullable PartitionPredicate partitionFilter) throws
IOException {
+ return new ScanPlan(enumerate(partitionFilter), OptionalLong.empty());
+ }
+
/** Enumerate splits for a partitioned table; partitions come from the
concrete source. */
abstract List<Split> enumeratePartitions(@Nullable PartitionPredicate
partitionFilter)
throws IOException;
@@ -98,6 +103,39 @@ abstract class SplitEnumerator {
abstract List<PartitionEntry> listPartitionEntries();
+ List<PartitionEntry> listPartitionEntries(@Nullable PartitionPredicate
partitionFilter) {
+ List<PartitionEntry> entries = listPartitionEntries();
+ if (partitionFilter == null) {
+ return entries;
+ }
+ List<PartitionEntry> filtered = new ArrayList<>();
+ for (PartitionEntry entry : entries) {
+ if (partitionFilter.test(entry.partition())) {
+ filtered.add(entry);
+ }
+ }
+ return filtered;
+ }
+
+ static final class ScanPlan {
+
+ private final List<Split> splits;
+ private final OptionalLong rowCount;
+
+ ScanPlan(List<Split> splits, OptionalLong rowCount) {
+ this.splits = splits;
+ this.rowCount = rowCount;
+ }
+
+ List<Split> splits() {
+ return splits;
+ }
+
+ OptionalLong rowCount() {
+ return rowCount;
+ }
+ }
+
BinaryRow toPartitionRow(LinkedHashMap<String, String> partitionSpec) {
RowType partitionType = table.partitionType();
GenericRow row =
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/format/CatalogManagedPartitionScanTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/format/CatalogManagedPartitionScanTest.java
index 56b7a18493..5ce899694d 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/format/CatalogManagedPartitionScanTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/format/CatalogManagedPartitionScanTest.java
@@ -54,6 +54,7 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.OptionalLong;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@@ -183,6 +184,28 @@ class CatalogManagedPartitionScanTest {
assertThat(fileIO.listedPaths).isEmpty();
}
+ @Test
+ void testPlanReusesCatalogListingForSplitsAndRowCount() throws Exception {
+ Catalog catalog = mock(Catalog.class);
+ Partition october =
+ new Partition(partition("2025", "10").spec(), 2L, 0L, 0L, 0L,
-1, false);
+ Partition november =
+ new Partition(partition("2025", "11").spec(), 3L, 0L, 0L, 0L,
-1, false);
+ when(catalog.listPartitionsPaged(eq(IDENTIFIER), eq(1000), isNull(),
isNull()))
+ .thenReturn(new PagedList<>(Arrays.asList(october, november),
null));
+ LocalFileIO fileIO = LocalFileIO.create();
+ Path tablePath = new Path(tempDir.toUri());
+ writeDataFile(fileIO, tablePath, "year=2025/month=10");
+ writeDataFile(fileIO, tablePath, "year=2025/month=11");
+ FormatTable table = createTable(fileIO, tablePath,
partitionManager(catalog), false);
+
+ FormatTableScan.Plan plan = new FormatTableScan(table, null,
null).plan();
+
+ assertThat(plan.splits()).hasSize(2);
+ assertThat(plan.rowCount()).isEqualTo(OptionalLong.of(5L));
+ verify(catalog).listPartitionsPaged(IDENTIFIER, 1000, null, null);
+ }
+
@Test
void testUnderscoreInPartitionNameRemainsLiteralPrefix() {
LocalFileIO fileIO = LocalFileIO.create();
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonFormatTableScan.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonFormatTableScan.scala
index 81c3614833..913bcb66cd 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonFormatTableScan.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonFormatTableScan.scala
@@ -20,12 +20,16 @@ package org.apache.paimon.spark
import org.apache.paimon.partition.PartitionPredicate
import org.apache.paimon.predicate.Predicate
-import org.apache.paimon.spark.read.{BaseScan, PaimonSupportsRuntimeFiltering}
+import org.apache.paimon.spark.read.{BaseScan, PaimonStatistics,
PaimonSupportsRuntimeFiltering}
import org.apache.paimon.table.FormatTable
+import org.apache.paimon.table.format.FormatTableScan
import org.apache.paimon.table.source.Split
+import org.apache.spark.sql.connector.read.Statistics
import org.apache.spark.sql.types.StructType
+import java.util.OptionalLong
+
import scala.collection.JavaConverters._
/** Scan implementation for [[FormatTable]] */
@@ -38,12 +42,20 @@ case class PaimonFormatTableScan(
extends BaseScan
with PaimonSupportsRuntimeFiltering {
+ @volatile private var plannedRowCount: OptionalLong = OptionalLong.empty()
+
protected def getInputSplits: Array[Split] = {
- readBuilder
- .newScan()
- .plan()
- .splits()
- .asScala
- .toArray
+ val plan = readBuilder.newScan().plan()
+ val splits = plan.splits().asScala.toArray
+ plannedRowCount = plan match {
+ case formatPlan: FormatTableScan.Plan => formatPlan.rowCount()
+ case _ => OptionalLong.empty()
+ }
+ splits
+ }
+
+ override def estimateStatistics: Statistics = {
+ val splits = inputSplits
+ PaimonStatistics(splits, readTableRowType, table.rowType(),
table.statistics(), plannedRowCount)
}
}
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/read/PaimonStatistics.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/read/PaimonStatistics.scala
index d0b99bcbbf..9b06223aee 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/read/PaimonStatistics.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/read/PaimonStatistics.scala
@@ -39,11 +39,14 @@ case class PaimonStatistics(
splits: Array[Split],
readRowType: RowType,
tableRowType: RowType,
- paimonStats: Optional[stats.Statistics]
+ paimonStats: Optional[stats.Statistics],
+ scanRowCount: OptionalLong = OptionalLong.empty()
) extends Statistics {
lazy val numRows: OptionalLong = {
- if (splits.exists(_.rowCount() == -1)) {
+ if (scanRowCount.isPresent) {
+ scanRowCount
+ } else if (splits.exists(_.rowCount() == -1)) {
OptionalLong.empty()
} else {
OptionalLong.of(splits.map(_.rowCount()).sum)
diff --git
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/CatalogManagedPartitionAnalyzeTest.scala
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/CatalogManagedPartitionAnalyzeTest.scala
index 89d57134f8..cd3193ab42 100644
---
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/CatalogManagedPartitionAnalyzeTest.scala
+++
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/CatalogManagedPartitionAnalyzeTest.scala
@@ -25,6 +25,8 @@ import
org.apache.paimon.spark.PaimonSparkTestWithRestCatalogBase
import org.apache.paimon.table.FormatTable
import org.apache.spark.sql.catalyst.analysis.NoSuchPartitionException
+import org.apache.spark.sql.catalyst.optimizer.BuildRight
+import org.apache.spark.sql.execution.joins.{BroadcastHashJoinExec,
SortMergeJoinExec}
import java.util.Locale
@@ -336,6 +338,136 @@ class CatalogManagedPartitionAnalyzeTest extends
PaimonSparkTestWithRestCatalogB
}
}
+ test("catalog partition row counts feed scan statistics after partition
pruning") {
+ val tableName = "analyze_scan_statistics"
+ withTable(tableName) {
+ createTable(tableName)
+ sql(s"""INSERT INTO ${qualified(tableName)} VALUES
+ |(1, 'a', '20260101', '00'),
+ |(2, 'b', '20260101', '00'),
+ |(3, 'c', '20260102', '00')
+ |""".stripMargin)
+
+ val all = getFormatTableScan(s"SELECT * FROM ${qualified(tableName)}")
+ assert(all.estimateStatistics.numRows().getAsLong == 3L)
+
+ val pruned =
+ getFormatTableScan(s"SELECT * FROM ${qualified(tableName)} WHERE dt =
'20260101'")
+ assert(pruned.estimateStatistics.numRows().getAsLong == 2L)
+ }
+ }
+
+ test("one unknown selected partition makes scan row count unknown") {
+ val tableName = "analyze_partial_scan_statistics"
+ withTable(tableName) {
+ createTable(tableName)
+ sql(
+ s"INSERT INTO ${qualified(tableName)} VALUES " +
+ s"(1, 'known', '20260101', '00')")
+ writeCsvPartition(tableName, "20260102", "00", 2)
+ repair(tableName)
+
+ val known = getFormatTableScan(s"SELECT * FROM ${qualified(tableName)}
WHERE dt = '20260101'")
+ assert(known.estimateStatistics.numRows().getAsLong == 1L)
+
+ val partiallyUnknown =
+ getFormatTableScan(s"SELECT * FROM
${qualified(tableName)}").estimateStatistics
+ assert(!partiallyUnknown.numRows().isPresent)
+ }
+ }
+
+ test("partition row count is not duplicated across format data splits") {
+ val tableName = "analyze_multi_split_statistics"
+ withTable(tableName) {
+ sql(s"""CREATE TABLE $tableName (id INT, payload STRING, dt STRING, hour
STRING)
+ |USING CSV
+ |PARTITIONED BY (dt, hour)
+ |TBLPROPERTIES (
+ | 'format-table.implementation' = 'paimon',
+ | 'metastore.partitioned-table' = 'true',
+ | 'source.split.target-size' = '1 B')
+ |""".stripMargin)
+ sql(s"""INSERT INTO ${qualified(tableName)} VALUES
+ |(1, 'one', '20260101', '00'),
+ |(2, 'two', '20260101', '00'),
+ |(3, 'three', '20260101', '00')
+ |""".stripMargin)
+
+ val scan = getFormatTableScan(s"SELECT * FROM ${qualified(tableName)}")
+ assert(scan.inputSplits.length > 1)
+ assert(scan.inputSplits.forall(_.rowCount() == -1L))
+ assert(scan.estimateStatistics.numRows().getAsLong == 3L)
+ }
+ }
+
+ test("TPC-DS-style dimension join uses partition row count to avoid
fact-side shuffle") {
+ val tableName = "date_dim_format"
+ withSparkSQLConf(
+ "spark.sql.adaptive.enabled" -> "false",
+ "spark.sql.cbo.enabled" -> "true",
+ "spark.sql.autoBroadcastJoinThreshold" -> "128",
+ "spark.sql.join.preferSortMergeJoin" -> "true"
+ ) {
+ withTable(tableName) {
+ sql(s"""CREATE TABLE $tableName (id INT, payload STRING, dt STRING,
hour STRING)
+ |USING PARQUET
+ |PARTITIONED BY (dt, hour)
+ |TBLPROPERTIES (
+ | 'format-table.implementation' = 'paimon',
+ | 'metastore.partitioned-table' = 'true')
+ |""".stripMargin)
+
+ // Keep the physical file above the broadcast threshold while the
projected dimension row
+ // is tiny. The first partition is written normally, then copied as if
an external writer
+ // had added a new date partition without reporting its row count.
+ val sparkSession = spark
+ import sparkSession.implicits._
+ val payload = new scala.util.Random(42L).alphanumeric.take(256 *
1024).mkString
+ withTempView("date_dim_rows") {
+ Seq((1, payload, "20260101", "00"))
+ .toDF("id", "payload", "dt", "hour")
+ .createOrReplaceTempView("date_dim_rows")
+ sql(s"INSERT INTO ${qualified(tableName)} SELECT * FROM
date_dim_rows")
+ }
+ copyPartitionFiles(tableName, "20260101", "20260102")
+ repair(tableName)
+
+ val query =
+ s"""SELECT store_sales.id
+ |FROM range(0, 1000000) store_sales
+ |JOIN ${qualified(tableName)} date_dim
+ | ON store_sales.id = date_dim.id
+ |WHERE date_dim.dt = '20260102' AND date_dim.hour = '00'
+ |""".stripMargin
+
+ val beforeStats = getFormatTableScan(query).estimateStatistics
+ assert(!beforeStats.numRows().isPresent)
+ val beforePlan = sql(query).queryExecution.executedPlan
+ assert(
+ beforePlan.collectFirst { case join: SortMergeJoinExec => join
}.isDefined,
+ beforePlan)
+ assert(
+ beforePlan.collectFirst { case join: BroadcastHashJoinExec => join
}.isEmpty,
+ beforePlan)
+
+ sql(
+ s"ANALYZE TABLE ${qualified(tableName)} " +
+ s"PARTITION (dt = '20260102', hour = '00') COMPUTE
STATISTICS").collect()
+
+ val afterStats = getFormatTableScan(query).estimateStatistics
+ assert(afterStats.numRows().getAsLong == 1L)
+ val afterQuery = sql(query)
+ val afterPlan = afterQuery.queryExecution.executedPlan
+ val broadcastJoin = afterPlan
+ .collectFirst { case join: BroadcastHashJoinExec => join }
+ .getOrElse(fail(afterPlan.toString))
+ .asInstanceOf[BroadcastHashJoinExec]
+ assert(broadcastJoin.buildSide == BuildRight, afterPlan)
+ checkAnswer(afterQuery, Seq(org.apache.spark.sql.Row(1L)))
+ }
+ }
+ }
+
test("ANALYZE run twice reports the same measurement rather than
accumulating") {
val tableName = "analyze_idempotent"
withTable(tableName) {