Repository: pingcap/tiflash
Pull request #10151: Storage: Adapt to ingesting snapshot with irregular region range
Author: login: JaySon-Huang
Labels: name: size/XXL
description: Denotes a PR that changes 1000+ lines, ignoring generated files.
name: release-note
description: Denotes a PR that will be considered when it comes time to generate release notes.
name: approved
name: lgtm
name: needs-cherry-pick-release-7.5
description: Should cherry pick this PR to release-7.5 branch.
name: needs-cherry-pick-release-8.1
description: Should cherry pick this PR to release-8.1 branch.
name: needs-cherry-pick-release-8.5
description: Should cherry pick this PR to release-8.5 branch.
Created: 2025-05-08T03:31:39Z | Merged: 2025-05-09T07:40:15Z

### What problem does this PR solve?

Issue Number: close https://github.com/pingcap/tiflash/issues/10147

Problem Summary: As the issue describe

### What is changed and how it works?

* Adapt to irregular region range with any suffix, not only "\x00"
* Refine the irregular region range parsing warning that we can get more information for better diagnosing
  * Turn `RowKeyValue(bool is_common_handle_, HandleValuePtr value_)` to `RowKeyValue::fromHandleWithSuffix(bool is_common_handle_, HandleValuePtr value_)`
  * Move the parse warning from parsing `RowKeyValue` to parsing `RowKeyRange::fromRegionRange`

```commit-message
Storage: Adapt to ingesting snapshot with irregular region range
If the key format is "t${tableID}_r${handleID1}{AnySuffix}", and `AnySuffix` is not empty, `"t${tableID}_r${handleID1}{AnySuffix}" > "t${tableID}_r${handleID1}"` according to the the comparison semantics of "Key" on TiDB/TiKV.

For example,
If a table is non-clustered, that is with Int64 as the HandleID type.
And the range of a Region is `["xxx_r{handleId1}{AnySuffix}", "xxx_r{handleId2}{AnySuffix}")`, where handleId1 = 100, handleId2 = 200.

If `AnySuffix` is empty, then Region contains the left-closed-right-open range of `[handleID1, handleID2)`
If `AnySuffix` is not empty, then Region contains the range `(handleID1, handleID2]`, which is actually the left-closed-right-open range of `[handleID1+1, handleID2+1)`.
```

### Check List

Tests <!-- At least one of them must be included. -->

- [x] Unit test
- [ ] Integration test
- [x] Manual test (add detailed scripts or steps below)
```
create table if not exists t (a bigint primary key, b int);
insert into t values (1,2),(2,3),(3,4),(4,5),(20, 50), (21, 51),(200002,2),(200003,3),(200004,4), (9223372036854775807, 101),(9223372036854775806, 100),(-9223372036854775808, -101),(-9223372036854775807, -100);

// The keys is related to the table_id. For these verification cases, the table_id=471

tiup ctl pd -u {pd_ip}:{pd_port} -i
# split with table_id=471, row_id=20, suffix='\x00'
operator add split-region 2077 --policy usekey --keys 7480000000000001FFD75F728000000000FF0000140000000000FB
# then add tiflash replica
alter table set tiflash replica 1;

# split with table_id=471, row_id=20, suffix='\x01'
operator add split-region 2077 --policy usekey --keys 7480000000000001FFD75F728000000000FF0000140100000000FB
# then add tiflash replica
alter table set tiflash replica 1;

# split with table_id=471, row_id=20, suffix='\x0130FF'
operator add split-region 2077 --policy usekey --keys 7480000000000001FFD75F728000000000FF0000140130FF0000FD
# then add tiflash replica
alter table set tiflash replica 1;

# split with table_id=471, row_id=-9223372036854775808, suffix=''
operator add split-region 2077 --policy usekey --keys 7480000000000001FFD75F720000000000FF0000000000000000FA
# then add tiflash replica
alter table set tiflash replica 1;

# split with table_id=471, row_id=-9223372036854775808, suffix='\x01'
operator add split-region 2077 --policy usekey --keys 7480000000000001FFD75F720000000000FF0000000100000000FB
# then add tiflash replica
alter table set tiflash replica 1;

# split with table_id=471, row_id=9223372036854775807, suffix=''
operator add split-region 2077 --policy usekey --keys 7480000000000001FFD75F72FFFFFFFFFFFFFFFFFF0000000000FA
# then add tiflash replica
alter table set tiflash replica 1;

# split with table_id=471, row_id=9223372036854775807, suffix='\x01'
operator add split-region 2077 --policy usekey --keys 7480000000000001FFD75F72FFFFFFFFFFFFFFFFFF0100000000FB
# then add tiflash replica
alter table set tiflash replica 1;
```
- [ ] No code

Side effects

- [ ] Performance regression: Consumes more CPU
- [ ] Performance regression: Consumes more Memory
- [ ] Breaking backward compatibility

Documentation

- [ ] Affects user behaviors
- [ ] Contains syntax changes
- [ ] Contains variable changes
- [ ] Contains experimental features
- [ ] Changes MySQL compatibility

### Release note

<!-- bugfix or new feature needs a release note -->

```release-note
Fix the issue that TiFlash might panic when handling snapshot with irregular region range
```



Diff:
diff --git a/dbms/src/Storages/DeltaMerge/DeltaMergeStore_Ingest.cpp b/dbms/src/Storages/DeltaMerge/DeltaMergeStore_Ingest.cpp
index dc8d099aff2..e7b2cef6670 100644
--- a/dbms/src/Storages/DeltaMerge/DeltaMergeStore_Ingest.cpp
+++ b/dbms/src/Storages/DeltaMerge/DeltaMergeStore_Ingest.cpp
@@ -30,6 +30,7 @@
 #include <Storages/KVStore/MultiRaft/Disagg/FastAddPeerContext.h>
 #include <Storages/KVStore/TMTContext.h>
 #include <Storages/PathPool.h>
+#include <common/logger_useful.h>
 
 #include <magic_enum.hpp>
 
