This is an automated email from the ASF dual-hosted git repository.

csun5285 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new fdfc63b0207 [refactor](be) Replace PrimitiveTypeConvertor with 
StorageLayout keyed by FieldType (#68664)
fdfc63b0207 is described below

commit fdfc63b0207a76373f94229a72267bf2ee3dee6c
Author: Chenyang Sun <[email protected]>
AuthorDate: Fri Oct 9 20:07:38 2026 +0800

    [refactor](be) Replace PrimitiveTypeConvertor with StorageLayout keyed by 
FieldType (#68664)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #68608, #68609
    
    Problem Summary: How a compute-layer value becomes the value a segment
    stores is written down in several places. PrimitiveTypeConvertor<PT>
    gives the V1 DATE, DATETIME and DECIMAL layouts to the key encodings of
    RowCursor and the BKD index; KeyCoderTraitsForFloat canonicalises NaN on
    its own; and the comparison and IN-list predicates repeat the V1
    conversions by hand to probe a bloom filter. The key-encodable types are
    listed as (FieldType, PrimitiveType) pairs so that the key helpers can
    be keyed by PrimitiveType.
    
    Add storage/storage_layout.h. For every fixed-width FieldType,
    StorageLayout<FT> gives the StorageValue a segment stores, the
    PrimitiveValue the compute layer holds, and to_storage from one to the
    other: a bit cast, the quiet NaN for every NaN of FLOAT/DOUBLE, and the
    V1 packing of DATE, DATETIME and DECIMAL. Make the PrimitiveType <->
    FieldType mapping in storage_field_type.h constexpr, so that a template
    picks its layout with
    StorageLayout<primitive_type_to_storage_field_type(PT)>; data_type.h now
    declares FieldType itself instead of including that header.
    
    Move the users onto it: full_encode_field_as_key and so the RowCursor
    and BKD query key encodings, the BKD +/- infinity sentinels, the NaN
    handling of KeyCoderTraitsForFloat, and the bloom filter probes of the
    comparison and IN-list predicates. The key-encodable types are listed by
    FieldType alone. Delete PrimitiveTypeConvertor, whose last user was a
    test helper.
    
    ### What problem does this PR solve?
    
    Issue Number: close #xxx
    
    Related PR: #xxx
    
    Problem Summary:
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [ ] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [ ] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [ ] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 be/src/core/data_type/data_type.h                  |   2 +-
 be/src/core/data_type/primitive_type.h             |  44 -----
 be/src/core/data_type/storage_field_type.cpp       | 210 ---------------------
 be/src/core/data_type/storage_field_type.h         | 193 ++++++++++++++++++-
 be/src/storage/index/bkd_field_encoding.cpp        |  32 ++--
 .../index/inverted/inverted_index_reader.cpp       |  31 +--
 be/src/storage/key_coder.h                         |  83 ++++----
 be/src/storage/predicate/comparison_predicate.h    |  23 +--
 be/src/storage/predicate/in_list_predicate.h       |  31 +--
 be/src/storage/row_cursor.cpp                      |   6 +-
 be/src/storage/row_cursor.h                        |   4 +-
 be/src/storage/storage_layout.h                    | 159 ++++++++++++++++
 be/test/storage/key_coder_test.cpp                 | 114 +++++------
 .../storage/segment/inverted_index_reader_test.cpp |   4 +-
 be/test/storage/segment/test_segment_writer.h      |   3 +-
 be/test/storage/storage_layout_test.cpp            | 119 ++++++++++++
 16 files changed, 604 insertions(+), 454 deletions(-)

diff --git a/be/src/core/data_type/data_type.h 
b/be/src/core/data_type/data_type.h
index f0c6ad80596..8948b6072bc 100644
--- a/be/src/core/data_type/data_type.h
+++ b/be/src/core/data_type/data_type.h
@@ -38,11 +38,11 @@
 #include "core/cow.h"
 #include "core/data_type/define_primitive_type.h"
 #include "core/data_type/primitive_type.h"
-#include "core/data_type/storage_field_type.h"
 #include "core/data_type_serde/data_type_serde.h"
 #include "core/types.h"
 
 namespace doris {
+enum class FieldType;
 class PColumnMeta;
 class PTypeDesc;
 class PTypeNode;
diff --git a/be/src/core/data_type/primitive_type.h 
b/be/src/core/data_type/primitive_type.h
index 9b4ec11a244..6169076a035 100644
--- a/be/src/core/data_type/primitive_type.h
+++ b/be/src/core/data_type/primitive_type.h
@@ -555,50 +555,6 @@ struct PrimitiveTypeTraits<TYPE_UINT64> {
     using ColumnType = ColumnOffset64;
 };
 
-template <PrimitiveType PT>
-struct PrimitiveTypeConvertor {
-    using CppType = typename PrimitiveTypeTraits<PT>::CppType;
-    using StorageFieldType = typename 
PrimitiveTypeTraits<PT>::StorageFieldType;
-
-    static inline StorageFieldType&& to_storage_field_type(CppType&& value) {
-        return static_cast<StorageFieldType&&>(std::forward<CppType>(value));
-    }
-
-    static inline const StorageFieldType& to_storage_field_type(const CppType& 
value) {
-        return *reinterpret_cast<const StorageFieldType*>(&value);
-    }
-};
-
-template <>
-struct PrimitiveTypeConvertor<TYPE_DATE> {
-    using CppType = typename PrimitiveTypeTraits<TYPE_DATE>::CppType;
-    using StorageFieldType = typename 
PrimitiveTypeTraits<TYPE_DATE>::StorageFieldType;
-
-    static inline StorageFieldType to_storage_field_type(const CppType& value) 
{
-        return StorageFieldType(cast_set<uint32_t>(value.to_olap_date()));
-    }
-};
-
-template <>
-struct PrimitiveTypeConvertor<TYPE_DATETIME> {
-    using CppType = typename PrimitiveTypeTraits<TYPE_DATETIME>::CppType;
-    using StorageFieldType = typename 
PrimitiveTypeTraits<TYPE_DATETIME>::StorageFieldType;
-
-    static inline StorageFieldType to_storage_field_type(const CppType& value) 
{
-        return value.to_olap_datetime();
-    }
-};
-
-template <>
-struct PrimitiveTypeConvertor<TYPE_DECIMALV2> {
-    using CppType = typename PrimitiveTypeTraits<TYPE_DECIMALV2>::CppType;
-    using StorageFieldType = typename 
PrimitiveTypeTraits<TYPE_DECIMALV2>::StorageFieldType;
-
-    static inline StorageFieldType to_storage_field_type(const CppType& value) 
{
-        return {value.int_value(), value.frac_value()};
-    }
-};
-
 inline TTypeDesc create_type_desc(PrimitiveType type, int precision = 0, int 
scale = 0) {
     TTypeDesc type_desc;
     std::vector<TTypeNode> node_type;
diff --git a/be/src/core/data_type/storage_field_type.cpp 
b/be/src/core/data_type/storage_field_type.cpp
deleted file mode 100644
index 7eb96d83029..00000000000
--- a/be/src/core/data_type/storage_field_type.cpp
+++ /dev/null
@@ -1,210 +0,0 @@
-// 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.
-
-#include "core/data_type/storage_field_type.h"
-
-#include "common/exception.h"
-#include "storage/olap_common.h"
-
-namespace doris {
-
-// NOLINTNEXTLINE(readability-function-size): keep the exhaustive mapping 
together for auditability.
-FieldType primitive_type_to_storage_field_type(PrimitiveType type) {
-    switch (type) {
-    case PrimitiveType::INVALID_TYPE:
-        return FieldType::OLAP_FIELD_TYPE_UNKNOWN;
-    case PrimitiveType::TYPE_NULL:
-        return FieldType::OLAP_FIELD_TYPE_NONE;
-    case PrimitiveType::TYPE_BOOLEAN:
-        return FieldType::OLAP_FIELD_TYPE_BOOL;
-    case PrimitiveType::TYPE_TINYINT:
-        return FieldType::OLAP_FIELD_TYPE_TINYINT;
-    case PrimitiveType::TYPE_SMALLINT:
-        return FieldType::OLAP_FIELD_TYPE_SMALLINT;
-    case PrimitiveType::TYPE_INT:
-        return FieldType::OLAP_FIELD_TYPE_INT;
-    case PrimitiveType::TYPE_BIGINT:
-        return FieldType::OLAP_FIELD_TYPE_BIGINT;
-    case PrimitiveType::TYPE_LARGEINT:
-        return FieldType::OLAP_FIELD_TYPE_LARGEINT;
-    case PrimitiveType::TYPE_FLOAT:
-        return FieldType::OLAP_FIELD_TYPE_FLOAT;
-    case PrimitiveType::TYPE_DOUBLE:
-        return FieldType::OLAP_FIELD_TYPE_DOUBLE;
-    case PrimitiveType::TYPE_VARCHAR:
-        return FieldType::OLAP_FIELD_TYPE_VARCHAR;
-    case PrimitiveType::TYPE_DATE:
-        return FieldType::OLAP_FIELD_TYPE_DATE;
-    case PrimitiveType::TYPE_DATETIME:
-        return FieldType::OLAP_FIELD_TYPE_DATETIME;
-    case PrimitiveType::TYPE_CHAR:
-        return FieldType::OLAP_FIELD_TYPE_CHAR;
-    case PrimitiveType::TYPE_STRUCT:
-        return FieldType::OLAP_FIELD_TYPE_STRUCT;
-    case PrimitiveType::TYPE_ARRAY:
-        return FieldType::OLAP_FIELD_TYPE_ARRAY;
-    case PrimitiveType::TYPE_MAP:
-        return FieldType::OLAP_FIELD_TYPE_MAP;
-    case PrimitiveType::TYPE_HLL:
-        return FieldType::OLAP_FIELD_TYPE_HLL;
-    case PrimitiveType::TYPE_DECIMALV2:
-        return FieldType::OLAP_FIELD_TYPE_DECIMAL;
-    case PrimitiveType::TYPE_BITMAP:
-        return FieldType::OLAP_FIELD_TYPE_BITMAP;
-    case PrimitiveType::TYPE_STRING:
-        return FieldType::OLAP_FIELD_TYPE_STRING;
-    case PrimitiveType::TYPE_QUANTILE_STATE:
-        return FieldType::OLAP_FIELD_TYPE_QUANTILE_STATE;
-    case PrimitiveType::TYPE_DATEV2:
-        return FieldType::OLAP_FIELD_TYPE_DATEV2;
-    case PrimitiveType::TYPE_DATETIMEV2:
-        return FieldType::OLAP_FIELD_TYPE_DATETIMEV2;
-    case PrimitiveType::TYPE_TIMESTAMP_NS:
-        return FieldType::OLAP_FIELD_TYPE_TIMESTAMP_NS;
-    case PrimitiveType::TYPE_TIMEV2:
-        return FieldType::OLAP_FIELD_TYPE_TIMEV2;
-    case PrimitiveType::TYPE_DECIMAL32:
-        return FieldType::OLAP_FIELD_TYPE_DECIMAL32;
-    case PrimitiveType::TYPE_DECIMAL64:
-        return FieldType::OLAP_FIELD_TYPE_DECIMAL64;
-    case PrimitiveType::TYPE_DECIMAL128I:
-        return FieldType::OLAP_FIELD_TYPE_DECIMAL128I;
-    case PrimitiveType::TYPE_JSONB:
-        return FieldType::OLAP_FIELD_TYPE_JSONB;
-    case PrimitiveType::TYPE_VARIANT:
-        return FieldType::OLAP_FIELD_TYPE_VARIANT;
-    case PrimitiveType::TYPE_AGG_STATE:
-        return FieldType::OLAP_FIELD_TYPE_AGG_STATE;
-    case PrimitiveType::TYPE_DECIMAL256:
-        return FieldType::OLAP_FIELD_TYPE_DECIMAL256;
-    case PrimitiveType::TYPE_IPV4:
-        return FieldType::OLAP_FIELD_TYPE_IPV4;
-    case PrimitiveType::TYPE_IPV6:
-        return FieldType::OLAP_FIELD_TYPE_IPV6;
-    case PrimitiveType::TYPE_UUID:
-        return FieldType::OLAP_FIELD_TYPE_UUID;
-    case PrimitiveType::TYPE_UINT32:
-        return FieldType::OLAP_FIELD_TYPE_UNSIGNED_INT;
-    case PrimitiveType::TYPE_UINT64:
-        return FieldType::OLAP_FIELD_TYPE_UNSIGNED_BIGINT;
-    case PrimitiveType::TYPE_TIMESTAMPTZ:
-        return FieldType::OLAP_FIELD_TYPE_TIMESTAMPTZ;
-    case PrimitiveType::TYPE_BINARY:
-    case static_cast<PrimitiveType>(14): // TYPE_DECIMAL (deprecated)
-    case static_cast<PrimitiveType>(21): // TYPE_TIME (deprecated)
-    case static_cast<PrimitiveType>(33): // TYPE_LAMBDA_FUNCTION (deprecated)
-    case PrimitiveType::TYPE_FIXED_LENGTH_OBJECT:
-    case PrimitiveType::TYPE_VARBINARY:
-        break;
-    }
-
-    throw Exception(ErrorCode::INTERNAL_ERROR, "Cannot convert PrimitiveType 
{} to FieldType",
-                    static_cast<int>(type));
-}
-
-// NOLINTNEXTLINE(readability-function-size): keep the exhaustive mapping 
together for auditability.
-PrimitiveType storage_field_type_to_primitive_type(FieldType type) {
-    switch (type) {
-    case FieldType::OLAP_FIELD_TYPE_TINYINT:
-        return PrimitiveType::TYPE_TINYINT;
-    case FieldType::OLAP_FIELD_TYPE_SMALLINT:
-        return PrimitiveType::TYPE_SMALLINT;
-    case FieldType::OLAP_FIELD_TYPE_INT:
-        return PrimitiveType::TYPE_INT;
-    case FieldType::OLAP_FIELD_TYPE_UNSIGNED_INT:
-        return PrimitiveType::TYPE_UINT32;
-    case FieldType::OLAP_FIELD_TYPE_BIGINT:
-        return PrimitiveType::TYPE_BIGINT;
-    case FieldType::OLAP_FIELD_TYPE_UNSIGNED_BIGINT:
-        return PrimitiveType::TYPE_UINT64;
-    case FieldType::OLAP_FIELD_TYPE_LARGEINT:
-        return PrimitiveType::TYPE_LARGEINT;
-    case FieldType::OLAP_FIELD_TYPE_FLOAT:
-        return PrimitiveType::TYPE_FLOAT;
-    case FieldType::OLAP_FIELD_TYPE_DOUBLE:
-        return PrimitiveType::TYPE_DOUBLE;
-    case FieldType::OLAP_FIELD_TYPE_CHAR:
-        return PrimitiveType::TYPE_CHAR;
-    case FieldType::OLAP_FIELD_TYPE_DATE:
-        return PrimitiveType::TYPE_DATE;
-    case FieldType::OLAP_FIELD_TYPE_DATETIME:
-        return PrimitiveType::TYPE_DATETIME;
-    case FieldType::OLAP_FIELD_TYPE_DECIMAL:
-        return PrimitiveType::TYPE_DECIMALV2;
-    case FieldType::OLAP_FIELD_TYPE_VARCHAR:
-        return PrimitiveType::TYPE_VARCHAR;
-    case FieldType::OLAP_FIELD_TYPE_STRUCT:
-        return PrimitiveType::TYPE_STRUCT;
-    case FieldType::OLAP_FIELD_TYPE_ARRAY:
-        return PrimitiveType::TYPE_ARRAY;
-    case FieldType::OLAP_FIELD_TYPE_MAP:
-        return PrimitiveType::TYPE_MAP;
-    case FieldType::OLAP_FIELD_TYPE_UNKNOWN:
-        return PrimitiveType::INVALID_TYPE;
-    case FieldType::OLAP_FIELD_TYPE_NONE:
-        return PrimitiveType::TYPE_NULL;
-    case FieldType::OLAP_FIELD_TYPE_HLL:
-        return PrimitiveType::TYPE_HLL;
-    case FieldType::OLAP_FIELD_TYPE_BOOL:
-        return PrimitiveType::TYPE_BOOLEAN;
-    case FieldType::OLAP_FIELD_TYPE_BITMAP:
-        return PrimitiveType::TYPE_BITMAP;
-    case FieldType::OLAP_FIELD_TYPE_STRING:
-        return PrimitiveType::TYPE_STRING;
-    case FieldType::OLAP_FIELD_TYPE_QUANTILE_STATE:
-        return PrimitiveType::TYPE_QUANTILE_STATE;
-    case FieldType::OLAP_FIELD_TYPE_DATEV2:
-        return PrimitiveType::TYPE_DATEV2;
-    case FieldType::OLAP_FIELD_TYPE_DATETIMEV2:
-        return PrimitiveType::TYPE_DATETIMEV2;
-    case FieldType::OLAP_FIELD_TYPE_TIMESTAMP_NS:
-        return PrimitiveType::TYPE_TIMESTAMP_NS;
-    case FieldType::OLAP_FIELD_TYPE_TIMEV2:
-        return PrimitiveType::TYPE_TIMEV2;
-    case FieldType::OLAP_FIELD_TYPE_DECIMAL32:
-        return PrimitiveType::TYPE_DECIMAL32;
-    case FieldType::OLAP_FIELD_TYPE_DECIMAL64:
-        return PrimitiveType::TYPE_DECIMAL64;
-    case FieldType::OLAP_FIELD_TYPE_DECIMAL128I:
-        return PrimitiveType::TYPE_DECIMAL128I;
-    case FieldType::OLAP_FIELD_TYPE_JSONB:
-        return PrimitiveType::TYPE_JSONB;
-    case FieldType::OLAP_FIELD_TYPE_VARIANT:
-        return PrimitiveType::TYPE_VARIANT;
-    case FieldType::OLAP_FIELD_TYPE_AGG_STATE:
-        return PrimitiveType::TYPE_AGG_STATE;
-    case FieldType::OLAP_FIELD_TYPE_DECIMAL256:
-        return PrimitiveType::TYPE_DECIMAL256;
-    case FieldType::OLAP_FIELD_TYPE_IPV4:
-        return PrimitiveType::TYPE_IPV4;
-    case FieldType::OLAP_FIELD_TYPE_IPV6:
-        return PrimitiveType::TYPE_IPV6;
-    case FieldType::OLAP_FIELD_TYPE_UUID:
-        return PrimitiveType::TYPE_UUID;
-    case FieldType::OLAP_FIELD_TYPE_TIMESTAMPTZ:
-        return PrimitiveType::TYPE_TIMESTAMPTZ;
-    case FieldType::OLAP_FIELD_TYPE_UNSIGNED_TINYINT:
-    case FieldType::OLAP_FIELD_TYPE_UNSIGNED_SMALLINT:
-    case FieldType::OLAP_FIELD_TYPE_DISCRETE_DOUBLE:
-        break;
-    }
-
-    throw Exception(ErrorCode::INTERNAL_ERROR, "Cannot convert FieldType {} to 
PrimitiveType",
-                    static_cast<int>(type));
-}
-
-} // namespace doris
diff --git a/be/src/core/data_type/storage_field_type.h 
b/be/src/core/data_type/storage_field_type.h
index a1d48e53eea..8145f205f8a 100644
--- a/be/src/core/data_type/storage_field_type.h
+++ b/be/src/core/data_type/storage_field_type.h
@@ -17,16 +17,199 @@
 
 #pragma once
 
+#include "common/exception.h"
 #include "core/data_type/define_primitive_type.h"
+#include "storage/field_type.h"
 
 namespace doris {
 
-enum class FieldType;
+// The FieldType a segment stores a PrimitiveType's values as, and back. Both
+// directions are constexpr, so a template can pick the storage traits of a
+// compute-layer type; at run time an unmapped type throws.
 
-// Throws Exception when the primitive type has no storage field type mapping.
-[[nodiscard]] FieldType primitive_type_to_storage_field_type(PrimitiveType 
type);
+// NOLINTNEXTLINE(readability-function-size): keep the exhaustive mapping 
together for auditability.
+constexpr FieldType primitive_type_to_storage_field_type(PrimitiveType type) {
+    switch (type) {
+    case PrimitiveType::INVALID_TYPE:
+        return FieldType::OLAP_FIELD_TYPE_UNKNOWN;
+    case PrimitiveType::TYPE_NULL:
+        return FieldType::OLAP_FIELD_TYPE_NONE;
+    case PrimitiveType::TYPE_BOOLEAN:
+        return FieldType::OLAP_FIELD_TYPE_BOOL;
+    case PrimitiveType::TYPE_TINYINT:
+        return FieldType::OLAP_FIELD_TYPE_TINYINT;
+    case PrimitiveType::TYPE_SMALLINT:
+        return FieldType::OLAP_FIELD_TYPE_SMALLINT;
+    case PrimitiveType::TYPE_INT:
+        return FieldType::OLAP_FIELD_TYPE_INT;
+    case PrimitiveType::TYPE_BIGINT:
+        return FieldType::OLAP_FIELD_TYPE_BIGINT;
+    case PrimitiveType::TYPE_LARGEINT:
+        return FieldType::OLAP_FIELD_TYPE_LARGEINT;
+    case PrimitiveType::TYPE_FLOAT:
+        return FieldType::OLAP_FIELD_TYPE_FLOAT;
+    case PrimitiveType::TYPE_DOUBLE:
+        return FieldType::OLAP_FIELD_TYPE_DOUBLE;
+    case PrimitiveType::TYPE_VARCHAR:
+        return FieldType::OLAP_FIELD_TYPE_VARCHAR;
+    case PrimitiveType::TYPE_DATE:
+        return FieldType::OLAP_FIELD_TYPE_DATE;
+    case PrimitiveType::TYPE_DATETIME:
+        return FieldType::OLAP_FIELD_TYPE_DATETIME;
+    case PrimitiveType::TYPE_CHAR:
+        return FieldType::OLAP_FIELD_TYPE_CHAR;
+    case PrimitiveType::TYPE_STRUCT:
+        return FieldType::OLAP_FIELD_TYPE_STRUCT;
+    case PrimitiveType::TYPE_ARRAY:
+        return FieldType::OLAP_FIELD_TYPE_ARRAY;
+    case PrimitiveType::TYPE_MAP:
+        return FieldType::OLAP_FIELD_TYPE_MAP;
+    case PrimitiveType::TYPE_HLL:
+        return FieldType::OLAP_FIELD_TYPE_HLL;
+    case PrimitiveType::TYPE_DECIMALV2:
+        return FieldType::OLAP_FIELD_TYPE_DECIMAL;
+    case PrimitiveType::TYPE_BITMAP:
+        return FieldType::OLAP_FIELD_TYPE_BITMAP;
+    case PrimitiveType::TYPE_STRING:
+        return FieldType::OLAP_FIELD_TYPE_STRING;
+    case PrimitiveType::TYPE_QUANTILE_STATE:
+        return FieldType::OLAP_FIELD_TYPE_QUANTILE_STATE;
+    case PrimitiveType::TYPE_DATEV2:
+        return FieldType::OLAP_FIELD_TYPE_DATEV2;
+    case PrimitiveType::TYPE_DATETIMEV2:
+        return FieldType::OLAP_FIELD_TYPE_DATETIMEV2;
+    case PrimitiveType::TYPE_TIMESTAMP_NS:
+        return FieldType::OLAP_FIELD_TYPE_TIMESTAMP_NS;
+    case PrimitiveType::TYPE_TIMEV2:
+        return FieldType::OLAP_FIELD_TYPE_TIMEV2;
+    case PrimitiveType::TYPE_DECIMAL32:
+        return FieldType::OLAP_FIELD_TYPE_DECIMAL32;
+    case PrimitiveType::TYPE_DECIMAL64:
+        return FieldType::OLAP_FIELD_TYPE_DECIMAL64;
+    case PrimitiveType::TYPE_DECIMAL128I:
+        return FieldType::OLAP_FIELD_TYPE_DECIMAL128I;
+    case PrimitiveType::TYPE_JSONB:
+        return FieldType::OLAP_FIELD_TYPE_JSONB;
+    case PrimitiveType::TYPE_VARIANT:
+        return FieldType::OLAP_FIELD_TYPE_VARIANT;
+    case PrimitiveType::TYPE_AGG_STATE:
+        return FieldType::OLAP_FIELD_TYPE_AGG_STATE;
+    case PrimitiveType::TYPE_DECIMAL256:
+        return FieldType::OLAP_FIELD_TYPE_DECIMAL256;
+    case PrimitiveType::TYPE_IPV4:
+        return FieldType::OLAP_FIELD_TYPE_IPV4;
+    case PrimitiveType::TYPE_IPV6:
+        return FieldType::OLAP_FIELD_TYPE_IPV6;
+    case PrimitiveType::TYPE_UUID:
+        return FieldType::OLAP_FIELD_TYPE_UUID;
+    case PrimitiveType::TYPE_UINT32:
+        return FieldType::OLAP_FIELD_TYPE_UNSIGNED_INT;
+    case PrimitiveType::TYPE_UINT64:
+        return FieldType::OLAP_FIELD_TYPE_UNSIGNED_BIGINT;
+    case PrimitiveType::TYPE_TIMESTAMPTZ:
+        return FieldType::OLAP_FIELD_TYPE_TIMESTAMPTZ;
+    case PrimitiveType::TYPE_BINARY:
+    case static_cast<PrimitiveType>(14): // TYPE_DECIMAL (deprecated)
+    case static_cast<PrimitiveType>(21): // TYPE_TIME (deprecated)
+    case static_cast<PrimitiveType>(33): // TYPE_LAMBDA_FUNCTION (deprecated)
+    case PrimitiveType::TYPE_FIXED_LENGTH_OBJECT:
+    case PrimitiveType::TYPE_VARBINARY:
+        break;
+    }
 
-// Throws Exception when the storage field type has no primitive type mapping.
-[[nodiscard]] PrimitiveType storage_field_type_to_primitive_type(FieldType 
type);
+    throw Exception(ErrorCode::INTERNAL_ERROR, "Cannot convert PrimitiveType 
{} to FieldType",
+                    static_cast<int>(type));
+}
+
+// NOLINTNEXTLINE(readability-function-size): keep the exhaustive mapping 
together for auditability.
+constexpr PrimitiveType storage_field_type_to_primitive_type(FieldType type) {
+    switch (type) {
+    case FieldType::OLAP_FIELD_TYPE_TINYINT:
+        return PrimitiveType::TYPE_TINYINT;
+    case FieldType::OLAP_FIELD_TYPE_SMALLINT:
+        return PrimitiveType::TYPE_SMALLINT;
+    case FieldType::OLAP_FIELD_TYPE_INT:
+        return PrimitiveType::TYPE_INT;
+    case FieldType::OLAP_FIELD_TYPE_UNSIGNED_INT:
+        return PrimitiveType::TYPE_UINT32;
+    case FieldType::OLAP_FIELD_TYPE_BIGINT:
+        return PrimitiveType::TYPE_BIGINT;
+    case FieldType::OLAP_FIELD_TYPE_UNSIGNED_BIGINT:
+        return PrimitiveType::TYPE_UINT64;
+    case FieldType::OLAP_FIELD_TYPE_LARGEINT:
+        return PrimitiveType::TYPE_LARGEINT;
+    case FieldType::OLAP_FIELD_TYPE_FLOAT:
+        return PrimitiveType::TYPE_FLOAT;
+    case FieldType::OLAP_FIELD_TYPE_DOUBLE:
+        return PrimitiveType::TYPE_DOUBLE;
+    case FieldType::OLAP_FIELD_TYPE_CHAR:
+        return PrimitiveType::TYPE_CHAR;
+    case FieldType::OLAP_FIELD_TYPE_DATE:
+        return PrimitiveType::TYPE_DATE;
+    case FieldType::OLAP_FIELD_TYPE_DATETIME:
+        return PrimitiveType::TYPE_DATETIME;
+    case FieldType::OLAP_FIELD_TYPE_DECIMAL:
+        return PrimitiveType::TYPE_DECIMALV2;
+    case FieldType::OLAP_FIELD_TYPE_VARCHAR:
+        return PrimitiveType::TYPE_VARCHAR;
+    case FieldType::OLAP_FIELD_TYPE_STRUCT:
+        return PrimitiveType::TYPE_STRUCT;
+    case FieldType::OLAP_FIELD_TYPE_ARRAY:
+        return PrimitiveType::TYPE_ARRAY;
+    case FieldType::OLAP_FIELD_TYPE_MAP:
+        return PrimitiveType::TYPE_MAP;
+    case FieldType::OLAP_FIELD_TYPE_UNKNOWN:
+        return PrimitiveType::INVALID_TYPE;
+    case FieldType::OLAP_FIELD_TYPE_NONE:
+        return PrimitiveType::TYPE_NULL;
+    case FieldType::OLAP_FIELD_TYPE_HLL:
+        return PrimitiveType::TYPE_HLL;
+    case FieldType::OLAP_FIELD_TYPE_BOOL:
+        return PrimitiveType::TYPE_BOOLEAN;
+    case FieldType::OLAP_FIELD_TYPE_BITMAP:
+        return PrimitiveType::TYPE_BITMAP;
+    case FieldType::OLAP_FIELD_TYPE_STRING:
+        return PrimitiveType::TYPE_STRING;
+    case FieldType::OLAP_FIELD_TYPE_QUANTILE_STATE:
+        return PrimitiveType::TYPE_QUANTILE_STATE;
+    case FieldType::OLAP_FIELD_TYPE_DATEV2:
+        return PrimitiveType::TYPE_DATEV2;
+    case FieldType::OLAP_FIELD_TYPE_DATETIMEV2:
+        return PrimitiveType::TYPE_DATETIMEV2;
+    case FieldType::OLAP_FIELD_TYPE_TIMESTAMP_NS:
+        return PrimitiveType::TYPE_TIMESTAMP_NS;
+    case FieldType::OLAP_FIELD_TYPE_TIMEV2:
+        return PrimitiveType::TYPE_TIMEV2;
+    case FieldType::OLAP_FIELD_TYPE_DECIMAL32:
+        return PrimitiveType::TYPE_DECIMAL32;
+    case FieldType::OLAP_FIELD_TYPE_DECIMAL64:
+        return PrimitiveType::TYPE_DECIMAL64;
+    case FieldType::OLAP_FIELD_TYPE_DECIMAL128I:
+        return PrimitiveType::TYPE_DECIMAL128I;
+    case FieldType::OLAP_FIELD_TYPE_JSONB:
+        return PrimitiveType::TYPE_JSONB;
+    case FieldType::OLAP_FIELD_TYPE_VARIANT:
+        return PrimitiveType::TYPE_VARIANT;
+    case FieldType::OLAP_FIELD_TYPE_AGG_STATE:
+        return PrimitiveType::TYPE_AGG_STATE;
+    case FieldType::OLAP_FIELD_TYPE_DECIMAL256:
+        return PrimitiveType::TYPE_DECIMAL256;
+    case FieldType::OLAP_FIELD_TYPE_IPV4:
+        return PrimitiveType::TYPE_IPV4;
+    case FieldType::OLAP_FIELD_TYPE_IPV6:
+        return PrimitiveType::TYPE_IPV6;
+    case FieldType::OLAP_FIELD_TYPE_UUID:
+        return PrimitiveType::TYPE_UUID;
+    case FieldType::OLAP_FIELD_TYPE_TIMESTAMPTZ:
+        return PrimitiveType::TYPE_TIMESTAMPTZ;
+    case FieldType::OLAP_FIELD_TYPE_UNSIGNED_TINYINT:
+    case FieldType::OLAP_FIELD_TYPE_UNSIGNED_SMALLINT:
+    case FieldType::OLAP_FIELD_TYPE_DISCRETE_DOUBLE:
+        break;
+    }
+
+    throw Exception(ErrorCode::INTERNAL_ERROR, "Cannot convert FieldType {} to 
PrimitiveType",
+                    static_cast<int>(type));
+}
 
 } // namespace doris
diff --git a/be/src/storage/index/bkd_field_encoding.cpp 
b/be/src/storage/index/bkd_field_encoding.cpp
index ba50871897e..b737670bbd4 100644
--- a/be/src/storage/index/bkd_field_encoding.cpp
+++ b/be/src/storage/index/bkd_field_encoding.cpp
@@ -18,30 +18,32 @@
 #include "storage/index/bkd_field_encoding.h"
 
 #include "core/data_type/primitive_type.h"
+#include "core/data_type/storage_field_type.h"
 
 namespace doris {
 
 Status encode_bkd_field_ascending(FieldType ft, const Field& field, const 
KeyCoder* coder,
                                   std::string* out) {
-    // `actual` is the primitive type of the query Field from the caller; 
`PrimitiveType::PT` is the
-    // scalar type the BKD index stores (e.g. INT for an INT column or 
ARRAY<INT> index).
+    // `actual` is the primitive type of the query Field from the caller; `pt` 
is the scalar
+    // type the BKD index stores (e.g. INT for an INT column or ARRAY<INT> 
index).
     // Normally they match: `int_col = 1` -> both INT; 
`array_contains(int_arr, 2)` -> both INT.
     // Mismatch happens when the query Field carries a non-scalar while BKD 
records the inner scalar:
     // `arr = []` reaches here via `FunctionComparison<EqualsOp>` with the 
entire const ARRAY literal
-    // as the query Field, so `actual = TYPE_ARRAY` while PT is the inner 
scalar -- the predicate
+    // as the query Field, so `actual = TYPE_ARRAY` while `pt` is the inner 
scalar -- the predicate
     // cannot be answered by BKD. Return INVERTED_INDEX_EVALUATE_SKIPPED so 
`_apply_index_expr`
-    // downgrades to scalar evaluation instead of crashing on 
`Field::get<PT>()` DCHECK below.
-#define CASE(FT, PT)                                                           
      \
-    case FieldType::FT: {                                                      
      \
-        const auto actual = field.get_type();                                  
      \
-        if (actual != PrimitiveType::PT && actual != PrimitiveType::TYPE_NULL 
&&     \
-            !(is_string_type(actual) && is_string_type(PrimitiveType::PT))) {  
      \
-            return Status::Error<ErrorCode::INVERTED_INDEX_EVALUATE_SKIPPED, 
false>( \
-                    "BKD query value type {} does not match index type {}",    
      \
-                    static_cast<int>(actual), static_cast<int>(ft));           
      \
-        }                                                                      
      \
-        full_encode_field_as_key<PrimitiveType::PT>(field, coder, out);        
      \
-        return Status::OK();                                                   
      \
+    // downgrades to scalar evaluation instead of crashing on 
`Field::get<pt>()` DCHECK below.
+#define CASE(FT)                                                               
           \
+    case FieldType::FT: {                                                      
           \
+        constexpr PrimitiveType pt = 
storage_field_type_to_primitive_type(FieldType::FT); \
+        const auto actual = field.get_type();                                  
           \
+        if (actual != pt && actual != PrimitiveType::TYPE_NULL &&              
           \
+            !(is_string_type(actual) && is_string_type(pt))) {                 
           \
+            return Status::Error<ErrorCode::INVERTED_INDEX_EVALUATE_SKIPPED, 
false>(      \
+                    "BKD query value type {} does not match index type {}",    
           \
+                    static_cast<int>(actual), static_cast<int>(ft));           
           \
+        }                                                                      
           \
+        full_encode_field_as_key<FieldType::FT>(field, coder, out);            
           \
+        return Status::OK();                                                   
           \
     }
     switch (ft) {
         DORIS_APPLY_FOR_KEY_ENCODABLE_NON_STRING_TYPES(CASE)
diff --git a/be/src/storage/index/inverted/inverted_index_reader.cpp 
b/be/src/storage/index/inverted/inverted_index_reader.cpp
index 84e2f110b66..c244872f140 100644
--- a/be/src/storage/index/inverted/inverted_index_reader.cpp
+++ b/be/src/storage/index/inverted/inverted_index_reader.cpp
@@ -58,31 +58,36 @@
 #include "storage/index/inverted/query/query_factory.h"
 #include "storage/key_coder.h"
 #include "storage/olap_common.h"
+#include "storage/storage_layout.h"
 #include "storage/types.h"
 #include "util/faststring.h"
 
 namespace {
 
 // Sentinel values are sourced from the compute-layer `type_limit<CppType>` and
-// then projected onto the storage-layer POD via `PrimitiveTypeConvertor<PT>`.
+// then projected onto the storage-layer POD via 
`StorageLayout<FT>::to_storage`.
 // Routing through the compute layer keeps the +/- infinity constants
 // single-sourced (e.g. DecimalV2 max lives only in 
DecimalV2Value::get_max_decimal,
 // DATE bounds only in VecDateTimeValue::datetime_min/max_value), so types like
 // decimal12_t and uint24_t — which have no std::numeric_limits specialisation 
—
 // no longer need their own type_limit<> entries.
-template <doris::PrimitiveType PT>
+template <doris::FieldType FT>
 static void bkd_encode_min(const doris::KeyCoder* coder, std::string* out) {
-    using compute_t = typename doris::PrimitiveTypeTraits<PT>::CppType;
+    using compute_t =
+            typename 
doris::PrimitiveTypeTraits<doris::storage_field_type_to_primitive_type(
+                    FT)>::CppType;
     auto compute_v = doris::type_limit<compute_t>::min();
-    auto v = 
doris::PrimitiveTypeConvertor<PT>::to_storage_field_type(compute_v);
+    auto v = doris::StorageLayout<FT>::to_storage(compute_v);
     coder->full_encode_ascending(&v, out);
 }
 
-template <doris::PrimitiveType PT>
+template <doris::FieldType FT>
 static void bkd_encode_max(const doris::KeyCoder* coder, std::string* out) {
-    using compute_t = typename doris::PrimitiveTypeTraits<PT>::CppType;
+    using compute_t =
+            typename 
doris::PrimitiveTypeTraits<doris::storage_field_type_to_primitive_type(
+                    FT)>::CppType;
     auto compute_v = doris::type_limit<compute_t>::max();
-    auto v = 
doris::PrimitiveTypeConvertor<PT>::to_storage_field_type(compute_v);
+    auto v = doris::StorageLayout<FT>::to_storage(compute_v);
     coder->full_encode_ascending(&v, out);
 }
 
@@ -94,9 +99,9 @@ using doris::encode_bkd_field_ascending;
 
 static doris::Status encode_bkd_min_ascending(doris::FieldType ft, const 
doris::KeyCoder* coder,
                                               std::string* out) {
-#define CASE(FT, PT)                                          \
-    case doris::FieldType::FT:                                \
-        bkd_encode_min<doris::PrimitiveType::PT>(coder, out); \
+#define CASE(FT)                                          \
+    case doris::FieldType::FT:                            \
+        bkd_encode_min<doris::FieldType::FT>(coder, out); \
         return doris::Status::OK();
     switch (ft) {
         DORIS_APPLY_FOR_KEY_ENCODABLE_NON_STRING_TYPES(CASE)
@@ -109,9 +114,9 @@ static doris::Status 
encode_bkd_min_ascending(doris::FieldType ft, const doris::
 
 static doris::Status encode_bkd_max_ascending(doris::FieldType ft, const 
doris::KeyCoder* coder,
                                               std::string* out) {
-#define CASE(FT, PT)                                          \
-    case doris::FieldType::FT:                                \
-        bkd_encode_max<doris::PrimitiveType::PT>(coder, out); \
+#define CASE(FT)                                          \
+    case doris::FieldType::FT:                            \
+        bkd_encode_max<doris::FieldType::FT>(coder, out); \
         return doris::Status::OK();
     switch (ft) {
         DORIS_APPLY_FOR_KEY_ENCODABLE_NON_STRING_TYPES(CASE)
diff --git a/be/src/storage/key_coder.h b/be/src/storage/key_coder.h
index f8d8a1e009b..8076e940174 100644
--- a/be/src/storage/key_coder.h
+++ b/be/src/storage/key_coder.h
@@ -30,12 +30,14 @@
 #include "absl/strings/substitute.h"
 #include "common/status.h"
 #include "core/data_type/primitive_type.h"
+#include "core/data_type/storage_field_type.h"
 #include "core/decimal12.h"
 #include "core/extended_types.h"
 #include "core/field.h"
 #include "core/types.h"
 #include "exec/common/endian.h"
 #include "storage/olap_common.h"
+#include "storage/storage_layout.h"
 #include "storage/types.h"
 #include "util/slice.h"
 
@@ -406,16 +408,9 @@ public:
 
 private:
     static UnsignedCppType float_to_int_bits(CppType value) {
-        if (std::isnan(value)) {
-            if constexpr (std::is_same_v<CppType, float>) {
-                return 0x7FC00000U;
-            } else {
-                return 0x7FF8000000000000ULL;
-            }
-        }
-
+        const CppType canonical = 
StorageLayout<field_type>::canonicalize_nan(value);
         UnsignedCppType result;
-        std::memcpy(&result, &value, sizeof(CppType));
+        std::memcpy(&result, &canonical, sizeof(CppType));
         return result;
     }
 
@@ -448,43 +443,43 @@ template <>
 class KeyCoderTraits<FieldType::OLAP_FIELD_TYPE_DOUBLE>
         : public KeyCoderTraitsForFloat<FieldType::OLAP_FIELD_TYPE_DOUBLE> {};
 
-// X-macro listing every (FieldType, PrimitiveType) pair that goes through 
KeyCoder
-// as a non-string scalar key. Strings are handled separately because they need
-// length / padding logic outside KeyCoder. Each entry: M(FT_suffix, 
PT_suffix).
+// Every FieldType that goes through KeyCoder as a non-string scalar key.
+// Strings are handled separately because they need length / padding logic
+// outside KeyCoder.
 #define DORIS_APPLY_FOR_KEY_ENCODABLE_NON_STRING_TYPES(M) \
-    M(OLAP_FIELD_TYPE_BOOL, TYPE_BOOLEAN)                 \
-    M(OLAP_FIELD_TYPE_TINYINT, TYPE_TINYINT)              \
-    M(OLAP_FIELD_TYPE_SMALLINT, TYPE_SMALLINT)            \
-    M(OLAP_FIELD_TYPE_INT, TYPE_INT)                      \
-    M(OLAP_FIELD_TYPE_BIGINT, TYPE_BIGINT)                \
-    M(OLAP_FIELD_TYPE_LARGEINT, TYPE_LARGEINT)            \
-    M(OLAP_FIELD_TYPE_FLOAT, TYPE_FLOAT)                  \
-    M(OLAP_FIELD_TYPE_DOUBLE, TYPE_DOUBLE)                \
-    M(OLAP_FIELD_TYPE_DECIMAL, TYPE_DECIMALV2)            \
-    M(OLAP_FIELD_TYPE_DECIMAL32, TYPE_DECIMAL32)          \
-    M(OLAP_FIELD_TYPE_DECIMAL64, TYPE_DECIMAL64)          \
-    M(OLAP_FIELD_TYPE_DECIMAL128I, TYPE_DECIMAL128I)      \
-    M(OLAP_FIELD_TYPE_DECIMAL256, TYPE_DECIMAL256)        \
-    M(OLAP_FIELD_TYPE_DATE, TYPE_DATE)                    \
-    M(OLAP_FIELD_TYPE_DATETIME, TYPE_DATETIME)            \
-    M(OLAP_FIELD_TYPE_DATEV2, TYPE_DATEV2)                \
-    M(OLAP_FIELD_TYPE_DATETIMEV2, TYPE_DATETIMEV2)        \
-    M(OLAP_FIELD_TYPE_TIMESTAMP_NS, TYPE_TIMESTAMP_NS)    \
-    M(OLAP_FIELD_TYPE_TIMESTAMPTZ, TYPE_TIMESTAMPTZ)      \
-    M(OLAP_FIELD_TYPE_IPV4, TYPE_IPV4)                    \
-    M(OLAP_FIELD_TYPE_IPV6, TYPE_IPV6)                    \
-    M(OLAP_FIELD_TYPE_UUID, TYPE_UUID)
-
-// True for exactly the PrimitiveTypes listed in
+    M(OLAP_FIELD_TYPE_BOOL)                               \
+    M(OLAP_FIELD_TYPE_TINYINT)                            \
+    M(OLAP_FIELD_TYPE_SMALLINT)                           \
+    M(OLAP_FIELD_TYPE_INT)                                \
+    M(OLAP_FIELD_TYPE_BIGINT)                             \
+    M(OLAP_FIELD_TYPE_LARGEINT)                           \
+    M(OLAP_FIELD_TYPE_FLOAT)                              \
+    M(OLAP_FIELD_TYPE_DOUBLE)                             \
+    M(OLAP_FIELD_TYPE_DECIMAL)                            \
+    M(OLAP_FIELD_TYPE_DECIMAL32)                          \
+    M(OLAP_FIELD_TYPE_DECIMAL64)                          \
+    M(OLAP_FIELD_TYPE_DECIMAL128I)                        \
+    M(OLAP_FIELD_TYPE_DECIMAL256)                         \
+    M(OLAP_FIELD_TYPE_DATE)                               \
+    M(OLAP_FIELD_TYPE_DATETIME)                           \
+    M(OLAP_FIELD_TYPE_DATEV2)                             \
+    M(OLAP_FIELD_TYPE_DATETIMEV2)                         \
+    M(OLAP_FIELD_TYPE_TIMESTAMP_NS)                       \
+    M(OLAP_FIELD_TYPE_TIMESTAMPTZ)                        \
+    M(OLAP_FIELD_TYPE_IPV4)                               \
+    M(OLAP_FIELD_TYPE_IPV6)                               \
+    M(OLAP_FIELD_TYPE_UUID)
+
+// True for exactly the FieldTypes listed in
 // DORIS_APPLY_FOR_KEY_ENCODABLE_NON_STRING_TYPES. Strings 
(CHAR/VARCHAR/STRING/
 // VARBINARY) have their own short-key code path in row_cursor.cpp that calls
 // storage_field->full_encode_ascending directly, and nested/aggregate types
 // (ARRAY/MAP/STRUCT/VARIANT/HLL/BITMAP/JSONB/QUANTILE_STATE/AGG_STATE) are not
 // key-encodable at all -- both groups must never reach the helpers below.
-constexpr bool is_key_encodable_non_string_type(PrimitiveType pt) {
-    switch (pt) {
-#define DORIS_KEY_ENCODABLE_CASE(FT, PT) \
-    case PrimitiveType::PT:              \
+constexpr bool is_key_encodable_non_string_type(FieldType ft) {
+    switch (ft) {
+#define DORIS_KEY_ENCODABLE_CASE(FT) \
+    case FieldType::FT:              \
         return true;
         
DORIS_APPLY_FOR_KEY_ENCODABLE_NON_STRING_TYPES(DORIS_KEY_ENCODABLE_CASE)
 #undef DORIS_KEY_ENCODABLE_CASE
@@ -493,16 +488,16 @@ constexpr bool 
is_key_encodable_non_string_type(PrimitiveType pt) {
     }
 }
 
-// Convert a Field value to its storage representation (via 
PrimitiveTypeConvertor)
+// Convert a Field value to its StorageValue (via StorageLayout)
 // and full-encode it as a byte-comparable ascending key via KeyCoder.
-template <PrimitiveType PT>
+template <FieldType FT>
 inline void full_encode_field_as_key(const Field& f, const KeyCoder* coder, 
std::string* buf) {
-    static_assert(is_key_encodable_non_string_type(PT),
+    static_assert(is_key_encodable_non_string_type(FT),
                   "full_encode_field_as_key is for non-string scalar keys 
only; "
                   "strings have their own path in RowCursor that calls "
                   "storage_field->full_encode_ascending directly, and nested / 
"
                   "aggregate types are not key-encodable");
-    auto v = PrimitiveTypeConvertor<PT>::to_storage_field_type(f.get<PT>());
+    const auto v = 
StorageLayout<FT>::to_storage(f.get<storage_field_type_to_primitive_type(FT)>());
     coder->full_encode_ascending(&v, buf);
 }
 
diff --git a/be/src/storage/predicate/comparison_predicate.h 
b/be/src/storage/predicate/comparison_predicate.h
index d420e550ff5..fb43fb6a119 100644
--- a/be/src/storage/predicate/comparison_predicate.h
+++ b/be/src/storage/predicate/comparison_predicate.h
@@ -29,6 +29,7 @@
 #include "storage/index/inverted/inverted_index_cache.h" // IWYU pragma: keep
 #include "storage/index/inverted/inverted_index_reader.h"
 #include "storage/predicate/column_predicate.h"
+#include "storage/storage_layout.h"
 
 namespace doris {
 template <PrimitiveType Type, PredicateType PT>
@@ -290,24 +291,10 @@ public:
             } else if constexpr (is_string_type(Type)) {
                 return bf->test_bytes(_value.data(), _value.size());
             } else {
-                // DecimalV2 using decimal12_t in bloom filter, should convert 
value to decimal12_t
-                if constexpr (Type == PrimitiveType::TYPE_DECIMALV2) {
-                    decimal12_t decimal12_t_val(_value.int_value(), 
_value.frac_value());
-                    return bf->test_bytes(reinterpret_cast<const 
char*>(&decimal12_t_val),
-                                          sizeof(decimal12_t));
-                    // Datev1 using uint24_t in bloom filter
-                } else if constexpr (Type == PrimitiveType::TYPE_DATE) {
-                    uint24_t date_value(uint32_t(_value.to_olap_date()));
-                    return bf->test_bytes(reinterpret_cast<const 
char*>(&date_value),
-                                          sizeof(uint24_t));
-                    // DatetimeV1 using int64_t in bloom filter
-                } else if constexpr (Type == PrimitiveType::TYPE_DATETIME) {
-                    int64_t datetime_value(_value.to_olap_datetime());
-                    return bf->test_bytes(reinterpret_cast<const 
char*>(&datetime_value),
-                                          sizeof(int64_t));
-                } else {
-                    return bf->test_bytes(reinterpret_cast<const 
char*>(&_value), sizeof(T));
-                }
+                const auto stored =
+                        
StorageLayout<primitive_type_to_storage_field_type(Type)>::to_storage(
+                                _value);
+                return bf->test_bytes(reinterpret_cast<const char*>(&stored), 
sizeof(stored));
             }
         } else {
             LOG(FATAL) << "Bloom filter is not supported by predicate type.";
diff --git a/be/src/storage/predicate/in_list_predicate.h 
b/be/src/storage/predicate/in_list_predicate.h
index a0de781415b..b1ee283c253 100644
--- a/be/src/storage/predicate/in_list_predicate.h
+++ b/be/src/storage/predicate/in_list_predicate.h
@@ -40,6 +40,7 @@
 #include "storage/index/inverted/inverted_index_reader.h"
 #include "storage/olap_common.h"
 #include "storage/predicate/column_predicate.h"
+#include "storage/storage_layout.h"
 
 // for uint24_t
 template <>
@@ -358,34 +359,12 @@ public:
                     if (bf->test_bytes(value->data, value->size)) {
                         return true;
                     }
-                } else if constexpr (Type == PrimitiveType::TYPE_DECIMALV2) {
-                    // DecimalV2 using decimal12_t in bloom filter in storage 
layer,
-                    // should convert value to decimal12_t
-                    // Datev1/DatetimeV1 using VecDatetimeValue in bloom 
filter, NO need to convert.
-                    const T* value = (const T*)(iter->get_value());
-                    decimal12_t decimal12_t_val(value->int_value(), 
value->frac_value());
-                    if (bf->test_bytes(reinterpret_cast<const 
char*>(&decimal12_t_val),
-                                       sizeof(decimal12_t))) {
-                        return true;
-                    }
-                } else if constexpr (Type == PrimitiveType::TYPE_DATE) {
-                    const T* value = (const T*)(iter->get_value());
-                    uint24_t date_value(uint32_t(value->to_olap_date()));
-                    if (bf->test_bytes(reinterpret_cast<const 
char*>(&date_value),
-                                       sizeof(uint24_t))) {
-                        return true;
-                    }
-                    // DatetimeV1 using int64_t in bloom filter
-                } else if constexpr (Type == PrimitiveType::TYPE_DATETIME) {
-                    const T* value = (const T*)(iter->get_value());
-                    int64_t datetime_value(value->to_olap_datetime());
-                    if (bf->test_bytes(reinterpret_cast<const 
char*>(&datetime_value),
-                                       sizeof(int64_t))) {
-                        return true;
-                    }
                 } else {
                     const T* value = (const T*)(iter->get_value());
-                    if (bf->test_bytes(reinterpret_cast<const char*>(value), 
sizeof(*value))) {
+                    const auto stored =
+                            
StorageLayout<primitive_type_to_storage_field_type(Type)>::to_storage(
+                                    *value);
+                    if (bf->test_bytes(reinterpret_cast<const char*>(&stored), 
sizeof(stored))) {
                         return true;
                     }
                 }
diff --git a/be/src/storage/row_cursor.cpp b/be/src/storage/row_cursor.cpp
index b2cabb70fa1..4cf22dfac37 100644
--- a/be/src/storage/row_cursor.cpp
+++ b/be/src/storage/row_cursor.cpp
@@ -147,9 +147,9 @@ void RowCursor::_encode_column_value(const TabletColumn* 
column, const Field& va
     // ignores `index_size` and delegates to full_encode_ascending, so the
     // `full_encode` flag here is a no-op and we always call the full helper.
     switch (ft) {
-#define CASE(FT, PT)                                                    \
-    case FieldType::FT:                                                 \
-        full_encode_field_as_key<PrimitiveType::PT>(value, coder, buf); \
+#define CASE(FT)                                                    \
+    case FieldType::FT:                                             \
+        full_encode_field_as_key<FieldType::FT>(value, coder, buf); \
         break;
         DORIS_APPLY_FOR_KEY_ENCODABLE_NON_STRING_TYPES(CASE)
 #undef CASE
diff --git a/be/src/storage/row_cursor.h b/be/src/storage/row_cursor.h
index 94d4eec63cc..3b505434408 100644
--- a/be/src/storage/row_cursor.h
+++ b/be/src/storage/row_cursor.h
@@ -68,8 +68,8 @@ public:
     std::string to_string() const;
 
     // Encode one row into binary according given num_keys.
-    // Internally converts each core::Field to its storage representation via
-    // PrimitiveTypeConvertor before passing to KeyCoder.
+    // Internally converts each core::Field to its StorageValue via 
StorageLayout
+    // before passing to KeyCoder.
     // CHAR fields are zero-padded to column.length() for encoding.
     template <bool is_mow = false>
     void encode_key_with_padding(std::string* buf, size_t num_keys, bool 
padding_minimal) const;
diff --git a/be/src/storage/storage_layout.h b/be/src/storage/storage_layout.h
new file mode 100644
index 00000000000..6034529a71e
--- /dev/null
+++ b/be/src/storage/storage_layout.h
@@ -0,0 +1,159 @@
+// 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.
+
+#pragma once
+
+#include <bit>
+#include <cmath>
+#include <cstdint>
+#include <limits>
+#include <type_traits>
+
+#include "common/cast_set.h"
+#include "core/data_type/primitive_type.h"
+#include "core/data_type/storage_field_type.h"
+#include "core/decimal12.h"
+#include "core/uint24.h"
+#include "core/value/decimalv2_value.h"
+#include "core/value/vdatetime_value.h"
+#include "storage/field_type.h"
+#include "storage/types.h"
+
+namespace doris {
+
+// How a compute-layer value becomes what a segment stores for FieldType FT: 
its own
+// bytes, except for the V1 DATE, DATETIME and DECIMAL layouts and NaN, stored 
as the
+// quiet NaN. The two types are PrimitiveTypeTraits<PT>::CppType and
+// CppTypeTraits<FT>::CppType; the aliases inside are private shorthand. 
Declared only,
+// so a FieldType without a specialisation is a compile error, not a silent 
byte copy.
+template <FieldType FT>
+struct StorageLayout;
+
+namespace storage_layout_detail {
+
+// A StorageValue is the PrimitiveValue's own bytes.
+template <FieldType FT>
+struct BitCast {
+private:
+    using StorageValue = typename CppTypeTraits<FT>::CppType;
+    using PrimitiveValue =
+            typename 
PrimitiveTypeTraits<storage_field_type_to_primitive_type(FT)>::CppType;
+    static_assert(sizeof(StorageValue) == sizeof(PrimitiveValue));
+
+public:
+    static StorageValue to_storage(const PrimitiveValue& value) {
+        return std::bit_cast<StorageValue>(value);
+    }
+};
+
+// Every NaN is stored as the quiet NaN, so equal values have equal stored 
bytes
+// and the bloom filter / dictionary / key encodings that hash or compare bytes
+// treat all NaNs as one value.
+template <FieldType FT>
+struct FloatingPoint {
+private:
+    using StorageValue = typename CppTypeTraits<FT>::CppType;
+    using PrimitiveValue =
+            typename 
PrimitiveTypeTraits<storage_field_type_to_primitive_type(FT)>::CppType;
+    static_assert(std::is_floating_point_v<StorageValue>);
+    static_assert(std::is_same_v<StorageValue, PrimitiveValue>);
+
+public:
+    static StorageValue canonicalize_nan(StorageValue value) {
+        return std::isnan(value) ? 
std::numeric_limits<StorageValue>::quiet_NaN() : value;
+    }
+    static StorageValue to_storage(PrimitiveValue value) { return 
canonicalize_nan(value); }
+};
+
+} // namespace storage_layout_detail
+
+#define DORIS_STORAGE_LAYOUT_BIT_CAST(FT) \
+    template <>                           \
+    struct StorageLayout<FieldType::FT> : 
storage_layout_detail::BitCast<FieldType::FT> {};
+
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_BOOL)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_TINYINT)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_SMALLINT)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_INT)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_BIGINT)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_LARGEINT)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_UNSIGNED_INT)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_UNSIGNED_BIGINT)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_DECIMAL32)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_DECIMAL64)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_DECIMAL128I)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_DECIMAL256)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_DATEV2)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_DATETIMEV2)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_TIMESTAMP_NS)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_TIMESTAMPTZ)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_IPV4)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_IPV6)
+DORIS_STORAGE_LAYOUT_BIT_CAST(OLAP_FIELD_TYPE_UUID)
+#undef DORIS_STORAGE_LAYOUT_BIT_CAST
+
+template <>
+struct StorageLayout<FieldType::OLAP_FIELD_TYPE_FLOAT>
+        : 
storage_layout_detail::FloatingPoint<FieldType::OLAP_FIELD_TYPE_FLOAT> {};
+
+template <>
+struct StorageLayout<FieldType::OLAP_FIELD_TYPE_DOUBLE>
+        : 
storage_layout_detail::FloatingPoint<FieldType::OLAP_FIELD_TYPE_DOUBLE> {};
+
+// V1 DATE: three bytes of (year << 9) | (month << 5) | day.
+template <>
+struct StorageLayout<FieldType::OLAP_FIELD_TYPE_DATE> {
+private:
+    using StorageValue = typename 
CppTypeTraits<FieldType::OLAP_FIELD_TYPE_DATE>::CppType;
+    using PrimitiveValue = typename PrimitiveTypeTraits<TYPE_DATE>::CppType;
+    static_assert(std::is_same_v<StorageValue, uint24_t>);
+
+public:
+    static StorageValue to_storage(const PrimitiveValue& value) {
+        return StorageValue(cast_set<uint32_t>(value.to_olap_date()));
+    }
+};
+
+// V1 DATETIME: the decimal digits YYYYMMDDhhmmss packed into one integer.
+template <>
+struct StorageLayout<FieldType::OLAP_FIELD_TYPE_DATETIME> {
+private:
+    using StorageValue = typename 
CppTypeTraits<FieldType::OLAP_FIELD_TYPE_DATETIME>::CppType;
+    using PrimitiveValue = typename 
PrimitiveTypeTraits<TYPE_DATETIME>::CppType;
+    static_assert(std::is_same_v<StorageValue, int64_t>);
+
+public:
+    static StorageValue to_storage(const PrimitiveValue& value) {
+        return static_cast<StorageValue>(value.to_olap_datetime());
+    }
+};
+
+// V1 DECIMAL: the integer and the nine-digit fraction stored side by side.
+template <>
+struct StorageLayout<FieldType::OLAP_FIELD_TYPE_DECIMAL> {
+private:
+    using StorageValue = typename 
CppTypeTraits<FieldType::OLAP_FIELD_TYPE_DECIMAL>::CppType;
+    using PrimitiveValue = typename 
PrimitiveTypeTraits<TYPE_DECIMALV2>::CppType;
+    static_assert(std::is_same_v<StorageValue, decimal12_t>);
+
+public:
+    static StorageValue to_storage(const PrimitiveValue& value) {
+        return {value.int_value(), value.frac_value()};
+    }
+};
+
+} // namespace doris
diff --git a/be/test/storage/key_coder_test.cpp 
b/be/test/storage/key_coder_test.cpp
index 07d3f5021eb..ee14881ca42 100644
--- a/be/test/storage/key_coder_test.cpp
+++ b/be/test/storage/key_coder_test.cpp
@@ -285,32 +285,34 @@ TEST_F(KeyCoderTest, test_decimal) {
 }
 
 // Encode an ascending sequence of compute-layer values via
