This is an automated email from the ASF dual-hosted git repository.
voonhous pushed a commit to branch branch-1.2.x
in repository https://gitbox.apache.org/repos/asf/hudi.git
The following commit(s) were added to refs/heads/branch-1.2.x by this push:
new 6f8ea45f2d70 fix(variant): port #20041 to branch-1.2.x (#20160)
6f8ea45f2d70 is described below
commit 6f8ea45f2d706051373084f79fee5e859653aac9
Author: voonhous <[email protected]>
AuthorDate: Thu Oct 1 12:25:24 2026 +0800
fix(variant): port #20041 to branch-1.2.x (#20160)
On Spark 4.1, where spark.sql.variant.pushVariantIntoScan is on by
default, the record context builds its row writers from the engine
schema, which types a variant as VariantType, while the rows carry
the projection struct. A query that pushes several paths of which a
row holds few then fails with NegativeArraySizeException on a MOR
read that merges a log file and on a bootstrap read. The
partial-update merge reads the base record through the same types.
Hand-port of master 76be6c65b2c1. Three of the four files apply as
they are. SparkFileFormatInternalRowReaderContext is resolved by
hand: this branch projects log records for every merger, so the row
shape is installed whenever the query carries a projection, without
master's payload-merger exclusion.
TestVariantPushVariantIntoScan gains a wide projection and a
partial-update MERGE INTO on the top-level sweep, and a bootstrap
test. The wide projection and the bootstrap test fail without the
port; the MERGE INTO check fails with the SparkRecordMergingUtils
hunk alone left out.
---
.../apache/hudi/merge/SparkRecordMergingUtils.java | 12 ++-
.../hudi/BaseSparkInternalRecordContext.java | 44 ++++++++-
.../hudi/BaseSparkInternalRowReaderContext.java | 7 +-
.../SparkFileFormatInternalRowReaderContext.scala | 22 ++++-
.../schema/TestVariantPushVariantIntoScan.scala | 107 ++++++++++++++++++++-
5 files changed, 179 insertions(+), 13 deletions(-)
diff --git
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/merge/SparkRecordMergingUtils.java
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/merge/SparkRecordMergingUtils.java
index 29408c131dfa..ebb91f48d717 100644
---
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/merge/SparkRecordMergingUtils.java
+++
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/merge/SparkRecordMergingUtils.java
@@ -19,6 +19,7 @@
package org.apache.hudi.merge;
+import org.apache.hudi.BaseSparkInternalRecordContext;
import org.apache.hudi.HoodieSchemaConversionUtils;
import org.apache.hudi.common.engine.RecordContext;
import org.apache.hudi.common.model.HoodieRecordMerger;
@@ -32,6 +33,7 @@ import org.apache.hudi.common.util.collection.Pair;
import org.apache.spark.sql.HoodieInternalRowUtils;
import org.apache.spark.sql.catalyst.InternalRow;
import org.apache.spark.sql.catalyst.expressions.GenericInternalRow;
+import org.apache.spark.sql.types.DataType;
import org.apache.spark.sql.types.StructField;
import org.apache.spark.sql.types.StructType;
@@ -119,16 +121,22 @@ public class SparkRecordMergingUtils {
Map<Integer, StructField> mergedIdToFieldMapping =
mergedSchemaPair.getLeft();
Map<String, Integer> oldNameToIdMapping =
getCachedFieldNameToIdMapping(oldSchema);
Map<String, Integer> newPartialNameToIdMapping =
getCachedFieldNameToIdMapping(newSchema);
+ // The values are read in the shape the rows carry: under a
PushVariantIntoScan projection a variant is the
+ // projection struct, which a VariantType read would decode through
UnsafeRow.getVariant.
+ StructType mergedRowStruct = recordContext instanceof
BaseSparkInternalRecordContext
+ ? ((BaseSparkInternalRecordContext)
recordContext).getRowStructType(mergedSchemaPair.getRight().getRight())
+ : mergedSchemaPair.getRight().getLeft();
List<Object> values = new ArrayList<>(mergedIdToFieldMapping.size());
for (int fieldId = 0; fieldId < mergedIdToFieldMapping.size();
fieldId++) {
StructField structField = mergedIdToFieldMapping.get(fieldId);
+ DataType dataType = mergedRowStruct.fields()[fieldId].dataType();
Integer ordInPartialUpdate =
newPartialNameToIdMapping.get(structField.name());
if (ordInPartialUpdate != null) {
// The field exists in the newer record; picks the value from newer
record
- values.add(newPartialRow.get(ordInPartialUpdate,
structField.dataType()));
+ values.add(newPartialRow.get(ordInPartialUpdate, dataType));
} else {
// The field does not exist in the newer record; picks the value
from older record
- values.add(oldRow.get(oldNameToIdMapping.get(structField.name()),
structField.dataType()));
+ values.add(oldRow.get(oldNameToIdMapping.get(structField.name()),
dataType));
}
}
InternalRow mergedRow = new GenericInternalRow(values.toArray());
diff --git
a/hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/BaseSparkInternalRecordContext.java
b/hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/BaseSparkInternalRecordContext.java
index 6bd5420af014..53c0c85dbfa2 100644
---
a/hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/BaseSparkInternalRecordContext.java
+++
b/hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/BaseSparkInternalRecordContext.java
@@ -46,6 +46,7 @@ import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.function.UnaryOperator;
import scala.Function1;
@@ -55,6 +56,9 @@ import static
org.apache.spark.sql.HoodieInternalRowUtils.getCachedSchema;
public abstract class BaseSparkInternalRecordContext extends
RecordContext<InternalRow> {
private OrderingValueEngineTypeConverter orderingValueConverter;
+ private UnaryOperator<StructType> rowShape;
+ // The row shape applied per engine schema, so the per-record accessors do
not rebuild the overlay.
+ private final Map<HoodieSchema, StructType> rowStructTypes = new
ConcurrentHashMap<>();
protected BaseSparkInternalRecordContext(HoodieTableConfig tableConfig) {
super(tableConfig, new DefaultJavaTypeConverter());
@@ -73,7 +77,10 @@ public abstract class BaseSparkInternalRecordContext extends
RecordContext<Inter
}
private static Object getFieldValueFromInternalRowInternal(InternalRow row,
HoodieSchema recordSchema, String fieldName, boolean convertToJavaType) {
- StructType structType = getCachedSchema(recordSchema);
+ return getFieldValueFromInternalRowInternal(row,
getCachedSchema(recordSchema), fieldName, convertToJavaType);
+ }
+
+ private static Object getFieldValueFromInternalRowInternal(InternalRow row,
StructType structType, String fieldName, boolean convertToJavaType) {
scala.Option<HoodieUnsafeRowUtils.NestedFieldPath> cachedNestedFieldPath =
HoodieInternalRowUtils.getCachedPosList(structType, fieldName);
if (cachedNestedFieldPath.isDefined()) {
@@ -106,7 +113,7 @@ public abstract class BaseSparkInternalRecordContext
extends RecordContext<Inter
@Override
public Object getValue(InternalRow row, HoodieSchema schema, String
fieldName) {
- return getFieldValueFromInternalRow(row, schema, fieldName);
+ return getFieldValueFromInternalRowInternal(row, getRowStructType(schema),
fieldName, false);
}
@Override
@@ -215,14 +222,43 @@ public abstract class BaseSparkInternalRecordContext
extends RecordContext<Inter
if (internalRow instanceof UnsafeRow) {
return internalRow;
}
- final UnsafeProjection unsafeProjection =
HoodieInternalRowUtils.getCachedUnsafeProjection(schema);
+ final UnsafeProjection unsafeProjection;
+ if (rowShape == null) {
+ unsafeProjection =
HoodieInternalRowUtils.getCachedUnsafeProjection(schema);
+ } else {
+ StructType rowStructType = getRowStructType(schema);
+ unsafeProjection =
HoodieInternalRowUtils.getCachedUnsafeProjection(rowStructType, rowStructType);
+ }
return unsafeProjection.apply(internalRow);
}
+ /**
+ * Installs the Spark type the rows of an engine schema actually carry in
this read; null, the default, means the
+ * plain conversion. {@code SparkFileFormatInternalRowReaderContext}
installs the PushVariantIntoScan overlay here
+ * (see its setSchemaHandler), because every row writer and field accessor
this context builds from an engine
+ * schema has to be typed over that shape: a VariantType-typed one reads a
projection struct through
+ * UnsafeRow.getVariant instead of copying it across. That covers {@link
#projectRecord}, {@link #getValue} (the
+ * partial-update merges read the older record through it) and {@link
#toBinaryRow}; SparkRecordMergingUtils
+ * reads the merged record's fields through {@link #getRowStructType} too.
+ */
+ public void setRowShape(UnaryOperator<StructType> rowShape) {
+ this.rowShape = rowShape;
+ rowStructTypes.clear();
+ }
+
+ /**
+ * The Spark type the rows of {@code schema} carry in this read: the plain
conversion, or the row shape installed
+ * by {@link #setRowShape} when the reader hands its rows over in a
rewritten shape.
+ */
+ public StructType getRowStructType(HoodieSchema schema) {
+ StructType structType = getCachedSchema(schema);
+ return rowShape == null ? structType :
rowStructTypes.computeIfAbsent(schema, s -> rowShape.apply(structType));
+ }
+
@Override
public UnaryOperator<InternalRow> projectRecord(HoodieSchema from,
HoodieSchema to, Map<String, String> renamedColumns) {
Function1<InternalRow, UnsafeRow> unsafeRowWriter =
- HoodieInternalRowUtils.getCachedUnsafeRowWriter(getCachedSchema(from),
getCachedSchema(to), renamedColumns, Collections.emptyMap());
+
HoodieInternalRowUtils.getCachedUnsafeRowWriter(getRowStructType(from),
getRowStructType(to), renamedColumns, Collections.emptyMap());
return row -> (InternalRow) unsafeRowWriter.apply(row);
}
diff --git
a/hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/BaseSparkInternalRowReaderContext.java
b/hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/BaseSparkInternalRowReaderContext.java
index e4a34ec60acf..5cde7f35cd9b 100644
---
a/hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/BaseSparkInternalRowReaderContext.java
+++
b/hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/BaseSparkInternalRowReaderContext.java
@@ -44,7 +44,6 @@ import java.util.stream.Collectors;
import scala.Function1;
import static
org.apache.hudi.common.config.HoodieReaderConfig.RECORD_MERGE_IMPL_CLASSES_WRITE_CONFIG_KEY;
-import static org.apache.spark.sql.HoodieInternalRowUtils.getCachedSchema;
/**
* An abstract class implementing {@link HoodieReaderContext} to handle {@link
InternalRow}s.
@@ -81,6 +80,8 @@ public abstract class BaseSparkInternalRowReaderContext
extends HoodieReaderCont
/**
* Constructs a transformation that will take a row and convert it to a new
row with the given schema and adds in the values for the partition columns if
they are missing in the returned row.
* It is assumed that the `to` schema will contain the partition fields.
+ * The data-file rows arrive in the shape {@link #getFileRecordIterator}
read them in, so the writer is typed over
+ * the record context's row shape rather than over the plain engine-schema
conversion.
* @param from the original schema
* @param to the schema the row will be converted to
* @param partitionFieldAndValues the partition fields and their values, if
any are required by the reader
@@ -89,8 +90,10 @@ public abstract class BaseSparkInternalRowReaderContext
extends HoodieReaderCont
protected UnaryOperator<InternalRow> getBootstrapProjection(HoodieSchema
from, HoodieSchema to, List<Pair<String, Object>> partitionFieldAndValues) {
Map<Integer, Object> partitionValuesByIndex =
partitionFieldAndValues.stream()
.collect(Collectors.toMap(pair ->
to.getField(pair.getKey()).orElseThrow(() -> new
IllegalArgumentException("Missing field: " + pair.getKey())).pos(),
Pair::getRight));
+ BaseSparkInternalRecordContext sparkRecordContext =
(BaseSparkInternalRecordContext) recordContext;
Function1<InternalRow, UnsafeRow> unsafeRowWriter =
- HoodieInternalRowUtils.getCachedUnsafeRowWriter(getCachedSchema(from),
getCachedSchema(to), Collections.emptyMap(), partitionValuesByIndex);
+
HoodieInternalRowUtils.getCachedUnsafeRowWriter(sparkRecordContext.getRowStructType(from),
+ sparkRecordContext.getRowStructType(to), Collections.emptyMap(),
partitionValuesByIndex);
return row -> (InternalRow) unsafeRowWriter.apply(row);
}
diff --git
a/hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/SparkFileFormatInternalRowReaderContext.scala
b/hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/SparkFileFormatInternalRowReaderContext.scala
index 1429f5d5363f..a17db4883744 100644
---
a/hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/SparkFileFormatInternalRowReaderContext.scala
+++
b/hudi-client/hudi-spark-client/src/main/scala/org/apache/hudi/SparkFileFormatInternalRowReaderContext.scala
@@ -26,6 +26,7 @@ import org.apache.hudi.common.fs.FSUtils
import org.apache.hudi.common.model.{HoodieFileFormat, HoodieRecord}
import org.apache.hudi.common.schema.{HoodieSchema, HoodieSchemaUtils}
import org.apache.hudi.common.table.HoodieTableConfig
+import org.apache.hudi.common.table.read.FileGroupReaderSchemaHandler
import
org.apache.hudi.common.table.read.buffer.PositionBasedFileGroupRecordBuffer.ROW_INDEX_TEMPORARY_COLUMN_NAME
import org.apache.hudi.common.util.{Option => HOption}
import org.apache.hudi.common.util.ValidationUtils.checkState
@@ -102,15 +103,30 @@ class
SparkFileFormatInternalRowReaderContext(baseFileReader: SparkColumnarFileR
})
}
+ // Whether the query carries a Spark 4.1 PushVariantIntoScan projection at
all.
+ private lazy val hasVariantProjection: Boolean =
+ sparkRequiredSchema.exists(_.fields.exists(f =>
sparkAdapter.containsVariantProjection(f.dataType)))
+
+ // HoodieFileGroupReader installs the schema handler before it asks for the
output converter, so
+ // this is where the record context learns what shape its rows carry.
Base-file rows are read in
+ // the projected shape and getLogBlockRecordProjection rewrites log rows
into it, so every row
+ // writer the record context builds from an engine schema needs the shape
(#20040).
+ override def setSchemaHandler(schemaHandler:
FileGroupReaderSchemaHandler[InternalRow]): Unit = {
+ super.setSchemaHandler(schemaHandler)
+ if (hasVariantProjection) {
+ val requiredStruct = sparkRequiredSchema.get
+ recordContext.asInstanceOf[BaseSparkInternalRecordContext].setRowShape(
+ overlayVariantProjections(_, requiredStruct))
+ }
+ }
+
// Aligns log-block records with the PushVariantIntoScan-projected variant
shape before
// they reach the merger. Preserves merger metadata cols (_hoodie_record_key,
// _tmp_metadata_row_index) which the merger reads by ordinal — projecting
down to the
// bare required schema would drop them and the merger would read garbage
offsets.
override def getLogBlockRecordProjection(
dataBlockSchema: HoodieSchema): HOption[JFunction[InternalRow,
InternalRow]] = {
- val needsProjection =
- sparkRequiredSchema.exists(_.fields.exists(f =>
sparkAdapter.containsVariantProjection(f.dataType)))
- if (!needsProjection) {
+ if (!hasVariantProjection) {
return HOption.empty[JFunction[InternalRow, InternalRow]]()
}
val req = sparkRequiredSchema.get
diff --git
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestVariantPushVariantIntoScan.scala
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestVariantPushVariantIntoScan.scala
index 790317b5c86d..a05a1f844cda 100644
---
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestVariantPushVariantIntoScan.scala
+++
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestVariantPushVariantIntoScan.scala
@@ -18,9 +18,14 @@
package org.apache.spark.sql.hudi.dml.schema
-import org.apache.hudi.{HoodieSparkUtils, SparkAdapterSupport}
+import org.apache.hudi.{DataSourceWriteOptions, HoodieSparkUtils,
SparkAdapterSupport}
+import org.apache.hudi.common.config.HoodieMetadataConfig
import org.apache.hudi.common.model.HoodieRecord.HoodieRecordType
+import org.apache.hudi.config.{HoodieBootstrapConfig, HoodieWriteConfig}
+import org.apache.hudi.keygen.NonpartitionedKeyGenerator
+import org.apache.hudi.testutils.HoodieClientTestUtils.createMetaClient
+import org.apache.spark.sql.SaveMode
import org.apache.spark.sql.execution.FileSourceScanExec
import org.apache.spark.sql.hudi.common.HoodieSparkSqlTestBase
import org.apache.spark.sql.types.{DataType, StructType}
@@ -37,7 +42,8 @@ import scala.util.{Failure, Success, Try}
* (AVRO) and parquet (SPARK) log blocks on the MOR legs.
*
* Every leg runs to the end and its verdict is recorded, so one wrong result
does not hide the
- * others.
+ * others. The wide-projection, partial-update and bootstrap checks are the
regression tests of
+ * #20041 (row writers typed over the projected shape).
*/
class TestVariantPushVariantIntoScan extends HoodieSparkSqlTestBase {
@@ -47,6 +53,15 @@ class TestVariantPushVariantIntoScan extends
HoodieSparkSqlTestBase {
private def merged(id: Int): String = if (id < 3) s"x$id" else if (id == 3)
"y3" else null
private def mergedJson(id: Int): String = Option(merged(id)).map(k =>
s"""{"k":"$k"}""").orNull
+ // Eight pushed paths of which a row holds one, so seven members of the
projection struct are
+ // null. A row writer typed over VariantType instead of the projected shape
reads that struct's
+ // null bitset as a variant length and throws NegativeArraySizeException
(#20040).
+ private def widePaths(column: String): String =
+ (s"try_variant_get($column, '$$.k', 'string')" +:
+ ('b' to 'h').map(c => s"try_variant_get($column, '$$.$c',
'bigint')")).mkString(", ")
+
+ private def wideRow(id: Int): Seq[Any] = Seq(id, merged(id)) ++
Seq.fill(7)(null)
+
private def variantProjectionPushedIntoScan(sql: String): Boolean = {
def containsProjection(dataType: DataType): Boolean = dataType match {
case st: StructType =>
@@ -142,7 +157,21 @@ class TestVariantPushVariantIntoScan extends
HoodieSparkSqlTestBase {
// projected as a struct OF nulls instead of a NULL struct loses this
row.
checkAnswer(s"select id from $tableName where v is null")(Seq(4))
checkAnswer(s"select count(*) from $tableName where v is not
null")(Seq(4))
+ checkAnswer(s"select id, ${widePaths("v")} from $tableName order by id")(
+ (0 until 5).map(wideRow): _*)
assertPushed(s"select id, variant_get(v, '$$.k', 'string') from
$tableName", pushed, tableName)
+ if (tableType == "mor") {
+ // A MERGE INTO that assigns ts alone writes a partial log block on
MOR, so the read rebuilds
+ // id 1 field by field (SparkRecordMergingUtils.mergePartialRecords)
with v taken from the
+ // base-file record, which carries the projection struct on the pushed
arm.
+ withSQLConf("hoodie.spark.sql.merge.into.partial.updates" -> "true") {
+ spark.sql(s"merge into $tableName t using (select 1 as id, 1003L as
ts) s on t.id = s.id " +
+ "when matched then update set ts = s.ts")
+ }
+ checkAnswer(s"select id, variant_get(v, '$$.k', 'string'), ts from
$tableName where id = 1")(
+ Seq(1, "x1", 1003L))
+ checkAnswer(s"select id, ${widePaths("v")} from $tableName where id =
1")(wideRow(1))
+ }
}
}
@@ -184,4 +213,78 @@ class TestVariantPushVariantIntoScan extends
HoodieSparkSqlTestBase {
assertPushed(s"select id, variant_get(s.inner, '$$.k', 'string') from
$tableName", pushed, tableName)
}
}
+
+ test("bootstrapped tables read the projection through the skeleton and data
file join") {
+ assume(HoodieSparkUtils.gteqSpark4_1, SPARK_4_1_GATE)
+
+ // The record type is not swept: a METADATA_ONLY bootstrap on the SPARK
record type fails in the
+ // skeleton write (HUDI-5807), so every leg runs on the default record
type.
+ Seq("cow", "mor").foreach { tableType =>
+ Seq("false", "true").foreach { pushIntoScan =>
+ withSQLConf("spark.sql.variant.pushVariantIntoScan" -> pushIntoScan) {
+ withTempDir { tmp =>
+ val tableName = generateTableName
+ val leg = s"bootstrap $tableType
pushVariantIntoScan=$pushIntoScan, $tableName"
+ val srcPath = s"${tmp.getCanonicalPath}/source"
+ val tablePath = s"${tmp.getCanonicalPath}/hudi"
+ spark.sql("""select cast(id as int) as id,
parse_json(concat('{"a":', id, '}')) as v, """ +
+ "1000L as ts from range(0, 10, 1, 1)")
+ .write.parquet(srcPath)
+ val writeOpts = Map(
+ DataSourceWriteOptions.TABLE_TYPE.key ->
+ (if (tableType == "mor")
DataSourceWriteOptions.MOR_TABLE_TYPE_OPT_VAL
+ else DataSourceWriteOptions.COW_TABLE_TYPE_OPT_VAL),
+ HoodieWriteConfig.TBL_NAME.key -> tableName,
+ DataSourceWriteOptions.RECORDKEY_FIELD.key -> "id",
+ DataSourceWriteOptions.ORDERING_FIELDS.key -> "ts",
+ DataSourceWriteOptions.KEYGENERATOR_CLASS_NAME.key ->
classOf[NonpartitionedKeyGenerator].getName,
+ // col stats is not supported with bootstrap operation
+ HoodieMetadataConfig.ENABLE_METADATA_INDEX_COLUMN_STATS.key ->
"false")
+ spark.emptyDataFrame.write.format("hudi")
+ .options(writeOpts)
+ .option(DataSourceWriteOptions.OPERATION.key,
DataSourceWriteOptions.BOOTSTRAP_OPERATION_OPT_VAL)
+ .option(HoodieBootstrapConfig.BASE_PATH.key, srcPath)
+ .mode(SaveMode.Overwrite)
+ .save(tablePath)
+ assert(createMetaClient(spark,
tablePath).getTableConfig.getBootstrapBasePath.isPresent,
+ s"[$leg] table should be bootstrapped from $srcPath")
+ // COW stays read-only: an upsert would rewrite the file group
into a regular base file and
+ // drop the skeleton and data file join. On MOR the upsert lands
in a log file, so the merged
+ // read joins skeleton, source file and log.
+ if (tableType == "mor") {
+ spark.sql("""select cast(id as int) as id, case when id = 9 then
null """ +
+ """else parse_json(concat('{"a":', 100 + id, '}')) end as v,
""" +
+ "1001L as ts from range(5, 10, 1, 1)")
+ .write.format("hudi")
+ .options(writeOpts)
+ .option(DataSourceWriteOptions.OPERATION.key,
DataSourceWriteOptions.UPSERT_OPERATION_OPT_VAL)
+ .mode(SaveMode.Append)
+ .save(tablePath)
+ }
+ val view = s"${tableName}_view"
+
spark.read.format("hudi").load(tablePath).createOrReplaceTempView(view)
+
+ val projectedSql = s"select id, variant_get(v, '$$.a', 'bigint')
from $view order by id"
+ if (tableType == "mor") {
+ checkAnswer(projectedSql)(
+ (0 until 9).map(id => Seq(id, if (id < 5) id.toLong else 100L
+ id)) :+ Seq(9, null): _*)
+ } else {
+ checkAnswer(projectedSql)((0 until 10).map(id => Seq(id,
id.toLong)): _*)
+ }
+ checkAnswer(s"select _hoodie_record_key, variant_get(v, '$$.a',
'bigint') from $view " +
+ "where id in (2, 7) order by id")(
+ Seq("2", 2L), Seq("7", if (tableType == "mor") 107L else 7L))
+ checkAnswer(s"select count(*) from $view where v is null")(
+ Seq(if (tableType == "mor") 1L else 0L))
+ // A wide read over the join: seven of the eight pushed paths are
absent from the row.
+ val wide = ('a' to 'h').map(c => s"try_variant_get(v, '$$.$c',
'bigint')").mkString(", ")
+ checkAnswer(s"select _hoodie_record_key, $wide from $view where id
= 2")(
+ Seq("2", 2L) ++ Seq.fill(7)(null))
+ assertPushed(projectedSql, pushIntoScan.toBoolean, leg)
+ spark.catalog.dropTempView(view)
+ }
+ }
+ }
+ }
+ }
}