github-actions[bot] commented on code in PR #68962:
URL: https://github.com/apache/doris/pull/68962#discussion_r4246537986


##########
be/src/format_v2/table/iceberg_reader.cpp:
##########
@@ -199,6 +199,47 @@ Status IcebergTableReader::validate_variant_file_mappings(
             file_format_name(format));
 }
 
+Status IcebergTableReader::annotate_file_schema(
+        std::vector<format::ColumnDefinition>* file_schema) {
+    if (file_schema == nullptr || file_schema->empty()) {
+        return Status::OK();
+    }
+    // Iceberg's positional fallback is a Parquet-reader-only compatibility 
path. Its ORC/Avro
+    // readers instead derive a name mapping from the expected schema for 
id-less files, which is
+    // exactly the current-name resurrection that v2 scan semantics 
deliberately rejects (#65784),
+    // so ORC keeps its existing behavior and stays out of scope here.
+    if (_format != FileFormat::PARQUET) {
+        return Status::OK();
+    }
+    // Iceberg's `hasIds` contract is existential: a single real field id 
keeps the file on the id
+    // path and, exactly as in Iceberg, skips both the name mapping and the 
positional fallback.
+    if (schema_has_any_field_id(*file_schema)) {
+        return Status::OK();
+    }
+    // An old-FE plan keeps the legacy name fallback (rolling upgrade), and an 
explicit table-level
+    // name mapping stays authoritative even when it does not cover this file.
+    if (!supports_iceberg_scan_semantics_v2(_scan_params) ||
+        _scan_has_any_authoritative_name_mapping()) {
+        return Status::OK();
+    }
+    // ParquetSchemaUtil.addFallbackIds: ordinals start at 1, are counted over 
the physical
+    // top-level fields in file order, and are not propagated to nested 
fields. Reader-synthesized
+    // carriers (row number / global row id) are not file fields and must not 
consume an ordinal.
+    int32_t ordinal = 0;
+    for (auto& field : *file_schema) {
+        if (field.column_type != format::ColumnType::DATA_COLUMN) {
+            continue;
+        }
+        ++ordinal;
+        if (field.has_identifier()) {

Review Comment:
   [P1] Replace name identifiers when assigning positional field IDs
   
   ID-less Parquet columns already have identifiers: 
`ParquetReader::_fill_column_definition()` sets `identifier` to 
`TYPE_STRING(column_schema.name)` when `parquet_field_id` is absent, and 
`has_identifier()` returns true for that string. Consequently every physical 
field takes this `continue` and no ordinal ID is assigned. With v2 semantics 
and no name mapping, `BY_FIELD_ID` still resolves none of the columns, so the 
migrated-file query remains all NULL and `WHERE id = 1` drops its real row. 
Remove this guard or use `has_identifier_field_id()` so string identifiers can 
be replaced with ordinal IDs.



##########
be/src/format_v2/table/iceberg_reader.h:
##########
@@ -205,6 +216,10 @@ class IcebergTableReader : public format::TableReader {
     size_t _row_position_block_position = 0;
     std::optional<TIcebergFileDesc> _iceberg_params;
     bool _delete_predicates_initialized = false;
+    // Set when this split's Parquet file carried no field ids and the table 
exposed no usable name
+    // mapping, so the reader stamped Iceberg's positional fallback ids on the 
file's top-level
+    // fields. Observability only; the decision itself lives in 
annotate_file_schema().
+    bool _positional_id_fallback = false;

Review Comment:
   [P3] Reset the fallback diagnostic when preparing a new split
   
   This member belongs to the reused `IcebergTableReader`, and 
`prepare_split()` never clears it. Once an ID-less Parquet file sets it true, a 
subsequent ID-bearing Parquet or ORC file returns early from 
`annotate_file_schema()` and continues to report `positional_id_fallback=1` in 
`debug_string()`/VLOG, although no fallback was used for that file. Reset the 
flag at the start of `prepare_split()` so the per-file diagnostic also 
describes pruned and metadata-only splits correctly.



##########
be/test/format_v2/table/iceberg_reader_test.cpp:
##########
@@ -2971,6 +3033,285 @@ TEST(IcebergV2ReaderTest, 
IcebergV2IdlessFileRequiresAuthoritativeNameMapping) {
               TableColumnMappingMode::BY_NAME);
 }
 
+// DORIS-29685: an add_files split (no field ids) cherry-picked back into an 
evolved table read as a
+// whole all-NULL row because the table no longer carried a usable name 
mapping. Iceberg's Parquet
+// reader binds such files by position (`ParquetSchemaUtil.addFallbackIds` + 
`pruneColumnsFallback`,
+// `iceberg.netflix.unsafe-parquet-id-fallback.enabled` defaults to true), so 
Doris must do the same
+// instead of silently materializing NULL. The physical names deliberately 
differ from the table
+// names: the fallback is "file ordinal k == Iceberg field id k", never name 
based.
+TEST(IcebergV2ReaderTest, 
IcebergIdlessParquetWithoutNameMappingUsesPositionalIds) {
+    const auto test_dir =
+            std::filesystem::temp_directory_path() / 
"doris_iceberg_positional_id_fallback_test";
+    std::filesystem::remove_all(test_dir);
+    std::filesystem::create_directories(test_dir);
+    const auto file_path = (test_dir / "split.parquet").string();
+    write_idless_int_columns_parquet_file(file_path, {"legacy_id", 
"legacy_value"},
+                                          {{1, 2, 3}, {10, 20, 30}});
+
+    std::vector<ColumnDefinition> projected_columns;
+    projected_columns.push_back(make_table_column(1, "id", 
std::make_shared<DataTypeInt32>()));
+    projected_columns.push_back(make_table_column(2, "value", 
std::make_shared<DataTypeInt32>()));
+
+    RuntimeProfile profile("test_profile");
+    RuntimeState state {TQueryOptions(), TQueryGlobals()};
+    auto scan_params = make_local_parquet_scan_params();
+    
scan_params.__set_iceberg_scan_semantics_version(ICEBERG_SCAN_SEMANTICS_VERSION_2);
+    io::FileReaderStats file_reader_stats;
+    io::FileCacheStatistics file_cache_stats;
+    auto io_ctx = make_io_context(&file_reader_stats, &file_cache_stats);
+    ShardedKVCache cache(1);
+    doris::format::iceberg::IcebergTableReader reader;
+    init_iceberg_reader(&reader, projected_columns, &scan_params, io_ctx, 
&state, &profile);
+
+    auto split_options = build_split_options(file_path);
+    split_options.cache = &cache;
+    split_options.current_range.__set_table_format_params(
+            make_iceberg_table_format_desc(file_path, {}));
+    ASSERT_TRUE(reader.prepare_split(split_options).ok());
+    EXPECT_NE(reader.debug_string().find("positional_id_fallback=1"), 
std::string::npos);

Review Comment:
   [P2] Check the fallback flag after the lazy reader opens
   
   `prepare_split()` records the ScanTask and delete metadata; it does not open 
the data file. `annotate_file_schema()`, the only place that sets 
`_positional_id_fallback=true`, first runs through `get_block() -> 
create_next_reader() -> open_reader()`. A fresh reader therefore still reports 
`positional_id_fallback=0` at this `EXPECT_NE`, so this test and 
`IcebergIdlessParquetStructChildStaysNull` fail even after the 
identifier-stamping bug is fixed. Move the flag checks after the first 
successful `get_block()`; the `=0` checks should also happen after opening so 
they exercise the intended exclusion paths.



-- 
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]

Reply via email to