@@ -132,7 +133,7 @@ void DeltaMergeStore::cleanPreIngestFiles(
         {
             // For disagg mode
             // - if the job has been finished, it means the local files is likely all uploaded to S3
-            // - if the job is intrrupted, it means the `SSTFilesToDTFilesOutputStream::cancel` is called
+            // - if the job is interrupted, it means the `SSTFilesToDTFilesOutputStream::cancel` is called
             //   and local files are also removed.
             // So we ignore the files on disk.
             removePreIngestFile(f.id, false);
@@ -270,9 +271,9 @@ Segments DeltaMergeStore::ingestDTFilesUsingSplit(
                 /*throw_if_notfound*/ true);
 
             const auto delete_range = remaining_delete_range.shrink(segment->getRowKeyRange());
+            // as remaining_delete_range is not none, we expect the shrunk range to be not none.
             RUNTIME_CHECK(
-                !delete_range
-                     .none(), // as remaining_delete_range is not none, we expect the shrinked range to be not none.
+                !delete_range.none(),
                 delete_range.toDebugString(),
                 segment->simpleInfo(),
                 remaining_delete_range.toDebugString());
@@ -617,13 +618,13 @@ UInt64 DeltaMergeStore::ingestFiles(
         }
 
         // Check whether all external files are contained by the range.
-        if (dm_context->global_context.getSettingsRef().dt_enable_ingest_check)
+        for (const auto & ext_file : external_files)
         {
-            for (const auto & ext_file : external_files)
+            if (dm_context->global_context.getSettingsRef().dt_enable_ingest_check)
             {
                 RUNTIME_CHECK_MSG(
                     range.getStart() <= ext_file.range.getStart() && range.getEnd() >= ext_file.range.getEnd(),
-                    "Detected illegal region boundary: range={} file_range={} keyspace={} table_id={}. "
+                    "Detected illegal region boundary: keyspace={} table_id={} range={} file_range={}. "
                     "TiFlash will exit to prevent data inconsistency. "
                     "If you accept data inconsistency and want to continue the service, "
                     "set profiles.default.dt_enable_ingest_check=false .",
@@ -632,6 +633,21 @@ UInt64 DeltaMergeStore::ingestFiles(
                     range.toDebugString(),
                     ext_file.range.toDebugString());
             }
+            else
+            {
+                // If the check is disabled, we just log a warning for better diagnosing.
+                if (unlikely(
+                        !(range.getStart() <= ext_file.range.getStart() && range.getEnd() >= ext_file.range.getEnd())))
+                {
+                    LOG_WARNING(
+                        log,
+                        "Detected illegal region boundary: keyspace={} table_id={} range={} file_range={}",
+                        keyspace_id,
+                        physical_table_id,
+                        range.toDebugString(),
+                        ext_file.range.toDebugString());
+                }
+            }
         }
     }
 
@@ -863,9 +879,9 @@ std::vector<SegmentPtr> DeltaMergeStore::ingestSegmentsUsingSplit(
                 /*throw_if_notfound*/ true);
 
             const auto delete_range = remaining_delete_range.shrink(segment->getRowKeyRange());
+            // as remaining_delete_range is not none, we expect the shrunk range to be not none.
             RUNTIME_CHECK(
-                !delete_range
-                     .none(), // as remaining_delete_range is not none, we expect the shrinked range to be not none.
+                !delete_range.none(),
                 delete_range.toDebugString(),
                 segment->simpleInfo(),
                 remaining_delete_range.toDebugString());
diff --git a/dbms/src/Storages/DeltaMerge/Index/VectorIndex/tests/gtest_dm_delta_merge_store_vector_index.cpp b/dbms/src/Storages/DeltaMerge/Index/VectorIndex/tests/gtest_dm_delta_merge_store_vector_index.cpp
index 6274c871446..d6d452ab2a4 100644
--- a/dbms/src/Storages/DeltaMerge/Index/VectorIndex/tests/gtest_dm_delta_merge_store_vector_index.cpp
+++ b/dbms/src/Storages/DeltaMerge/Index/VectorIndex/tests/gtest_dm_delta_merge_store_vector_index.cpp
@@ -1000,7 +1000,7 @@ try
             segment = store->segments.begin()->second;
         }
         auto dm_context = store->newDMContext(*db_context, db_context->getSettingsRef());
-        auto breakpoint = RowKeyValue::fromHandle(num_rows_write);
+        auto breakpoint = RowKeyValue::fromIntHandle(num_rows_write);
         const auto [left, right] = store->segmentSplit(
             *dm_context,
             segment,
@@ -1102,7 +1102,7 @@ try
             segment = store->segments.begin()->second;
         }
         auto dm_context = store->newDMContext(*db_context, db_context->getSettingsRef());
-        auto breakpoint = RowKeyValue::fromHandle(num_rows_write);
+        auto breakpoint = RowKeyValue::fromIntHandle(num_rows_write);
         return store->segmentSplit(
             *dm_context,
             segment,
@@ -1120,8 +1120,8 @@ try
         std::tie(left, right) = physical_split();
     }
 
-    ASSERT_TRUE(left->rowkey_range.end == RowKeyValue::fromHandle(num_rows_write));
-    ASSERT_TRUE(right->rowkey_range.start == RowKeyValue::fromHandle(num_rows_write));
+    ASSERT_TRUE(left->rowkey_range.end == RowKeyValue::fromIntHandle(num_rows_write));
+    ASSERT_TRUE(right->rowkey_range.start == RowKeyValue::fromIntHandle(num_rows_write));
     RowKeyRange left_segment_range = RowKeyRange(
         left->rowkey_range.start,
         left->rowkey_range.end,
diff --git a/dbms/src/Storages/DeltaMerge/RowKeyRange.cpp b/dbms/src/Storages/DeltaMerge/RowKeyRange.cpp
index b4a6b8def8b..610104ebced 100644
--- a/dbms/src/Storages/DeltaMerge/RowKeyRange.cpp
+++ b/dbms/src/Storages/DeltaMerge/RowKeyRange.cpp
@@ -46,6 +46,59 @@ const RowKeyValue RowKeyValue::COMMON_HANDLE_MAX_KEY
     = RowKeyValue(true, std::make_shared<String>(1, TiDB::CodecFlag::CodecFlagMax), 0);
 const RowKeyValue RowKeyValue::EMPTY_STRING_KEY = RowKeyValue(true, std::make_shared<String>(""), 0);
 
+std::pair<RowKeyValue, std::string_view> RowKeyValue::fromHandleWithSuffix(
+    bool is_common_handle_,
+    const HandleValuePtr value_)
+{
+    if (is_common_handle_)
+    {
+        RowKeyValue rowkey_value(is_common_handle_, value_, 0);
+        return {rowkey_value, std::string_view{nullptr, 0}};
+    }
+
+    assert(!is_common_handle_);
+    // According to the tidb encoding rule, the int handle must be larger or equal to 8 bytes.
+    RUNTIME_CHECK_MSG(
+        value_->size() >= sizeof(Int64),
+        "Meet illegal int handle size less than expected, value_size={} value={}",
+        value_->size(),
+        Redact::keyToHexString(value_->data(), value_->size()));
+
+    size_t cursor = 0;
+    Int64 int_value = DB::DecodeInt64(cursor, *value_);
+    if (unlikely(value_->size() > sizeof(UInt64)))
+    {
+        // For int type handle, the standard key encoding format should be t{table_id}_r{handle_value}.
+        // But TiKV may generate region range keys which are not strictly following the standard format.
+        // More concretely, the key may be t{table_id}_r{handle_value}{any_suffix}.
+        // We need to adapt the key to the standard format.
+        // For example, the key may be t100_r1000 + '0x00', '0x01' or any other suffix, we need to adapt it to t100_r1001.
+        // This is ok, because
+        //  1) if the key is the start range, then [t100_r1000 + 0x00, xxx) has the same semantics with [t100_r1001, xxx)
+        //  2) if the key is the end range, then [xxx, t100_r1000 + 0x00) also has the same semantics with [xxx, t100_r1001)
+        //
+        // Note if the `int_value` is Int64::max_value,
+        // it is a value generated by tiflash itself to means +inf()(which is RowKeyValue::INT_HANDLE_MAX_KEY).
+        // So we can just ignore it.
+        if (int_value < std::numeric_limits<Int64>::max())
+        {
+            int_value = int_value + 1;
+            WriteBufferFromOwnString ss;
+            DB::EncodeInt64(int_value, ss);
+            HandleValuePtr fixed_value = std::make_shared<String>(ss.releaseStr());
+            return {
+                RowKeyValue(is_common_handle_, fixed_value, int_value),
+                std::string_view{value_->data() + sizeof(int_value), value_->size() - sizeof(int_value)},
+            };
+        }
+        else
+        {
+            // keep the int_value unchanged if it is RowKeyValue::INT_HANDLE_MAX_KEY
+        }
+    }
+    return {RowKeyValue(is_common_handle_, value_, int_value), std::string_view{nullptr, 0}};
+}
+
 RowKeyValue RowKeyValueRef::toRowKeyValue() const
 {
     if (data == nullptr)
diff --git a/dbms/src/Storages/DeltaMerge/RowKeyRange.h b/dbms/src/Storages/DeltaMerge/RowKeyRange.h
index 6f934522ce5..65d8305c16e 100644
--- a/dbms/src/Storages/DeltaMerge/RowKeyRange.h
+++ b/dbms/src/Storages/DeltaMerge/RowKeyRange.h
@@ -23,11 +23,15 @@
 #include <Storages/DeltaMerge/Range.h>
 #include <Storages/DeltaMerge/dtpb/column_file.pb.h>
 #include <Storages/KVStore/Decode/DecodedTiKVKeyValue.h>
-#include <Storages/KVStore/MultiRaft/RegionRangeKeys.h>
+#include <Storages/KVStore/Region_fwd.h>
 #include <Storages/KVStore/TiKVHelpers/TiKVRecordFormat.h>
 #include <Storages/KVStore/Types.h>
 #include <TiDB/Decode/DatumCodec.h>
 
+namespace DB
+{
+class RegionRangeKeys;
+}
 namespace DB::DM
 {
 using HandleValuePtr = std::shared_ptr<String>;
@@ -87,62 +91,9 @@ struct RowKeyValue
         , int_value(int_value_)
     {}
 
-    RowKeyValue(bool is_common_handle_, HandleValuePtr value_)
-        : is_common_handle(is_common_handle_)
-        , value(value_)
-    {
-        if (is_common_handle)
-            int_value = 0;
-        else
-        {
-            size_t cursor = 0;
-            int_value = DB::DecodeInt64(cursor, *value);
-            if (unlikely(value->size() != sizeof(Int64)))
-            {
-                // For int type handle, the standard key enconding format should be t{table_id}_r{handle_value}.
-                // But TiKV may generate region range keys which are not strictly following the standard format.
-                // More concretely, the key may be t{table_id}_r{handle_value} + some other bytes.
-                // We need to adapt the key to the standard format.
-                // For example, the key may be t100_r1000 + 0x00, we need to adapt it to t100_r1001.
-                // This is ok, because
-                //  1) if the key is the start range, then [t100_r1000 + 0x00, xxx) has the same semantics with [t100_r1001, xxx)
-                //  2) if the key is the end range, then [xxx, t100_r1000 + 0x00) also has the same semantics with [xxx, t100_r1001)
-                //
-                // Note if the `int_value` is Int64::max_value,
-                // it is a value generated by tiflash itself to means +inf()(which is RowKeyValue::INT_HANDLE_MAX_KEY).
-                // So we can just ignore it.
-                if (value->size() != sizeof(UInt64) + 1 || value->back() != 0x00)
-                {
-                    LOG_WARNING(
-                        Logger::get(),
-                        "Meet rowkey {} with unexpected encoding format",
-                        Redact::keyToDebugString(value->data(), value->size()));
-                }
-                else
-                {
-                    if (int_value < std::numeric_limits<Int64>::max())
-                    {
-                        LOG_WARNING(
-                            Logger::get(),
-                            "Meet rowkey {} which has an extra zero suffix",
-                            Redact::keyToDebugString(value->data(), value->size()));
-                        int_value = int_value + 1;
-                        WriteBufferFromOwnString ss;
-                        DB::EncodeInt64(int_value, ss);
-                        value = std::make_shared<String>(ss.releaseStr());
-                    }
-                    else
-                    {
-                        // ignore RowKeyValue::INT_HANDLE_MAX_KEY
-                    }
-                }
-            }
-        }
-    }
-
     explicit RowKeyValue(const RowKeyValueRef & rowkey_value)
+        : is_common_handle(rowkey_value.is_common_handle)
     {
-        is_common_handle = rowkey_value.is_common_handle;
         if (is_common_handle)
             value = std::make_shared<String>(rowkey_value.data, rowkey_value.size);
         else
@@ -154,12 +105,32 @@ struct RowKeyValue
         int_value = rowkey_value.int_value;
     }
 
-    static RowKeyValue fromHandle(Handle value)
+#ifndef NDEBUG
+    // Generate from int handle
+    static RowKeyValue fromIntHandle(Handle value)
     {
         WriteBufferFromOwnString ss;
         DB::EncodeInt64(value, ss);
         return RowKeyValue(false, std::make_shared<String>(ss.releaseStr()), value);
     }
+#endif
+
+    static RowKeyValue fromHandle(bool is_common_handle_, const HandleValuePtr value_)
+    {
+        return fromHandleWithSuffix(is_common_handle_, value_).first;
+    }
+
+    /**
+     * Parse the handle from `value_`.
+     * If the handle is a common handle, the return RowKeyValue is the same with `value_`.
+     * If the handle is an int handle, the `int_value` will be the int value.
+     * Specially, for int handle, if there is any suffix rather than the regular format,
+     * a.k.a "t${tableID}_r{handleID}{AnySuffix}" `AnySuffix` is not empty, then
+     * the `int_value` will be the next int value, and the `AnySuffix` will be returned.
+     *
+     * Note that the `AnySuffix` rely on the lifetime of `value_`
+     */
+    static std::pair<RowKeyValue, std::string_view> fromHandleWithSuffix(bool is_common_handle_, HandleValuePtr value_);
 
     // Format as a string
     String toString() const;
@@ -172,18 +143,6 @@ struct RowKeyValue
         return RowKeyValueRef{is_common_handle, value->data(), value->size(), int_value};
     }
 
-    DecodedTiKVKeyPtr toRegionKey(TableID table_id) const
-    {
-        // FIXME: move this to TiKVRecordFormat.h
-        WriteBufferFromOwnString ss;
-        ss.write('t');
-        EncodeInt64(table_id, ss);
-        ss.write('_');
-        ss.write('r');
-        String prefix = ss.releaseStr();
-        return std::make_shared<DecodedTiKVKey>(prefix + *value);
-    }
-
     bool operator==(const RowKeyValue & v) const
     {
         return is_common_handle == v.is_common_handle && (*value) == (*v.value) && int_value == v.int_value;
@@ -250,7 +209,7 @@ struct RowKeyValue
         readBoolText(is_common_handle, buf);
         readStringBinary(value, buf);
         HandleValuePtr start_ptr = std::make_shared<String>(value);
-        return RowKeyValue(is_common_handle, start_ptr);
+        return RowKeyValue::fromHandle(is_common_handle, start_ptr);
     }
 
     bool is_common_handle = false;
@@ -578,8 +537,8 @@ struct RowKeyRange
             end_ptr = RowKeyValue::COMMON_HANDLE_MAX_KEY.value;
         }
         return RowKeyRange(
-            RowKeyValue(is_common_handle, start_ptr),
-            RowKeyValue(is_common_handle, end_ptr),
+            RowKeyValue::fromHandle(is_common_handle, start_ptr),
+            RowKeyValue::fromHandle(is_common_handle, end_ptr),
             is_common_handle,
             rowkey_column_size);
     }
@@ -599,8 +558,8 @@ struct RowKeyRange
             end_ptr = RowKeyValue::COMMON_HANDLE_MAX_KEY.value;
         }
         return RowKeyRange(
-            RowKeyValue(is_common_handle, start_ptr),
-            RowKeyValue(is_common_handle, end_ptr),
+            RowKeyValue::fromHandle(is_common_handle, start_ptr),
+            RowKeyValue::fromHandle(is_common_handle, end_ptr),
             is_common_handle,
             rowkey_column_size);
     }
