diff --git a/config/config.md b/config/config.md
index 0cf78c0fb95..3a6e187375f 100644
--- a/config/config.md
+++ b/config/config.md
@@ -32,6 +32,7 @@
| `http.enable_cors` | Bool | `true` | HTTP CORS support, it's turned on by default
This allows browser to access http APIs without CORS restrictions |
| `http.cors_allowed_origins` | Array | Unset | Customize allowed origins for HTTP CORS. |
| `http.prom_validation_mode` | String | `strict` | Whether to enable validation for Prometheus remote write requests.
Available options:
- strict: deny invalid UTF-8 strings (default).
- lossy: allow invalid UTF-8 strings, replace invalid characters with REPLACEMENT_CHARACTER(U+FFFD).
- unchecked: do not valid strings. |
+| `http.experimental_enable_prometheus_native_histogram` | Bool | `false` | Experimental: enable Prometheus remote write v2 native histogram ingestion. |
| `http.experimental_enable_explain_analyze_stream` | Bool | `false` | Experimental: enable POST /v1/sql/analyze/stream for streaming EXPLAIN ANALYZE VERBOSE metrics. |
| `grpc` | -- | -- | The gRPC server options. |
| `grpc.bind_addr` | String | `127.0.0.1:4001` | The address to bind the gRPC server. |
@@ -248,6 +249,7 @@
| `http.enable_cors` | Bool | `true` | HTTP CORS support, it's turned on by default
This allows browser to access http APIs without CORS restrictions |
| `http.cors_allowed_origins` | Array | Unset | Customize allowed origins for HTTP CORS. |
| `http.prom_validation_mode` | String | `strict` | Whether to enable validation for Prometheus remote write requests.
Available options:
- strict: deny invalid UTF-8 strings (default).
- lossy: allow invalid UTF-8 strings, replace invalid characters with REPLACEMENT_CHARACTER(U+FFFD).
- unchecked: do not valid strings. |
+| `http.experimental_enable_prometheus_native_histogram` | Bool | `false` | Experimental: enable Prometheus remote write v2 native histogram ingestion. |
| `http.experimental_enable_explain_analyze_stream` | Bool | `false` | Experimental: enable POST /v1/sql/analyze/stream for streaming EXPLAIN ANALYZE VERBOSE metrics. |
| `grpc` | -- | -- | The gRPC server options. |
| `grpc.bind_addr` | String | `127.0.0.1:4001` | The address to bind the gRPC server. |
diff --git a/config/frontend.example.toml b/config/frontend.example.toml
index 06b4efd36e2..badba500623 100644
--- a/config/frontend.example.toml
+++ b/config/frontend.example.toml
@@ -57,6 +57,8 @@ cors_allowed_origins = ["https://example.com"]
## - lossy: allow invalid UTF-8 strings, replace invalid characters with REPLACEMENT_CHARACTER(U+FFFD).
## - unchecked: do not valid strings.
prom_validation_mode = "strict"
+## Experimental: enable Prometheus remote write v2 native histogram ingestion.
+experimental_enable_prometheus_native_histogram = false
## Experimental: enable POST /v1/sql/analyze/stream for streaming EXPLAIN ANALYZE VERBOSE metrics.
experimental_enable_explain_analyze_stream = false
diff --git a/config/standalone.example.toml b/config/standalone.example.toml
index 407280278d8..de3fa7a49d8 100644
--- a/config/standalone.example.toml
+++ b/config/standalone.example.toml
@@ -71,6 +71,8 @@ cors_allowed_origins = ["https://example.com"]
## - lossy: allow invalid UTF-8 strings, replace invalid characters with REPLACEMENT_CHARACTER(U+FFFD).
## - unchecked: do not valid strings.
prom_validation_mode = "strict"
+## Experimental: enable Prometheus remote write v2 native histogram ingestion.
+experimental_enable_prometheus_native_histogram = false
## Experimental: enable POST /v1/sql/analyze/stream for streaming EXPLAIN ANALYZE VERBOSE metrics.
experimental_enable_explain_analyze_stream = false
diff --git a/src/common/query/src/lib.rs b/src/common/query/src/lib.rs
index 91a417d3560..bdba49eaf70 100644
--- a/src/common/query/src/lib.rs
+++ b/src/common/query/src/lib.rs
@@ -15,6 +15,7 @@
pub mod columnar_value;
pub mod error;
pub mod logical_plan;
+pub mod native_histogram;
pub mod prelude;
pub mod request;
pub mod stream;
diff --git a/src/common/query/src/native_histogram.rs b/src/common/query/src/native_histogram.rs
new file mode 100644
index 00000000000..b10c78104a5
--- /dev/null
+++ b/src/common/query/src/native_histogram.rs
@@ -0,0 +1,111 @@
+// Copyright 2023 Greptime Team
+//
+// 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.
+
+//! Shared native histogram field contract.
+//!
+//! Prom remote-write v2 stores these names as children of one Struct field, while
+//! metric-engine uses the same contract to recognize native histogram tables.
+
+use std::sync::Arc;
+
+use datatypes::data_type::ConcreteDataType;
+use datatypes::types::{StructField, StructType};
+use once_cell::sync::Lazy;
+
+pub const NATIVE_HISTOGRAM_FIELD: &str = "greptime_native_histogram";
+pub const SCHEMA_FIELD: &str = "schema";
+pub const ZERO_THRESHOLD_FIELD: &str = "zero_threshold";
+pub const SUM_FIELD: &str = "sum";
+pub const RESET_HINT_FIELD: &str = "reset_hint";
+pub const START_TIMESTAMP_FIELD: &str = "start_timestamp";
+pub const CUSTOM_VALUES_FIELD: &str = "custom_values";
+pub const POSITIVE_SPAN_OFFSETS_FIELD: &str = "positive_span_offsets";
+pub const POSITIVE_SPAN_LENGTHS_FIELD: &str = "positive_span_lengths";
+pub const NEGATIVE_SPAN_OFFSETS_FIELD: &str = "negative_span_offsets";
+pub const NEGATIVE_SPAN_LENGTHS_FIELD: &str = "negative_span_lengths";
+pub const COUNT_U64_FIELD: &str = "count_u64";
+pub const ZERO_COUNT_U64_FIELD: &str = "zero_count_u64";
+pub const POSITIVE_BUCKETS_I64_FIELD: &str = "positive_buckets_i64";
+pub const NEGATIVE_BUCKETS_I64_FIELD: &str = "negative_buckets_i64";
+pub const COUNT_F64_FIELD: &str = "count_f64";
+pub const ZERO_COUNT_F64_FIELD: &str = "zero_count_f64";
+pub const POSITIVE_BUCKETS_F64_FIELD: &str = "positive_buckets_f64";
+pub const NEGATIVE_BUCKETS_F64_FIELD: &str = "negative_buckets_f64";
+
+// Keep int and float payloads in separate columns. The populated family is the
+// type signal, so we don't need to persist an extra histogram-type tag.
+pub const NATIVE_HISTOGRAM_FIELD_NAMES: &[&str] = &[
+ SCHEMA_FIELD,
+ ZERO_THRESHOLD_FIELD,
+ SUM_FIELD,
+ RESET_HINT_FIELD,
+ START_TIMESTAMP_FIELD,
+ CUSTOM_VALUES_FIELD,
+ POSITIVE_SPAN_OFFSETS_FIELD,
+ POSITIVE_SPAN_LENGTHS_FIELD,
+ NEGATIVE_SPAN_OFFSETS_FIELD,
+ NEGATIVE_SPAN_LENGTHS_FIELD,
+ COUNT_U64_FIELD,
+ ZERO_COUNT_U64_FIELD,
+ POSITIVE_BUCKETS_I64_FIELD,
+ NEGATIVE_BUCKETS_I64_FIELD,
+ COUNT_F64_FIELD,
+ ZERO_COUNT_F64_FIELD,
+ POSITIVE_BUCKETS_F64_FIELD,
+ NEGATIVE_BUCKETS_F64_FIELD,
+];
+
+static NATIVE_HISTOGRAM_VALUE_TYPE: Lazy = Lazy::new(|| {
+ let fields = NATIVE_HISTOGRAM_FIELD_NAMES
+ .iter()
+ .filter_map(|name| {
+ let data_type = native_histogram_field_type(name)?;
+ Some(StructField::new((*name).to_string(), data_type, true))
+ })
+ .collect();
+ ConcreteDataType::struct_datatype(StructType::new(Arc::new(fields)))
+});
+
+/// Returns the exact Greptime type for a persisted native histogram field.
+pub fn native_histogram_field_type(name: &str) -> Option {
+ match name {
+ SCHEMA_FIELD | RESET_HINT_FIELD => Some(ConcreteDataType::int32_datatype()),
+ ZERO_THRESHOLD_FIELD | SUM_FIELD | COUNT_F64_FIELD | ZERO_COUNT_F64_FIELD => {
+ Some(ConcreteDataType::float64_datatype())
+ }
+ START_TIMESTAMP_FIELD => Some(ConcreteDataType::timestamp_millisecond_datatype()),
+ CUSTOM_VALUES_FIELD | POSITIVE_BUCKETS_F64_FIELD | NEGATIVE_BUCKETS_F64_FIELD => Some(
+ ConcreteDataType::list_datatype(Arc::new(ConcreteDataType::float64_datatype())),
+ ),
+ POSITIVE_SPAN_OFFSETS_FIELD | NEGATIVE_SPAN_OFFSETS_FIELD => Some(
+ ConcreteDataType::list_datatype(Arc::new(ConcreteDataType::int32_datatype())),
+ ),
+ POSITIVE_SPAN_LENGTHS_FIELD | NEGATIVE_SPAN_LENGTHS_FIELD => Some(
+ ConcreteDataType::list_datatype(Arc::new(ConcreteDataType::uint32_datatype())),
+ ),
+ COUNT_U64_FIELD | ZERO_COUNT_U64_FIELD => Some(ConcreteDataType::uint64_datatype()),
+ POSITIVE_BUCKETS_I64_FIELD | NEGATIVE_BUCKETS_I64_FIELD => Some(
+ ConcreteDataType::list_datatype(Arc::new(ConcreteDataType::int64_datatype())),
+ ),
+ _ => None,
+ }
+}
+
+pub fn native_histogram_value_type() -> &'static ConcreteDataType {
+ &NATIVE_HISTOGRAM_VALUE_TYPE
+}
+
+pub fn is_native_histogram_value_schema(name: &str, data_type: &ConcreteDataType) -> bool {
+ name == NATIVE_HISTOGRAM_FIELD && data_type == native_histogram_value_type()
+}
diff --git a/src/metric-engine/AGENTS.md b/src/metric-engine/AGENTS.md
index 86b676db48c..4471de3faee 100644
--- a/src/metric-engine/AGENTS.md
+++ b/src/metric-engine/AGENTS.md
@@ -59,7 +59,9 @@ Mito2 data region. Queries against a physical region pass straight through.
`store-api`'s metric engine consts; keep them in sync with `engine.rs`.
- **Metadata K-V encoding** (`metadata_region.rs`): changes the on-disk metadata layout.
- **RegionId group mapping** (`utils.rs`): data vs metadata region derivation.
-- **Physical column rules** (`engine/alter/`): a physical region allows only one field column.
+- **Physical column rules** (`engine/alter/`): ordinary metric fields stay on
+ logical tables; shared physical field adds are only for the native histogram
+ Struct field.
## Testing
diff --git a/src/metric-engine/src/data_region.rs b/src/metric-engine/src/data_region.rs
index b64d61a287e..ec9ff15c8d9 100644
--- a/src/metric-engine/src/data_region.rs
+++ b/src/metric-engine/src/data_region.rs
@@ -13,6 +13,7 @@
// limitations under the License.
use api::v1::SemanticType;
+use common_query::native_histogram::is_native_histogram_value_schema;
use common_telemetry::{debug, info};
use datatypes::schema::{SkippingIndexOptions, SkippingIndexType};
use mito2::engine::MitoEngine;
@@ -123,42 +124,55 @@ impl DataRegion {
.into_iter()
.enumerate()
.map(|(delta, mut c)| {
- if c.semantic_type == SemanticType::Tag {
- if !c.column_schema.data_type.is_string() {
- return ColumnTypeMismatchSnafu {
- expect: ConcreteDataType::string_datatype(),
- actual: c.column_schema.data_type.clone(),
+ match c.semantic_type {
+ SemanticType::Tag => {
+ if !c.column_schema.data_type.is_string() {
+ return ColumnTypeMismatchSnafu {
+ expect: ConcreteDataType::string_datatype(),
+ actual: c.column_schema.data_type.clone(),
+ }
+ .fail();
+ }
+ }
+ // Field columns can only be added to the shared physical
+ // table for native histograms; ordinary metric fields are
+ // created with the logical table.
+ SemanticType::Field
+ if is_native_histogram_value_schema(
+ &c.column_schema.name,
+ &c.column_schema.data_type,
+ ) => {}
+ _ => {
+ return AddingFieldColumnSnafu {
+ name: &c.column_schema.name,
}
.fail();
}
- } else {
- return AddingFieldColumnSnafu {
- name: &c.column_schema.name,
- }
- .fail();
- };
+ }
c.column_id = new_column_id_start + delta as u32;
c.column_schema.set_nullable();
- match index_options {
- IndexOptions::None => {}
- IndexOptions::Inverted => {
- c.column_schema.set_inverted_index(true);
- }
- IndexOptions::Skipping {
- granularity,
- false_positive_rate,
- } => {
- c.column_schema
- .set_skipping_options(
- &SkippingIndexOptions::new(
- granularity,
- false_positive_rate,
- SkippingIndexType::BloomFilter,
+ if c.semantic_type == SemanticType::Tag {
+ match index_options {
+ IndexOptions::None => {}
+ IndexOptions::Inverted => {
+ c.column_schema.set_inverted_index(true);
+ }
+ IndexOptions::Skipping {
+ granularity,
+ false_positive_rate,
+ } => {
+ c.column_schema
+ .set_skipping_options(
+ &SkippingIndexOptions::new(
+ granularity,
+ false_positive_rate,
+ SkippingIndexType::BloomFilter,
+ )
+ .context(SetSkippingIndexOptionSnafu)?,
)
- .context(SetSkippingIndexOptionSnafu)?,
- )
- .context(SetSkippingIndexOptionSnafu)?;
+ .context(SetSkippingIndexOptionSnafu)?;
+ }
}
}
diff --git a/src/metric-engine/src/engine/alter.rs b/src/metric-engine/src/engine/alter.rs
index 892e0c91c2d..e9d8e878df7 100644
--- a/src/metric-engine/src/engine/alter.rs
+++ b/src/metric-engine/src/engine/alter.rs
@@ -17,6 +17,8 @@ mod validate;
use std::collections::{BTreeSet, HashMap, HashSet};
+use api::v1::SemanticType;
+use common_query::native_histogram::is_native_histogram_value_schema;
use extract_new_columns::extract_new_columns;
use snafu::{OptionExt, ResultExt, ensure};
use store_api::metadata::ColumnMetadata;
@@ -27,8 +29,8 @@ use validate::validate_alter_region_requests;
use crate::engine::MetricEngineInner;
use crate::error::{
- LogicalRegionNotFoundSnafu, PhysicalRegionNotFoundSnafu, Result, SerializeColumnMetadataSnafu,
- UnexpectedRequestSnafu,
+ AddingFieldColumnSnafu, LogicalRegionNotFoundSnafu, PhysicalRegionNotFoundSnafu, Result,
+ SerializeColumnMetadataSnafu, UnexpectedRequestSnafu,
};
use crate::utils::{append_manifest_info, encode_manifest_info_to_extensions, to_data_region_id};
@@ -117,6 +119,8 @@ impl MetricEngineInner {
) -> Result {
// Checks all alter requests are add columns.
validate_alter_region_requests(&requests)?;
+ self.validate_logical_field_alters(physical_region_id, &requests)
+ .await?;
// Finds new columns to add
let mut new_column_names = HashSet::new();
@@ -209,6 +213,52 @@ impl MetricEngineInner {
Ok(0)
}
+ async fn validate_logical_field_alters(
+ &self,
+ physical_region_id: RegionId,
+ requests: &[(RegionId, RegionAlterRequest)],
+ ) -> Result<()> {
+ // Logical metric tables have one field column. Native histograms are a
+ // special struct field, so field alters must leave exactly that field.
+ for (region_id, request) in requests {
+ let AlterKind::AddColumns { columns } = &request.kind else {
+ unreachable!()
+ };
+ let added_fields = columns
+ .iter()
+ .filter(|col| col.column_metadata.semantic_type == SemanticType::Field)
+ .collect::>();
+ let Some(&first_added_field) = added_fields.first() else {
+ continue;
+ };
+
+ let mut fields = self
+ .load_logical_columns(physical_region_id, *region_id)
+ .await?
+ .into_iter()
+ .filter(|col| col.semantic_type == SemanticType::Field)
+ .collect::>();
+ fields.extend(
+ added_fields
+ .into_iter()
+ .map(|col| col.column_metadata.clone()),
+ );
+
+ ensure!(
+ fields.len() == 1
+ && is_native_histogram_value_schema(
+ &fields[0].column_schema.name,
+ &fields[0].column_schema.data_type
+ ),
+ AddingFieldColumnSnafu {
+ name: first_added_field.column_metadata.column_schema.name.clone(),
+ }
+ );
+ }
+
+ Ok(())
+ }
+
async fn alter_physical_region(
&self,
region_id: RegionId,
diff --git a/src/metric-engine/src/engine/alter/extract_new_columns.rs b/src/metric-engine/src/engine/alter/extract_new_columns.rs
index c3bef1ba4fd..3573762d742 100644
--- a/src/metric-engine/src/engine/alter/extract_new_columns.rs
+++ b/src/metric-engine/src/engine/alter/extract_new_columns.rs
@@ -15,6 +15,7 @@
use std::collections::{HashMap, HashSet};
use api::v1::SemanticType;
+use common_query::native_histogram::is_native_histogram_value_schema;
use snafu::ensure;
use store_api::metadata::ColumnMetadata;
use store_api::region_request::{AlterKind, RegionAlterRequest};
@@ -43,7 +44,11 @@ pub fn extract_new_columns<'a>(
&& !new_column_names.contains(column_name)
{
ensure!(
- col.column_metadata.semantic_type != SemanticType::Field,
+ col.column_metadata.semantic_type != SemanticType::Field
+ || is_native_histogram_value_schema(
+ &col.column_metadata.column_schema.name,
+ &col.column_metadata.column_schema.data_type
+ ),
AddingFieldColumnSnafu {
name: column_name.to_string(),
}
diff --git a/src/metric-engine/src/engine/create.rs b/src/metric-engine/src/engine/create.rs
index c0b5da4145f..a5cbd566723 100644
--- a/src/metric-engine/src/engine/create.rs
+++ b/src/metric-engine/src/engine/create.rs
@@ -17,6 +17,7 @@ mod extract_new_columns;
use std::collections::{HashMap, HashSet};
use api::v1::SemanticType;
+use common_query::native_histogram::is_native_histogram_value_schema;
use common_telemetry::info;
use common_time::{FOREVER, Timestamp};
use datatypes::data_type::ConcreteDataType;
@@ -390,8 +391,8 @@ impl MetricEngineInner {
ConflictRegionOptionSnafu {}
);
- // check if only one field column is declared, and all tag columns are string
- let mut field_col: Option<&ColumnMetadata> = None;
+ // check if field columns are either a normal metric value or a native histogram.
+ let mut field_cols = Vec::new();
for col in &request.column_metadatas {
// Verified in above steps.
if is_metric_engine_internal_column(&col.column_schema.name) {
@@ -406,21 +407,30 @@ impl MetricEngineInner {
}
),
SemanticType::Field => {
- if let Some(field_col) = field_col {
- MultipleFieldColumnSnafu {
- previous: field_col.column_schema.name.clone(),
- current: col.column_schema.name.clone(),
- }
- .fail()?;
- }
- field_col = Some(col)
+ field_cols.push(col);
}
SemanticType::Timestamp => {}
}
}
- let field_col = field_col.context(NoFieldColumnSnafu)?;
+ let [field_col] = field_cols.as_slice() else {
+ if field_cols.is_empty() {
+ NoFieldColumnSnafu.fail()?;
+ }
+ return MultipleFieldColumnSnafu {
+ previous: field_cols[0].column_schema.name.clone(),
+ current: field_cols[1].column_schema.name.clone(),
+ }
+ .fail();
+ };
- // make sure the field column is float64 type
+ if is_native_histogram_value_schema(
+ &field_col.column_schema.name,
+ &field_col.column_schema.data_type,
+ ) {
+ return Ok(());
+ }
+
+ // make sure the normal field column is float64 type
ensure!(
field_col.column_schema.data_type == ConcreteDataType::float64_datatype(),
ColumnTypeMismatchSnafu {
@@ -653,6 +663,7 @@ pub(crate) fn region_options_for_metadata_region(
mod test {
use common_meta::ddl::test_util::assert_column_name_and_id;
use common_meta::ddl::utils::{parse_column_metadatas, parse_manifest_infos_from_extensions};
+ use common_query::native_histogram::{NATIVE_HISTOGRAM_FIELD, native_histogram_value_type};
use common_query::prelude::{greptime_timestamp, greptime_value};
use store_api::metric_engine_consts::{METRIC_ENGINE_NAME, PHYSICAL_TABLE_METADATA_KEY};
use store_api::region_request::{BatchRegionDdlRequest, RegionRequirements};
@@ -845,6 +856,96 @@ mod test {
MetricEngineInner::verify_region_create_request(&request).unwrap();
}
+ #[test]
+ fn test_verify_region_create_request_native_histogram_fields() {
+ let native_histogram_columns = vec![
+ ColumnMetadata {
+ column_id: 0,
+ semantic_type: SemanticType::Timestamp,
+ column_schema: ColumnSchema::new(
+ greptime_timestamp(),
+ ConcreteDataType::timestamp_millisecond_datatype(),
+ false,
+ ),
+ },
+ ColumnMetadata {
+ column_id: 1,
+ semantic_type: SemanticType::Tag,
+ column_schema: ColumnSchema::new("job", ConcreteDataType::string_datatype(), true),
+ },
+ ColumnMetadata {
+ column_id: 2,
+ semantic_type: SemanticType::Field,
+ column_schema: ColumnSchema::new(
+ NATIVE_HISTOGRAM_FIELD,
+ native_histogram_value_type().clone(),
+ true,
+ ),
+ },
+ ];
+ let request = RegionCreateRequest {
+ column_metadatas: native_histogram_columns,
+ table_dir: "test_dir".to_string(),
+ path_type: PathType::Bare,
+ engine: METRIC_ENGINE_NAME.to_string(),
+ primary_key: vec![],
+ options: [(
+ LOGICAL_TABLE_METADATA_KEY.to_string(),
+ "physical".to_string(),
+ )]
+ .into_iter()
+ .collect(),
+ partition_expr_json: Some("".to_string()),
+ requirements: Default::default(),
+ };
+ MetricEngineInner::verify_region_create_request(&request).unwrap();
+
+ let request = RegionCreateRequest {
+ column_metadatas: vec![
+ ColumnMetadata {
+ column_id: 0,
+ semantic_type: SemanticType::Timestamp,
+ column_schema: ColumnSchema::new(
+ greptime_timestamp(),
+ ConcreteDataType::timestamp_millisecond_datatype(),
+ false,
+ ),
+ },
+ ColumnMetadata {
+ column_id: 1,
+ semantic_type: SemanticType::Field,
+ column_schema: ColumnSchema::new(
+ "value_a",
+ ConcreteDataType::float64_datatype(),
+ true,
+ ),
+ },
+ ColumnMetadata {
+ column_id: 2,
+ semantic_type: SemanticType::Field,
+ column_schema: ColumnSchema::new(
+ "value_b",
+ ConcreteDataType::float64_datatype(),
+ true,
+ ),
+ },
+ ],
+ table_dir: "test_dir".to_string(),
+ path_type: PathType::Bare,
+ engine: METRIC_ENGINE_NAME.to_string(),
+ primary_key: vec![],
+ options: [(
+ LOGICAL_TABLE_METADATA_KEY.to_string(),
+ "physical".to_string(),
+ )]
+ .into_iter()
+ .collect(),
+ partition_expr_json: Some("".to_string()),
+ requirements: Default::default(),
+ };
+ assert!(MetricEngineInner::verify_region_create_request(&request).is_err());
+ }
+
#[tokio::test]
async fn test_create_request_for_physical_regions() {
// original request
diff --git a/src/metric-engine/src/engine/create/extract_new_columns.rs b/src/metric-engine/src/engine/create/extract_new_columns.rs
index 9859caa06b8..f0d3af87487 100644
--- a/src/metric-engine/src/engine/create/extract_new_columns.rs
+++ b/src/metric-engine/src/engine/create/extract_new_columns.rs
@@ -15,6 +15,7 @@
use std::collections::{HashMap, HashSet};
use api::v1::SemanticType;
+use common_query::native_histogram::is_native_histogram_value_schema;
use snafu::ensure;
use store_api::metadata::ColumnMetadata;
use store_api::region_request::RegionCreateRequest;
@@ -35,7 +36,11 @@ pub fn extract_new_columns<'a>(
&& !new_column_names.contains(&col.column_schema.name.as_str())
{
ensure!(
- col.semantic_type != SemanticType::Field,
+ col.semantic_type != SemanticType::Field
+ || is_native_histogram_value_schema(
+ &col.column_schema.name,
+ &col.column_schema.data_type
+ ),
AddingFieldColumnSnafu {
name: col.column_schema.name.clone(),
}
diff --git a/src/metric-engine/src/engine/put.rs b/src/metric-engine/src/engine/put.rs
index 103b15e596e..21865ecdbb1 100644
--- a/src/metric-engine/src/engine/put.rs
+++ b/src/metric-engine/src/engine/put.rs
@@ -195,12 +195,10 @@ impl MetricEngineInner {
requests: Vec<(RegionId, RegionPutRequest)>,
) -> Result<(RegionPutRequest, AffectedRows)> {
let total_rows: usize = requests.iter().map(|(_, req)| req.rows.rows.len()).sum();
- let mut merged_rows = Vec::with_capacity(total_rows);
+ let mut modified_requests = Vec::with_capacity(requests.len());
let mut total_affected_rows: AffectedRows = 0;
- let mut output_schema: Option> = None;
let mut merged_version: Option = None;
- // Modify and collect rows from each request
for (logical_region_id, mut request) in requests {
if let Some(request_version) = request.partition_expr_version {
if let Some(merged_version) = merged_version {
@@ -224,17 +222,15 @@ impl MetricEngineInner {
let row_count = request.rows.rows.len();
total_affected_rows += row_count as AffectedRows;
-
- // Capture the output schema from the first modified request
- if output_schema.is_none() {
- output_schema = Some(request.rows.schema.clone());
- }
-
- merged_rows.extend(request.rows.rows);
+ modified_requests.push(request.rows);
}
- // Safe to unwrap: requests is guaranteed non-empty by caller
- let schema = output_schema.unwrap();
+ let schema =
+ Self::build_union_schema(modified_requests.iter().map(|rows| rows.schema.as_slice()));
+ let mut merged_rows = Vec::with_capacity(total_rows);
+ for rows in modified_requests {
+ merged_rows.extend(Self::align_rows_to_schema(rows, &schema));
+ }
let merged_request = RegionPutRequest {
rows: Rows {
@@ -261,7 +257,8 @@ impl MetricEngineInner {
requests: Vec<(RegionId, RegionPutRequest)>,
) -> Result<(RegionPutRequest, AffectedRows)> {
// Build union schema from all requests
- let merged_schema = Self::build_union_schema(&requests);
+ let merged_schema =
+ Self::build_union_schema(requests.iter().map(|(_, req)| req.rows.schema.as_slice()));
// Align all rows to the merged schema and collect table_ids
let (merged_rows, table_ids, merged_version) =
@@ -302,17 +299,21 @@ impl MetricEngineInner {
Ok((merged_request, table_ids.len() as AffectedRows))
}
- /// Builds a union schema containing all columns from all requests.
- fn build_union_schema(requests: &[(RegionId, RegionPutRequest)]) -> Vec {
- let mut schema_map: HashMap<&str, ColumnSchema> = HashMap::new();
- for (_, request) in requests {
- for col in &request.rows.schema {
- schema_map
- .entry(col.column_name.as_str())
- .or_insert_with(|| col.clone());
+ fn build_union_schema<'a>(
+ schemas: impl IntoIterator- ,
+ ) -> Vec {
+ let mut schema = Vec::new();
+ for columns in schemas {
+ for col in columns {
+ if !schema
+ .iter()
+ .any(|existing: &ColumnSchema| existing.column_name == col.column_name)
+ {
+ schema.push(col.clone());
+ }
}
}
- schema_map.into_values().collect()
+ schema
}
fn align_requests_to_schema(
@@ -325,8 +326,6 @@ impl MetricEngineInner {
let mut table_ids = Vec::with_capacity(total_rows);
let mut merged_version: Option = None;
- let null_value = Value { value_data: None };
-
for (logical_region_id, request) in requests {
if let Some(request_version) = request.partition_expr_version {
if let Some(merged_version) = merged_version {
@@ -342,47 +341,54 @@ impl MetricEngineInner {
}
}
let table_id = logical_region_id.table_id();
-
- // Build column name to index mapping once per request
- let col_name_to_idx: FxHashMap<&str, usize> = request
- .rows
- .schema
- .iter()
- .enumerate()
- .map(|(idx, col)| (col.column_name.as_str(), idx))
- .collect();
-
- // Build column mapping array once per request
- // col_mapping[i] = Some(idx) means merged_schema[i] is at request.schema[idx]
- // col_mapping[i] = None means merged_schema[i] doesn't exist in request.schema
- let col_mapping: Vec