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


##########
be/src/format_v2/table/paimon_reader.cpp:
##########
@@ -199,7 +178,10 @@ Status 
PaimonHybridReader::_ensure_current_split_reader(const format::SplitReadO
     } else {
         format::FileFormat file_format;
         RETURN_IF_ERROR(_to_file_format(options.current_range, &file_format));
-        DCHECK(options.current_split_format == file_format);
+        // Old FE plans encoded a native file as FORMAT_JNI without 
paimon_split and carried the
+        // physical format only in paimon_params.file_format.
+        DCHECK(options.current_split_format == file_format ||

Review Comment:
   [P1] Preserve the Paimon split kind and resolved native format
   
   This native branch receives two `FORMAT_JNI` compatibility shapes. 
`PAIMON_CPP` carries a complete serialized `DataSplit`, but the raw child never 
consumes it and its synthetic path names only the first file. A legacy range 
with no `reader_type`/`paimon_split` is a valid raw file, but `prepare_split()` 
forwards `current_split_format == JNI`, overwrites the resolved Parquet/ORC 
format, and fails to open it. Please keep encoded CPP splits on V1 until V2 has 
an equivalent child, and pass the resolved physical format when preparing 
legacy raw splits.



##########
be/src/format_v2/column_mapper.cpp:
##########
@@ -2332,28 +2300,17 @@ Status TableColumnMapper::localize_filters(const 
std::vector<TableFilter>& table
     // This keeps expression localization independent from filter iteration 
order.
     filter_mappings = _filter_visible_mappings();
     const auto global_to_file_slot = 
build_file_slot_rewrite_map(filter_mappings, _filter_entries);
-    for (size_t filter_index = 0; filter_index < table_filters.size(); 
++filter_index) {
-        const auto& table_filter = table_filters[filter_index];
+    for (const auto& table_filter : table_filters) {
         if (table_filter.conjunct != nullptr && table_filter.conjunct->root() 
!= nullptr) {
             const auto root = table_filter.conjunct->root();
             const auto impl = root->get_impl();
             const auto predicate = impl != nullptr ? impl : root;
-            if (!table_filter.can_localize || !predicate->is_deterministic() ||
+            if (!predicate->is_deterministic() ||

Review Comment:
   [P1] Keep truncation-sensitive predicates above materialization
   
   This gate no longer excludes columns whose physical CHAR/VARCHAR value must 
be shortened to the table width. The localized clone runs in the FileReader 
before `finalize_chunk()` performs that truncation, so it can irreversibly drop 
rows: with physical `"abcd"`, table `VARCHAR(3)`, and `value = 'abc'`, the 
local comparison rejects the row before finalization would produce `"abc"` for 
Scanner's authoritative predicate. Please restore the width-change guard (and 
its deleted regression), or make the localized expression observe the exact 
bounded table value.



##########
be/src/exec/operator/file_scan_operator.cpp:
##########
@@ -118,12 +118,8 @@ bool FileScanLocalState::_should_use_file_scanner_v2(const 
TQueryOptions& query_
     const bool is_transactional_hive =
             scan_params.__isset.table_format_params &&
             scan_params.table_format_params.table_format_type == 
"transactional_hive";
-    // JNI reader selection is stored per split, but this scan-level selector 
cannot inspect the
-    // split yet. Older FEs may omit both the scan-level Paimon marker and 
split-level reader_type,
-    // so keep JNI scans on V1 until scanner selection can distinguish every 
compatibility shape.
     return query_options.__isset.enable_file_scanner_v2 && 
query_options.enable_file_scanner_v2 &&

Review Comment:
   [P1] Keep unsupported JNI sources on FileScanner V1
   
   This scan-wide selector now sends every query-side `FORMAT_JNI` plan to V2 
before a split/table format is visible. LakeSoul always emits `FORMAT_JNI`, but 
`FileScannerV2::is_supported_jni_table_format()` rejects `table_format_type == 
"lakesoul"` and there is no per-range fallback; the old scanner's 
`LakeSoulJniReader` is therefore never reached. Because FileScanner V2 is 
enabled by default, existing LakeSoul scans now fail `NotSupported`. Please 
retain the JNI exclusion until all sources are supported, or make 
selection/fallback range-aware and include LakeSoul.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/FileQueryScanNode.java:
##########
@@ -733,6 +747,29 @@ public TableScanParams getScanParams() {
         return this.scanParams;
     }
 
+    /**
+     * Return metadata pinned for this scan relation.
+     */
+    protected Optional<MvccSnapshot> getRelationSnapshot() {
+        if (relationSnapshotInitialized) {
+            return relationSnapshot;
+        }
+        relationSnapshotInitialized = true;
+        TableIf targetTable = desc.getTable();
+        if (!(targetTable instanceof MvccTable)) {
+            return Optional.empty();
+        }
+        if (tableSnapshot != null || scanParams != null) {
+            // A statement can scan several versions of one table, so 
execution must reconstruct
+            // the snapshot from this scan node's own qualifiers rather than 
the table-only map.
+            relationSnapshot = Optional.of(((MvccTable) 
targetTable).loadSnapshot(

Review Comment:
   [P1] Carry the already pinned snapshot instead of resolving the ref again
   
   Logical binding has already resolved this branch/tag and captured its 
columns, but the physical plan carries only the mutable qualifier, so this call 
resolves it again. If the branch advances meanwhile, tuple slots can describe 
head A while schema mappings/defaults and scan files come from head B, 
defeating the statement's MVCC pin. Please carry the resolved `MvccSnapshot` 
through `LogicalFileScan`/`PhysicalFileScan` and reuse it here.



##########
be/test/format_v2/table_reader_test.cpp:
##########
@@ -2020,13 +1756,8 @@ TEST(TableReaderTest, 
SlotlessConjunctDisablesAggregatePushdown) {
     // presence still prevents the fake aggregate count (3) from replacing the 
two physical rows.
     ASSERT_NE(fake_state->last_request, nullptr);
     EXPECT_TRUE(fake_state->last_request->conjuncts.empty());
-    // The two physical rows are then filtered at the table boundary, where 
slotless predicates are
-    // evaluated exactly even though they cannot be localized to a file column.
-    EXPECT_EQ(block.rows(), 0);
-    EXPECT_FALSE(eos);
+    EXPECT_EQ(block.rows(), 2);
     EXPECT_TRUE(predicate_executed);

Review Comment:
   [P1] Update the direct-TableReader expectations for scanner-owned predicates
   
   After this patch TableReader no longer executes residual conjuncts, and this 
non-deterministic predicate is deliberately not localized 
(`last_request->conjuncts` is empty). Its flag can therefore never become true 
in this direct `TableReader::get_block()` test. The same stale `EXPECT_TRUE` 
remains at line 1466. Nearby `UnsafePredicateStaysOnScannerPath` already 
expects false under the new ownership model, so these assertions will fail the 
BE unit target. Change both to false, or exercise final filtering through 
Scanner if execution is what the test intends.



##########
regression-test/suites/external_table_p0/iceberg/write/test_iceberg_write_nullable_truncate_negative.groovy:
##########
@@ -0,0 +1,78 @@
+// 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.
+
+suite("test_iceberg_write_nullable_truncate_negative",
+        "p0,external,iceberg,external_docker,external_docker_iceberg") {
+    String enabled = context.config.otherConfigs.get("enableIcebergTest")
+    if (enabled == null || !enabled.equalsIgnoreCase("true")) {
+        logger.info("disable iceberg test")
+        return
+    }
+
+    // This opt-in switch isolates a BE-fatal negative scenario from the 
shared P0 cluster.
+    // Enable it only in a cluster whose BE processes can be restarted after 
the suite.
+    String crashTestEnabled = 
context.config.otherConfigs.get("enableIcebergCrashTest")
+    if (crashTestEnabled == null || 
!crashTestEnabled.equalsIgnoreCase("true")) {
+        logger.info("skip isolated Iceberg crash regression")
+        return
+    }
+
+    String restPort = context.config.otherConfigs.get("iceberg_rest_uri_port")
+    String minioPort = context.config.otherConfigs.get("iceberg_minio_port")
+    String externalEnvIp = context.config.otherConfigs.get("externalEnvIp")
+    String catalogName = "test_iceberg_write_nullable_truncate_negative"
+    String dbName = "iceberg_write_nullable_truncate_negative_db"
+
+    sql """drop catalog if exists ${catalogName}"""
+    sql """
+        create catalog ${catalogName} properties (
+            "type" = "iceberg",
+            "iceberg.catalog.type" = "rest",
+            "uri" = "http://${externalEnvIp}:${restPort}";,
+            "s3.access_key" = "admin",
+            "s3.secret_key" = "password",
+            "s3.endpoint" = "http://${externalEnvIp}:${minioPort}";,
+            "s3.region" = "us-east-1"
+        )
+    """
+    sql """switch ${catalogName}"""
+    sql """drop database if exists ${dbName} force"""
+    sql """create database ${dbName}"""
+    sql """use ${dbName}"""
+
+    sql """
+        create table nullable_truncate (
+            id int not null,
+            zone string
+        )
+        partition by list (zone) ()
+        properties ("format-version" = "2")
+    """
+    sql """insert into nullable_truncate values (1, 'CN'), (2, null)"""
+
+    // Negative scenario: evolve to a truncate transform whose source remains 
nullable,
+    // then write both non-NULL and NULL partition values through Doris.
+    sql """
+        alter table nullable_truncate
+        add partition key truncate(2, zone) as zone_prefix
+    """
+    sql """insert into nullable_truncate values (3, 'US-east'), (4, null)"""
+
+    order_qt_nullable_truncate_rows """

Review Comment:
   [P2] Add result oracles for the opt-in negative suites
   
   This `order_qt` tag has no corresponding `.out` file, so a normal 
verification run throws `Missing outputFile` once `enableIcebergCrashTest` 
reaches it. The same omission exists for `order_qt_merge_truncate_after_fix` 
and `order_qt_duplicate_source_atomic_state` in the other two new opt-in 
suites. Please add the three golden outputs or use explicit assertions so these 
tests can actually validate a fixed cluster.



##########
be/src/format_v2/table_reader.cpp:
##########
@@ -739,152 +736,35 @@ Status TableReader::init(TableReadOptions&& options) {
     }
     _system_properties = create_system_properties(_scan_params);
     _mapper_options.mode = TableColumnMappingMode::BY_NAME;
-    return _replace_conjuncts(options.conjuncts);
-}
-
-Status TableReader::_prepare_conjunct(const VExprContextSPtr& source, 
VExprContextSPtr* prepared) {
-    DORIS_CHECK(source != nullptr);
-    DORIS_CHECK(source->root() != nullptr);
-    DORIS_CHECK(prepared != nullptr);
-    VExprSPtr root;
-    RETURN_IF_ERROR(clone_table_expr_tree(source->root(), &root));
-    auto conjunct = VExprContext::create_shared(std::move(root));
-    RETURN_IF_ERROR(conjunct->prepare(_runtime_state, RowDescriptor {}));
-    RETURN_IF_ERROR(conjunct->open(_runtime_state));
-    *prepared = std::move(conjunct);
-    return Status::OK();
-}
-
-Status TableReader::_replace_conjuncts(const VExprContextSPtrs& conjuncts) {
-    VExprContextSPtrs prepared;
-    prepared.reserve(conjuncts.size());
-    for (const auto& source : conjuncts) {
-        VExprContextSPtr conjunct;
-        RETURN_IF_ERROR(_prepare_conjunct(source, &conjunct));
-        prepared.push_back(std::move(conjunct));
-    }
-    _conjuncts = std::move(prepared);
-    return Status::OK();
-}
-
-Status TableReader::append_conjuncts_with_ownership(const VExprContextSPtrs& 
conjuncts,
-                                                    size_t 
table_reader_owned_conjunct_count) {
-    DORIS_CHECK(!_appended_table_reader_owned_conjunct_count.has_value());
-    _appended_table_reader_owned_conjunct_count = 
table_reader_owned_conjunct_count;
-    auto status = append_conjuncts(conjuncts);
-    _appended_table_reader_owned_conjunct_count.reset();
-    return status;
-}
-
-Status TableReader::append_conjuncts(const VExprContextSPtrs& conjuncts) {
-    const size_t owned_count =
-            
_appended_table_reader_owned_conjunct_count.value_or(conjuncts.size());
-    DORIS_CHECK_LE(owned_count, conjuncts.size());
-    // Once Scanner owns a suffix, later predicates cannot be inserted into 
the TableReader-owned
-    // prefix without reordering them ahead of that stateful/error-preserving 
barrier.
-    DORIS_CHECK(owned_count == 0 || _table_reader_owned_conjunct_count == 
_conjuncts.size());
-    for (size_t conjunct_index = 0; conjunct_index < conjuncts.size(); 
++conjunct_index) {
-        const auto& source = conjuncts[conjunct_index];
-        VExprContextSPtr conjunct;
-        RETURN_IF_ERROR(_prepare_conjunct(source, &conjunct));
-        _conjuncts.push_back(conjunct);
-        if (conjunct_index < owned_count) {
-            ++_table_reader_owned_conjunct_count;
-        }
-        if (_current_task != nullptr && conjunct_index < owned_count) {
-            // The active reader has already fixed its localized predicate 
set. Appended runtime
-            // filters must remain residual until the next split rebuilds its 
FileScanRequest.
-            _remaining_conjuncts.push_back(std::move(conjunct));
-        }
-    }
+    _conjuncts = std::move(options.conjuncts);
     return Status::OK();
 }
 
 Status TableReader::_build_table_filters_from_conjuncts() {
     _table_filters.clear();
     _constant_pruning_safe_filter_count = 0;
     bool in_safe_prefix = true;
-    for (size_t conjunct_index = 0; conjunct_index < _conjuncts.size(); 
++conjunct_index) {
-        const auto& conjunct = _conjuncts[conjunct_index];
+    for (const auto& conjunct : _conjuncts) {
         DORIS_CHECK(conjunct != nullptr);
         DORIS_CHECK(conjunct->root() != nullptr);
         // `_table_filters` omits expressions without slot references, but 
such an expression still
         // occupies a position in the row-level conjunct order. Record how 
many localized filters
         // precede the first unsafe original conjunct so constant pruning 
cannot jump over a
-        // slotless non-deterministic/error-preserving barrier. An unsafe 
predicate is either kept
-        // on TableReader's post-materialization path by a standalone caller 
or carried only for
-        // analysis when FileScannerV2 owns the ordered suffix.
-        if (in_safe_prefix && !is_safe_to_pre_execute(conjunct)) {
+        // slotless non-deterministic/error-preserving barrier. Unsafe 
predicates remain solely on
+        // Scanner's original row-level path because localizing a clone would 
execute their state
+        // twice with independent state.
+        if (in_safe_prefix && !_is_safe_to_pre_execute(conjunct)) {

Review Comment:
   [P1] Preserve the unsafe-conjunct barrier for localization
   
   `in_safe_prefix` now limits only constant pruning. Because the patch removes 
the bit that carried this decision into each `TableFilter`, 
`localize_filters()` can push a later deterministic predicate below an earlier 
stateful or error-preserving conjunct. The FileReader may then discard a row 
before Scanner reaches the original ordered vector, suppressing the earlier 
invocation or error. Please propagate the safe-prefix decision to localization 
and keep the first unsafe conjunct and the entire suffix off the file-local 
path.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java:
##########
@@ -934,7 +934,12 @@ public void loadSnapshots(TableIf specificTable, 
Optional<TableSnapshot> tableSn
             Optional<TableScanParams> scanParams) {
         if (specificTable instanceof MvccTable) {
             MvccTableInfo mvccTableInfo = new MvccTableInfo(specificTable);
-            if (!snapshots.containsKey(mvccTableInfo)) {
+            if (tableSnapshot.isPresent() || scanParams.isPresent()) {
+                // Explicit time-travel relations must pin their own metadata 
even when another
+                // relation for the same table was already bound in this 
statement.
+                snapshots.put(mvccTableInfo,
+                        ((MvccTable) 
specificTable).loadSnapshot(tableSnapshot, scanParams));
+            } else if (!snapshots.containsKey(mvccTableInfo)) {

Review Comment:
   [P1] Pin the snapshot per relation, including the current relation
   
   This is still a single snapshot slot keyed only by catalog/database/table. 
If a historical relation binds first, a later unqualified current relation hits 
this `containsKey` branch and captures the historical schema too. In the 
reverse order, the historical relation overwrites the map after current slots 
were captured, and the current scan node later consumes that historical 
snapshot. Thus a current+historical self-join is wrong in either order; the 
added tests qualify both sides and do not cover it. Please carry the resolved 
snapshot per relation through planning.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalTable.java:
##########
@@ -178,6 +178,10 @@ public List<Column> getFullSchema() {
         return schemaCacheValue.map(SchemaCacheValue::getSchema).orElse(null);
     }
 
+    public List<Column> getFullSchema(Optional<MvccSnapshot> snapshot) {

Review Comment:
   [P1] Honor the relation snapshot for HMS-backed Iceberg
   
   `HMSExternalTable` with `DLAType.ICEBERG` is an MVCC Iceberg scan, but it 
does not override this overload, so the supplied relation snapshot is discarded 
and its no-argument schema lookup falls back to the table-keyed context. 
`LogicalFileScan.captureRelationSchema()` also excludes this subtype. Two 
qualified HMS-Iceberg relations can therefore bind against whichever snapshot 
remains in that shared map. Please add the snapshot-aware HMS Iceberg 
implementation and make relation-schema capture capability-based.



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