github-actions[bot] commented on code in PR #66421:
URL: https://github.com/apache/doris/pull/66421#discussion_r3710264289
##########
be/test/format_v2/parquet/variant_column_reader_test.cpp:
##########
@@ -776,6 +1109,273 @@ TEST(VariantColumnReaderTest,
MaterializesShreddedArrayElements) {
EXPECT_EQ(value.array_at(1).get_int(), 4);
}
+TEST(VariantColumnReaderTest, RejectsCorruptShreddedWrappersWithoutCrashing) {
+ const std::array<char, 2> int_seven {
+ static_cast<char>(static_cast<uint8_t>(VariantPrimitiveId::INT8)
+ << VARIANT_VALUE_HEADER_SHIFT),
+ 7};
+ const std::array<char, 1> invalid_value {static_cast<char>(0xff)};
+ const std::array<char, 1> ignored {0};
+ const StringRef metadata(VARIANT_EMPTY_METADATA.data(),
VARIANT_EMPTY_METADATA.size());
+ const StringRef residual_int(int_seven.data(), int_seven.size());
+ auto expect_error = [](const std::string& error, std::string_view
expected) {
+ EXPECT_NE(error.find(expected), std::string::npos) << error;
+ };
+ std::string_view current_case;
+
+ try {
+ {
+ current_case = "null metadata";
+ SCOPED_TRACE("null metadata");
+ MutableColumns fields;
+ fields.push_back(nullable_strings({metadata}, {1}));
+ fields.push_back(nullable_strings({residual_int}, {0}));
+ expect_error(
+ materialization_error(unshredded_schema(),
root_wrapper(std::move(fields))),
+ "null metadata");
+ }
+ {
+ current_case = "wrapper without carriers";
+ SCOPED_TRACE("wrapper without carriers");
+ auto schema = unshredded_schema();
+ schema.children.pop_back();
+ MutableColumns fields;
+ fields.push_back(nullable_strings({metadata}, {0}));
+ expect_error(materialization_error(schema,
root_wrapper(std::move(fields))),
+ "neither value nor typed_value");
+ }
+ {
+ current_case = "scalar with residual";
+ SCOPED_TRACE("scalar with residual");
+ MutableColumns fields;
+ fields.push_back(nullable_strings({metadata}, {0}));
+ fields.push_back(nullable_strings({residual_int}, {0}));
+ fields.push_back(nullable_int64({8}, {0}));
+ expect_error(
+ materialization_error(shredded_int64_schema(),
root_wrapper(std::move(fields))),
+ "scalar typed_value cannot have residual");
+ }
+ {
+ current_case = "object with scalar residual";
+ SCOPED_TRACE("object with scalar residual");
+ MutableColumns fields;
+ fields.push_back(nullable_strings({metadata}, {0}));
+ fields.push_back(nullable_strings({residual_int}, {0}));
+ MutableColumns wrapper_fields;
+ wrapper_fields.push_back(nullable_int64({9}, {0}));
+ MutableColumns object_fields;
+ object_fields.push_back(ColumnNullable::create(
+ ColumnStruct::create(std::move(wrapper_fields)),
ColumnUInt8::create(1, 0)));
+
fields.push_back(ColumnNullable::create(ColumnStruct::create(std::move(object_fields)),
+ ColumnUInt8::create(1,
0)));
+ expect_error(materialization_error(shredded_object_schema(),
+
root_wrapper(std::move(fields))),
+ "non-object residual");
+ }
+ {
+ current_case = "object field count mismatch";
+ SCOPED_TRACE("object field count mismatch");
+ MutableColumns fields;
+ fields.push_back(nullable_strings({metadata}, {0}));
+ fields.push_back(nullable_strings({{ignored.data(), 0}}, {1}));
+ MutableColumns unexpected_object_fields;
+ unexpected_object_fields.push_back(nullable_int64({1}, {0}));
+ unexpected_object_fields.push_back(nullable_int64({2}, {0}));
+ fields.push_back(ColumnNullable::create(
+ ColumnStruct::create(std::move(unexpected_object_fields)),
+ ColumnUInt8::create(1, 0)));
+ expect_error(materialization_error(shredded_object_schema(),
+
root_wrapper(std::move(fields))),
+ "physical field count mismatch");
+ }
+ {
+ current_case = "array with residual";
+ SCOPED_TRACE("array with residual");
+ MutableColumns empty_wrapper_fields;
+ empty_wrapper_fields.push_back(nullable_int64({}, {}));
+ auto empty_elements = ColumnNullable::create(
+ ColumnStruct::create(std::move(empty_wrapper_fields)),
ColumnUInt8::create());
+ auto offsets = ColumnArray::ColumnOffsets::create();
+ offsets->insert_value(0);
+ MutableColumns fields;
+ fields.push_back(nullable_strings({metadata}, {0}));
+ fields.push_back(nullable_strings({residual_int}, {0}));
+ fields.push_back(ColumnNullable::create(
+ ColumnArray::create(std::move(empty_elements),
std::move(offsets)),
+ ColumnUInt8::create(1, 0)));
+ expect_error(
+ materialization_error(shredded_array_schema(),
root_wrapper(std::move(fields))),
+ "array typed_value cannot have residual");
+ }
+ {
+ current_case = "null array element wrapper";
+ SCOPED_TRACE("null array element wrapper");
+ MutableColumns wrapper_fields;
+ wrapper_fields.push_back(nullable_int64({0}, {1}));
+ auto wrappers = ColumnStruct::create(std::move(wrapper_fields));
+ auto elements = ColumnNullable::create(std::move(wrappers),
ColumnUInt8::create(1, 1));
+ auto offsets = ColumnArray::ColumnOffsets::create();
+ offsets->insert_value(1);
+ MutableColumns fields;
+ fields.push_back(nullable_strings({metadata}, {0}));
+ fields.push_back(nullable_strings({{ignored.data(), 0}}, {1}));
+ fields.push_back(ColumnNullable::create(
+ ColumnArray::create(std::move(elements),
std::move(offsets)),
+ ColumnUInt8::create(1, 0)));
+ expect_error(
+ materialization_error(shredded_array_schema(),
root_wrapper(std::move(fields))),
+ "array element wrapper is null");
+ }
+ {
+ current_case = "missing array element";
+ SCOPED_TRACE("missing array element");
+ MutableColumns element_fields;
+ element_fields.push_back(nullable_strings({{ignored.data(), 0}},
{1}));
+ element_fields.push_back(nullable_int64({0}, {1}));
+ auto elements =
ColumnNullable::create(ColumnStruct::create(std::move(element_fields)),
+ ColumnUInt8::create(1, 0));
+ auto offsets = ColumnArray::ColumnOffsets::create();
+ offsets->insert_value(1);
+ MutableColumns fields;
+ fields.push_back(nullable_strings({metadata}, {0}));
+ fields.push_back(nullable_strings({{ignored.data(), 0}}, {1}));
+ fields.push_back(ColumnNullable::create(
+ ColumnArray::create(std::move(elements),
std::move(offsets)),
+ ColumnUInt8::create(1, 0)));
+ expect_error(materialization_error(shredded_mixed_array_schema(),
+
root_wrapper(std::move(fields))),
+ "array element is missing");
+ }
+ {
+ current_case = "root field count mismatch";
+ SCOPED_TRACE("root field count mismatch");
+ MutableColumns fields;
+ fields.push_back(nullable_strings({metadata}, {0}));
+ fields.push_back(nullable_strings({residual_int}, {0}));
+ fields.push_back(nullable_int64({8}, {0}));
+ fields.push_back(nullable_int64({9}, {0}));
+ expect_error(
+ materialization_error(shredded_int64_schema(),
root_wrapper(std::move(fields))),
+ "physical field count mismatch");
+ }
+ {
+ current_case = "invalid metadata";
+ SCOPED_TRACE("invalid metadata");
+ MutableColumns fields;
+ fields.push_back(nullable_strings({StringRef("bad")}, {0}));
+ fields.push_back(nullable_strings({residual_int}, {0}));
+ expect_error(
+ materialization_error(unshredded_schema(),
root_wrapper(std::move(fields))),
+ "metadata");
+ }
+ {
+ current_case = "invalid residual value";
+ SCOPED_TRACE("invalid residual value");
+ MutableColumns fields;
+ fields.push_back(nullable_strings({metadata}, {0}));
+ fields.push_back(nullable_strings({{invalid_value.data(),
invalid_value.size()}}, {0}));
+ expect_error(
+ materialization_error(unshredded_schema(),
root_wrapper(std::move(fields))),
+ "Variant");
+ }
+ } catch (const std::exception& error) {
+ FAIL() << "Unexpected exception in " << current_case << ": " <<
error.what();
+ }
+}
+
+TEST(VariantColumnReaderTest, ImmediateCorruptionLeavesDestinationUnchanged) {
+ auto output =
make_nullable(std::make_shared<DataTypeVariantV2>())->create_column();
+ ASSERT_TRUE(
+ materialize_variant_rows(shredded_int64_schema(),
shredded_int64_physical({7}), output)
+ .ok());
+ MutableColumns invalid_fields;
+ invalid_fields.push_back(nullable_strings(
+ {{VARIANT_EMPTY_METADATA.data(), VARIANT_EMPTY_METADATA.size()}},
{0}));
+ const Status status = materialize_variant_rows(shredded_int64_schema(),
Review Comment:
[P1] Exercise the lazy append failure in this atomicity test
This malformed batch has only `metadata`, so `ParquetVariantShreddedState`
rejects its field count before `append_materialized_column()` mutates anything.
It therefore cannot catch the dangerous nested case: compatibility validation
accepts a Variant child without materializing it, STRUCT append mutates earlier
siblings in order, and a later incompatible/corrupt shredded Variant can throw
during `insert_range_from`, leaving those siblings grown despite the error
Status. Please add a pre-populated STRUCT/LIST/MAP case with a primitive
sibling before the Variant and corruption that is discovered only during lazy
fallback, then assert every child/null-map/offset remains unchanged (and fix
the transactional append path if it fails).
##########
regression-test/suites/external_table_p0/iceberg/test_iceberg_variant_read.groovy:
##########
@@ -337,44 +586,167 @@ suite("test_iceberg_variant_read",
ORDER BY id
"""
- // Keep the root Variant as output while the implicit scalar comparison
drives the shredded
- // typed_value statistics/page-index path.
- order_qt_variant_implicit_shredded_filter """
+ order_qt_variant_multi_file_serial """
+ SELECT id,
+ CAST(v['shared'] AS INT),
+ CAST(v['a'] AS INT),
+ CAST(v['b'] AS INT),
+ CAST(v['new_field']['k'] AS INT),
+ CAST(v AS STRING)
+ FROM variant_multi_file
+ WHERE v['shared'] >= 20
+ ORDER BY id
+ """
+ sql "set parallel_pipeline_task_num=4"
Review Comment:
[P2] Prove that the parallel case actually uses multiple scanners
These settings only raise concurrency ceilings;
`min_file_scanners_concurrency` remains 1, so the query may run serially and
still match the byte-for-byte serial result above. That leaves cross-file state
races uncovered. Force a minimum concurrency greater than one, assert the
fixture has multiple data files, and bind this query to a completed profile
showing actual concurrent/non-empty scanners rather than only a configured
maximum.
##########
regression-test/suites/external_table_p0/iceberg/test_iceberg_variant_read.groovy:
##########
@@ -219,6 +437,25 @@ suite("test_iceberg_variant_read",
}
return sum
}
+ def getProfileByToken = { String token, List<String> positiveCounters = []
->
+ String lastProfile = ""
+ for (int retry = 0; retry < 20; ++retry) {
+ List profileData = profileAction.getProfileList()
Review Comment:
[P1] Use the completion-aware profile waiter
This custom loop gives asynchronously published scanner counters only 10
seconds and never requires the profile list/detail to be COMPLETE.
`ProfileAction.getProfileBySql` already provides a completion-aware 60-second
wait, and the existing DV suite explicitly notes that detailed scanner counters
arrive after query return. On a loaded external CI run these new multi-counter
calls can fail even though the query and final profile are correct. Please use
the framework waiter (then poll positivity if needed) without shortening its
established timeout.
##########
regression-test/suites/external_table_p0/iceberg/test_iceberg_variant_read.groovy:
##########
@@ -100,6 +133,115 @@ suite("test_iceberg_variant_read",
(10,
parse_json('{"name":"dave","n":50,"ratio":5.5,"ok":false,"arr":[7,8],"nested":{"city":"sz"}}')),
(11,
parse_json('{"name":null,"n":60,"ratio":6.5,"ok":true,"arr":[9,10],"nested":{"city":null}}'));
+ DROP TABLE IF EXISTS demo.${dbName}.variant_root_arrays;
+ CREATE TABLE demo.${dbName}.variant_root_arrays (id INT, v VARIANT)
USING iceberg
+ TBLPROPERTIES (
+ 'format-version'='3',
+ 'write.format.default'='parquet',
+ 'write.parquet.shred-variants'='true',
+ 'write.parquet.variant-inference-buffer-size'='100'
+ );
+ INSERT INTO demo.${dbName}.variant_root_arrays VALUES
+ (1, parse_json('[]')),
+ (2, parse_json('[null,1,{"x":2},[3,4],"tail"]')),
+ (3, parse_json('[{"nested":[null,{"y":5}]}]')),
+ (4, parse_json('null')),
+ (5, NULL);
+
+ DROP TABLE IF EXISTS demo.${dbName}.variant_multi_file;
+ CREATE TABLE demo.${dbName}.variant_multi_file (id INT, v VARIANT)
USING iceberg
+ TBLPROPERTIES (
+ 'format-version'='3',
+ 'write.format.default'='parquet',
+ 'write.parquet.shred-variants'='false'
+ );
+ INSERT INTO demo.${dbName}.variant_multi_file
+ VALUES (1, parse_json('{"a":1,"shared":10}'));
+ ALTER TABLE demo.${dbName}.variant_multi_file SET TBLPROPERTIES (
+ 'write.parquet.shred-variants'='true',
+ 'write.parquet.variant-inference-buffer-size'='1'
+ );
+ INSERT INTO demo.${dbName}.variant_multi_file
+ VALUES (2, parse_json('{"b":2,"shared":20,"z":200}'));
+ INSERT INTO demo.${dbName}.variant_multi_file
+ VALUES (3, parse_json('{"z":300,"shared":30,"a":3}'));
+ ALTER TABLE demo.${dbName}.variant_multi_file SET TBLPROPERTIES
+ ('write.parquet.shred-variants'='false');
+ INSERT INTO demo.${dbName}.variant_multi_file
+ VALUES (4, parse_json('{"c":4,"shared":40}'));
+ ALTER TABLE demo.${dbName}.variant_multi_file SET TBLPROPERTIES
+ ('write.parquet.shred-variants'='true');
+ INSERT INTO demo.${dbName}.variant_multi_file
+ VALUES (5,
parse_json('{"shared":50,"b":5,"new_field":{"k":500}}'));
+
+ DROP TABLE IF EXISTS demo.${dbName}.variant_type_matrix;
+ CREATE TABLE demo.${dbName}.variant_type_matrix (id INT, v VARIANT)
USING iceberg
+ TBLPROPERTIES (
+ 'format-version'='3',
+ 'write.format.default'='parquet',
+ 'write.parquet.shred-variants'='true',
+ 'write.parquet.variant-inference-buffer-size'='100'
+ );
+ INSERT INTO demo.${dbName}.variant_type_matrix SELECT 1,
to_variant_object(named_struct(
+ 'bool_value', true,
+ 'tiny_value', CAST(-128 AS TINYINT),
+ 'small_value', CAST(-32768 AS SMALLINT),
+ 'int_value', CAST(2147483647 AS INT),
+ 'big_value', CAST('-9223372036854775808' AS BIGINT),
+ 'float_value', CAST('NaN' AS FLOAT),
+ 'double_value', CAST('Infinity' AS DOUBLE),
+ 'decimal_value', CAST('-1234567890.1234' AS DECIMAL(20, 4)),
+ 'date_value', CAST('1970-01-02' AS DATE),
+ 'timestamp_value', TIMESTAMP'1970-01-01 00:00:01.234567',
+ 'binary_value', CAST('binary' AS BINARY),
+ 'null_value', CAST(NULL AS INT)
+ ));
+
+ DROP TABLE IF EXISTS demo.${dbName}.variant_multi_row_group;
+ CREATE TABLE demo.${dbName}.variant_multi_row_group (id INT, v
VARIANT) USING iceberg
+ TBLPROPERTIES (
+ 'format-version'='3',
+ 'write.format.default'='parquet',
+ 'write.parquet.shred-variants'='true',
Review Comment:
[P1] Make this fixture's physical Variant mode explicit
This table requests `write.parquet.shred-variants=true`, but the checks
below require `VariantDirectLeafPathMisses` and reconstruction and call the
file unshredded. In the repository's pinned Iceberg 1.10.1 Spark runtime the
property is not defined in
[TableProperties](https://github.com/apache/iceberg/blob/apache-iceberg-1.10.1/core/src/main/java/org/apache/iceberg/TableProperties.java),
so the test passes only because it is ignored; a writer that honors it will
shred `n` and make the fallback profile time out. The other new Spark-generated
`true` fixtures likewise do not prove shredded decoding under the current pin.
Set `false` for fallback fixtures, and use a capable/checked physical fixture
plus a schema or direct-leaf oracle for cases intended to cover shredding.
##########
regression-test/suites/external_table_p0/iceberg/test_iceberg_variant_read.groovy:
##########
@@ -337,44 +586,167 @@ suite("test_iceberg_variant_read",
ORDER BY id
"""
- // Keep the root Variant as output while the implicit scalar comparison
drives the shredded
- // typed_value statistics/page-index path.
- order_qt_variant_implicit_shredded_filter """
+ order_qt_variant_multi_file_serial """
+ SELECT id,
+ CAST(v['shared'] AS INT),
+ CAST(v['a'] AS INT),
+ CAST(v['b'] AS INT),
+ CAST(v['new_field']['k'] AS INT),
+ CAST(v AS STRING)
+ FROM variant_multi_file
+ WHERE v['shared'] >= 20
+ ORDER BY id
+ """
+ sql "set parallel_pipeline_task_num=4"
+ sql "set max_file_scanners_concurrency=8"
+ order_qt_variant_multi_file_parallel """
+ SELECT id,
+ CAST(v['shared'] AS INT),
+ CAST(v['a'] AS INT),
+ CAST(v['b'] AS INT),
+ CAST(v['new_field']['k'] AS INT),
+ CAST(v AS STRING)
+ FROM variant_multi_file
+ WHERE v['shared'] >= 20
+ ORDER BY id
+ """
+
+ order_qt_variant_type_matrix """
+ SELECT CAST(v['bool_value'] AS BOOLEAN),
+ CAST(v['tiny_value'] AS TINYINT),
+ CAST(v['small_value'] AS SMALLINT),
+ CAST(v['int_value'] AS INT),
+ CAST(v['big_value'] AS BIGINT),
+ ISNAN(CAST(v['float_value'] AS FLOAT)),
+ ISINF(CAST(v['double_value'] AS DOUBLE)),
+ CAST(v['decimal_value'] AS DECIMAL(20, 4)),
+ CAST(v['date_value'] AS DATE),
+ CAST(v['timestamp_value'] AS DATETIMEV2(6)),
+ CAST(v['binary_value'] AS STRING),
+ v['null_value'] IS NULL
+ FROM variant_type_matrix
+ """
+
+ String multiRowGroupColdToken =
+ "iceberg_variant_multi_row_group_cold_" +
UUID.randomUUID().toString()
+ sql """
+ SELECT '${multiRowGroupColdToken}', COUNT(*), MIN(id), MAX(id)
+ FROM variant_multi_row_group
+ WHERE CAST(v['n'] AS INT) >= 8000
+ """
+ String multiRowGroupColdProfile = getProfileByToken(multiRowGroupColdToken,
+ ["RowGroupsTotalNum", "VariantDirectLeafPathMisses",
"VariantReconstructedRows",
+ "FilteredRowsByLazyRead"]).toString()
+ assertTrue(counterSum(multiRowGroupColdProfile, "RowGroupsTotalNum") > 1,
+ "The generated Variant file did not contain multiple Parquet
row groups")
+ assertTrue(counterSum(multiRowGroupColdProfile,
"VariantDirectLeafPathMisses") > 0,
+ "The unshredded scan did not record its direct-leaf fallback")
+ assertTrue(counterSum(multiRowGroupColdProfile,
"VariantReconstructedRows") > 0,
+ "The unshredded scan did not reconstruct Variant rows")
+ assertTrue(counterSum(multiRowGroupColdProfile, "FilteredRowsByLazyRead")
> 0,
+ "The unshredded Variant predicate did not defer non-predicate
columns")
+ String multiRowGroupWarmToken =
+ "iceberg_variant_multi_row_group_warm_" +
UUID.randomUUID().toString()
+ sql """
+ SELECT '${multiRowGroupWarmToken}', COUNT(*), MIN(id), MAX(id)
+ FROM variant_multi_row_group
+ WHERE CAST(v['n'] AS INT) >= 8000
+ """
+ String multiRowGroupWarmProfile = getProfileByToken(multiRowGroupWarmToken,
+ ["VariantDirectLeafPathMisses"]).toString()
+ assertTrue(counterSum(multiRowGroupWarmProfile,
"VariantDirectLeafPathMisses") > 0,
+ "The warm unshredded scan did not preserve its direct-leaf
fallback")
+ qt_variant_multi_row_group_result """
+ SELECT COUNT(*), MIN(id), MAX(id), SUM(CAST(v['n'] AS BIGINT))
+ FROM variant_multi_row_group
+ WHERE CAST(v['n'] AS INT) >= 8000
+ """
+
+ qt_variant_deletion_vector_current """
+ SELECT COUNT(*), MIN(id), MAX(id), SUM(CAST(v['n'] AS BIGINT))
+ FROM variant_deletion_vector
+ WHERE v['keep'] = true
+ """
+ qt_variant_deletion_vector_before_delete """
+ SELECT COUNT(*), MIN(id), MAX(id), SUM(CAST(v['n'] AS BIGINT))
+ FROM variant_deletion_vector FOR VERSION AS OF
${deletionVectorBaseSnapshot}
+ WHERE v['n'] >= 0
+ """
+ order_qt_variant_equality_delete_current """
+ SELECT id, CAST(v['n'] AS INT), CAST(v['label'] AS STRING), CAST(v AS
STRING)
+ FROM variant_equality_delete
+ WHERE v['n'] >= 0
+ ORDER BY id
+ """
+ order_qt_variant_equality_delete_before_delete """
+ SELECT id, CAST(v['n'] AS INT), CAST(v['label'] AS STRING), CAST(v AS
STRING)
+ FROM variant_equality_delete FOR VERSION AS OF
${equalityDeleteBaseSnapshot}
+ WHERE v['n'] >= 0
+ ORDER BY id
+ """
+
+ // Keep the root Variant as output while the scalar comparison exercises
the fallback path for
+ // the unshredded Spark files.
+ order_qt_variant_implicit_filter """
SELECT id, CAST(v AS STRING)
FROM variant_values
WHERE v['n'] > 35
ORDER BY id
"""
- // The query projects the root Variant, while the predicate uses
typed_value page metadata.
+ qt_variant_shredded_only_time_travel """
+ SELECT COUNT(*), MIN(id), MAX(id), SUM(CAST(v['n'] AS BIGINT))
+ FROM variant_page_pruning FOR VERSION AS OF ${shreddedOnlySnapshot}
+ WHERE CAST(v['n'] AS INT) > 3000
+ """
+ qt_variant_mixed_before_delete """
+ SELECT COUNT(*), MIN(id), MAX(id), SUM(CAST(v['n'] AS BIGINT))
+ FROM variant_page_pruning FOR VERSION AS OF
${mixedBeforeDeleteSnapshot}
+ WHERE CAST(v['n'] AS INT) > 3000
+ """
+
+ // The query projects the complete Variant while its predicate reads the
shredded typed leaf.
+ // The appended unshredded file must fall back independently in the same
scan.
String pagePruningToken = "iceberg_variant_page_pruning_" +
UUID.randomUUID().toString()
sql """
SELECT '${pagePruningToken}', id, CAST(v AS STRING)
FROM variant_page_pruning
- WHERE v['n'] > 3000
+ WHERE CAST(v['n'] AS INT) > 3000
ORDER BY id
"""
- String pagePruningProfile = getProfileByToken(pagePruningToken).toString()
+ String pagePruningProfile = getProfileByToken(pagePruningToken,
+ ["FilteredRowsByPage", "VariantLeafProjections",
"VariantDirectLeafPathMisses",
+ "VariantDirectLeafRows", "VariantReconstructedRows",
+ "FilteredRowsByLazyRead"]).toString()
assertTrue(counterSum(pagePruningProfile, "FilteredRowsByPage") > 0,
"Shredded Variant typed_value did not filter any Parquet page")
// The predicate_access_paths contract keeps the typed leaf eager while
the complete Variant
// root is read through the independent deferred-output projection.
assertTrue(counterSum(pagePruningProfile, "VariantLeafProjections") > 0,
"A root Variant output query did not retain its typed predicate
leaf projection")
+ assertTrue(counterSum(pagePruningProfile, "VariantDirectLeafPathMisses") >
0,
+ "The mixed scan did not fall back for its unshredded Variant
file")
+ assertTrue(counterSum(pagePruningProfile, "VariantDirectLeafRows") > 0,
+ "The mixed scan did not evaluate rows from the shredded typed
leaf")
+ assertTrue(counterSum(pagePruningProfile, "VariantReconstructedRows") > 0,
+ "The mixed scan did not reconstruct complete Variant output")
+ assertTrue(counterSum(pagePruningProfile, "FilteredRowsByLazyRead") > 0,
Review Comment:
[P1] Make delayed materialization specific to the Variant output
`FilteredRowsByLazyRead` is incremented whenever any non-predicate column is
deferred. Because this query also outputs `id`, the counter stays positive if
only `id` is lazy while the complete Variant root regresses to eager
reconstruction; `VariantReconstructedRows > 0` does not distinguish that case.
Assert a Variant-specific relationship (for example reconstructed rows equal
selected/output rows and are below pre-filter candidates), or add/use a
Variant-specific deferred-materialization counter.
##########
regression-test/suites/external_table_p0/iceberg/test_iceberg_variant_read.groovy:
##########
@@ -337,44 +586,167 @@ suite("test_iceberg_variant_read",
ORDER BY id
"""
- // Keep the root Variant as output while the implicit scalar comparison
drives the shredded
- // typed_value statistics/page-index path.
- order_qt_variant_implicit_shredded_filter """
+ order_qt_variant_multi_file_serial """
+ SELECT id,
+ CAST(v['shared'] AS INT),
+ CAST(v['a'] AS INT),
+ CAST(v['b'] AS INT),
+ CAST(v['new_field']['k'] AS INT),
+ CAST(v AS STRING)
+ FROM variant_multi_file
+ WHERE v['shared'] >= 20
+ ORDER BY id
+ """
+ sql "set parallel_pipeline_task_num=4"
+ sql "set max_file_scanners_concurrency=8"
+ order_qt_variant_multi_file_parallel """
+ SELECT id,
+ CAST(v['shared'] AS INT),
+ CAST(v['a'] AS INT),
+ CAST(v['b'] AS INT),
+ CAST(v['new_field']['k'] AS INT),
+ CAST(v AS STRING)
+ FROM variant_multi_file
+ WHERE v['shared'] >= 20
+ ORDER BY id
+ """
+
+ order_qt_variant_type_matrix """
+ SELECT CAST(v['bool_value'] AS BOOLEAN),
+ CAST(v['tiny_value'] AS TINYINT),
+ CAST(v['small_value'] AS SMALLINT),
+ CAST(v['int_value'] AS INT),
+ CAST(v['big_value'] AS BIGINT),
+ ISNAN(CAST(v['float_value'] AS FLOAT)),
+ ISINF(CAST(v['double_value'] AS DOUBLE)),
+ CAST(v['decimal_value'] AS DECIMAL(20, 4)),
+ CAST(v['date_value'] AS DATE),
+ CAST(v['timestamp_value'] AS DATETIMEV2(6)),
+ CAST(v['binary_value'] AS STRING),
+ v['null_value'] IS NULL
+ FROM variant_type_matrix
+ """
+
+ String multiRowGroupColdToken =
+ "iceberg_variant_multi_row_group_cold_" +
UUID.randomUUID().toString()
+ sql """
+ SELECT '${multiRowGroupColdToken}', COUNT(*), MIN(id), MAX(id)
+ FROM variant_multi_row_group
+ WHERE CAST(v['n'] AS INT) >= 8000
+ """
+ String multiRowGroupColdProfile = getProfileByToken(multiRowGroupColdToken,
+ ["RowGroupsTotalNum", "VariantDirectLeafPathMisses",
"VariantReconstructedRows",
+ "FilteredRowsByLazyRead"]).toString()
+ assertTrue(counterSum(multiRowGroupColdProfile, "RowGroupsTotalNum") > 1,
+ "The generated Variant file did not contain multiple Parquet
row groups")
+ assertTrue(counterSum(multiRowGroupColdProfile,
"VariantDirectLeafPathMisses") > 0,
+ "The unshredded scan did not record its direct-leaf fallback")
+ assertTrue(counterSum(multiRowGroupColdProfile,
"VariantReconstructedRows") > 0,
+ "The unshredded scan did not reconstruct Variant rows")
+ assertTrue(counterSum(multiRowGroupColdProfile, "FilteredRowsByLazyRead")
> 0,
+ "The unshredded Variant predicate did not defer non-predicate
columns")
+ String multiRowGroupWarmToken =
+ "iceberg_variant_multi_row_group_warm_" +
UUID.randomUUID().toString()
+ sql """
+ SELECT '${multiRowGroupWarmToken}', COUNT(*), MIN(id), MAX(id)
+ FROM variant_multi_row_group
+ WHERE CAST(v['n'] AS INT) >= 8000
+ """
+ String multiRowGroupWarmProfile = getProfileByToken(multiRowGroupWarmToken,
+ ["VariantDirectLeafPathMisses"]).toString()
+ assertTrue(counterSum(multiRowGroupWarmProfile,
"VariantDirectLeafPathMisses") > 0,
+ "The warm unshredded scan did not preserve its direct-leaf
fallback")
+ qt_variant_multi_row_group_result """
+ SELECT COUNT(*), MIN(id), MAX(id), SUM(CAST(v['n'] AS BIGINT))
+ FROM variant_multi_row_group
+ WHERE CAST(v['n'] AS INT) >= 8000
+ """
+
+ qt_variant_deletion_vector_current """
+ SELECT COUNT(*), MIN(id), MAX(id), SUM(CAST(v['n'] AS BIGINT))
+ FROM variant_deletion_vector
+ WHERE v['keep'] = true
Review Comment:
[P1] Make the DV checkpoint observe the deleted rows
The DELETE removes odd ids, and those rows were initialized with
`keep=false`; this `WHERE v['keep'] = true` therefore returns the same 2048
even rows and sum even if Doris ignores the delete entirely. It also cannot
distinguish a PUFFIN deletion vector from an ordinary position-delete file.
Query a predicate that includes the deleted rows (for example `v['n'] >= 0`)
and assert the current even-id result, then verify the live delete metadata is
PUFFIN/content-offset DV data for this fixture.
##########
regression-test/suites/external_table_p0/iceberg/test_iceberg_variant_read.groovy:
##########
@@ -337,44 +586,167 @@ suite("test_iceberg_variant_read",
ORDER BY id
"""
- // Keep the root Variant as output while the implicit scalar comparison
drives the shredded
- // typed_value statistics/page-index path.
- order_qt_variant_implicit_shredded_filter """
+ order_qt_variant_multi_file_serial """
+ SELECT id,
+ CAST(v['shared'] AS INT),
+ CAST(v['a'] AS INT),
+ CAST(v['b'] AS INT),
+ CAST(v['new_field']['k'] AS INT),
+ CAST(v AS STRING)
+ FROM variant_multi_file
+ WHERE v['shared'] >= 20
+ ORDER BY id
+ """
+ sql "set parallel_pipeline_task_num=4"
+ sql "set max_file_scanners_concurrency=8"
+ order_qt_variant_multi_file_parallel """
+ SELECT id,
+ CAST(v['shared'] AS INT),
+ CAST(v['a'] AS INT),
+ CAST(v['b'] AS INT),
+ CAST(v['new_field']['k'] AS INT),
+ CAST(v AS STRING)
+ FROM variant_multi_file
+ WHERE v['shared'] >= 20
+ ORDER BY id
+ """
+
+ order_qt_variant_type_matrix """
+ SELECT CAST(v['bool_value'] AS BOOLEAN),
+ CAST(v['tiny_value'] AS TINYINT),
+ CAST(v['small_value'] AS SMALLINT),
+ CAST(v['int_value'] AS INT),
+ CAST(v['big_value'] AS BIGINT),
+ ISNAN(CAST(v['float_value'] AS FLOAT)),
+ ISINF(CAST(v['double_value'] AS DOUBLE)),
+ CAST(v['decimal_value'] AS DECIMAL(20, 4)),
+ CAST(v['date_value'] AS DATE),
+ CAST(v['timestamp_value'] AS DATETIMEV2(6)),
+ CAST(v['binary_value'] AS STRING),
+ v['null_value'] IS NULL
+ FROM variant_type_matrix
+ """
+
+ String multiRowGroupColdToken =
+ "iceberg_variant_multi_row_group_cold_" +
UUID.randomUUID().toString()
+ sql """
+ SELECT '${multiRowGroupColdToken}', COUNT(*), MIN(id), MAX(id)
+ FROM variant_multi_row_group
+ WHERE CAST(v['n'] AS INT) >= 8000
+ """
+ String multiRowGroupColdProfile = getProfileByToken(multiRowGroupColdToken,
+ ["RowGroupsTotalNum", "VariantDirectLeafPathMisses",
"VariantReconstructedRows",
+ "FilteredRowsByLazyRead"]).toString()
+ assertTrue(counterSum(multiRowGroupColdProfile, "RowGroupsTotalNum") > 1,
Review Comment:
[P1] Do not double-count merged and detailed profile views
`counterSum` scans the complete REST profile, which presents scanner
counters in both merged and detailed/execution sections. A single real
`RowGroupsTotalNum: 1` can therefore appear twice, sum to 2, and satisfy this
`> 1` assertion even though the file exercised only one row group. The existing
DV suite avoids this by slicing the authoritative `MergedProfile` section
before aggregation. Scope this threshold to one profile view (or parse one
ParquetReader block) and assert its actual row-group count.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]