@@ -760,9 +719,10 @@ struct RowKeyRange
             String end = ss.releaseStr();
             /// when handle_range.end == HandleRange::MAX, according to previous implementation, it should be +Inf
             return RowKeyRange(
-                RowKeyValue(is_common_handle, std::make_shared<String>(start)),
-                handle_range.end == HandleRange::MAX ? RowKeyValue::COMMON_HANDLE_MAX_KEY
-                                                     : RowKeyValue(is_common_handle, std::make_shared<String>(end)),
+                RowKeyValue::fromHandle(is_common_handle, std::make_shared<String>(start)),
+                handle_range.end == HandleRange::MAX
+                    ? RowKeyValue::COMMON_HANDLE_MAX_KEY
+                    : RowKeyValue::fromHandle(is_common_handle, std::make_shared<String>(end)),
                 /*is_common_handle=*/is_common_handle,
                 1);
         }
@@ -796,64 +756,17 @@ struct RowKeyRange
 
     static RowKeyRange fromRegionRange(
         const std::shared_ptr<const RegionRangeKeys> & region_range,
-        const TableID table_id,
+        TableID table_id,
         bool is_common_handle,
-        size_t rowkey_column_size)
-    {
-        return fromRegionRange(
-            region_range->rawKeys(),
-            region_range->getMappedTableID(),
-            table_id,
-            is_common_handle,
-            rowkey_column_size);
-    }
+        size_t rowkey_column_size,
+        const String & tracing_msg = "");
     static RowKeyRange fromRegionRange(
         const std::pair<DecodedTiKVKeyPtr, DecodedTiKVKeyPtr> & raw_keys,
-        const TableID table_id_in_raw_key,
-        const TableID table_id,
+        TableID table_id_in_raw_key,
+        TableID table_id,
         bool is_common_handle,
-        size_t rowkey_column_size)
-    {
-        if (likely(table_id_in_raw_key == table_id))
-        {
-            auto & start_key = *raw_keys.first;
-            auto & end_key = *raw_keys.second;
-            auto keyspace_id = start_key.getKeyspaceID();
-            const auto & table_range_min_max = getTableMinMaxData(keyspace_id, table_id, is_common_handle);
-            RowKeyValue start_value, end_value;
-            if (start_key <= *table_range_min_max.min)
-            {
-                if (is_common_handle)
-                    start_value = RowKeyValue::COMMON_HANDLE_MIN_KEY;
-                else
-                    start_value = RowKeyValue::INT_HANDLE_MIN_KEY;
-            }
-            else
-            {
-                start_value = RowKeyValue(
-                    is_common_handle,
-                    std::make_shared<std::string>(RecordKVFormat::getRawTiDBPKView(start_key)));
-            }
-            if (end_key >= *table_range_min_max.max)
-            {
-                if (is_common_handle)
-                    end_value = RowKeyValue::COMMON_HANDLE_MAX_KEY;
-                else
-                    end_value = RowKeyValue::INT_HANDLE_MAX_KEY;
-            }
-            else
-                end_value = RowKeyValue(
-                    is_common_handle,
-                    std::make_shared<std::string>(RecordKVFormat::getRawTiDBPKView(end_key)));
-            return RowKeyRange(start_value, end_value, is_common_handle, rowkey_column_size);
-        }
-        else
-        {
-            /// if table id is not the same, just return none range
-            /// maybe should throw exception since it should not happen
-            return newNone(is_common_handle, rowkey_column_size);
-        }
-    }
+        size_t rowkey_column_size,
+        const String & tracing_msg);
 
     // Format as a string
     String toString() const;