-// full_encode_field_as_key<PT> -- the same helper RowCursor::encode_key and
+// full_encode_field_as_key<FT> -- the same helper RowCursor::encode_key and
 // the BKD inverted-index reader use -- and assert the byte order matches.
 //
-// Locks in the contract that PrimitiveTypeConvertor<PT> + KeyCoder together
-// preserve the compute-layer ordering for every (PrimitiveType, FieldType)
-// pair used as a sortable key.
+// Locks in the contract that StorageLayout<FT>::to_storage + KeyCoder together
+// preserve the compute-layer ordering for every FieldType used as a sortable
+// key.
 //
 // Why no `scale` or `frac` parameter? The contract under test is exactly
 // "the encode path does not read scale/frac". They live on TabletColumn
 // metadata (`_precision`, `_frac`) one layer above and never reach
-// `Field::create_field<PT>`, `PrimitiveTypeConvertor<PT>`, or `KeyCoder`.
+// `Field::create_field<PT>`, `StorageLayout<FT>`, or `KeyCoder`.
 // So each subgroup below picks raw ints that *would* arise from a column at
 // some hypothetical scale, labels the subgroup `scale=N` to make the human
 // interpretation explicit, and asserts ordering -- the encode result is by
 // construction identical regardless of which scale the column declared.
