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


##########
be/src/exec/rowid_fetcher.cpp:
##########
@@ -1032,13 +1034,18 @@ Status RowIdStorageReader::read_doris_format_row(
                                       .rowset_id = rowset_id,
                                       .segment_id = segment_id,
                                       .slot_id = slots[x].id()};
-            IteratorItem& iterator_item = iterator_map[iterator_key];
+            IteratorItem& iterator_item =
+                    iterator_map.try_emplace(iterator_key, 
stats).first->second;
             if (iterator_item.segment == nullptr) {
-                iterator_map[iterator_key].segment = segment;
-                iterator_item.storage_read_options.stats = &stats;
+                iterator_item.segment = segment;
                 iterator_item.storage_read_options.io_ctx.reader_type = 
ReaderType::READER_QUERY;
                 
iterator_item.storage_read_options.io_ctx.file_cache_miss_policy =
                         file_cache_miss_policy;
+                // A rowid fetch bypasses TabletReader, so copy the rowset 
context it would have

Review Comment:
   [P1] Synthesize hidden columns in row-store lazy fetch
   
   The rowset context only reaches the column-store branch. When 
`fetch_row_store=true`, the branch above decodes full-row JSONB directly, even 
though that JSONB persists hidden VERSION/COMMIT_TSO placeholders. 
`RowStoreFetchChecker` does not exclude those hidden slots, so TopN lazy 
materialization on a `store_row_column=true` table can still return 0. Please 
either keep runtime hidden columns off row-store lazy fetch or overwrite them 
from this rowset context, and cover that full-row-store case in the TopN 
regression.



##########
be/src/storage/segment/variant/nested_group_streaming_write_plan.cpp:
##########
@@ -149,20 +150,18 @@ Status append_plan_from_rowset_reader(const 
RowsetReaderSharedPtr& input_rs_read
             std::static_pointer_cast<BetaRowset>(rowset), &segment_cache));
 
     for (const auto& segment : segment_cache.get_segments()) {
-        std::shared_ptr<ColumnReader> column_reader;
+        std::shared_ptr<VariantColumnReader> variant_reader;
         OlapReaderStatistics stats;
-        Status st = segment->get_column_reader(variant_uid, &column_reader, 
&stats);
+        StorageReadOptions read_options(stats);
+        const auto& variant_column = 
rowset->tablet_schema()->column_by_uid(variant_uid);

Review Comment:
   [P1] Keep current VARIANT identity in compaction
   
   This lookup happens in the historical rowset schema, so a rowset that 
predates `ADD VARIANT` (or predates a same-name DROP/ADD with a new UID) has no 
`variant_uid`; `column_by_uid()` uses `.at()` and throws before the NOT_FOUND 
branch can run. The caller already has the current `variant_column`; please 
pass that descriptor through and let `get_variant_root_reader()` decide whether 
each segment has physical metadata, then cover both schema-evolution cases in 
compaction.



##########
be/src/exprs/function/variant_inverted_index_search.cpp:
##########
@@ -669,14 +670,19 @@ Status VariantNestedSearchEvaluator::evaluate(
     }
     const ColumnId column_id = static_cast<ColumnId>(ordinal);
 
-    std::shared_ptr<segment_v2::ColumnReader> column_reader;
-    
RETURN_IF_ERROR(segment->get_column_reader(segment->tablet_schema()->column(column_id),
-                                               &column_reader,
-                                               
index_exec_ctx->column_iter_opts().stats));
-    auto* variant_reader = 
dynamic_cast<segment_v2::VariantColumnReader*>(column_reader.get());
-    if (variant_reader == nullptr) {
-        return Status::InvalidArgument("Column '{}' is not VARIANT for nested 
query", root_field);
+    std::shared_ptr<segment_v2::VariantColumnReader> variant_reader;
+    DORIS_CHECK(index_exec_ctx->column_iter_opts().stats != nullptr);
+    StorageReadOptions read_options(*index_exec_ctx->column_iter_opts().stats);
+    read_options.io_ctx = index_exec_ctx->column_iter_opts().io_ctx;
+    Status st = 
segment->get_variant_root_reader(segment->tablet_schema()->column(column_id),

Review Comment:
   [P1] Resolve NESTED roots from the current schema
   
   The descriptor passed here was resolved by name from the segment's 
historical schema. For a segment predating `ADD VARIANT`, the earlier 
`field_index` returns -1 and errors instead of reaching this NOT_FOUND 
handling; after same-name DROP/ADD, it can bind the old UID and search dropped 
data. Please resolve the current logical root/UID from the scan schema (the 
current field map is presently ignored) and pass that descriptor here, with ADD 
and DROP/ADD NESTED SEARCH coverage.



##########
be/src/service/point_query_executor.cpp:
##########
@@ -101,7 +101,12 @@ static void get_missing_and_include_cids(const 
TabletSchema& schema,
     }
     const TabletColumn& target_rs_column = 
schema.column_by_uid(target_rs_column_id);
     DCHECK(target_rs_column.is_row_store_column());
-    // The full column group is considered a full match, thus no missing cids
+    // An empty row_columns_uids() means the row-store column contains the 
full row. Keep
+    // missing_cids empty so full-row point queries can be served entirely 
from the row store,
+    // including row-cache hits. Read-time-synthesized hidden columns are 
intentionally not
+    // supported on this fast path: for example, JSONB stores 0 for 
__DORIS_VERSION_COL__, while

Review Comment:
   [P1] Do not return hidden placeholders from point queries
   
   This knowingly turns a valid hidden-column projection into the persisted 
placeholder: full-row short-circuit eligibility does not reject 
`__DORIS_VERSION_COL__`/`__DORIS_COMMIT_TSO_COL__`, and clearing their UIDs 
makes both live JSONB decoding and row-cache hits skip the rowset-aware branch 
below. A prepared point query can therefore return 0 instead of the visible 
rowset value. Please preserve these runtime columns as missing (and bypass the 
row cache for them), or disqualify such projections from the short-circuit 
path, with a regression covering both hidden fields.



##########
be/src/storage/segment/segment.cpp:
##########
@@ -1056,26 +1013,176 @@ Status Segment::traverse_column_meta_pbs(const 
std::function<void(const ColumnMe
     return _column_meta_accessor->traverse_metas(*footer_pb_shared, visitor, 
&dummy_stats);
 }
 
-Status Segment::get_column_reader(const TabletColumn& col,
-                                  std::shared_ptr<ColumnReader>* column_reader,
-                                  OlapReaderStatistics* stats, const 
io::IOContext* source_io_ctx,
-                                  std::optional<Field> const_value) {
-    RETURN_IF_ERROR(_create_column_meta_once(stats, source_io_ctx));
-    SCOPED_RAW_TIMER(&stats->segment_create_column_readers_timer_ns);
-    int col_uid = col.unique_id() >= 0 ? col.unique_id() : 
col.parent_unique_id();
-    // The column is not in this segment, return nullptr
-    if (!_tablet_schema->has_column_unique_id(col_uid)) {
-        *column_reader = nullptr;
-        return Status::Error<ErrorCode::NOT_FOUND, false>("column not found in 
segment, col_uid={}",
-                                                          col_uid);
-    }
-    if (col.has_path_info()) {
-        PathInData relative_path = col.path_info_ptr()->copy_pop_front();
-        return _column_reader_cache->get_path_column_reader(col_uid, 
relative_path, column_reader,
-                                                            stats, nullptr, 
source_io_ctx);
-    }
-    return _column_reader_cache->get_column_reader(col_uid, column_reader, 
stats, source_io_ctx,
-                                                   std::move(const_value));
+Status Segment::_get_column_reader_for_read(const TabletColumn& col,
+                                            const StorageReadOptions& 
read_options,
+                                            std::shared_ptr<ColumnReader>* 
column_reader) {
+    DORIS_CHECK(read_options.stats != nullptr);
+    const int32_t col_uid = col.unique_id() >= 0 ? col.unique_id() : 
col.parent_unique_id();
+    DORIS_CHECK_GE(col_uid, 0) << "column does not have a resolvable uid: " << 
col.debug_string();
+    RETURN_IF_ERROR(_create_column_meta_once(read_options.stats, 
&read_options.io_ctx));
+    
SCOPED_RAW_TIMER(&read_options.stats->segment_create_column_readers_timer_ns);
+
+    if (col.name() == VERSION_COL) {
+        // A singleton rowset exposes its rowset version for every row. 
Segment writers store a
+        // placeholder because the publish version is not known when the 
segment is written.
+        if (read_options.version.first == read_options.version.second) {
+            *column_reader = std::make_shared<ConstantColumnReader>(
+                    
Field::create_field<TYPE_BIGINT>(read_options.version.second), col.type());
+            return Status::OK();
+        }
+        if (!_column_meta_accessor->has_column_uid(col_uid)) {
+            return Status::InternalError("could not find version column read 
version is {}-{}",
+                                         read_options.version.first, 
read_options.version.second);
+        }
+        return _column_reader_cache->get_column_reader(col_uid, column_reader, 
read_options.stats,
+                                                       &read_options.io_ctx);
+    }
+
+    // Only row-binlog reads reinterpret the NULL/0 placeholder as commit_tso. 
For example, an
+    // incremental binlog read of rowset [9-9] should expose its commit_tso, 
while a checksum or
+    // schema-change read that happens to include the hidden column must keep 
the physical value.
+    // TODO yiguolei: 需要问问boyang 这块read_row_binlog的逻辑
+    if (read_options.read_row_binlog && col.name() == BINLOG_TSO_COL) {
+        const int64_t commit_tso = read_options.commit_tso.end_tso();
+        if (read_options.version.first == read_options.version.second) {
+            DCHECK_EQ(read_options.commit_tso.start_tso(), commit_tso);
+            // TODO yiguolei: zhge -1 也得看看
+            *column_reader = std::make_shared<ConstantColumnReader>(
+                    Field::create_field<TYPE_BIGINT>(commit_tso == -1 ? 0 : 
commit_tso),
+                    col.type());
+            return Status::OK();
+        }
+        if (!_column_meta_accessor->has_column_uid(col_uid)) {
+            return Status::InternalError("could not find binlog tso column");
+        }
+        return _column_reader_cache->get_column_reader(col_uid, column_reader, 
read_options.stats,
+                                                       &read_options.io_ctx);
+    }
+
+    if (col.name() == COMMIT_TSO_COL) {
+        const int64_t commit_tso = read_options.commit_tso.end_tso();
+        // For example, a published rowset [12-12] with commit_tso=100 
replaces the on-disk 0 with
+        // 100. Before publish, commit_tso=-1 keeps the physical placeholder 
reader.
+        if (read_options.version.first == read_options.version.second && 
commit_tso != -1) {
+            *column_reader = std::make_shared<ConstantColumnReader>(
+                    Field::create_field<TYPE_BIGINT>(commit_tso), col.type());
+            return Status::OK();
+        }
+        if (!_column_meta_accessor->has_column_uid(col_uid)) {
+            return Status::InternalError("could not find commit tso column");
+        }
+        return _column_reader_cache->get_column_reader(col_uid, column_reader, 
read_options.stats,
+                                                       &read_options.io_ctx);
+    }
+
+    // A column added after this segment was written uses its schema default. 
For example, after
+    // `ADD COLUMN city STRING DEFAULT 'Paris'`, old segments produce "Paris" 
without a data page.
+    // For a BE-generated path such as `v.city` whose uid is -1, col_uid is 
the VARIANT parent uid,
+    // so sparse/doc data in the existing root is considered before falling 
back to the path
+    // default.
+    if (!_column_meta_accessor->has_column_uid(col_uid)) {
+        Field field;
+        RETURN_IF_ERROR(get_default_value_field(col, &field));
+        *column_reader = 
std::make_shared<ConstantColumnReader>(std::move(field), col.type());
+        return Status::OK();
+    }
+
+    // Preserve the established value-read contract: a VARIANT root or a 
BE-generated path
+    // (uid=-1, resolved to its parent) returns the root reader, and 
new_column_iterator() delegates
+    // typed/sparse/default selection to VariantColumnReader. A schema path 
with its own uid keeps
+    // using that physical reader, exactly like an ordinary persisted column.
+    return _column_reader_cache->get_column_reader(col_uid, column_reader, 
read_options.stats,
+                                                   &read_options.io_ctx);
+}
+
+Status Segment::get_variant_root_reader(const TabletColumn& col,
+                                        const StorageReadOptions& read_options,
+                                        std::shared_ptr<VariantColumnReader>* 
column_reader) {
+    DORIS_CHECK(read_options.stats != nullptr);
+
+    // A root VARIANT has parent_uid=-1 and therefore uses its own uid. An 
extracted path may have
+    // its own schema uid, but its physical path metadata is owned by the root 
identified by
+    // parent_uid. For example, root `v` uses uid=7 and `v.user.id` also 
resolves to root uid=7.
+    const int32_t root_uid = col.parent_unique_id() >= 0 ? 
col.parent_unique_id() : col.unique_id();
+    DORIS_CHECK_GE(root_uid, 0) << "VARIANT column does not have a resolvable 
root uid: "
+                                << col.debug_string();
+
+    // Validate the root rather than an extracted path's leaf type. For 
example, `v.user.id` may be
+    // BIGINT while its root uid=7 still identifies a physical VARIANT reader.
+    const TabletColumn* root_column = &col;

Review Comment:
   [P1] Handle a historically absent VARIANT root before leaf validation
   
   On a production pre-ADD rowset, `Segment` is opened with the historical 
rowset schema, so it does not contain the current `root_uid`. For a generated 
child such as `v.a`, `root_column` then remains this scalar leaf and the 
VARIANT check below returns `InvalidArgument`; `get_data_type_of()` never 
receives the NOT_FOUND it expects for its declared-type fallback, and ordinary 
`SegmentIterator` initialization fails. The new unit test masks this by opening 
old physical data with the current full schema. Please preserve/resolve the 
current root descriptor (or detect physical absence before leaf validation) and 
test with an actually historical segment schema, including same-name DROP/ADD.



##########
regression-test/suites/query_p0/topn_lazy/test_constant_column_topn_lazy_rowid.groovy:
##########
@@ -0,0 +1,121 @@
+// 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_constant_column_topn_lazy_rowid") {
+    sql "DROP TABLE IF EXISTS test_constant_column_topn_lazy_rowid"
+    sql """
+        CREATE TABLE test_constant_column_topn_lazy_rowid (
+            id INT NOT NULL,
+            score INT NOT NULL
+        )
+        UNIQUE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES (
+            "replication_num" = "1",
+            "enable_unique_key_merge_on_write" = "true",
+            "light_schema_change" = "true",
+            "disable_auto_compaction" = "true"
+        )
+    """
+
+    // Case 1: prepare a TopN result containing old constant-backed rows and a 
new physical row.
+    sql """
+        INSERT INTO test_constant_column_topn_lazy_rowid VALUES
+            (1, 100),
+            (2, 80)
+    """
+
+    sql """
+        ALTER TABLE test_constant_column_topn_lazy_rowid
+        ADD COLUMN payload VARCHAR(32) NOT NULL DEFAULT 'old-default'
+    """
+    waitForSchemaChangeDone {
+        sql """
+            SHOW ALTER TABLE COLUMN
+            WHERE TableName = 'test_constant_column_topn_lazy_rowid'
+            ORDER BY CreateTime DESC LIMIT 1
+        """
+        time 600
+    }
+
+    sql """
+        INSERT INTO test_constant_column_topn_lazy_rowid VALUES
+            (3, 90, 'new-physical'),
+            (4, 70, 'not-in-topn')
+    """
+
+    // Case 2: lazy rowid fetch must produce the same added-column and hidden 
VERSION values as a
+    // normal scan when constant-backed and physical rows are mixed.
+    sql "SET show_hidden_columns = true"
+    sql "SET topn_lazy_materialization_threshold = -1"
+    def normalRead = sql """
+        SELECT id, payload, __DORIS_VERSION_COL__
+        FROM test_constant_column_topn_lazy_rowid
+        ORDER BY score DESC
+        LIMIT 3
+    """
+    def normalHiddenOnlyRead = sql """
+        SELECT __DORIS_VERSION_COL__
+        FROM test_constant_column_topn_lazy_rowid
+        ORDER BY score DESC
+        LIMIT 3
+    """
+
+    sql "SET topn_lazy_materialization_threshold = 1024"
+    explain {
+        sql """
+            SHAPE PLAN
+            SELECT __DORIS_VERSION_COL__
+            FROM test_constant_column_topn_lazy_rowid
+            ORDER BY score DESC
+            LIMIT 3
+        """
+        contains "PhysicalLazyMaterialize"
+    }
+
+    def lazyRead = sql """
+        SELECT id, payload, __DORIS_VERSION_COL__
+        FROM test_constant_column_topn_lazy_rowid
+        ORDER BY score DESC
+        LIMIT 3
+    """
+    assertEquals(normalRead, lazyRead)
+    assertTrue(lazyRead.every { row -> (row[2] as Long) > 0 },
+            "TopN rowid fetch returned a hidden version placeholder: 
${lazyRead}")
+
+    // Case 3: VERSION is the only projected value in this query. Seeing 
PhysicalLazyMaterialize above
+    // therefore proves that the hidden column itself reaches the rowid-fetch 
phase.
+    def lazyHiddenOnlyRead = sql """
+        SELECT __DORIS_VERSION_COL__
+        FROM test_constant_column_topn_lazy_rowid
+        ORDER BY score DESC
+        LIMIT 3
+    """
+    assertEquals(normalHiddenOnlyRead, lazyHiddenOnlyRead)
+    assertTrue(lazyHiddenOnlyRead.every { row -> (row[0] as Long) > 0 },
+            "TopN rowid fetch returned an invalid hidden version: 
${lazyHiddenOnlyRead}")
+
+    order_qt_topn_mixes_constant_and_physical_rows """

Review Comment:
   [P1] Commit the TopN expected output
   
   This `order_qt` requires a checked-in expected-output file, but 
`regression-test/data/query_p0/topn_lazy/test_constant_column_topn_lazy_rowid.out`
 is absent. Normal regression mode throws `Missing outputFile` rather than 
generating it, so this new suite cannot pass as committed. Please generate and 
commit the `.out` with the regression runner.



##########
regression-test/suites/time_travel_p0/test_constant_hidden_column_statistics.groovy:
##########
@@ -0,0 +1,204 @@
+// 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_constant_hidden_column_statistics", "nonConcurrent") {
+    sql "DROP TABLE IF EXISTS test_constant_hidden_column_statistics FORCE"
+    sql """
+        CREATE TABLE test_constant_hidden_column_statistics (
+            `k` INT NOT NULL,
+            `v` INT NOT NULL
+        )
+        UNIQUE KEY(`k`)
+        DISTRIBUTED BY HASH(`k`) BUCKETS 1
+        PROPERTIES (
+            "replication_num" = "1",
+            "enable_unique_key_merge_on_write" = "true",
+            "light_schema_change" = "true",
+            "disable_auto_compaction" = "true",
+            "binlog.enable" = "true",
+            "binlog.format" = "ROW",
+            "binlog.need_historical_value" = "true"
+        )
+    """
+
+    sql "SET show_hidden_columns = true"
+
+    def readHiddenState = {
+        def rows = sql """
+            SELECT k, __DORIS_VERSION_COL__
+            FROM test_constant_hidden_column_statistics
+            ORDER BY k
+        """
+        return rows.collectEntries { row ->
+            [(Integer.parseInt(row[0].toString())): [
+                    version: Long.parseLong(row[1].toString())
+            ]]
+        }
+    }
+
+    def readHiddenMinMax = {
+        def row = sql """
+            SELECT MIN(__DORIS_VERSION_COL__), MAX(__DORIS_VERSION_COL__)
+            FROM test_constant_hidden_column_statistics
+        """
+        return row[0].collect { value -> Long.parseLong(value.toString()) }
+    }
+
+    // Case 1: the first rowset predates c_default. Its hidden version is 
captured before ALTER so the
+    // test can verify that schema evolution does not replace it with a 
persisted placeholder.
+    sql """
+        INSERT INTO test_constant_hidden_column_statistics VALUES
+            (1, 10),
+            (2, 20)
+    """
+    sql "SYNC"
+
+    def beforeAlter = readHiddenState()
+    assertEquals(2, beforeAlter.size())
+    assertEquals(beforeAlter[1].version, beforeAlter[2].version)
+    assertTrue(beforeAlter[1].version > 0)
+    def oldVersion = beforeAlter[1].version
+
+    sql """
+        ALTER TABLE test_constant_hidden_column_statistics
+        ADD COLUMN `c_default` INT NOT NULL DEFAULT "10"
+    """
+    waitForSchemaChangeDone({
+        sql """
+            SHOW ALTER TABLE COLUMN
+            WHERE IndexName = 'test_constant_hidden_column_statistics'
+            ORDER BY CreateTime DESC LIMIT 1
+        """
+        time 600
+    })
+
+    order_qt_hidden_after_alter_add_default """
+        SELECT k, v, c_default
+        FROM test_constant_hidden_column_statistics
+        ORDER BY k
+    """
+
+    def afterAlter = readHiddenState()
+    assertEquals(beforeAlter, afterAlter)
+
+    // Case 2: with only the pre-ALTER rowset, the hidden version is a 
rowset-scoped constant. SQL
+    // MIN/MAX must observe that runtime value rather than the persisted zero 
placeholder. FE does
+    // not push this aggregate through a MOW delete bitmap; the direct 
statistics-iterator contract
+    // is covered by 
SegmentIteratorExprZonemapTest.HiddenConstantsFeedStatisticsIterator.
+    def oldMinMax = readHiddenMinMax()
+    assertEquals([oldVersion, oldVersion], oldMinMax)
+
+    // Case 3: these predicates are evaluated against the constant zonemaps of 
the historical rowset.
+    // c_default in the same predicates is also a constant because it was 
added after the write.
+    order_qt_hidden_initial_version_equal """

Review Comment:
   [P1] Generate every expected time-travel block
   
   The committed `.out` contains only `hidden_after_alter_add_default`; this 
label and the other nine labels that follow have no output blocks. 
`Suite.quickRunTest` throws `Missing output block for tag` in normal mode, so 
the suite stops here. Please regenerate and commit all expected blocks using 
the regression runner rather than hand-writing them.



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