diff --git a/dbms/src/Storages/DeltaMerge/RowKeyRangeFromRegion.cpp b/dbms/src/Storages/DeltaMerge/RowKeyRangeFromRegion.cpp
new file mode 100644
index 00000000000..677a9e4f9f0
--- /dev/null
+++ b/dbms/src/Storages/DeltaMerge/RowKeyRangeFromRegion.cpp
@@ -0,0 +1,114 @@
+// Copyright 2025 PingCAP, Inc.
+//
+// Licensed 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.
+
+#include <Storages/DeltaMerge/RowKeyRange.h>
+#include <Storages/KVStore/MultiRaft/RegionRangeKeys.h>
+#include <Storages/KVStore/Region.h>
+
+namespace DB::DM
+{
+
+RowKeyRange RowKeyRange::fromRegionRange(
+    const std::shared_ptr<const RegionRangeKeys> & region_range,
+    const TableID table_id,
+    bool is_common_handle,
+    size_t rowkey_column_size,
+    const String & tracing_msg)
+{
+    return fromRegionRange(
+        region_range->rawKeys(),
+        region_range->getMappedTableID(),
+        table_id,
+        is_common_handle,
+        rowkey_column_size,
+        tracing_msg);
+}
+
+RowKeyRange RowKeyRange::fromRegionRange(
+    const std::pair<DecodedTiKVKeyPtr, DecodedTiKVKeyPtr> & raw_keys,
+    const TableID table_id_in_raw_key,
+    const TableID table_id,
+    bool is_common_handle,
+    size_t rowkey_column_size,
+    const String & tracing_msg)
+{
+    if (unlikely(table_id_in_raw_key != table_id))
+    {
+        /// if table id is not the same, just return none range
+        /// maybe should throw exception since it should not happen
+        return newNone(is_common_handle, rowkey_column_size);
+    }
+
+    const auto & start_key = *raw_keys.first;
+    const auto & end_key = *raw_keys.second;
+    const auto keyspace_id = start_key.getKeyspaceID();
+    const auto & table_range_min_max = getTableMinMaxData(keyspace_id, table_id, is_common_handle);
+    RowKeyValue start_value, end_value;
+    std::string warning_message;
+    std::string_view extra_suffix_holder;
+    if (start_key <= *table_range_min_max.min)
+    {
+        if (is_common_handle)
+            start_value = RowKeyValue::COMMON_HANDLE_MIN_KEY;
+        else
+            start_value = RowKeyValue::INT_HANDLE_MIN_KEY;
+    }
+    else
+    {
+        // keep the ptr to handle because `extra_suffix_holder` rely on this instance lifetime
+        auto start_handle = std::make_shared<std::string>(RecordKVFormat::getRawTiDBPKView(start_key));
+        std::tie(start_value, extra_suffix_holder) = RowKeyValue::fromHandleWithSuffix(is_common_handle, start_handle);
+        if (unlikely(!extra_suffix_holder.empty()))
+        {
+            warning_message = fmt::format(
+                "{} start_key={} start_key_extra_suffix={}",
+                warning_message,
+                Redact::keyToDebugString(start_key.data(), start_key.size()),
+                Redact::keyToHexString(extra_suffix_holder.data(), extra_suffix_holder.size()));
+        }
+    }
+    if (end_key >= *table_range_min_max.max)
+    {
+        if (is_common_handle)
+            end_value = RowKeyValue::COMMON_HANDLE_MAX_KEY;
+        else
+            end_value = RowKeyValue::INT_HANDLE_MAX_KEY;
+    }
+    else
+    {
+        // keep the ptr to handle because `extra_suffix_holder` rely on this instance lifetime
+        auto end_handle = std::make_shared<std::string>(RecordKVFormat::getRawTiDBPKView(end_key));
+        std::tie(end_value, extra_suffix_holder) = RowKeyValue::fromHandleWithSuffix(is_common_handle, end_handle);
+        if (unlikely(!extra_suffix_holder.empty()))
+        {
+            warning_message = fmt::format(
+                "{} end_key={} end_key_extra_suffix={}",
+                warning_message,
+                Redact::keyToDebugString(end_key.data(), end_key.size()),
+                Redact::keyToHexString(extra_suffix_holder.data(), extra_suffix_holder.size()));
+        }
+    }
+    if (unlikely(!warning_message.empty()))
+    {
+        LOG_WARNING(
+            Logger::get(),
+            "Meet rowkey which has extra suffix, keyspace={} table_id={} {} {}",
+            keyspace_id,
+            table_id,
+            warning_message,
+            tracing_msg);
+    }
+    return RowKeyRange(start_value, end_value, is_common_handle, rowkey_column_size);
+}
+} // namespace DB::DM
diff --git a/dbms/src/Storages/DeltaMerge/tests/DMTestEnv.h b/dbms/src/Storages/DeltaMerge/tests/DMTestEnv.h
index 697f37cc3ed..884d8ed84ed 100644
--- a/dbms/src/Storages/DeltaMerge/tests/DMTestEnv.h
+++ b/dbms/src/Storages/DeltaMerge/tests/DMTestEnv.h
@@ -538,8 +538,9 @@ class DMTestEnv
     static RowKeyRange getRowKeyRangeForClusteredIndex(Int64 start, Int64 end, size_t rowkey_column_size)
     {
         RowKeyValue start_key
-            = RowKeyValue(true, std::make_shared<String>(genMockCommonHandle(start, rowkey_column_size)));
-        RowKeyValue end_key = RowKeyValue(true, std::make_shared<String>(genMockCommonHandle(end, rowkey_column_size)));
+            = RowKeyValue::fromHandle(true, std::make_shared<String>(genMockCommonHandle(start, rowkey_column_size)));
+        RowKeyValue end_key
+            = RowKeyValue::fromHandle(true, std::make_shared<String>(genMockCommonHandle(end, rowkey_column_size)));
         return RowKeyRange(start_key, end_key, true, rowkey_column_size);
     }
 
diff --git a/dbms/src/Storages/DeltaMerge/tests/gtest_dm_delta_merge_store.cpp b/dbms/src/Storages/DeltaMerge/tests/gtest_dm_delta_merge_store.cpp
index 75a96170d24..cccafe524b3 100644
--- a/dbms/src/Storages/DeltaMerge/tests/gtest_dm_delta_merge_store.cpp
+++ b/dbms/src/Storages/DeltaMerge/tests/gtest_dm_delta_merge_store.cpp
@@ -3407,9 +3407,7 @@ try
     // Delete range [0, 64)
     const size_t num_deleted_rows = 64;
     {
-        RowKeyValue start(true, std::make_shared<String>(genMockCommonHandle(0, rowkey_column_size)));
-        RowKeyValue end(true, std::make_shared<String>(genMockCommonHandle(num_deleted_rows, rowkey_column_size)));
-        RowKeyRange range(start, end, true, rowkey_column_size);
+        auto range = DMTestEnv::getRowKeyRangeForClusteredIndex(0, num_deleted_rows, rowkey_column_size);
         store->deleteRange(*db_context, db_context->getSettingsRef(), range);
     }
     // Read after deletion
diff --git a/dbms/src/Storages/DeltaMerge/tests/gtest_dm_segment.cpp b/dbms/src/Storages/DeltaMerge/tests/gtest_dm_segment.cpp
index d512204976e..cf8f74433e5 100644
--- a/dbms/src/Storages/DeltaMerge/tests/gtest_dm_segment.cpp
+++ b/dbms/src/Storages/DeltaMerge/tests/gtest_dm_segment.cpp
@@ -1961,8 +1961,8 @@ try
         ASSERT_EQ(stable->getRows(), num_rows_write_every_round * write_round);
         // calculate StableProperty
         ASSERT_EQ(stable->isStablePropertyCached(), false);
-        auto start = RowKeyValue::fromHandle(0);
-        auto end = RowKeyValue::fromHandle(num_rows_write_every_round);
+        auto start = RowKeyValue::fromIntHandle(0);
+        auto end = RowKeyValue::fromIntHandle(num_rows_write_every_round);
         RowKeyRange range(start, end, false, 1);
         // calculate the StableProperty for packs in the key range [0, num_rows_write_every_round)
         stable->calculateStableProperty(dmContext(), range, false);