-template <PrimitiveType PT, typename ComputeT>
-static void check_full_encode_preserves_order(FieldType ft, const 
std::vector<ComputeT>& ascending,
-                                              const char* label) {
-    const KeyCoder* coder = get_key_coder(ft);
+template <FieldType FT>
+static void check_full_encode_preserves_order(
+        const std::vector<typename PrimitiveTypeTraits<
+                storage_field_type_to_primitive_type(FT)>::CppType>& ascending,
+        const char* label) {
+    const KeyCoder* coder = get_key_coder(FT);
     ASSERT_NE(coder, nullptr) << label;
     std::vector<std::string> encoded;
     encoded.reserve(ascending.size());
     for (const auto& v : ascending) {
-        Field f = Field::create_field<PT>(v);
+        Field f = 
Field::create_field<storage_field_type_to_primitive_type(FT)>(v);
         std::string buf;
-        full_encode_field_as_key<PT>(f, coder, &buf);
+        full_encode_field_as_key<FT>(f, coder, &buf);
         encoded.push_back(std::move(buf));
     }
     for (size_t i = 0; i + 1 < encoded.size(); ++i) {
@@ -347,8 +349,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
 
     // -------- DECIMAL32 (compute=Decimal32, storage=Int32) --------
     // scale=0 (DECIMAL(9,0)): whole int32 range
-    check_full_encode_preserves_order<TYPE_DECIMAL32, Decimal32>(
-            FieldType::OLAP_FIELD_TYPE_DECIMAL32,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DECIMAL32>(
             {Decimal32(std::numeric_limits<int32_t>::min()),  //  INT32_MIN
              Decimal32(int32_t(-99999)),                      //  -99999
              Decimal32(int32_t(-1)),                          //      -1
@@ -360,8 +361,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
     // scale=2 (DECIMAL(9,2)): raw = whole * 100 + frac
     {
         const int32_t s = pow10_i32(2);
-        check_full_encode_preserves_order<TYPE_DECIMAL32, Decimal32>(
-                FieldType::OLAP_FIELD_TYPE_DECIMAL32,
+        
check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DECIMAL32>(
                 {Decimal32(-123456 * s),      // -123456.00
                  Decimal32(-12 * s - 34),     //     -12.34
                  Decimal32(-1),               //      -0.01
@@ -374,8 +374,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
     // scale=9 (DECIMAL(9,9)): -0.999999999 .. +0.999999999 (whole always 0)
     {
         const int32_t s = pow10_i32(9);
-        check_full_encode_preserves_order<TYPE_DECIMAL32, Decimal32>(
-                FieldType::OLAP_FIELD_TYPE_DECIMAL32,
+        
check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DECIMAL32>(
                 {Decimal32(-(s - 1)), // -0.999999999
                  Decimal32(-1),       // -0.000000001
                  Decimal32(0),        //  0
@@ -386,8 +385,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
 
     // -------- DECIMAL64 (compute=Decimal64, storage=Int64) --------
     // scale=0: whole int64 range
-    check_full_encode_preserves_order<TYPE_DECIMAL64, Decimal64>(
-            FieldType::OLAP_FIELD_TYPE_DECIMAL64,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DECIMAL64>(
             {Decimal64(std::numeric_limits<int64_t>::min()), //  INT64_MIN
              Decimal64(int64_t(-1'000'000'000LL)),           //   -1 000 000 
000
              Decimal64(int64_t(-1)), Decimal64(int64_t(0)), 
Decimal64(int64_t(1)),
@@ -397,8 +395,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
     // scale=4 (DECIMAL(18,4))
     {
         const int64_t s = pow10_i64(4);
-        check_full_encode_preserves_order<TYPE_DECIMAL64, Decimal64>(
-                FieldType::OLAP_FIELD_TYPE_DECIMAL64,
+        
check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DECIMAL64>(
                 {Decimal64(-123456 * s - 7890),                  // 
-123456.7890
                  Decimal64(-int64_t(1)),                         //      
-0.0001
                  Decimal64(int64_t(0)),                          //       
0.0000
@@ -409,8 +406,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
     // scale=18 (DECIMAL(18,18)): whole always 0, full fractional range
     {
         const int64_t s = pow10_i64(18);
-        check_full_encode_preserves_order<TYPE_DECIMAL64, Decimal64>(
-                FieldType::OLAP_FIELD_TYPE_DECIMAL64,
+        
check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DECIMAL64>(
                 {Decimal64(-(s - 1)), // -0.999999999999999999
                  Decimal64(int64_t(-1)), Decimal64(int64_t(0)), 
Decimal64(int64_t(1)),
                  Decimal64(s - 1)}, //  0.999999999999999999
@@ -419,8 +415,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
 
     // -------- DECIMAL128I (compute=Decimal128V3, storage=Int128) --------
     // scale=0: span ±2^100 to exercise both halves of int128.
-    check_full_encode_preserves_order<TYPE_DECIMAL128I, Decimal128V3>(
-            FieldType::OLAP_FIELD_TYPE_DECIMAL128I,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DECIMAL128I>(
             {Decimal128V3(-(static_cast<int128_t>(1) << 100)), // -2^100
              Decimal128V3(int128_t(-1)), Decimal128V3(int128_t(0)), 
Decimal128V3(int128_t(1)),
              Decimal128V3(static_cast<int128_t>(1) << 100)}, //  2^100
@@ -428,8 +423,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
     // scale=10 (DECIMAL(38,10))
     {
         const int128_t s = pow10_i128(10);
-        check_full_encode_preserves_order<TYPE_DECIMAL128I, Decimal128V3>(
-                FieldType::OLAP_FIELD_TYPE_DECIMAL128I,
+        
check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DECIMAL128I>(
                 {Decimal128V3(-int128_t(100) * s), // -100.0000000000
                  Decimal128V3(int128_t(-1)),       //   -0.0000000001
                  Decimal128V3(int128_t(0)),
@@ -442,8 +436,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
 
     // -------- DECIMAL256 (compute=Decimal256, storage=wide::Int256) --------
     // scale=0: span beyond int128 to exercise the upper halves of int256.
-    check_full_encode_preserves_order<TYPE_DECIMAL256, Decimal256>(
-            FieldType::OLAP_FIELD_TYPE_DECIMAL256,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DECIMAL256>(
             {Decimal256(wide::Int256(-1'000'000'000'000LL)), //  -10^12
              Decimal256(wide::Int256(-1)), Decimal256(wide::Int256(0)), 
Decimal256(wide::Int256(1)),
              Decimal256(wide::Int256(1'000'000'000'000'000'000LL))}, //  10^18
@@ -452,8 +445,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
     {
         const wide::Int256 s = pow10_i256(20);
         const wide::Int256 big = pow10_i256(36); // far beyond int128
-        check_full_encode_preserves_order<TYPE_DECIMAL256, Decimal256>(
-                FieldType::OLAP_FIELD_TYPE_DECIMAL256,
+        
check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DECIMAL256>(
                 {Decimal256(-big),             // -10^36
                  Decimal256(-s),               //     -1.0
                  Decimal256(wide::Int256(-1)), //     -1e-20
@@ -469,8 +461,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
     // splits the int128 into {int_part, frac_part} where frac is the lower
     // 9 digits (DecimalV2Value's `frac_value()` * 10^-9). Caller passes the
     // two parts with matching sign.
-    check_full_encode_preserves_order<TYPE_DECIMALV2, DecimalV2Value>(
-            FieldType::OLAP_FIELD_TYPE_DECIMAL,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DECIMAL>(
             {DecimalV2Value::get_min_decimal(),                  // 
-999999999999999999.999999999
              DecimalV2Value(int64_t(-100), int64_t(0)),          // 
-100.000000000
              DecimalV2Value(int64_t(-1), int64_t(-500'000'000)), //   
-1.500000000
@@ -482,8 +473,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
 
     // -------- DATEV2 (compute=DateV2Value<DateV2ValueType>, storage=uint32) 
--------
     auto pack_d = [](int y, int m, int d) -> uint32_t { return uint32_t((y << 
9) | (m << 5) | d); };
-    check_full_encode_preserves_order<TYPE_DATEV2, 
DateV2Value<DateV2ValueType>>(
-            FieldType::OLAP_FIELD_TYPE_DATEV2,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DATEV2>(
             {DateV2Value<DateV2ValueType>(pack_d(0001, 1, 1)),
              DateV2Value<DateV2ValueType>(pack_d(1970, 1, 1)),
              DateV2Value<DateV2ValueType>(pack_d(2024, 12, 31)),
@@ -503,14 +493,12 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
     };
     using DTV2 = DateV2Value<DateTimeV2ValueType>;
     // scale=0: microsecond always zero
-    check_full_encode_preserves_order<TYPE_DATETIMEV2, DTV2>(
-            FieldType::OLAP_FIELD_TYPE_DATETIMEV2,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DATETIMEV2>(
             {DTV2(pack_dt(2020, 1, 1, 12, 0, 0)), DTV2(pack_dt(2024, 3, 10, 9, 
30, 0)),
              DTV2(pack_dt(2024, 12, 31, 23, 59, 59))},
             "DATETIMEV2 scale=0");
     // scale=3: microsecond multiples of 1000
-    check_full_encode_preserves_order<TYPE_DATETIMEV2, DTV2>(
-            FieldType::OLAP_FIELD_TYPE_DATETIMEV2,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DATETIMEV2>(
             {DTV2(pack_dt(2024, 3, 10, 9, 30, 0, 1'000)),
              DTV2(pack_dt(2024, 3, 10, 9, 30, 0, 123'000)),
              DTV2(pack_dt(2024, 3, 10, 9, 30, 0, 999'000)),
@@ -518,8 +506,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
             "DATETIMEV2 scale=3");
     // scale=6: full microsecond resolution; verifies ordering is byte-stable
     // even at the boundary between us=999999 and the next-second carry.
-    check_full_encode_preserves_order<TYPE_DATETIMEV2, DTV2>(
-            FieldType::OLAP_FIELD_TYPE_DATETIMEV2,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DATETIMEV2>(
             {DTV2(pack_dt(2024, 3, 10, 9, 30, 0, 0)), DTV2(pack_dt(2024, 3, 
10, 9, 30, 0, 1)),
              DTV2(pack_dt(2024, 3, 10, 9, 30, 0, 999'998)),
              DTV2(pack_dt(2024, 3, 10, 9, 30, 0, 999'999)),
@@ -527,40 +514,34 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
             "DATETIMEV2 scale=6");
 
     // -------- TIMESTAMPTZ (same packing as DATETIMEV2) --------
-    check_full_encode_preserves_order<TYPE_TIMESTAMPTZ, TimestampTzValue>(
-            FieldType::OLAP_FIELD_TYPE_TIMESTAMPTZ,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_TIMESTAMPTZ>(
             {TimestampTzValue(pack_dt(2020, 1, 1, 12, 0, 0)),
              TimestampTzValue(pack_dt(2024, 3, 10, 9, 30, 0, 123'456)),
              TimestampTzValue(pack_dt(2024, 12, 31, 23, 59, 59, 999'999))},
             "TIMESTAMPTZ");
 
     // -------- BOOLEAN (compute=storage=UInt8, only {0, 1}) --------
-    check_full_encode_preserves_order<TYPE_BOOLEAN, 
UInt8>(FieldType::OLAP_FIELD_TYPE_BOOL,
-                                                           {UInt8(0), 
UInt8(1)}, "BOOLEAN");
+    
check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_BOOL>({UInt8(0), 
UInt8(1)},
+                                                                       
"BOOLEAN");
 
     // -------- Plain integer keys (compute=storage) --------
-    check_full_encode_preserves_order<TYPE_TINYINT, int8_t>(
-            FieldType::OLAP_FIELD_TYPE_TINYINT,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_TINYINT>(
             {std::numeric_limits<int8_t>::min(), int8_t(-1), int8_t(0), 
int8_t(1),
              std::numeric_limits<int8_t>::max()},
             "TINYINT");
-    check_full_encode_preserves_order<TYPE_SMALLINT, int16_t>(
-            FieldType::OLAP_FIELD_TYPE_SMALLINT,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_SMALLINT>(
             {std::numeric_limits<int16_t>::min(), int16_t(-1), int16_t(0), 
int16_t(1),
              std::numeric_limits<int16_t>::max()},
             "SMALLINT");
-    check_full_encode_preserves_order<TYPE_INT, int32_t>(
-            FieldType::OLAP_FIELD_TYPE_INT,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_INT>(
             {std::numeric_limits<int32_t>::min(), int32_t(-1), int32_t(0), 
int32_t(1),
              std::numeric_limits<int32_t>::max()},
             "INT");
-    check_full_encode_preserves_order<TYPE_BIGINT, int64_t>(
-            FieldType::OLAP_FIELD_TYPE_BIGINT,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_BIGINT>(
             {std::numeric_limits<int64_t>::min(), int64_t(-1), int64_t(0), 
int64_t(1),
              std::numeric_limits<int64_t>::max()},
             "BIGINT");
-    check_full_encode_preserves_order<TYPE_LARGEINT, int128_t>(
-            FieldType::OLAP_FIELD_TYPE_LARGEINT,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_LARGEINT>(
             {-(static_cast<int128_t>(1) << 100), int128_t(-1), int128_t(0), 
int128_t(1),
              static_cast<int128_t>(1) << 100},
             "LARGEINT");
@@ -569,23 +550,20 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
     // KeyCoderTraitsForFloat byte-encodes finite values byte-comparably; NaN 
and
     // signed-zero ambiguity have their own dedicated tests above 
(FloatOrdering /
     // FloatComprehensiveOrdering), so here we just exercise the typical path.
-    check_full_encode_preserves_order<TYPE_FLOAT, float>(
-            FieldType::OLAP_FIELD_TYPE_FLOAT,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_FLOAT>(
             {-std::numeric_limits<float>::infinity(), -1e10f, -1.0f, -1e-10f, 
0.0f, 1e-10f, 1.0f,
              1e10f, std::numeric_limits<float>::infinity()},
             "FLOAT");
-    check_full_encode_preserves_order<TYPE_DOUBLE, double>(
-            FieldType::OLAP_FIELD_TYPE_DOUBLE,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DOUBLE>(
             {-std::numeric_limits<double>::infinity(), -1e100, -1.0, -1e-100, 
0.0, 1e-100, 1.0,
              1e100, std::numeric_limits<double>::infinity()},
             "DOUBLE");
 
     // -------- DATE V1 (compute=VecDateTimeValue, storage=uint24_t packed) 
--------
     // Storage = (year << 9) | (month << 5) | day, same packing as DATEV2 just
-    // narrower. The PrimitiveTypeConvertor specialisation runs to_olap_date()
+    // narrower. StorageLayout<DATE>::to_storage runs to_olap_date()
     // to project compute -> storage.
-    check_full_encode_preserves_order<TYPE_DATE, VecDateTimeValue>(
-            FieldType::OLAP_FIELD_TYPE_DATE,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DATE>(
             {VecDateTimeValue::create_from_olap_date(pack_d(1900, 1, 1)),
              VecDateTimeValue::create_from_olap_date(pack_d(1970, 1, 1)),
              VecDateTimeValue::create_from_olap_date(pack_d(2024, 12, 31)),
@@ -595,8 +573,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
     // -------- DATETIME V1 (compute=VecDateTimeValue, storage=int64 
decimal-packed) --------
     // Storage = YYYYMMDDhhmmss interpreted as int64 (sparse compared to V2 bit
     // packing but still chronologically ordered as integers).
-    check_full_encode_preserves_order<TYPE_DATETIME, VecDateTimeValue>(
-            FieldType::OLAP_FIELD_TYPE_DATETIME,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_DATETIME>(
             
{VecDateTimeValue::create_from_olap_datetime(uint64_t(19700101000000ULL)),
              
VecDateTimeValue::create_from_olap_datetime(uint64_t(20240310093000ULL)),
              
VecDateTimeValue::create_from_olap_datetime(uint64_t(20241231235959ULL)),
@@ -607,8 +584,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
     auto ip4 = [](uint8_t a, uint8_t b, uint8_t c, uint8_t d) -> uint32_t {
         return (uint32_t(a) << 24) | (uint32_t(b) << 16) | (uint32_t(c) << 8) 
| uint32_t(d);
     };
-    check_full_encode_preserves_order<TYPE_IPV4, uint32_t>(
-            FieldType::OLAP_FIELD_TYPE_IPV4,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_IPV4>(
             {ip4(0, 0, 0, 1),          // 0.0.0.1
              ip4(10, 0, 0, 1),         // 10.0.0.1
              ip4(127, 0, 0, 1),        // 127.0.0.1
@@ -620,8 +596,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
     auto ip6 = [](uint64_t hi, uint64_t lo) -> uint128_t {
         return (static_cast<uint128_t>(hi) << 64) | lo;
     };
-    check_full_encode_preserves_order<TYPE_IPV6, uint128_t>(
-            FieldType::OLAP_FIELD_TYPE_IPV6,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_IPV6>(
             {ip6(0, 1),                     // ::1
              ip6(0, 0x0000FFFF7F000001ULL), // ::ffff:127.0.0.1
              ip6(0x20010DB800000000ULL, 1), // 2001:db8::1
@@ -631,8 +606,7 @@ TEST_F(KeyCoderTest, 
full_encode_field_as_key_preserves_compute_layer_ordering)
             "IPV6");
 
     // -------- UUID (uint128_t, canonical unsigned order) --------
-    check_full_encode_preserves_order<TYPE_UUID, uint128_t>(
-            FieldType::OLAP_FIELD_TYPE_UUID,
+    check_full_encode_preserves_order<FieldType::OLAP_FIELD_TYPE_UUID>(
             {ip6(0, 0), ip6(0, 1), ip6(0, 0xFFFFFFFFFFFFFFFFULL), ip6(1, 0),
              ip6(0x8000000000000000ULL, 0), ip6(0xFFFFFFFFFFFFFFFFULL, 
0xFFFFFFFFFFFFFFFFULL)},
             "UUID");
diff --git a/be/test/storage/segment/inverted_index_reader_test.cpp 
b/be/test/storage/segment/inverted_index_reader_test.cpp
index a2ca5d29ac9..d53277f43ac 100644
--- a/be/test/storage/segment/inverted_index_reader_test.cpp
+++ b/be/test/storage/segment/inverted_index_reader_test.cpp
@@ -3312,7 +3312,7 @@ public:
     // Locks in:
     //  * the typed-param interface (TypedInvertedIndexQueryParam<PT>)
     //  * the +/-infinity sentinels routed through type_limit<compute_t> +
-    //    PrimitiveTypeConvertor<PT>
+    //    StorageLayout<FT>::to_storage
     //  * BKD's writer/reader/visitor agreement on KeyCoder-encoded bytes
     template <PrimitiveType PT, typename T>
     void verify_bkd_range_queries(int col_id, std::string_view rowset_id,
@@ -4398,7 +4398,7 @@ TEST_F(InvertedIndexReaderTest, BkdIndexRead) {
 //    cardinalities derived from the values via std::count_if.
 //
 // Locks in the typed-param interface, the +/-infinity sentinels routed
-// through type_limit<compute_t> + PrimitiveTypeConvertor<PT>, and BKD
+// through type_limit<compute_t> + StorageLayout<FT>::to_storage, and BKD
 // writer/reader/visitor agreement.
 TEST_F(InvertedIndexReaderTest, BkdRangeIntRangeQuery) {
     test_bkd_range_int();
diff --git a/be/test/storage/segment/test_segment_writer.h 
b/be/test/storage/segment/test_segment_writer.h
index f1f398f8581..9448205681a 100644
--- a/be/test/storage/segment/test_segment_writer.h
+++ b/be/test/storage/segment/test_segment_writer.h
@@ -29,6 +29,7 @@
 #include "storage/row_cursor.h"
 #include "storage/segment/column_writer.h"
 #include "storage/segment/vertical_segment_writer.h"
+#include "storage/storage_layout.h"
 #include "util/slice.h"
 
 namespace doris::segment_v2 {
@@ -73,7 +74,7 @@ public:
                 break;
             }
             case FieldType::OLAP_FIELD_TYPE_DECIMAL: {
-                auto v = 
PrimitiveTypeConvertor<TYPE_DECIMALV2>::to_storage_field_type(
+                auto v = 
StorageLayout<FieldType::OLAP_FIELD_TYPE_DECIMAL>::to_storage(
                         f.get<TYPE_DECIMALV2>());
                 memcpy(buf, &v, sizeof(v));
                 ptr = buf;
diff --git a/be/test/storage/storage_layout_test.cpp 
b/be/test/storage/storage_layout_test.cpp
new file mode 100644
index 00000000000..35936aeb032
--- /dev/null
+++ b/be/test/storage/storage_layout_test.cpp
@@ -0,0 +1,119 @@
+// 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.
+
+#include "storage/storage_layout.h"
+
+#include <gtest/gtest.h>
+
+#include <bit>
+#include <cstdint>
+#include <limits>
+
+#include "core/types.h"
+#include "core/value/decimalv2_value.h"
+#include "core/value/vdatetime_value.h"
+#include "storage/types.h"
+
+namespace doris {
+
+TEST(StorageLayoutTest, StorageValueSizeIsFieldTypeSize) {
+#define CHECK_SIZE(FT) \
+    EXPECT_EQ(sizeof(CppTypeTraits<FieldType::FT>::CppType), 
field_type_size(FieldType::FT)) << #FT;
+    CHECK_SIZE(OLAP_FIELD_TYPE_BOOL)
+    CHECK_SIZE(OLAP_FIELD_TYPE_TINYINT)
+    CHECK_SIZE(OLAP_FIELD_TYPE_SMALLINT)
+    CHECK_SIZE(OLAP_FIELD_TYPE_INT)
+    CHECK_SIZE(OLAP_FIELD_TYPE_BIGINT)
+    CHECK_SIZE(OLAP_FIELD_TYPE_LARGEINT)
+    CHECK_SIZE(OLAP_FIELD_TYPE_UNSIGNED_INT)
+    CHECK_SIZE(OLAP_FIELD_TYPE_UNSIGNED_BIGINT)
+    CHECK_SIZE(OLAP_FIELD_TYPE_FLOAT)
+    CHECK_SIZE(OLAP_FIELD_TYPE_DOUBLE)
+    CHECK_SIZE(OLAP_FIELD_TYPE_DECIMAL)
+    CHECK_SIZE(OLAP_FIELD_TYPE_DECIMAL32)
+    CHECK_SIZE(OLAP_FIELD_TYPE_DECIMAL64)
+    CHECK_SIZE(OLAP_FIELD_TYPE_DECIMAL128I)
+    CHECK_SIZE(OLAP_FIELD_TYPE_DECIMAL256)
+    CHECK_SIZE(OLAP_FIELD_TYPE_DATE)
+    CHECK_SIZE(OLAP_FIELD_TYPE_DATETIME)
+    CHECK_SIZE(OLAP_FIELD_TYPE_DATEV2)
+    CHECK_SIZE(OLAP_FIELD_TYPE_DATETIMEV2)
+    CHECK_SIZE(OLAP_FIELD_TYPE_TIMESTAMP_NS)
+    CHECK_SIZE(OLAP_FIELD_TYPE_TIMESTAMPTZ)
+    CHECK_SIZE(OLAP_FIELD_TYPE_IPV4)
+    CHECK_SIZE(OLAP_FIELD_TYPE_IPV6)
+    CHECK_SIZE(OLAP_FIELD_TYPE_UUID)
+#undef CHECK_SIZE
+}
+
+TEST(StorageLayoutTest, DateV1PacksYearMonthDayIntoThreeBytes) {
+    VecDateTimeValue value;
+    ASSERT_TRUE(value.from_date_int64(20200101));
+    const uint24_t stored = 
StorageLayout<FieldType::OLAP_FIELD_TYPE_DATE>::to_storage(value);
+    EXPECT_EQ(static_cast<uint32_t>(stored), (2020U << 9) | (1U << 5) | 1U);
+}
+
+TEST(StorageLayoutTest, DateTimeV1PacksDecimalDigits) {
+    VecDateTimeValue value;
+    ASSERT_TRUE(value.from_date_int64(20200101123045));
+    
EXPECT_EQ(StorageLayout<FieldType::OLAP_FIELD_TYPE_DATETIME>::to_storage(value),
+              20200101123045);
+}
+
+TEST(StorageLayoutTest, DecimalV1SplitsIntegerAndFraction) {
+    using Layout = StorageLayout<FieldType::OLAP_FIELD_TYPE_DECIMAL>;
+    decimal12_t stored = Layout::to_storage(DecimalV2Value(123, 456000000));
+    EXPECT_EQ(stored.integer, 123);
+    EXPECT_EQ(stored.fraction, 456000000);
+
+    stored = Layout::to_storage(DecimalV2Value(-123, -456000000));
+    EXPECT_EQ(stored.integer, -123);
+    EXPECT_EQ(stored.fraction, -456000000);
+}
+
+TEST(StorageLayoutTest, FloatingPointCanonicalisesEveryNaN) {
+    using FloatLayout = StorageLayout<FieldType::OLAP_FIELD_TYPE_FLOAT>;
+    using DoubleLayout = StorageLayout<FieldType::OLAP_FIELD_TYPE_DOUBLE>;
+
+    const auto payload_nan = std::bit_cast<float>(0x7FC12345U);
+    
EXPECT_EQ(std::bit_cast<uint32_t>(FloatLayout::canonicalize_nan(payload_nan)), 
0x7FC00000U);
+    
EXPECT_EQ(std::bit_cast<uint32_t>(FloatLayout::canonicalize_nan(-payload_nan)), 
0x7FC00000U);
+    EXPECT_EQ(std::bit_cast<uint32_t>(FloatLayout::canonicalize_nan(-0.0F)), 
0x80000000U);
+    EXPECT_EQ(FloatLayout::canonicalize_nan(1.5F), 1.5F);
+    EXPECT_EQ(std::bit_cast<uint32_t>(FloatLayout::to_storage(-payload_nan)), 
0x7FC00000U);
+
+    const auto payload_nan64 = std::bit_cast<double>(0xFFF8000000000001ULL);
+    
EXPECT_EQ(std::bit_cast<uint64_t>(DoubleLayout::canonicalize_nan(payload_nan64)),
+              0x7FF8000000000000ULL);
+    EXPECT_EQ(DoubleLayout::canonicalize_nan(-2.25), -2.25);
+    
EXPECT_EQ(DoubleLayout::canonicalize_nan(std::numeric_limits<double>::infinity()),
+              std::numeric_limits<double>::infinity());
+    EXPECT_EQ(std::bit_cast<uint64_t>(DoubleLayout::to_storage(payload_nan64)),
+              0x7FF8000000000000ULL);
+}
+
+TEST(StorageLayoutTest, BitCastStoresThePrimitiveBytes) {
+    DateV2Value<DateV2ValueType> date;
+    ASSERT_TRUE(date.from_date_int64(20200101));
+    
EXPECT_EQ(StorageLayout<FieldType::OLAP_FIELD_TYPE_DATEV2>::to_storage(date),
+              date.to_date_int_val());
+
+    
EXPECT_EQ(StorageLayout<FieldType::OLAP_FIELD_TYPE_DECIMAL32>::to_storage(Decimal32(-12345)),
+              -12345);
+}
+
+} // namespace doris


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to