@@ -2012,10 +2012,10 @@ try
         // clear PackProperties to force it to calculate from scratch
         dmfile->clearPackProperties();
         ASSERT_EQ(dmfile->getPackProperties().property_size(), 0);
-        // caculate StableProperty
+        // calculate StableProperty
         ASSERT_EQ(stable->isStablePropertyCached(), false);
-        auto start = RowKeyValue::fromHandle(0);
-        auto end = RowKeyValue::fromHandle(num_rows_write_every_round);
+        auto start = RowKeyValue::fromIntHandle(0);
+        auto end = RowKeyValue::fromIntHandle(num_rows_write_every_round);
         RowKeyRange range(start, end, false, 1);
         // calculate the StableProperty for packs in the key range [0, num_rows_write_every_round)
         stable->calculateStableProperty(dmContext(), range, false);
diff --git a/dbms/src/Storages/DeltaMerge/tests/gtest_dm_simple_pk_test_basic.cpp b/dbms/src/Storages/DeltaMerge/tests/gtest_dm_simple_pk_test_basic.cpp
index 4fba87f6c09..03d4770dec8 100644
--- a/dbms/src/Storages/DeltaMerge/tests/gtest_dm_simple_pk_test_basic.cpp
+++ b/dbms/src/Storages/DeltaMerge/tests/gtest_dm_simple_pk_test_basic.cpp
@@ -129,7 +129,7 @@ std::vector<Int64> SimplePKTestBasic::getSegmentBreakpoints() const
 RowKeyValue SimplePKTestBasic::buildRowKey(Int64 pk) const
 {
     if (!is_common_handle)
-        return RowKeyValue::fromHandle(pk);
+        return RowKeyValue::fromIntHandle(pk);
 
     WriteBufferFromOwnString ss;
     ::DB::EncodeUInt(static_cast<UInt8>(TiDB::CodecFlagInt), ss);
diff --git a/dbms/src/Storages/DeltaMerge/tests/gtest_key_range.cpp b/dbms/src/Storages/DeltaMerge/tests/gtest_key_range.cpp
index b1aa8a7510d..1a56feaa285 100644
--- a/dbms/src/Storages/DeltaMerge/tests/gtest_key_range.cpp
+++ b/dbms/src/Storages/DeltaMerge/tests/gtest_key_range.cpp
@@ -12,11 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include <Common/RandomData.h>
 #include <Storages/DeltaMerge/Range.h>
 #include <Storages/DeltaMerge/RowKeyRange.h>
+#include <Storages/KVStore/MultiRaft/RegionRangeKeys.h>
+#include <Storages/KVStore/Types.h>
 #include <TestUtils/TiFlashTestBasic.h>
 #include <TiDB/Schema/TiDB.h>
 
+#include <limits>
+#include <random>
+
 namespace DB::DM::tests
 {
 
@@ -111,37 +117,96 @@ TEST(HandleRangeTest, RedactRangeFromCommonHandle)
 
 TEST(RowKey, ToNextKeyIntHandle)
 {
-    const auto key = RowKeyValue::fromHandle(20);
+    const TableID table_id = 1;
+    const auto key = RowKeyValue::fromIntHandle(20);
     const auto next = key.toNext();
     EXPECT_EQ("21", next.toDebugString());
 
     {
-        const auto expected_next_int = RowKeyValue::fromHandle(21);
+        const auto expected_next_int = RowKeyValue::fromIntHandle(21);
         EXPECT_EQ(next.toRowKeyValueRef(), expected_next_int.toRowKeyValueRef());
     }
     {
-        const auto range_keys
-            = std::make_shared<RegionRangeKeys>(RecordKVFormat::genKey(1, 0), RecordKVFormat::genKey(1, 21));
+        const auto range_keys = std::make_shared<RegionRangeKeys>(
+            RecordKVFormat::genKey(table_id, 0),
+            RecordKVFormat::genKey(table_id, 21));
         const auto range = RowKeyRange::fromRegionRange(
             range_keys,
-            /* table_id */ 1,
+            /* table_id */ table_id,
             /* is_common_handle */ false,
             /* row_key_column_size */ 1);
         EXPECT_EQ(next.toRowKeyValueRef(), range.getEnd());
+        EXPECT_EQ(range.getEnd().toDebugString(), "21");
+    }
+
+    // The whole key range of a specified table_id
+    {
+        const auto range_keys = std::make_shared<RegionRangeKeys>(
+            RecordKVFormat::genTableRecordStartKey(table_id),
+            RecordKVFormat::genTableRecordEndKey(table_id));
+        const auto range = RowKeyRange::fromRegionRange(
+            range_keys,
+            /* table_id */ table_id,
+            /* is_common_handle */ false,
+            /* row_key_column_size */ 1);
+        EXPECT_EQ(range.toDebugString(), "[-9223372036854775808,9223372036854775807)");
     }
+
+    // tiflash#7762
     // Note: {20,00} will be regarded as Key=21 in RowKeyRange::fromRegionRange.
     {
-        auto key_end = RecordKVFormat::genRawKey(1, 20);
-        key_end.push_back(0);
-        auto tikv_key_end = RecordKVFormat::encodeAsTiKVKey(key_end);
-        const auto range_keys
-            = std::make_shared<RegionRangeKeys>(RecordKVFormat::genKey(1, 0), std::move(tikv_key_end));
+        auto key_end = RecordKVFormat::genRawKey(table_id, 20);
+        key_end.push_back('\x00');
+        const auto range_keys = std::make_shared<RegionRangeKeys>(
+            RecordKVFormat::genKey(table_id, 0),
+            RecordKVFormat::encodeAsTiKVKey(key_end));
+        const auto range = RowKeyRange::fromRegionRange(
+            range_keys,
+            /* table_id */ table_id,
+            /* is_common_handle */ false,
+            /* row_key_column_size */ 1);
+        EXPECT_EQ(next.toRowKeyValueRef(), range.getEnd());
+        EXPECT_EQ(range.getEnd().toDebugString(), "21");
+    }
+    // tiflash#10147
+    // Note: {20,01} will be regarded as Key=21 in RowKeyRange::fromRegionRange.
+    {
+        auto key_end = RecordKVFormat::genRawKey(table_id, 20);
+        key_end.push_back('\x01');
+        const auto range_keys = std::make_shared<RegionRangeKeys>(
+            RecordKVFormat::genKey(table_id, 0),
+            RecordKVFormat::encodeAsTiKVKey(key_end));
+        const auto range = RowKeyRange::fromRegionRange(
+            range_keys,
+            /* table_id */ table_id,
+            /* is_common_handle */ false,
+            /* row_key_column_size */ 1);
+        EXPECT_EQ(next.toRowKeyValueRef(), range.getEnd());
+        EXPECT_EQ(range.getEnd().toDebugString(), "21");
+    }
+    // any suffix will be regarded as Key=21 in RowKeyRange::fromRegionRange.
+    {
+        auto key_end = RecordKVFormat::genRawKey(table_id, 20);
+        std::mt19937_64 rand_gen(std::random_device{}());
+        size_t rand_length = rand_gen() % 255;
+        auto rand_suffix = DB::random::randomString(rand_length);
+        key_end.insert(key_end.end(), rand_suffix.begin(), rand_suffix.end());
+        LOG_INFO(
+            Logger::get(),
+            "key_end={} rand_suffix={}",
+            key_end.toDebugString(),
+            Redact::keyToDebugString(rand_suffix.data(), rand_length));
+
+        const auto range_keys = std::make_shared<RegionRangeKeys>(
+            RecordKVFormat::genKey(table_id, 0),
+            RecordKVFormat::encodeAsTiKVKey(key_end));
         const auto range = RowKeyRange::fromRegionRange(
             range_keys,
-            /* table_id */ 1,
+            /* table_id */ table_id,
             /* is_common_handle */ false,
             /* row_key_column_size */ 1);
         EXPECT_EQ(next.toRowKeyValueRef(), range.getEnd());
+        EXPECT_EQ(range.getEnd().toDebugString(), "21");
     }
 }
 
@@ -160,7 +225,7 @@ TEST(RowKey, ToNextKeyCommonHandle)
 TEST(RowKey, NextIntHandleCompare)
 {
     auto int_max = RowKeyValue::INT_HANDLE_MAX_KEY;
-    auto int_max_i64 = RowKeyValue::fromHandle(static_cast<Handle>(std::numeric_limits<HandleID>::max()));
+    auto int_max_i64 = RowKeyValue::fromIntHandle(static_cast<Handle>(std::numeric_limits<HandleID>::max()));
 
     EXPECT_GT(int_max.toRowKeyValueRef(), int_max_i64.toRowKeyValueRef());
 
@@ -177,9 +242,9 @@ TEST(RowKey, NextIntHandleCompare)
 
 TEST(RowKey, NextIntHandleMinMax)
 {
-    auto v0 = RowKeyValue::fromHandle(static_cast<Handle>(1178400));
+    auto v0 = RowKeyValue::fromIntHandle(static_cast<Handle>(1178400));
     auto v0_next = v0.toNext();
-    auto v1 = RowKeyValue::fromHandle(static_cast<Handle>(1178401));
+    auto v1 = RowKeyValue::fromIntHandle(static_cast<Handle>(1178401));
 
     EXPECT_EQ(v0, std::min(v0, v1));
     EXPECT_EQ(v0, std::min(v0, v0_next));
diff --git a/dbms/src/Storages/DeltaMerge/tests/gtest_segment_test_basic.cpp b/dbms/src/Storages/DeltaMerge/tests/gtest_segment_test_basic.cpp
index 29e663b1812..76540fd0949 100644
--- a/dbms/src/Storages/DeltaMerge/tests/gtest_segment_test_basic.cpp
+++ b/dbms/src/Storages/DeltaMerge/tests/gtest_segment_test_basic.cpp
@@ -212,7 +212,7 @@ std::optional<PageIdU64> SegmentTestBasic::splitSegmentAt(
     }
     else
     {
-        split_at_key = RowKeyValue::fromHandle(split_at);
+        split_at_key = RowKeyValue::fromIntHandle(split_at);
     }
 
     RUNTIME_CHECK(segments.find(segment_id) != segments.end());
@@ -1213,14 +1213,14 @@ RowKeyRange SegmentTestBasic::buildRowKeyRange(
             DB::EncodeInt64(v, ss);
             return std::make_shared<String>(ss.releaseStr());
         };
-        auto left = RowKeyValue{is_common_handle, create_rowkey_value(begin)};
+        auto left = RowKeyValue::fromHandle(is_common_handle, create_rowkey_value(begin));
         auto right = including_right_boundary ? RowKeyValue::COMMON_HANDLE_MAX_KEY
-                                              : RowKeyValue{is_common_handle, create_rowkey_value(end)};
+                                              : RowKeyValue::fromHandle(is_common_handle, create_rowkey_value(end));
         return RowKeyRange{left, right, is_common_handle, 1};
     }
 
-    auto left = RowKeyValue::fromHandle(begin);
-    auto right = including_right_boundary ? RowKeyValue::INT_HANDLE_MAX_KEY : RowKeyValue::fromHandle(end);
+    auto left = RowKeyValue::fromIntHandle(begin);
+    auto right = including_right_boundary ? RowKeyValue::INT_HANDLE_MAX_KEY : RowKeyValue::fromIntHandle(end);
     return RowKeyRange{left, right, is_common_handle, 1};
 }
 
diff --git a/dbms/src/Storages/KVStore/Decode/RegionTable.cpp b/dbms/src/Storages/KVStore/Decode/RegionTable.cpp
index 75e10b8bb34..74e5036bd60 100644
--- a/dbms/src/Storages/KVStore/Decode/RegionTable.cpp
+++ b/dbms/src/Storages/KVStore/Decode/RegionTable.cpp
@@ -193,6 +193,7 @@ namespace
 /// Note that this function will try to acquire lock by `IStorage->lockForShare`
 void removeObsoleteDataInStorage(
     Context * const context,
+    RegionID region_id,
     const KeyspaceTableID ks_table_id,
     const std::pair<DecodedTiKVKeyPtr, DecodedTiKVKeyPtr> & handle_range)
 {
@@ -217,7 +218,8 @@ void removeObsoleteDataInStorage(
             ks_table_id.second,
             ks_table_id.second,
             storage->isCommonHandle(),
-            storage->getRowKeyColumnSize());
+            storage->getRowKeyColumnSize(),
+            fmt::format("region_id={} removeObsoleteDataInStorage", region_id));
         dm_storage->deleteRange(rowkey_range, context->getSettingsRef());
         dm_storage->flushCache(*context, rowkey_range, /*try_until_succeed*/ true); // flush to disk
     }
@@ -270,7 +272,7 @@ void RegionTable::removeRegion(const RegionID region_id, bool remove_data, const
         // But caller(KVStore) should ensure that no new data write into this handle_range
         // before `removeObsoleteDataInStorage` is done. (by param `RegionTaskLock`)
         // And this is expected not to block for long time.
-        removeObsoleteDataInStorage(context, ks_table_id, handle_range);
+        removeObsoleteDataInStorage(context, region_id, ks_table_id, handle_range);
         LOG_INFO(log, "remove region in storage done, region_id={}", region_id);
     }
 }
diff --git a/dbms/src/Storages/KVStore/KVStore.cpp b/dbms/src/Storages/KVStore/KVStore.cpp
index c3508f44128..21fe33649da 100644
--- a/dbms/src/Storages/KVStore/KVStore.cpp
+++ b/dbms/src/Storages/KVStore/KVStore.cpp
@@ -33,6 +33,7 @@
 #include <Storages/KVStore/Types.h>
 #include <Storages/StorageDeltaMerge.h>
 #include <common/likely.h>
+#include <fmt/core.h>
 
 #include <mutex>
 #include <tuple>
@@ -216,9 +217,10 @@ bool KVStore::tryFlushRegionCacheInStorage(
         auto storage_lock = storage->lockForShare(getThreadNameAndID());
         auto rowkey_range = DM::RowKeyRange::fromRegionRange(
             region.getRange(),
-            region.getRange()->getMappedTableID(),
+            table_id,
             storage->isCommonHandle(),
-            storage->getRowKeyColumnSize());
+            storage->getRowKeyColumnSize(),
+            fmt::format("region {} KVStore::tryFlushRegionCacheInStorage", region.toString()));
         return storage->flushCache(tmt.getContext(), rowkey_range, try_until_succeed);
     }
     catch (DB::Exception & e)
diff --git a/dbms/src/Storages/KVStore/MultiRaft/ApplySnapshot.cpp b/dbms/src/Storages/KVStore/MultiRaft/ApplySnapshot.cpp
index 3d013a119e9..109c9a66cd5 100644
--- a/dbms/src/Storages/KVStore/MultiRaft/ApplySnapshot.cpp
+++ b/dbms/src/Storages/KVStore/MultiRaft/ApplySnapshot.cpp
@@ -230,14 +230,16 @@ void KVStore::onSnapshot(
                     new_region_wrap->getRange(),
                     table_id,
                     storage->isCommonHandle(),
-                    storage->getRowKeyColumnSize());
+                    storage->getRowKeyColumnSize(),
+                    fmt::format("new_region_id={} KVStore::onSnapshot", region_id));
                 if (old_region)
                 {
                     auto old_key_range = DM::RowKeyRange::fromRegionRange(
                         old_region->getRange(),
                         table_id,
                         storage->isCommonHandle(),
-                        storage->getRowKeyColumnSize());
+                        storage->getRowKeyColumnSize(),
+                        fmt::format("old_region_id={} KVStore::onSnapshot", region_id));
                     if (old_key_range != new_key_range)
                     {
                         LOG_INFO(
diff --git a/dbms/src/Storages/KVStore/MultiRaft/Disagg/FastAddPeer.cpp b/dbms/src/Storages/KVStore/MultiRaft/Disagg/FastAddPeer.cpp
index 6cca6ef7ebb..7ae0e82d03a 100644
--- a/dbms/src/Storages/KVStore/MultiRaft/Disagg/FastAddPeer.cpp
+++ b/dbms/src/Storages/KVStore/MultiRaft/Disagg/FastAddPeer.cpp
@@ -362,7 +362,8 @@ FastAddPeerRes FastAddPeerImplWrite(
         region->getRange(),
         table_id,
         storage->isCommonHandle(),
-        storage->getRowKeyColumnSize());
+        storage->getRowKeyColumnSize(),
+        fmt::format("region_id={} new_peer_id={} FastAddPeerImplWrite", region_id, new_peer_id));
 
     if (cancel_handle->isCanceled())
     {
diff --git a/dbms/src/Storages/KVStore/MultiRaft/IngestSST.cpp b/dbms/src/Storages/KVStore/MultiRaft/IngestSST.cpp
index ede8a961308..23bff33b895 100644
--- a/dbms/src/Storages/KVStore/MultiRaft/IngestSST.cpp
+++ b/dbms/src/Storages/KVStore/MultiRaft/IngestSST.cpp
@@ -148,7 +148,12 @@ RegionPtr KVStore::handleIngestSSTByDTFile(
                     region->getRange(),
                     table_id,
                     storage->isCommonHandle(),
-                    storage->getRowKeyColumnSize());
+                    storage->getRowKeyColumnSize(),
+                    fmt::format(
+                        "region_id={} index={} term={} KVStore::handleIngestSSTByDTFile",
+                        region->id(),
+                        index,
+                        term));
                 // Call `ingestFiles` to ingest external DTFiles.
                 // Note that ingest sst won't remove the data in the key range
                 auto dm_storage = std::dynamic_pointer_cast<StorageDeltaMerge>(storage);
diff --git a/dbms/src/Storages/KVStore/TiKVHelpers/TiKVRecordFormat.h b/dbms/src/Storages/KVStore/TiKVHelpers/TiKVRecordFormat.h
index 5055b6c06df..78eadace43c 100644
--- a/dbms/src/Storages/KVStore/TiKVHelpers/TiKVRecordFormat.h
+++ b/dbms/src/Storages/KVStore/TiKVHelpers/TiKVRecordFormat.h
@@ -155,6 +155,27 @@ inline TiKVKey genKey(const TableID tableId, const HandleID handleId)
 
 TiKVKey genKey(const TiDB::TableInfo & table_info, std::vector<Field> keys);
 
+inline TiKVKey genTableRecordStartKey(const TableID tableId)
+{
+    // The key format is "t${tableId}_r"
+    std::string key(RecordKVFormat::RAW_KEY_NO_HANDLE_SIZE, 0);
+    memcpy(key.data(), &RecordKVFormat::TABLE_PREFIX, 1);
+    auto big_endian_table_id = encodeInt64(tableId);
+    memcpy(key.data() + 1, reinterpret_cast<const char *>(&big_endian_table_id), 8);
+    memcpy(key.data() + 1 + 8, RecordKVFormat::RECORD_PREFIX_SEP, 2);
+    return encodeAsTiKVKey(key);
+}
+
+inline TiKVKey genTableRecordEndKey(const TableID tableId)
+{
+    // The key format is "t${tableId+1}"
+    std::string key(1 + 8, 0);
+    memcpy(key.data(), &RecordKVFormat::TABLE_PREFIX, 1);
+    auto big_endian_table_id = encodeInt64(tableId + 1);
+    memcpy(key.data() + 1, reinterpret_cast<const char *>(&big_endian_table_id), 8);
+    return encodeAsTiKVKey(key);
+}
+
 inline bool checkKeyPaddingValid(const char * ptr, const UInt8 pad_size)
 {
     UInt64 p = (*reinterpret_cast<const UInt64 *>(ptr)) >> ((ENC_GROUP_SIZE - pad_size) * 8);
diff --git a/dbms/src/Storages/StorageDeltaMergeHelpers.h b/dbms/src/Storages/StorageDeltaMergeHelpers.h
index 78823bf917f..77965115727 100644
--- a/dbms/src/Storages/StorageDeltaMergeHelpers.h
+++ b/dbms/src/Storages/StorageDeltaMergeHelpers.h
@@ -20,8 +20,6 @@
 #include <Storages/KVStore/Decode/TiKVHandle.h>
 #include <Storages/RegionQueryInfo.h>
 
-#include <algorithm>
-#include <numeric>
 #include <vector>
 
 namespace DB
@@ -51,7 +49,8 @@ inline DM::RowKeyRanges getQueryRanges(
                     table_id,
                     table_id,
                     is_common_handle,
-                    rowkey_column_size));
+                    rowkey_column_size,
+                    fmt::format("region_id={} getQueryRanges", region_info.region_id)));
         }
         else
         {
@@ -61,7 +60,8 @@ inline DM::RowKeyRanges getQueryRanges(
                 table_id,
                 table_id,
                 is_common_handle,
-                rowkey_column_size));
+                rowkey_column_size,
+                fmt::format("region_id={} getQueryRanges", region_info.region_id)));
         }
     }
     if (ranges.empty())


Reviews:
user: login: Lloyd-Pottiger
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T02:45:42Z
user: login: CalvinNeo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T03:41:48Z
user: login: CalvinNeo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T03:44:57Z
user: login: CalvinNeo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T03:54:50Z
user: login: JaySon-Huang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T04:31:42Z
user: login: JaySon-Huang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T04:50:42Z
user: login: CalvinNeo
body: LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-09T05:45:37Z
user: login: JaySon-Huang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T08:47:21Z
user: login: JaySon-Huang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T09:42:18Z

Review comments:
diff_hunk: @@ -46,6 +46,59 @@ const RowKeyValue RowKeyValue::COMMON_HANDLE_MAX_KEY
     = RowKeyValue(true, std::make_shared<String>(1, TiDB::CodecFlag::CodecFlagMax), 0);
 const RowKeyValue RowKeyValue::EMPTY_STRING_KEY = RowKeyValue(true, std::make_shared<String>(""), 0);
 
+std::pair<RowKeyValue, std::string_view> RowKeyValue::fromHandleWithSuffix(
+    bool is_common_handle_,
+    const HandleValuePtr value_)
+{
+    if (is_common_handle_)
+    {
+        RowKeyValue rowkey_value(is_common_handle_, value_, 0);
+        return {rowkey_value, std::string_view{nullptr, 0}};
+    }
+
+    assert(!is_common_handle_);
+    // According to the tidb encoding rule, the int handle must be larger than 8 bytes.
+    RUNTIME_CHECK_MSG(
path: dbms/src/Storages/DeltaMerge/RowKeyRange.cpp
user: login: CalvinNeo
body: So we allow(I mean `RUNTIME_CHECK_MSG`) an encoded handle longer than sizeof(int64) here, but will still panic if we meet an encoded handle shorter than sizeof(int64)
created_at: 2025-05-09T03:41:48Z
author_association: MEMBER
diff_hunk: @@ -46,6 +46,59 @@ const RowKeyValue RowKeyValue::COMMON_HANDLE_MAX_KEY
     = RowKeyValue(true, std::make_shared<String>(1, TiDB::CodecFlag::CodecFlagMax), 0);
 const RowKeyValue RowKeyValue::EMPTY_STRING_KEY = RowKeyValue(true, std::make_shared<String>(""), 0);
 
+std::pair<RowKeyValue, std::string_view> RowKeyValue::fromHandleWithSuffix(
+    bool is_common_handle_,
+    const HandleValuePtr value_)
+{
+    if (is_common_handle_)
+    {
+        RowKeyValue rowkey_value(is_common_handle_, value_, 0);
+        return {rowkey_value, std::string_view{nullptr, 0}};
+    }
+
+    assert(!is_common_handle_);
+    // According to the tidb encoding rule, the int handle must be larger than 8 bytes.
+    RUNTIME_CHECK_MSG(
+        value_->size() >= sizeof(Int64),
+        "Meet illegal int handle size less than expected, value_size={} value={}",
+        value_->size(),
+        Redact::keyToHexString(value_->data(), value_->size()));
+
+    size_t cursor = 0;
+    Int64 int_value = DB::DecodeInt64(cursor, *value_);
+    if (unlikely(value_->size() > sizeof(UInt64)))
+    {
+        // For int type handle, the standard key encoding format should be t{table_id}_r{handle_value}.
+        // But TiKV may generate region range keys which are not strictly following the standard format.
+        // More concretely, the key may be t{table_id}_r{handle_value}{any_suffix}.
+        // We need to adapt the key to the standard format.
+        // For example, the key may be t100_r1000 + '0x00', '0x01' or any other suffix, we need to adapt it to t100_r1001.
+        // This is ok, because
+        //  1) if the key is the start range, then [t100_r1000 + 0x00, xxx) has the same semantics with [t100_r1001, xxx)
+        //  2) if the key is the end range, then [xxx, t100_r1000 + 0x00) also has the same semantics with [xxx, t100_r1001)
+        //
+        // Note if the `int_value` is Int64::max_value,
+        // it is a value generated by tiflash itself to means +inf()(which is RowKeyValue::INT_HANDLE_MAX_KEY).
+        // So we can just ignore it.
+        if (int_value < std::numeric_limits<Int64>::max())
+        {
+            int_value = int_value + 1;
+            WriteBufferFromOwnString ss;
+            DB::EncodeInt64(int_value, ss);
+            HandleValuePtr fixed_value = std::make_shared<String>(ss.releaseStr());
+            return {
+                RowKeyValue(is_common_handle_, fixed_value, int_value),
+                std::string_view{value_->data() + sizeof(int_value), value_->size() - sizeof(int_value)},
+            };
+        }
+        else
+        {
+            // ignore RowKeyValue::INT_HANDLE_MAX_KEY
path: dbms/src/Storages/DeltaMerge/RowKeyRange.cpp
user: login: CalvinNeo
body: Seems in this case, the right bound is the right border of the table's key range?
```
t{table_id}_r{+inf}{any_suffix}

==>

INT_HANDLE_MAX_KEY
```

 
created_at: 2025-05-09T03:44:56Z
author_association: MEMBER
diff_hunk: @@ -154,13 +105,29 @@ struct RowKeyValue
         int_value = rowkey_value.int_value;
     }
 
+    // Generate from int handle
     static RowKeyValue fromHandle(Handle value)
path: dbms/src/Storages/DeltaMerge/RowKeyRange.h
user: login: CalvinNeo
body: Seems this function is only used in test, just wrap it with `DBMS_PUBLIC_GTEST`? So there will be a single `fromHandle`.
created_at: 2025-05-09T03:54:50Z
author_association: MEMBER
diff_hunk: @@ -46,6 +46,59 @@ const RowKeyValue RowKeyValue::COMMON_HANDLE_MAX_KEY
     = RowKeyValue(true, std::make_shared<String>(1, TiDB::CodecFlag::CodecFlagMax), 0);
 const RowKeyValue RowKeyValue::EMPTY_STRING_KEY = RowKeyValue(true, std::make_shared<String>(""), 0);
 
+std::pair<RowKeyValue, std::string_view> RowKeyValue::fromHandleWithSuffix(
+    bool is_common_handle_,
+    const HandleValuePtr value_)
+{
+    if (is_common_handle_)
+    {
+        RowKeyValue rowkey_value(is_common_handle_, value_, 0);
+        return {rowkey_value, std::string_view{nullptr, 0}};
+    }
+
+    assert(!is_common_handle_);
+    // According to the tidb encoding rule, the int handle must be larger than 8 bytes.
+    RUNTIME_CHECK_MSG(
path: dbms/src/Storages/DeltaMerge/RowKeyRange.cpp
user: login: JaySon-Huang
body: Yes, according to @tangenta this case should not happen. If it happens, then tidb-server would panic.
created_at: 2025-05-09T04:31:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -154,13 +105,29 @@ struct RowKeyValue
         int_value = rowkey_value.int_value;
     }
 
+    // Generate from int handle
     static RowKeyValue fromHandle(Handle value)
path: dbms/src/Storages/DeltaMerge/RowKeyRange.h
user: login: JaySon-Huang
body: done
created_at: 2025-05-09T04:50:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,6 +46,59 @@ const RowKeyValue RowKeyValue::COMMON_HANDLE_MAX_KEY
     = RowKeyValue(true, std::make_shared<String>(1, TiDB::CodecFlag::CodecFlagMax), 0);
 const RowKeyValue RowKeyValue::EMPTY_STRING_KEY = RowKeyValue(true, std::make_shared<String>(""), 0);
 
+std::pair<RowKeyValue, std::string_view> RowKeyValue::fromHandleWithSuffix(
+    bool is_common_handle_,
+    const HandleValuePtr value_)
+{
+    if (is_common_handle_)
+    {
+        RowKeyValue rowkey_value(is_common_handle_, value_, 0);
+        return {rowkey_value, std::string_view{nullptr, 0}};
+    }
+
+    assert(!is_common_handle_);
+    // According to the tidb encoding rule, the int handle must be larger than 8 bytes.
+    RUNTIME_CHECK_MSG(
path: dbms/src/Storages/DeltaMerge/RowKeyRange.cpp
user: login: JaySon-Huang
body: And if the value_size is less than 8 bytes, then the `DB::DecodeInt64` will likely access random invalid address, which will cause random panic.
Adding a check here so that we can get explicit error rather than random panic.
created_at: 2025-05-09T08:47:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,6 +46,59 @@ const RowKeyValue RowKeyValue::COMMON_HANDLE_MAX_KEY
     = RowKeyValue(true, std::make_shared<String>(1, TiDB::CodecFlag::CodecFlagMax), 0);
 const RowKeyValue RowKeyValue::EMPTY_STRING_KEY = RowKeyValue(true, std::make_shared<String>(""), 0);
 
+std::pair<RowKeyValue, std::string_view> RowKeyValue::fromHandleWithSuffix(
+    bool is_common_handle_,
+    const HandleValuePtr value_)
+{
+    if (is_common_handle_)
+    {
+        RowKeyValue rowkey_value(is_common_handle_, value_, 0);
+        return {rowkey_value, std::string_view{nullptr, 0}};
+    }
+
+    assert(!is_common_handle_);
+    // According to the tidb encoding rule, the int handle must be larger than 8 bytes.
+    RUNTIME_CHECK_MSG(
path: dbms/src/Storages/DeltaMerge/RowKeyRange.cpp
user: login: JaySon-Huang
body: I've try to generate a split key like
* ` t{tableID}_r\x30`
* ` t{tableID}_r\x00\x00\x00\x00\x00\x00\x01`

TiKV will refuse to split, with logging like
```
[2025/05/09 17:41:36.242 +08:00] [INFO] [peer.rs:6460] ["invalid split request"] [source=pd] [peer_id=2078] [region_id=2077] [err="KeyNotInRegion([116, 128, 0, 0, 0, 0, 0, 1, 255, 255, 215, 95, 114, 0, 0, 0, 0, 255, 0, 250, 95, 114, 0, 0, 0, 0, 255, 0, 0, 1, 0, 0, 0, 0, 0, 250], id: 2077 start_key: 7480000000000001FFD75F730000000000FA end_key: 7480000000000001FFD800000000000000F8 region_epoch { conf_ver: 26 version: 217 } peers { id: 2078 store_id: 1 } peers { id: 2166 store_id: 132 role: Learner })"] [thread_id=377]
```
created_at: 2025-05-09T09:42:18Z
author_association: CONTRIBUTOR

PR comments:
user: login: JaySon-Huang
created_at: 2025-05-08T07:59:10Z
body: /test pull-integration-test
author_association: CONTRIBUTOR
user: login: JaySon-Huang
created_at: 2025-05-09T05:07:28Z
body: /hold
author_association: CONTRIBUTOR
user: login: ti-chi-bot[bot]
created_at: 2025-05-09T05:45:39Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/pingcap/tiflash/pull/10151#pullrequestreview-2827086533" title="Approved">CalvinNeo</a>*, *<a href="https://github.com/pingcap/tiflash/pull/10151#pullrequestreview-2826841934" title="Approved">Lloyd-Pottiger</a>*

The full list of commands accepted by this bot can be found [here](https://prow.tidb.net/command-help?repo=pingcap%2Ftiflash).

The pull request process is described [here](https://book.prow.tidb.net/#/workflows/pr)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/pingcap/tiflash/blob/master/OWNERS)~~ [CalvinNeo,Lloyd-Pottiger]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR
user: login: ti-chi-bot[bot]
created_at: 2025-05-09T05:45:41Z
body: [LGTM Timeline notifier]
---
Timeline:

- `2025-05-09 02:45:46.330926904 +0000 UTC m=+67612.478888122`: :ballot_box_with_check: agreed by [Lloyd-Pottiger](https://github.com/Lloyd-Pottiger).
- `2025-05-09 05:45:40.966560053 +0000 UTC m=+78407.114521271`: :ballot_box_with_check: agreed by [CalvinNeo](https://github.com/CalvinNeo).
author_association: CONTRIBUTOR
user: login: JaySon-Huang
created_at: 2025-05-09T07:37:12Z
body: /unhold
Verified that tiflash/tikv can handle the region with split key using int64_max correctly
author_association: CONTRIBUTOR
user: login: ti-chi-bot
created_at: 2025-05-09T07:56:11Z
body: In response to a cherrypick label: new pull request created to branch `release-8.5`: #10157.
But this PR has conflicts, please resolve them!
author_association: MEMBER
user: login: ti-chi-bot
created_at: 2025-05-09T07:56:14Z
body: In response to a cherrypick label: new pull request created to branch `release-8.1`: #10158.
But this PR has conflicts, please resolve them!
author_association: MEMBER
user: login: ti-chi-bot
created_at: 2025-05-09T07:56:20Z
body: In response to a cherrypick label: new pull request created to branch `release-7.5`: #10159.
But this PR has conflicts, please resolve them!
author_association: MEMBER