Skip to main content

metric_engine/engine/
put.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashMap;
16
17use api::helper::ColumnDataTypeWrapper;
18use api::v1::{
19    ColumnSchema, PrimaryKeyEncoding as PrimaryKeyEncodingProto, Row, Rows, SemanticType, Value,
20    WriteHint,
21};
22use common_telemetry::{error, info};
23use fxhash::FxHashMap;
24use snafu::{OptionExt, ResultExt, ensure};
25use store_api::codec::PrimaryKeyEncoding;
26use store_api::metadata::ColumnMetadata;
27use store_api::region_request::{
28    AffectedRows, RegionDeleteRequest, RegionPutRequest, RegionRequest,
29};
30use store_api::storage::{RegionId, TableId};
31
32use crate::engine::{MetricEngineInner, PutGroupResult};
33use crate::error::{
34    ColumnNotFoundSnafu, CreateDefaultSnafu, ForbiddenPhysicalWriteSnafu, InvalidRequestSnafu,
35    LogicalRegionNotFoundSnafu, PhysicalRegionNotFoundSnafu, Result, UnexpectedRequestSnafu,
36    UnsupportedRegionRequestSnafu,
37};
38use crate::metrics::{FORBIDDEN_OPERATION_COUNT, MITO_OPERATION_ELAPSED};
39use crate::row_modifier::{RowsIter, TableIdInput};
40use crate::utils::to_data_region_id;
41
42#[derive(Default)]
43struct PutGroup {
44    request_indices: Vec<usize>,
45    requests: Vec<(RegionId, RegionPutRequest)>,
46}
47
48impl MetricEngineInner {
49    /// Dispatch region put request
50    pub async fn put_region(
51        &self,
52        region_id: RegionId,
53        request: RegionPutRequest,
54    ) -> Result<AffectedRows> {
55        let is_putting_physical_region =
56            self.state.read().unwrap().exist_physical_region(region_id);
57
58        if is_putting_physical_region {
59            info!(
60                "Metric region received put request {request:?} on physical region {region_id:?}"
61            );
62            FORBIDDEN_OPERATION_COUNT.inc();
63
64            ForbiddenPhysicalWriteSnafu.fail()
65        } else {
66            self.put_logical_region(region_id, request).await
67        }
68    }
69
70    /// Retains every group's outcome instead of returning on the first failure.
71    pub async fn put_regions_batch(
72        &self,
73        mut requests: impl ExactSizeIterator<Item = (RegionId, RegionPutRequest)>,
74    ) -> Vec<PutGroupResult> {
75        let len = requests.len();
76        if len == 0 {
77            return Vec::new();
78        }
79        let _timer = MITO_OPERATION_ELAPSED
80            .with_label_values(&["put_batch"])
81            .start_timer();
82        if len == 1 {
83            let (region_id, request) = requests.next().unwrap();
84            return vec![PutGroupResult {
85                request_indices: vec![0],
86                result: self.put_region(region_id, request).await,
87            }];
88        }
89        let mut groups = HashMap::new();
90        let mut results = Vec::new();
91        for (index, (region_id, request)) in requests.enumerate() {
92            if let Err(error) = self.group_put_request(&mut groups, index, region_id, request) {
93                results.push(PutGroupResult {
94                    request_indices: vec![index],
95                    result: Err(error),
96                });
97            }
98        }
99        for (physical_region_id, group) in groups {
100            let result = self
101                .put_regions_batch_single_physical(physical_region_id, group.requests)
102                .await;
103            results.push(PutGroupResult {
104                request_indices: group.request_indices,
105                result,
106            });
107        }
108        results
109    }
110
111    fn group_put_request(
112        &self,
113        groups: &mut HashMap<RegionId, PutGroup>,
114        index: usize,
115        region_id: RegionId,
116        request: RegionPutRequest,
117    ) -> Result<()> {
118        if self.state.read().unwrap().exist_physical_region(region_id) {
119            FORBIDDEN_OPERATION_COUNT.inc();
120            return ForbiddenPhysicalWriteSnafu.fail();
121        }
122        let physical_region_id = self.find_physical_region_id(region_id)?;
123        let group = groups.entry(physical_region_id).or_default();
124        group.request_indices.push(index);
125        group.requests.push((region_id, request));
126        Ok(())
127    }
128
129    /// Write a batch of requests that all belong to the same physical region.
130    ///
131    /// This function orchestrates the batch write process:
132    /// 1. Validates all requests
133    /// 2. Merges requests according to the encoding strategy (sparse or dense)
134    /// 3. Writes the merged batch to the physical region
135    async fn put_regions_batch_single_physical(
136        &self,
137        physical_region_id: RegionId,
138        mut requests: Vec<(RegionId, RegionPutRequest)>,
139    ) -> Result<AffectedRows> {
140        if requests.is_empty() {
141            return Ok(0);
142        }
143
144        let data_region_id = to_data_region_id(physical_region_id);
145        let primary_key_encoding = self.get_primary_key_encoding(data_region_id)?;
146
147        // TODO(weny): Consolidate validation and merging to avoid redundant request traversals,
148        // while ensuring the entire batch is validated before writing.
149        // Validate all requests
150        self.validate_batch_requests(physical_region_id, &mut requests)
151            .await?;
152
153        // Merge requests according to encoding strategy
154        let (merged_request, total_affected_rows) = match primary_key_encoding {
155            PrimaryKeyEncoding::Sparse => self.merge_sparse_batch(physical_region_id, requests)?,
156            PrimaryKeyEncoding::Dense => self.merge_dense_batch(data_region_id, requests)?,
157        };
158
159        // Write once to the physical region
160        self.data_region
161            .write_data(data_region_id, RegionRequest::Put(merged_request))
162            .await?;
163
164        Ok(total_affected_rows)
165    }
166
167    /// Get primary key encoding for a data region.
168    fn get_primary_key_encoding(&self, data_region_id: RegionId) -> Result<PrimaryKeyEncoding> {
169        let state = self.state.read().unwrap();
170        state
171            .get_primary_key_encoding(data_region_id)
172            .context(PhysicalRegionNotFoundSnafu {
173                region_id: data_region_id,
174            })
175    }
176
177    /// Validates all requests in a batch.
178    async fn validate_batch_requests(
179        &self,
180        physical_region_id: RegionId,
181        requests: &mut [(RegionId, RegionPutRequest)],
182    ) -> Result<()> {
183        let skip_wal = requests
184            .first()
185            .is_some_and(|(_, request)| request.skip_wal);
186        ensure!(
187            requests
188                .iter()
189                .all(|(_, request)| request.skip_wal == skip_wal),
190            InvalidRequestSnafu {
191                region_id: physical_region_id,
192                reason: "inconsistent WAL policy in batch"
193            }
194        );
195        for (logical_region_id, request) in requests {
196            self.verify_rows(
197                *logical_region_id,
198                physical_region_id,
199                &mut request.rows,
200                true,
201            )
202            .await?;
203        }
204        Ok(())
205    }
206
207    /// Merges multiple requests using sparse primary key encoding.
208    fn merge_sparse_batch(
209        &self,
210        physical_region_id: RegionId,
211        requests: Vec<(RegionId, RegionPutRequest)>,
212    ) -> Result<(RegionPutRequest, AffectedRows)> {
213        let skip_wal = requests
214            .first()
215            .is_some_and(|(_, request)| request.skip_wal);
216        let total_rows: usize = requests.iter().map(|(_, req)| req.rows.rows.len()).sum();
217        let mut modified_requests = Vec::with_capacity(requests.len());
218        let mut total_affected_rows: AffectedRows = 0;
219        let mut merged_version: Option<u64> = None;
220
221        for (logical_region_id, mut request) in requests {
222            if let Some(request_version) = request.partition_expr_version {
223                if let Some(merged_version) = merged_version {
224                    ensure!(
225                        merged_version == request_version,
226                        InvalidRequestSnafu {
227                            region_id: physical_region_id,
228                            reason: "inconsistent partition expr version in batch"
229                        }
230                    );
231                } else {
232                    merged_version = Some(request_version);
233                }
234            }
235            self.modify_rows(
236                physical_region_id,
237                logical_region_id.table_id(),
238                &mut request.rows,
239                PrimaryKeyEncoding::Sparse,
240            )?;
241
242            let row_count = request.rows.rows.len();
243            total_affected_rows += row_count as AffectedRows;
244            modified_requests.push(request.rows);
245        }
246
247        let schema =
248            Self::build_union_schema(modified_requests.iter().map(|rows| rows.schema.as_slice()));
249        let mut merged_rows = Vec::with_capacity(total_rows);
250        for rows in modified_requests {
251            merged_rows.extend(Self::align_rows_to_schema(rows, &schema));
252        }
253
254        let merged_request = RegionPutRequest {
255            skip_wal,
256            rows: Rows {
257                schema,
258                rows: merged_rows,
259            },
260            hint: Some(WriteHint {
261                primary_key_encoding: PrimaryKeyEncodingProto::Sparse.into(),
262            }),
263            partition_expr_version: merged_version,
264        };
265
266        Ok((merged_request, total_affected_rows))
267    }
268
269    /// Merges multiple requests using dense primary key encoding.
270    ///
271    /// In dense mode, different requests can have different columns.
272    /// We merge all schemas into a union schema, align each row to this schema,
273    /// then batch-modify all rows together (adding __table_id and __tsid).
274    fn merge_dense_batch(
275        &self,
276        data_region_id: RegionId,
277        requests: Vec<(RegionId, RegionPutRequest)>,
278    ) -> Result<(RegionPutRequest, AffectedRows)> {
279        let skip_wal = requests
280            .first()
281            .is_some_and(|(_, request)| request.skip_wal);
282        // Build union schema from all requests
283        let merged_schema =
284            Self::build_union_schema(requests.iter().map(|(_, req)| req.rows.schema.as_slice()));
285
286        // Align all rows to the merged schema and collect table_ids
287        let (merged_rows, table_ids, merged_version) =
288            Self::align_requests_to_schema(requests, &merged_schema)?;
289
290        // Batch-modify all rows (add __table_id and __tsid columns)
291        let final_rows = {
292            let state = self.state.read().unwrap();
293            let physical_columns = state
294                .physical_region_states()
295                .get(&data_region_id)
296                .with_context(|| PhysicalRegionNotFoundSnafu {
297                    region_id: data_region_id,
298                })?
299                .physical_columns();
300
301            let iter = RowsIter::new(
302                Rows {
303                    schema: merged_schema,
304                    rows: merged_rows,
305                },
306                physical_columns,
307            );
308
309            self.row_modifier.modify_rows(
310                iter,
311                TableIdInput::Batch(&table_ids),
312                PrimaryKeyEncoding::Dense,
313            )?
314        };
315
316        let merged_request = RegionPutRequest {
317            skip_wal,
318            rows: final_rows,
319            hint: None,
320            partition_expr_version: merged_version,
321        };
322
323        Ok((merged_request, table_ids.len() as AffectedRows))
324    }
325
326    fn build_union_schema<'a>(
327        schemas: impl IntoIterator<Item = &'a [ColumnSchema]>,
328    ) -> Vec<ColumnSchema> {
329        let mut schema = Vec::new();
330        for columns in schemas {
331            for col in columns {
332                if !schema
333                    .iter()
334                    .any(|existing: &ColumnSchema| existing.column_name == col.column_name)
335                {
336                    schema.push(col.clone());
337                }
338            }
339        }
340        schema
341    }
342
343    fn align_requests_to_schema(
344        requests: Vec<(RegionId, RegionPutRequest)>,
345        merged_schema: &[ColumnSchema],
346    ) -> Result<(Vec<Row>, Vec<TableId>, Option<u64>)> {
347        // Pre-calculate total capacity
348        let total_rows: usize = requests.iter().map(|(_, req)| req.rows.rows.len()).sum();
349        let mut merged_rows = Vec::with_capacity(total_rows);
350        let mut table_ids = Vec::with_capacity(total_rows);
351        let mut merged_version: Option<u64> = None;
352
353        for (logical_region_id, request) in requests {
354            if let Some(request_version) = request.partition_expr_version {
355                if let Some(merged_version) = merged_version {
356                    ensure!(
357                        merged_version == request_version,
358                        InvalidRequestSnafu {
359                            region_id: logical_region_id,
360                            reason: "inconsistent partition expr version in batch"
361                        }
362                    );
363                } else {
364                    merged_version = Some(request_version);
365                }
366            }
367            let table_id = logical_region_id.table_id();
368            let row_count = request.rows.rows.len();
369            merged_rows.extend(Self::align_rows_to_schema(request.rows, merged_schema));
370            table_ids.extend(std::iter::repeat_n(table_id, row_count));
371        }
372
373        Ok((merged_rows, table_ids, merged_version))
374    }
375
376    fn align_rows_to_schema(rows: Rows, merged_schema: &[ColumnSchema]) -> Vec<Row> {
377        let Rows { schema, rows } = rows;
378        if schema.len() == merged_schema.len()
379            && schema
380                .iter()
381                .zip(merged_schema)
382                .all(|(left, right)| left.column_name == right.column_name)
383        {
384            return rows;
385        }
386
387        let col_name_to_idx: FxHashMap<&str, usize> = schema
388            .iter()
389            .enumerate()
390            .map(|(idx, col)| (col.column_name.as_str(), idx))
391            .collect();
392        let col_mapping: Vec<Option<usize>> = merged_schema
393            .iter()
394            .map(|merged_col| {
395                col_name_to_idx
396                    .get(merged_col.column_name.as_str())
397                    .copied()
398            })
399            .collect();
400        let null_value = Value { value_data: None };
401
402        rows.into_iter()
403            .map(|mut row| {
404                let values = col_mapping
405                    .iter()
406                    .map(|opt_idx| match opt_idx {
407                        Some(idx) => std::mem::take(&mut row.values[*idx]),
408                        None => null_value.clone(),
409                    })
410                    .collect();
411                Row { values }
412            })
413            .collect()
414    }
415
416    /// Find the physical region id for a logical region.
417    fn find_physical_region_id(&self, logical_region_id: RegionId) -> Result<RegionId> {
418        let state = self.state.read().unwrap();
419        state
420            .logical_regions()
421            .get(&logical_region_id)
422            .copied()
423            .context(LogicalRegionNotFoundSnafu {
424                region_id: logical_region_id,
425            })
426    }
427
428    /// Dispatch region delete request
429    pub async fn delete_region(
430        &self,
431        region_id: RegionId,
432        request: RegionDeleteRequest,
433    ) -> Result<AffectedRows> {
434        if self.is_physical_region(region_id) {
435            info!(
436                "Metric region received delete request {request:?} on physical region {region_id:?}"
437            );
438            FORBIDDEN_OPERATION_COUNT.inc();
439
440            UnsupportedRegionRequestSnafu {
441                request: RegionRequest::Delete(request),
442            }
443            .fail()
444        } else {
445            self.delete_logical_region(region_id, request).await
446        }
447    }
448
449    async fn put_logical_region(
450        &self,
451        logical_region_id: RegionId,
452        mut request: RegionPutRequest,
453    ) -> Result<AffectedRows> {
454        let _timer = MITO_OPERATION_ELAPSED
455            .with_label_values(&["put"])
456            .start_timer();
457
458        let (physical_region_id, data_region_id, primary_key_encoding) =
459            self.find_data_region_meta(logical_region_id)?;
460
461        self.verify_rows(
462            logical_region_id,
463            physical_region_id,
464            &mut request.rows,
465            true,
466        )
467        .await?;
468
469        // write to data region
470        // TODO: retrieve table name
471        self.modify_rows(
472            physical_region_id,
473            logical_region_id.table_id(),
474            &mut request.rows,
475            primary_key_encoding,
476        )?;
477        if primary_key_encoding == PrimaryKeyEncoding::Sparse {
478            request.hint = Some(WriteHint {
479                primary_key_encoding: PrimaryKeyEncodingProto::Sparse.into(),
480            });
481        }
482        self.data_region
483            .write_data(data_region_id, RegionRequest::Put(request))
484            .await
485    }
486
487    async fn delete_logical_region(
488        &self,
489        logical_region_id: RegionId,
490        mut request: RegionDeleteRequest,
491    ) -> Result<AffectedRows> {
492        let _timer = MITO_OPERATION_ELAPSED
493            .with_label_values(&["delete"])
494            .start_timer();
495
496        let (physical_region_id, data_region_id, primary_key_encoding) =
497            self.find_data_region_meta(logical_region_id)?;
498
499        self.verify_rows(
500            logical_region_id,
501            physical_region_id,
502            &mut request.rows,
503            false,
504        )
505        .await?;
506
507        // write to data region
508        // TODO: retrieve table name
509        self.modify_rows(
510            physical_region_id,
511            logical_region_id.table_id(),
512            &mut request.rows,
513            primary_key_encoding,
514        )?;
515        if primary_key_encoding == PrimaryKeyEncoding::Sparse {
516            request.hint = Some(WriteHint {
517                primary_key_encoding: PrimaryKeyEncodingProto::Sparse.into(),
518            });
519        }
520        self.data_region
521            .write_data(data_region_id, RegionRequest::Delete(request))
522            .await
523    }
524
525    pub(crate) fn find_data_region_meta(
526        &self,
527        logical_region_id: RegionId,
528    ) -> Result<(RegionId, RegionId, PrimaryKeyEncoding)> {
529        let state = self.state.read().unwrap();
530        let physical_region_id = *state
531            .logical_regions()
532            .get(&logical_region_id)
533            .with_context(|| LogicalRegionNotFoundSnafu {
534                region_id: logical_region_id,
535            })?;
536        let data_region_id = to_data_region_id(physical_region_id);
537        let primary_key_encoding = state.get_primary_key_encoding(data_region_id).context(
538            PhysicalRegionNotFoundSnafu {
539                region_id: data_region_id,
540            },
541        )?;
542        Ok((physical_region_id, data_region_id, primary_key_encoding))
543    }
544
545    /// Verifies a request for a logical region against its corresponding metadata region.
546    ///
547    /// Includes:
548    /// - Check if the logical region exists
549    /// - Check if every column in the request exists in the physical region
550    /// - Check each column's datatype and semantic type match the physical region's schema
551    /// - Check the time index column is present
552    /// - When `check_fields` is true, check every logical field column is present.
553    ///   Set this to `false` for delete requests, which legitimately carry only
554    ///   the primary key + timestamp.
555    async fn verify_rows(
556        &self,
557        logical_region_id: RegionId,
558        physical_region_id: RegionId,
559        rows: &mut Rows,
560        check_fields: bool,
561    ) -> Result<()> {
562        // Check if the region exists
563        let data_region_id = to_data_region_id(physical_region_id);
564        let (physical_columns, ts_name) = {
565            let state = self.state.read().unwrap();
566            if !state.is_logical_region_exist(logical_region_id) {
567                error!("Trying to write to an nonexistent region {logical_region_id}");
568                return LogicalRegionNotFoundSnafu {
569                    region_id: logical_region_id,
570                }
571                .fail();
572            }
573
574            let physical_state = state
575                .physical_region_states()
576                .get(&data_region_id)
577                .context(PhysicalRegionNotFoundSnafu {
578                    region_id: data_region_id,
579                })?;
580            (
581                physical_state.physical_columns_snapshot(),
582                physical_state.time_index_column_name().to_string(),
583            )
584        };
585
586        // Type + semantic check on every column in the request schema.
587        for col in &rows.schema {
588            let info = physical_columns
589                .get(&col.column_name)
590                .context(ColumnNotFoundSnafu {
591                    name: &col.column_name,
592                    region_id: logical_region_id,
593                })?;
594
595            ensure!(
596                api::helper::is_column_type_value_eq(
597                    col.datatype,
598                    col.datatype_extension.clone(),
599                    &info.column_schema.data_type
600                ),
601                InvalidRequestSnafu {
602                    region_id: logical_region_id,
603                    reason: format!(
604                        "column {} expect type {:?}, given: {}({})",
605                        col.column_name,
606                        info.column_schema.data_type,
607                        api::v1::ColumnDataType::try_from(col.datatype)
608                            .map(|v| v.as_str_name())
609                            .unwrap_or("Unknown"),
610                        col.datatype,
611                    ),
612                }
613            );
614
615            ensure!(
616                api::helper::is_semantic_type_eq(col.semantic_type, info.semantic_type),
617                InvalidRequestSnafu {
618                    region_id: logical_region_id,
619                    reason: format!(
620                        "column {} expect semantic type {:?}, given: {}({})",
621                        col.column_name,
622                        info.semantic_type,
623                        api::v1::SemanticType::try_from(col.semantic_type)
624                            .map(|v| v.as_str_name())
625                            .unwrap_or("Unknown"),
626                        col.semantic_type,
627                    ),
628                }
629            );
630        }
631
632        ensure!(
633            rows.schema.iter().any(|col| col.column_name == ts_name),
634            InvalidRequestSnafu {
635                region_id: logical_region_id,
636                reason: format!("missing required time index column {ts_name}"),
637            }
638        );
639
640        let logical_columns = self
641            .load_logical_columns(physical_region_id, logical_region_id)
642            .await?;
643        let logical_fields = logical_columns
644            .iter()
645            .filter(|col| col.semantic_type == SemanticType::Field)
646            .map(|col| (col.column_schema.name.as_str(), col))
647            .collect::<HashMap<_, _>>();
648
649        for col in &rows.schema {
650            if api::helper::is_semantic_type_eq(col.semantic_type, SemanticType::Field) {
651                ensure!(
652                    logical_fields.contains_key(col.column_name.as_str()),
653                    InvalidRequestSnafu {
654                        region_id: logical_region_id,
655                        reason: format!(
656                            "field column {} does not belong to logical region {logical_region_id}",
657                            col.column_name,
658                        ),
659                    }
660                );
661            }
662        }
663
664        if check_fields {
665            // Sparse logical writes may omit nullable field columns. Fill them
666            // before the rows are rewritten for the shared physical table.
667            for (field_name, field_meta) in logical_fields {
668                if !rows.schema.iter().any(|col| col.column_name == field_name) {
669                    Self::fill_missing_field_column(
670                        logical_region_id,
671                        field_name,
672                        field_meta,
673                        rows,
674                    )?;
675                }
676            }
677
678            for (field_name, field_meta) in physical_columns
679                .iter()
680                .filter(|(_, col)| col.semantic_type == SemanticType::Field)
681            {
682                if !rows.schema.iter().any(|col| col.column_name == *field_name) {
683                    Self::fill_missing_field_column(
684                        logical_region_id,
685                        field_name,
686                        field_meta,
687                        rows,
688                    )?;
689                }
690            }
691        }
692
693        Ok(())
694    }
695
696    fn fill_missing_field_column(
697        logical_region_id: RegionId,
698        field_name: &str,
699        field_meta: &ColumnMetadata,
700        rows: &mut Rows,
701    ) -> Result<()> {
702        // This is only for schema columns with a concrete default, usually NULL
703        // for field columns from other logical tables sharing this physical table.
704        ensure!(
705            !field_meta.column_schema.is_default_impure(),
706            UnexpectedRequestSnafu {
707                reason: format!(
708                    "unexpected impure default value with region_id: {logical_region_id}, column: {field_name}, default_value: {:?}",
709                    field_meta.column_schema.default_constraint(),
710                ),
711            }
712        );
713
714        let default_value = field_meta
715            .column_schema
716            .create_default()
717            .context(CreateDefaultSnafu {
718                region_id: logical_region_id,
719                column: field_name,
720            })?
721            .with_context(|| InvalidRequestSnafu {
722                region_id: logical_region_id,
723                reason: format!("missing required field column {field_name}"),
724            })?;
725        let default_value = api::helper::to_grpc_value(default_value);
726        let (datatype, datatype_extension) =
727            ColumnDataTypeWrapper::try_from(field_meta.column_schema.data_type.clone())
728                .map_err(|e| {
729                    InvalidRequestSnafu {
730                        region_id: logical_region_id,
731                        reason: format!(
732                            "no protobuf type for field column {field_name} ({:?}): {e}",
733                            field_meta.column_schema.data_type
734                        ),
735                    }
736                    .build()
737                })?
738                .to_parts();
739
740        rows.schema.push(ColumnSchema {
741            column_name: field_name.to_string(),
742            datatype: datatype as i32,
743            semantic_type: SemanticType::Field as i32,
744            datatype_extension,
745            options: None,
746        });
747
748        for row in &mut rows.rows {
749            row.values.push(default_value.clone());
750        }
751
752        Ok(())
753    }
754
755    /// Perform metric engine specific logic to incoming rows.
756    /// - Add table_id column
757    /// - Generate tsid
758    fn modify_rows(
759        &self,
760        physical_region_id: RegionId,
761        table_id: TableId,
762        rows: &mut Rows,
763        encoding: PrimaryKeyEncoding,
764    ) -> Result<()> {
765        let input = std::mem::take(rows);
766        let iter = {
767            let state = self.state.read().unwrap();
768            let physical_columns = state
769                .physical_region_states()
770                .get(&physical_region_id)
771                .with_context(|| PhysicalRegionNotFoundSnafu {
772                    region_id: physical_region_id,
773                })?
774                .physical_columns();
775            RowsIter::new(input, physical_columns)
776        };
777        let output =
778            self.row_modifier
779                .modify_rows(iter, TableIdInput::Single(table_id), encoding)?;
780        *rows = output;
781        Ok(())
782    }
783}
784
785#[cfg(test)]
786mod tests {
787    use std::collections::HashSet;
788
789    use api::v1::value::ValueData;
790    use api::v1::{ColumnDataType, ColumnSchema as PbColumnSchema};
791    use common_error::ext::ErrorExt;
792    use common_error::status_code::StatusCode;
793    use common_function::utils::partition_expr_version;
794    use common_query::prelude::{greptime_native_histogram, greptime_timestamp, greptime_value};
795    use common_recordbatch::RecordBatches;
796    use datatypes::arrow::array::{
797        Float64Array, TimestampMicrosecondArray, TimestampMillisecondArray,
798    };
799    use datatypes::prelude::ConcreteDataType;
800    use datatypes::schema::{ColumnDefaultConstraint, ColumnSchema};
801    use datatypes::value::Value as PartitionValue;
802    use partition::expr::col;
803    use store_api::metadata::ColumnMetadata;
804    use store_api::metric_engine_consts::{
805        DATA_SCHEMA_TABLE_ID_COLUMN_NAME, DATA_SCHEMA_TSID_COLUMN_NAME, METRIC_ENGINE_NAME,
806        PHYSICAL_TABLE_METADATA_KEY, PRIMARY_KEY_ENCODING,
807    };
808    use store_api::path_utils::table_dir;
809    use store_api::region_engine::RegionEngine;
810    use store_api::region_request::{
811        EnterStagingRequest, PathType, RegionCloseRequest, RegionOpenRequest, RegionRequest,
812        StagingPartitionDirective,
813    };
814    use store_api::storage::ScanRequest;
815    use store_api::storage::consts::PRIMARY_KEY_COLUMN_NAME;
816
817    use super::*;
818    use crate::engine::MetricEngine;
819    use crate::test_util::{self, TestEnv};
820
821    async fn scan_timestamp_values(engine: &MetricEngine, region_id: RegionId) -> Vec<(i64, f64)> {
822        let stream = engine
823            .scan_to_stream(region_id, ScanRequest::default())
824            .await
825            .unwrap();
826        let batches = RecordBatches::try_collect(stream).await.unwrap();
827        let mut rows = Vec::new();
828        for batch in batches.iter() {
829            let batch = batch.df_record_batch();
830            let timestamp_index = batch.schema().index_of(greptime_timestamp()).unwrap();
831            let value_index = batch.schema().index_of(greptime_value()).unwrap();
832            let timestamps = batch
833                .column(timestamp_index)
834                .as_any()
835                .downcast_ref::<TimestampMillisecondArray>()
836                .unwrap();
837            let values = batch
838                .column(value_index)
839                .as_any()
840                .downcast_ref::<Float64Array>()
841                .unwrap();
842            rows.extend(
843                timestamps
844                    .values()
845                    .iter()
846                    .copied()
847                    .zip(values.values().iter().copied()),
848            );
849        }
850        rows.sort_unstable_by_key(|(timestamp, _)| *timestamp);
851        rows
852    }
853
854    #[tokio::test]
855    async fn test_batch_partition_versions() {
856        check_batch_partition_versions("sparse").await;
857        check_batch_partition_versions("dense").await;
858    }
859
860    async fn check_batch_partition_versions(encoding: &str) {
861        let env = TestEnv::new().await;
862        let physical_region_id = env.default_physical_region_id();
863        let logical_region_id = env.default_logical_region_id();
864        env.create_physical_region(
865            physical_region_id,
866            &TestEnv::default_table_dir(),
867            vec![(PRIMARY_KEY_ENCODING.to_string(), encoding.to_string())],
868        )
869        .await;
870        create_logical_region_with_tags(&env, physical_region_id, logical_region_id, &["job"])
871            .await;
872        let build_requests = |versions: [Option<u64>; 3]| {
873            versions
874                .into_iter()
875                .map(|partition_expr_version| {
876                    (
877                        logical_region_id,
878                        RegionPutRequest {
879                            skip_wal: false,
880                            rows: Rows {
881                                schema: test_util::row_schema_with_tags(&["job"]),
882                                rows: test_util::build_rows(1, 1),
883                            },
884                            hint: None,
885                            partition_expr_version,
886                        },
887                    )
888                })
889                .collect::<Vec<_>>()
890        };
891        // Conflicting explicit versions must fail before any data is written.
892        let err = env
893            .metric()
894            .inner
895            .put_regions_batch_single_physical(
896                physical_region_id,
897                build_requests([None, Some(10), Some(11)]),
898            )
899            .await
900            .unwrap_err();
901        assert!(
902            err.to_string()
903                .contains("inconsistent partition expr version")
904        );
905        assert!(
906            scan_timestamp_values(&env.metric(), logical_region_id)
907                .await
908                .is_empty()
909        );
910
911        for (versions, expected) in [
912            ([None, None, None], None),
913            ([None, Some(7), None], Some(7)),
914            ([Some(7), None, Some(7)], Some(7)),
915        ] {
916            let mut requests = build_requests(versions);
917            let engine = env.metric();
918            engine
919                .inner
920                .validate_batch_requests(physical_region_id, &mut requests)
921                .await
922                .unwrap();
923            let (merged, _) = match encoding {
924                "sparse" => engine
925                    .inner
926                    .merge_sparse_batch(physical_region_id, requests),
927                "dense" => engine
928                    .inner
929                    .merge_dense_batch(to_data_region_id(physical_region_id), requests),
930                _ => unreachable!(),
931            }
932            .unwrap();
933            assert_eq!(merged.partition_expr_version, expected);
934        }
935    }
936
937    #[tokio::test]
938    async fn test_put_skip_wal_batch_recovery() {
939        check_put_skip_wal_batch_recovery("sparse", false, false).await;
940        check_put_skip_wal_batch_recovery("sparse", true, false).await;
941        check_put_skip_wal_batch_recovery("dense", false, false).await;
942        check_put_skip_wal_batch_recovery("dense", true, false).await;
943    }
944
945    #[tokio::test]
946    async fn test_put_skip_wal_batch_close_recovery() {
947        check_put_skip_wal_batch_recovery("sparse", false, true).await;
948        check_put_skip_wal_batch_recovery("sparse", true, true).await;
949        check_put_skip_wal_batch_recovery("dense", false, true).await;
950        check_put_skip_wal_batch_recovery("dense", true, true).await;
951    }
952
953    async fn check_put_skip_wal_batch_recovery(encoding: &str, skip_wal: bool, close: bool) {
954        let mut env = TestEnv::new().await;
955        let engine = env.metric();
956        engine.inner.flush_task.stop().await.unwrap();
957        let physical_region_id = env.default_physical_region_id();
958        let logical_region_id = env.default_logical_region_id();
959        env.create_physical_region(
960            physical_region_id,
961            &TestEnv::default_table_dir(),
962            vec![(PRIMARY_KEY_ENCODING.to_string(), encoding.to_string())],
963        )
964        .await;
965        create_logical_region_with_tags(&env, physical_region_id, logical_region_id, &["job"])
966            .await;
967        let metadata_before = engine.get_metadata(logical_region_id).await.unwrap();
968
969        let requests = [skip_wal; 3]
970            .into_iter()
971            .enumerate()
972            .map(|(index, skip_wal)| {
973                let timestamp = index as i64 + 1;
974                let value = timestamp as f64 * 10.0;
975                // Every request updates the same key at timestamp zero and
976                // also inserts a distinct key to verify merge order.
977                let rows = [0, timestamp]
978                    .into_iter()
979                    .map(|timestamp| Row {
980                        values: vec![
981                            Value {
982                                value_data: Some(ValueData::TimestampMillisecondValue(timestamp)),
983                            },
984                            Value {
985                                value_data: Some(ValueData::F64Value(value)),
986                            },
987                            Value {
988                                value_data: Some(ValueData::StringValue("tag_0".to_string())),
989                            },
990                        ],
991                    })
992                    .collect();
993                (
994                    logical_region_id,
995                    RegionPutRequest {
996                        rows: Rows {
997                            schema: test_util::row_schema_with_tags(&["job"]),
998                            rows,
999                        },
1000                        hint: None,
1001                        partition_expr_version: None,
1002                        skip_wal,
1003                    },
1004                )
1005            });
1006        let results = engine.put_regions_batch(requests).await;
1007        assert_eq!(results.len(), 1);
1008        let affected_rows = results.into_iter().next().unwrap().result.unwrap();
1009        assert_eq!(affected_rows, 6);
1010        assert_eq!(
1011            scan_timestamp_values(&engine, logical_region_id).await,
1012            vec![(0, 30.0), (1, 10.0), (2, 20.0), (3, 30.0)]
1013        );
1014
1015        // Neither data nor metadata has an SST to hide missing WAL.
1016        for region_id in [
1017            to_data_region_id(physical_region_id),
1018            crate::utils::to_metadata_region_id(physical_region_id),
1019        ] {
1020            let stat = env.mito().region_statistic(region_id).unwrap();
1021            assert!(stat.memtable_size > 0);
1022            assert_eq!(stat.sst_num, 0);
1023        }
1024        let reopened = if close {
1025            engine
1026                .handle_request(
1027                    physical_region_id,
1028                    RegionRequest::Close(RegionCloseRequest::default()),
1029                )
1030                .await
1031                .unwrap();
1032            // Recreate the wrapper as well, discarding its metadata cache.
1033            MetricEngine::try_new(env.mito(), Default::default()).unwrap()
1034        } else {
1035            // Bypass Close to retain coverage of rows lost without WAL protection.
1036            env.reopen_engine(Default::default()).await;
1037            env.metric()
1038        };
1039        reopened.inner.flush_task.stop().await.unwrap();
1040        reopened
1041            .handle_request(
1042                physical_region_id,
1043                RegionRequest::Open(RegionOpenRequest {
1044                    engine: METRIC_ENGINE_NAME.to_string(),
1045                    table_dir: TestEnv::default_table_dir(),
1046                    path_type: PathType::Bare,
1047                    options: [
1048                        (PHYSICAL_TABLE_METADATA_KEY.to_string(), String::new()),
1049                        (PRIMARY_KEY_ENCODING.to_string(), encoding.to_string()),
1050                    ]
1051                    .into_iter()
1052                    .collect(),
1053                    skip_wal_replay: false,
1054                    checkpoint: None,
1055                    requirements: Default::default(),
1056                }),
1057            )
1058            .await
1059            .unwrap();
1060        let stat = env
1061            .mito()
1062            .region_statistic(crate::utils::to_data_region_id(physical_region_id))
1063            .unwrap();
1064        assert_eq!(stat.sst_num > 0, skip_wal && close);
1065        let recovered_metadata = reopened.get_metadata(logical_region_id).await.unwrap();
1066        assert_eq!(
1067            metadata_before.column_metadatas,
1068            recovered_metadata.column_metadatas
1069        );
1070        let expected = if skip_wal && !close {
1071            vec![]
1072        } else {
1073            vec![(0, 30.0), (1, 10.0), (2, 20.0), (3, 30.0)]
1074        };
1075        assert_eq!(
1076            scan_timestamp_values(&reopened, logical_region_id).await,
1077            expected,
1078            "encoding={encoding}, skip_wal={skip_wal}, close={close}"
1079        );
1080    }
1081
1082    fn assert_merged_schema(rows: &Rows, expect_sparse: bool) {
1083        let column_names: HashSet<String> = rows
1084            .schema
1085            .iter()
1086            .map(|col| col.column_name.clone())
1087            .collect();
1088
1089        if expect_sparse {
1090            assert!(
1091                column_names.contains(PRIMARY_KEY_COLUMN_NAME),
1092                "sparse encoding should include primary key column"
1093            );
1094            assert!(
1095                !column_names.contains(DATA_SCHEMA_TABLE_ID_COLUMN_NAME),
1096                "sparse encoding should not include table id column"
1097            );
1098            assert!(
1099                !column_names.contains(DATA_SCHEMA_TSID_COLUMN_NAME),
1100                "sparse encoding should not include tsid column"
1101            );
1102            assert!(
1103                !column_names.contains("job"),
1104                "sparse encoding should not include tag columns"
1105            );
1106            assert!(
1107                !column_names.contains("instance"),
1108                "sparse encoding should not include tag columns"
1109            );
1110        } else {
1111            assert!(
1112                !column_names.contains(PRIMARY_KEY_COLUMN_NAME),
1113                "dense encoding should not include primary key column"
1114            );
1115            assert!(
1116                column_names.contains(DATA_SCHEMA_TABLE_ID_COLUMN_NAME),
1117                "dense encoding should include table id column"
1118            );
1119            assert!(
1120                column_names.contains(DATA_SCHEMA_TSID_COLUMN_NAME),
1121                "dense encoding should include tsid column"
1122            );
1123            assert!(
1124                column_names.contains("job"),
1125                "dense encoding should keep tag columns"
1126            );
1127            assert!(
1128                column_names.contains("instance"),
1129                "dense encoding should keep tag columns"
1130            );
1131        }
1132    }
1133
1134    fn job_partition_expr_json() -> String {
1135        let expr = col("job")
1136            .gt_eq(PartitionValue::String("job-0".into()))
1137            .and(col("job").lt(PartitionValue::String("job-9".into())));
1138        expr.as_json_str().unwrap()
1139    }
1140
1141    #[tokio::test]
1142    async fn test_put_and_scan_microsecond_physical_region() {
1143        let env = TestEnv::new().await;
1144        let engine = env.metric();
1145        let physical_region_id = env.default_physical_region_id();
1146        let logical_region_id = env.default_logical_region_id();
1147        env.create_physical_region_with_ts_type(
1148            physical_region_id,
1149            &TestEnv::default_table_dir(),
1150            vec![],
1151            ConcreteDataType::timestamp_microsecond_datatype(),
1152        )
1153        .await;
1154
1155        // A logical region with a matching microsecond time index is accepted.
1156        let region_create_request = test_util::create_logical_region_request_with_ts_type(
1157            &["job"],
1158            physical_region_id,
1159            &table_dir("test", logical_region_id.table_id()),
1160            ConcreteDataType::timestamp_microsecond_datatype(),
1161        );
1162        engine
1163            .handle_request(
1164                logical_region_id,
1165                RegionRequest::Create(region_create_request),
1166            )
1167            .await
1168            .unwrap();
1169
1170        // Writing microsecond rows works.
1171        let affected_rows = engine
1172            .handle_request(
1173                logical_region_id,
1174                RegionRequest::Put(RegionPutRequest {
1175                    rows: Rows {
1176                        schema: test_util::row_schema_with_tags_and_ts_datatype(
1177                            &["job"],
1178                            ColumnDataType::TimestampMicrosecond,
1179                        ),
1180                        rows: test_util::build_rows_with_ts_datatype(
1181                            1,
1182                            2,
1183                            ColumnDataType::TimestampMicrosecond,
1184                        ),
1185                    },
1186                    hint: None,
1187                    partition_expr_version: None,
1188                    skip_wal: false,
1189                }),
1190            )
1191            .await
1192            .unwrap();
1193        assert_eq!(affected_rows.affected_rows, 2);
1194
1195        // The scan returns timestamps in the physical region's unit.
1196        let stream = engine
1197            .scan_to_stream(logical_region_id, ScanRequest::default())
1198            .await
1199            .unwrap();
1200        let batches = RecordBatches::try_collect(stream).await.unwrap();
1201        let mut rows = Vec::new();
1202        for batch in batches.iter() {
1203            let batch = batch.df_record_batch();
1204            let timestamps = batch
1205                .column(batch.schema().index_of(greptime_timestamp()).unwrap())
1206                .as_any()
1207                .downcast_ref::<TimestampMicrosecondArray>()
1208                .unwrap();
1209            let values = batch
1210                .column(batch.schema().index_of(greptime_value()).unwrap())
1211                .as_any()
1212                .downcast_ref::<Float64Array>()
1213                .unwrap();
1214            rows.extend(
1215                timestamps
1216                    .values()
1217                    .iter()
1218                    .copied()
1219                    .zip(values.values().iter().copied()),
1220            );
1221        }
1222        rows.sort_unstable_by_key(|(timestamp, _)| *timestamp);
1223        assert_eq!(rows, vec![(0, 0.0), (1, 1.0)]);
1224    }
1225
1226    async fn create_logical_region_with_tags(
1227        env: &TestEnv,
1228        physical_region_id: RegionId,
1229        logical_region_id: RegionId,
1230        tags: &[&str],
1231    ) {
1232        let region_create_request = test_util::create_logical_region_request(
1233            tags,
1234            physical_region_id,
1235            &table_dir("test", logical_region_id.table_id()),
1236        );
1237        env.metric()
1238            .handle_request(
1239                logical_region_id,
1240                RegionRequest::Create(region_create_request),
1241            )
1242            .await
1243            .unwrap();
1244    }
1245
1246    fn column_index(rows: &Rows, name: &str) -> usize {
1247        rows.schema
1248            .iter()
1249            .position(|col| col.column_name == name)
1250            .unwrap()
1251    }
1252
1253    fn check_batch_merge_wal_policy(
1254        env: &TestEnv,
1255        physical_region_id: RegionId,
1256        mut requests: Vec<(RegionId, RegionPutRequest)>,
1257        expect_sparse: bool,
1258        skip_wal: bool,
1259    ) {
1260        for (_, request) in &mut requests {
1261            request.skip_wal = skip_wal;
1262        }
1263        let (merged_request, affected_rows) = if expect_sparse {
1264            let (merged_request, affected_rows) = env
1265                .metric()
1266                .inner
1267                .merge_sparse_batch(physical_region_id, requests)
1268                .unwrap();
1269            let hint = merged_request
1270                .hint
1271                .as_ref()
1272                .expect("missing sparse write hint");
1273            assert_eq!(
1274                hint.primary_key_encoding,
1275                PrimaryKeyEncodingProto::Sparse as i32
1276            );
1277            (merged_request, affected_rows)
1278        } else {
1279            let (merged_request, affected_rows) = env
1280                .metric()
1281                .inner
1282                .merge_dense_batch(to_data_region_id(physical_region_id), requests)
1283                .unwrap();
1284            assert!(merged_request.hint.is_none());
1285            (merged_request, affected_rows)
1286        };
1287        assert_merged_schema(&merged_request.rows, expect_sparse);
1288        assert_eq!(merged_request.skip_wal, skip_wal);
1289        assert_eq!(affected_rows, 5);
1290    }
1291
1292    async fn run_batch_write_with_schema_variants(
1293        env: &TestEnv,
1294        physical_region_id: RegionId,
1295        options: Vec<(String, String)>,
1296        expect_sparse: bool,
1297    ) {
1298        env.create_physical_region(physical_region_id, &TestEnv::default_table_dir(), options)
1299            .await;
1300
1301        let logical_region_1 = env.default_logical_region_id();
1302        let logical_region_2 = RegionId::new(1024, 1);
1303
1304        create_logical_region_with_tags(env, physical_region_id, logical_region_1, &["job"]).await;
1305        create_logical_region_with_tags(
1306            env,
1307            physical_region_id,
1308            logical_region_2,
1309            &["job", "instance"],
1310        )
1311        .await;
1312
1313        let schema_1 = test_util::row_schema_with_tags(&["job"]);
1314        let schema_2 = test_util::row_schema_with_tags(&["job", "instance"]);
1315
1316        let data_region_id = RegionId::new(physical_region_id.table_id(), 2);
1317        let primary_key_encoding = env
1318            .metric()
1319            .inner
1320            .get_primary_key_encoding(data_region_id)
1321            .unwrap();
1322        assert_eq!(
1323            primary_key_encoding,
1324            if expect_sparse {
1325                PrimaryKeyEncoding::Sparse
1326            } else {
1327                PrimaryKeyEncoding::Dense
1328            }
1329        );
1330
1331        let build_requests = || {
1332            let rows_1 = test_util::build_rows(1, 3);
1333            let rows_2 = test_util::build_rows(2, 2);
1334
1335            vec![
1336                (
1337                    logical_region_1,
1338                    RegionPutRequest {
1339                        skip_wal: false,
1340                        rows: Rows {
1341                            schema: schema_1.clone(),
1342                            rows: rows_1,
1343                        },
1344                        hint: None,
1345                        partition_expr_version: None,
1346                    },
1347                ),
1348                (
1349                    logical_region_2,
1350                    RegionPutRequest {
1351                        skip_wal: false,
1352                        rows: Rows {
1353                            schema: schema_2.clone(),
1354                            rows: rows_2,
1355                        },
1356                        hint: None,
1357                        partition_expr_version: None,
1358                    },
1359                ),
1360            ]
1361        };
1362
1363        check_batch_merge_wal_policy(
1364            env,
1365            physical_region_id,
1366            build_requests(),
1367            expect_sparse,
1368            false,
1369        );
1370        check_batch_merge_wal_policy(
1371            env,
1372            physical_region_id,
1373            build_requests(),
1374            expect_sparse,
1375            true,
1376        );
1377
1378        for policies in [[false, true], [true, false]] {
1379            let mut mixed_requests = build_requests();
1380            for ((_, request), skip_wal) in mixed_requests.iter_mut().zip(policies) {
1381                request.skip_wal = skip_wal;
1382            }
1383            let results = env
1384                .metric()
1385                .put_regions_batch(mixed_requests.into_iter())
1386                .await;
1387            assert_eq!(results.len(), 1);
1388            assert_eq!(results[0].request_indices, vec![0, 1]);
1389            let err = results.into_iter().next().unwrap().result.unwrap_err();
1390            assert!(err.to_string().contains("inconsistent WAL policy in batch"));
1391            for logical_region_id in [logical_region_1, logical_region_2] {
1392                assert!(
1393                    scan_timestamp_values(&env.metric(), logical_region_id)
1394                        .await
1395                        .is_empty()
1396                );
1397            }
1398        }
1399
1400        let results = env
1401            .metric()
1402            .put_regions_batch(build_requests().into_iter())
1403            .await;
1404        assert_eq!(results.len(), 1);
1405        let affected_rows = results.into_iter().next().unwrap().result.unwrap();
1406        assert_eq!(affected_rows, 5);
1407
1408        let request = ScanRequest::default();
1409        let stream = env
1410            .mito()
1411            .scan_to_stream(data_region_id, request)
1412            .await
1413            .unwrap();
1414        let batches = RecordBatches::try_collect(stream).await.unwrap();
1415
1416        assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 5);
1417    }
1418
1419    #[test]
1420    fn test_sparse_batch_aligns_mixed_field_order() {
1421        let primary_key = PbColumnSchema {
1422            column_name: PRIMARY_KEY_COLUMN_NAME.to_string(),
1423            datatype: ColumnDataType::Binary as i32,
1424            semantic_type: SemanticType::Tag as _,
1425            datatype_extension: None,
1426            options: None,
1427        };
1428        let timestamp = PbColumnSchema {
1429            column_name: greptime_timestamp().to_string(),
1430            datatype: ColumnDataType::TimestampMillisecond as i32,
1431            semantic_type: SemanticType::Timestamp as _,
1432            datatype_extension: None,
1433            options: None,
1434        };
1435        let value = PbColumnSchema {
1436            column_name: greptime_value().to_string(),
1437            datatype: ColumnDataType::Float64 as i32,
1438            semantic_type: SemanticType::Field as _,
1439            datatype_extension: None,
1440            options: None,
1441        };
1442        let histogram = PbColumnSchema {
1443            column_name: greptime_native_histogram().to_string(),
1444            datatype: ColumnDataType::Struct as i32,
1445            semantic_type: SemanticType::Field as _,
1446            datatype_extension: None,
1447            options: None,
1448        };
1449
1450        let sample_rows = Rows {
1451            schema: vec![
1452                primary_key.clone(),
1453                timestamp.clone(),
1454                value.clone(),
1455                histogram.clone(),
1456            ],
1457            rows: vec![Row {
1458                values: vec![
1459                    ValueData::BinaryValue(vec![1]).into(),
1460                    ValueData::TimestampMillisecondValue(0).into(),
1461                    ValueData::F64Value(1.0).into(),
1462                    Value { value_data: None },
1463                ],
1464            }],
1465        };
1466        let histogram_rows = Rows {
1467            schema: vec![primary_key, timestamp, histogram, value],
1468            rows: vec![Row {
1469                values: vec![
1470                    ValueData::BinaryValue(vec![2]).into(),
1471                    ValueData::TimestampMillisecondValue(0).into(),
1472                    ValueData::StructValue(api::v1::StructValue { items: vec![] }).into(),
1473                    Value { value_data: None },
1474                ],
1475            }],
1476        };
1477
1478        let schema = MetricEngineInner::build_union_schema([
1479            sample_rows.schema.as_slice(),
1480            histogram_rows.schema.as_slice(),
1481        ]);
1482        let merged_rows = MetricEngineInner::align_rows_to_schema(sample_rows, &schema)
1483            .into_iter()
1484            .chain(MetricEngineInner::align_rows_to_schema(
1485                histogram_rows,
1486                &schema,
1487            ))
1488            .collect();
1489        let merged_request = Rows {
1490            schema,
1491            rows: merged_rows,
1492        };
1493
1494        let value_idx = column_index(&merged_request, greptime_value());
1495        let histogram_idx = column_index(&merged_request, greptime_native_histogram());
1496        assert!(matches!(
1497            merged_request.rows[0].values[value_idx].value_data,
1498            Some(ValueData::F64Value(_))
1499        ));
1500        assert!(
1501            merged_request.rows[0].values[histogram_idx]
1502                .value_data
1503                .is_none()
1504        );
1505        assert!(
1506            merged_request.rows[1].values[value_idx]
1507                .value_data
1508                .is_none()
1509        );
1510        assert!(matches!(
1511            merged_request.rows[1].values[histogram_idx].value_data,
1512            Some(ValueData::StructValue(_))
1513        ));
1514    }
1515
1516    #[tokio::test]
1517    async fn test_write_logical_region() {
1518        let env = TestEnv::new().await;
1519        env.init_metric_region().await;
1520
1521        // prepare data
1522        let schema = test_util::row_schema_with_tags(&["job"]);
1523        let rows = test_util::build_rows(1, 5);
1524        let request = RegionRequest::Put(RegionPutRequest {
1525            skip_wal: false,
1526            rows: Rows { schema, rows },
1527            hint: None,
1528            partition_expr_version: None,
1529        });
1530
1531        // write data
1532        let logical_region_id = env.default_logical_region_id();
1533        let result = env
1534            .metric()
1535            .handle_request(logical_region_id, request)
1536            .await
1537            .unwrap();
1538        assert_eq!(result.affected_rows, 5);
1539
1540        // read data from physical region
1541        let physical_region_id = env.default_physical_region_id();
1542        let request = ScanRequest::default();
1543        let stream = env
1544            .metric()
1545            .scan_to_stream(physical_region_id, request)
1546            .await
1547            .unwrap();
1548        let batches = RecordBatches::try_collect(stream).await.unwrap();
1549        let expected = "\
1550+-------------------------+----------------+------------+---------------------+-------+
1551| greptime_timestamp      | greptime_value | __table_id | __tsid              | job   |
1552+-------------------------+----------------+------------+---------------------+-------+
1553| 1970-01-01T00:00:00     | 0.0            | 3          | 2955007454552897459 | tag_0 |
1554| 1970-01-01T00:00:00.001 | 1.0            | 3          | 2955007454552897459 | tag_0 |
1555| 1970-01-01T00:00:00.002 | 2.0            | 3          | 2955007454552897459 | tag_0 |
1556| 1970-01-01T00:00:00.003 | 3.0            | 3          | 2955007454552897459 | tag_0 |
1557| 1970-01-01T00:00:00.004 | 4.0            | 3          | 2955007454552897459 | tag_0 |
1558+-------------------------+----------------+------------+---------------------+-------+";
1559        assert_eq!(expected, batches.pretty_print().unwrap(), "physical region");
1560
1561        // read data from logical region
1562        let request = ScanRequest::default();
1563        let stream = env
1564            .metric()
1565            .scan_to_stream(logical_region_id, request)
1566            .await
1567            .unwrap();
1568        let batches = RecordBatches::try_collect(stream).await.unwrap();
1569        let expected = "\
1570+-------------------------+----------------+-------+
1571| greptime_timestamp      | greptime_value | job   |
1572+-------------------------+----------------+-------+
1573| 1970-01-01T00:00:00     | 0.0            | tag_0 |
1574| 1970-01-01T00:00:00.001 | 1.0            | tag_0 |
1575| 1970-01-01T00:00:00.002 | 2.0            | tag_0 |
1576| 1970-01-01T00:00:00.003 | 3.0            | tag_0 |
1577| 1970-01-01T00:00:00.004 | 4.0            | tag_0 |
1578+-------------------------+----------------+-------+";
1579        assert_eq!(expected, batches.pretty_print().unwrap(), "logical region");
1580    }
1581
1582    #[tokio::test]
1583    async fn test_write_logical_region_row_count() {
1584        let env = TestEnv::new().await;
1585        env.init_metric_region().await;
1586        let engine = env.metric();
1587
1588        // add columns
1589        let logical_region_id = env.default_logical_region_id();
1590        let columns = &["odd", "even", "Ev_En"];
1591        let alter_request = test_util::alter_logical_region_add_tag_columns(123456, columns);
1592        engine
1593            .handle_request(logical_region_id, RegionRequest::Alter(alter_request))
1594            .await
1595            .unwrap();
1596
1597        // prepare data
1598        let schema = test_util::row_schema_with_tags(columns);
1599        let rows = test_util::build_rows(3, 100);
1600        let request = RegionRequest::Put(RegionPutRequest {
1601            skip_wal: false,
1602            rows: Rows { schema, rows },
1603            hint: None,
1604            partition_expr_version: None,
1605        });
1606
1607        // write data
1608        let result = engine
1609            .handle_request(logical_region_id, request)
1610            .await
1611            .unwrap();
1612        assert_eq!(100, result.affected_rows);
1613    }
1614
1615    #[tokio::test]
1616    async fn test_write_physical_region() {
1617        let env = TestEnv::new().await;
1618        env.init_metric_region().await;
1619        let engine = env.metric();
1620
1621        let physical_region_id = env.default_physical_region_id();
1622        let schema = test_util::row_schema_with_tags(&["abc"]);
1623        let rows = test_util::build_rows(1, 100);
1624        let request = RegionRequest::Put(RegionPutRequest {
1625            skip_wal: false,
1626            rows: Rows { schema, rows },
1627            hint: None,
1628            partition_expr_version: None,
1629        });
1630
1631        engine
1632            .handle_request(physical_region_id, request)
1633            .await
1634            .unwrap_err();
1635    }
1636
1637    #[tokio::test]
1638    async fn test_write_nonexist_logical_region() {
1639        let env = TestEnv::new().await;
1640        env.init_metric_region().await;
1641        let engine = env.metric();
1642
1643        let logical_region_id = RegionId::new(175, 8345);
1644        let schema = test_util::row_schema_with_tags(&["def"]);
1645        let rows = test_util::build_rows(1, 100);
1646        let request = RegionRequest::Put(RegionPutRequest {
1647            skip_wal: false,
1648            rows: Rows { schema, rows },
1649            hint: None,
1650            partition_expr_version: None,
1651        });
1652
1653        engine
1654            .handle_request(logical_region_id, request)
1655            .await
1656            .unwrap_err();
1657    }
1658
1659    #[tokio::test]
1660    async fn test_batch_write_multiple_logical_regions() {
1661        let env = TestEnv::new().await;
1662        env.init_metric_region().await;
1663        let engine = env.metric();
1664
1665        // Create two additional logical regions
1666        let physical_region_id = env.default_physical_region_id();
1667        let logical_region_1 = env.default_logical_region_id();
1668        let logical_region_2 = RegionId::new(1024, 1);
1669        let logical_region_3 = RegionId::new(1024, 2);
1670
1671        env.create_logical_region(physical_region_id, logical_region_2)
1672            .await;
1673        env.create_logical_region(physical_region_id, logical_region_3)
1674            .await;
1675
1676        // Prepare batch requests with non-overlapping timestamps
1677        let schema = test_util::row_schema_with_tags(&["job"]);
1678
1679        // Use build_rows_with_ts to create non-overlapping timestamps
1680        // logical_region_1: ts 0, 1, 2
1681        // logical_region_2: ts 10, 11  (offset to avoid overlap)
1682        // logical_region_3: ts 20, 21, 22, 23, 24  (offset to avoid overlap)
1683        let rows1 = test_util::build_rows(1, 3);
1684        let mut rows2 = test_util::build_rows(1, 2);
1685        let mut rows3 = test_util::build_rows(1, 5);
1686
1687        // Adjust timestamps to avoid conflicts
1688        use api::v1::value::ValueData;
1689        for (i, row) in rows2.iter_mut().enumerate() {
1690            if let Some(ValueData::TimestampMillisecondValue(ts)) =
1691                row.values.get_mut(0).and_then(|v| v.value_data.as_mut())
1692            {
1693                *ts = (10 + i) as i64;
1694            }
1695        }
1696        for (i, row) in rows3.iter_mut().enumerate() {
1697            if let Some(ValueData::TimestampMillisecondValue(ts)) =
1698                row.values.get_mut(0).and_then(|v| v.value_data.as_mut())
1699            {
1700                *ts = (20 + i) as i64;
1701            }
1702        }
1703
1704        let requests = vec![
1705            (
1706                logical_region_1,
1707                RegionPutRequest {
1708                    skip_wal: false,
1709                    rows: Rows {
1710                        schema: schema.clone(),
1711                        rows: rows1,
1712                    },
1713                    hint: None,
1714                    partition_expr_version: None,
1715                },
1716            ),
1717            (
1718                logical_region_2,
1719                RegionPutRequest {
1720                    skip_wal: false,
1721                    rows: Rows {
1722                        schema: schema.clone(),
1723                        rows: rows2,
1724                    },
1725                    hint: None,
1726                    partition_expr_version: None,
1727                },
1728            ),
1729            (
1730                logical_region_3,
1731                RegionPutRequest {
1732                    skip_wal: false,
1733                    rows: Rows {
1734                        schema: schema.clone(),
1735                        rows: rows3,
1736                    },
1737                    hint: None,
1738                    partition_expr_version: None,
1739                },
1740            ),
1741        ];
1742
1743        // Batch write
1744        let results = engine.put_regions_batch(requests.into_iter()).await;
1745        assert_eq!(results.len(), 1);
1746        let affected_rows = results.into_iter().next().unwrap().result.unwrap();
1747        assert_eq!(affected_rows, 10);
1748
1749        // Verify physical region contains data from all logical regions
1750        let request = ScanRequest::default();
1751        let stream = env
1752            .metric()
1753            .scan_to_stream(physical_region_id, request)
1754            .await
1755            .unwrap();
1756        let batches = RecordBatches::try_collect(stream).await.unwrap();
1757
1758        // Should have 3 + 2 + 5 = 10 rows total
1759        assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 10);
1760    }
1761
1762    #[tokio::test]
1763    async fn test_batch_write_with_partial_failure() {
1764        let env = TestEnv::new().await;
1765        env.init_metric_region().await;
1766
1767        let physical = env.default_physical_region_id();
1768        let logical = env.default_logical_region_id();
1769        let other_physical = RegionId::new(4096, 0);
1770        let other_logical = RegionId::new(4097, 0);
1771        env.create_physical_region(other_physical, "batch_results", vec![])
1772            .await;
1773        env.create_logical_region(other_physical, other_logical)
1774            .await;
1775        let request = |index: i64, version| {
1776            let mut rows = test_util::build_rows(1, 1);
1777            rows[0].values[0].value_data =
1778                Some(api::v1::value::ValueData::TimestampMillisecondValue(index));
1779            RegionPutRequest {
1780                rows: Rows {
1781                    schema: test_util::row_schema_with_tags(&["job"]),
1782                    rows,
1783                },
1784                skip_wal: false,
1785                hint: None,
1786                partition_expr_version: version,
1787            }
1788        };
1789        let mut results = env
1790            .metric()
1791            .put_regions_batch(
1792                vec![
1793                    (logical, request(0, None)),
1794                    (RegionId::new(9999, 0), request(1, None)),
1795                    (other_logical, request(2, Some(1))),
1796                    (other_logical, request(3, None)),
1797                    (logical, request(4, None)),
1798                    (physical, request(5, None)),
1799                ]
1800                .into_iter(),
1801            )
1802            .await;
1803        results.sort_by_key(|result| result.request_indices[0]);
1804        assert_eq!(
1805            results
1806                .iter()
1807                .map(|result| result.request_indices.clone())
1808                .collect::<Vec<_>>(),
1809            vec![vec![0, 4], vec![1], vec![2, 3], vec![5]]
1810        );
1811        assert_eq!(results[0].result.as_ref().unwrap(), &2);
1812        assert!(results[1].result.is_err());
1813        assert!(results[2].result.is_err());
1814        assert!(matches!(
1815            results[3].result,
1816            Err(crate::error::Error::ForbiddenPhysicalWrite { .. })
1817        ));
1818        for (region, expected) in [(physical, 2), (other_physical, 0)] {
1819            let stream = env
1820                .metric()
1821                .scan_to_stream(region, ScanRequest::default())
1822                .await
1823                .unwrap();
1824            let batches = RecordBatches::try_collect(stream).await.unwrap();
1825            assert_eq!(
1826                batches.iter().map(|batch| batch.num_rows()).sum::<usize>(),
1827                expected
1828            );
1829        }
1830    }
1831
1832    #[tokio::test]
1833    async fn test_batch_write_single_physical_region_forbidden() {
1834        let env = TestEnv::new().await;
1835        env.init_metric_region().await;
1836        let engine = env.metric();
1837
1838        let physical_region_id = env.default_physical_region_id();
1839        let schema = test_util::row_schema_with_tags(&["job"]);
1840        let requests = vec![(
1841            physical_region_id,
1842            RegionPutRequest {
1843                skip_wal: false,
1844                rows: Rows {
1845                    schema,
1846                    rows: test_util::build_rows(1, 1),
1847                },
1848                hint: None,
1849                partition_expr_version: None,
1850            },
1851        )];
1852
1853        let results = engine.put_regions_batch(requests.into_iter()).await;
1854        assert_eq!(results.len(), 1);
1855        assert_eq!(results[0].request_indices, vec![0]);
1856        let err = results.into_iter().next().unwrap().result.unwrap_err();
1857
1858        assert!(matches!(
1859            err,
1860            crate::error::Error::ForbiddenPhysicalWrite { .. }
1861        ));
1862    }
1863
1864    #[tokio::test]
1865    async fn test_batch_write_physical_region_forbidden() {
1866        let env = TestEnv::new().await;
1867        env.init_metric_region().await;
1868        let engine = env.metric();
1869
1870        let physical_region_id = env.default_physical_region_id();
1871        let logical_region_id = env.default_logical_region_id();
1872        let schema = test_util::row_schema_with_tags(&["job"]);
1873        let requests = vec![
1874            (
1875                logical_region_id,
1876                RegionPutRequest {
1877                    skip_wal: false,
1878                    rows: Rows {
1879                        schema: schema.clone(),
1880                        rows: test_util::build_rows(1, 1),
1881                    },
1882                    hint: None,
1883                    partition_expr_version: None,
1884                },
1885            ),
1886            (
1887                physical_region_id,
1888                RegionPutRequest {
1889                    skip_wal: false,
1890                    rows: Rows {
1891                        schema,
1892                        rows: test_util::build_rows(1, 1),
1893                    },
1894                    hint: None,
1895                    partition_expr_version: None,
1896                },
1897            ),
1898        ];
1899
1900        let mut results = engine.put_regions_batch(requests.into_iter()).await;
1901        results.sort_by_key(|group| group.request_indices[0]);
1902        assert_eq!(results.len(), 2);
1903        assert_eq!(results[0].request_indices, vec![0]);
1904        assert_eq!(results[0].result.as_ref().unwrap(), &1);
1905        assert_eq!(results[1].request_indices, vec![1]);
1906        assert!(matches!(
1907            results[1].result,
1908            Err(crate::error::Error::ForbiddenPhysicalWrite { .. })
1909        ));
1910    }
1911
1912    #[tokio::test]
1913    async fn test_batch_write_single_request_fast_path() {
1914        let env = TestEnv::new().await;
1915        env.init_metric_region().await;
1916        let engine = env.metric();
1917
1918        let logical_region_id = env.default_logical_region_id();
1919        let schema = test_util::row_schema_with_tags(&["job"]);
1920
1921        // Single request should use fast path
1922        let requests = vec![(
1923            logical_region_id,
1924            RegionPutRequest {
1925                skip_wal: false,
1926                rows: Rows {
1927                    schema,
1928                    rows: test_util::build_rows(1, 5),
1929                },
1930                hint: None,
1931                partition_expr_version: None,
1932            },
1933        )];
1934
1935        let results = engine.put_regions_batch(requests.into_iter()).await;
1936        assert_eq!(results.len(), 1);
1937        assert_eq!(results[0].request_indices, vec![0]);
1938        let affected_rows = results.into_iter().next().unwrap().result.unwrap();
1939        assert_eq!(affected_rows, 5);
1940    }
1941
1942    #[tokio::test]
1943    async fn test_batch_write_empty_requests() {
1944        let env = TestEnv::new().await;
1945        env.init_metric_region().await;
1946        let engine = env.metric();
1947
1948        assert!(
1949            engine
1950                .put_regions_batch(vec![].into_iter())
1951                .await
1952                .is_empty()
1953        );
1954    }
1955
1956    #[tokio::test]
1957    async fn test_batch_write_sparse_encoding() {
1958        let env = TestEnv::new().await;
1959        let physical_region_id = env.default_physical_region_id();
1960
1961        run_batch_write_with_schema_variants(
1962            &env,
1963            physical_region_id,
1964            vec![(PRIMARY_KEY_ENCODING.to_string(), "sparse".to_string())],
1965            true,
1966        )
1967        .await;
1968    }
1969
1970    #[tokio::test]
1971    async fn test_batch_write_dense_encoding() {
1972        let env = TestEnv::new().await;
1973        let physical_region_id = env.default_physical_region_id();
1974
1975        run_batch_write_with_schema_variants(
1976            &env,
1977            physical_region_id,
1978            vec![(PRIMARY_KEY_ENCODING.to_string(), "dense".to_string())],
1979            false,
1980        )
1981        .await;
1982    }
1983
1984    #[tokio::test]
1985    async fn test_metric_put_rejects_bad_partition_expr_version() {
1986        let env = TestEnv::new().await;
1987        env.init_metric_region().await;
1988
1989        let logical_region_id = env.default_logical_region_id();
1990        let rows = Rows {
1991            schema: test_util::row_schema_with_tags(&["job"]),
1992            rows: test_util::build_rows(1, 3),
1993        };
1994
1995        let err = env
1996            .metric()
1997            .handle_request(
1998                logical_region_id,
1999                RegionRequest::Put(RegionPutRequest {
2000                    skip_wal: false,
2001                    rows,
2002                    hint: None,
2003                    partition_expr_version: Some(1),
2004                }),
2005            )
2006            .await
2007            .unwrap_err();
2008
2009        assert_eq!(err.status_code(), StatusCode::InvalidArguments);
2010    }
2011
2012    #[tokio::test]
2013    async fn test_metric_put_respects_staging_partition_expr_version() {
2014        let env = TestEnv::new().await;
2015        env.init_metric_region().await;
2016
2017        let logical_region_id = env.default_logical_region_id();
2018        let physical_region_id = env.default_physical_region_id();
2019        let partition_expr = job_partition_expr_json();
2020        env.metric()
2021            .handle_request(
2022                physical_region_id,
2023                RegionRequest::EnterStaging(EnterStagingRequest {
2024                    partition_directive: StagingPartitionDirective::UpdatePartitionExpr(
2025                        partition_expr.clone(),
2026                    ),
2027                }),
2028            )
2029            .await
2030            .unwrap();
2031
2032        let expected_version = partition_expr_version(Some(&partition_expr));
2033        let rows = Rows {
2034            schema: test_util::row_schema_with_tags(&["job"]),
2035            rows: test_util::build_rows(1, 3),
2036        };
2037
2038        let err = env
2039            .metric()
2040            .handle_request(
2041                logical_region_id,
2042                RegionRequest::Put(RegionPutRequest {
2043                    skip_wal: false,
2044                    rows: rows.clone(),
2045                    hint: None,
2046                    partition_expr_version: Some(expected_version.wrapping_add(1)),
2047                }),
2048            )
2049            .await
2050            .unwrap_err();
2051        assert_eq!(err.status_code(), StatusCode::InvalidArguments);
2052
2053        let response = env
2054            .metric()
2055            .handle_request(
2056                logical_region_id,
2057                RegionRequest::Put(RegionPutRequest {
2058                    skip_wal: false,
2059                    rows: rows.clone(),
2060                    hint: None,
2061                    partition_expr_version: None,
2062                }),
2063            )
2064            .await
2065            .unwrap();
2066        assert_eq!(response.affected_rows, 3);
2067
2068        let response = env
2069            .metric()
2070            .handle_request(
2071                logical_region_id,
2072                RegionRequest::Put(RegionPutRequest {
2073                    skip_wal: false,
2074                    rows,
2075                    hint: None,
2076                    partition_expr_version: Some(expected_version),
2077                }),
2078            )
2079            .await
2080            .unwrap();
2081        assert_eq!(response.affected_rows, 3);
2082    }
2083
2084    /// Regression test for issue #7990: the metric engine must reject a row
2085    /// whose timestamp column carries a non-timestamp datatype, rather than
2086    /// letting it panic inside mito's `ValueBuilder::push`.
2087    #[tokio::test]
2088    async fn test_verify_rows_rejects_wrong_type() {
2089        use api::v1::value::ValueData;
2090        use api::v1::{ColumnDataType, ColumnSchema as PbColumnSchema, SemanticType};
2091        use common_query::prelude::{greptime_timestamp, greptime_value};
2092
2093        let env = TestEnv::new().await;
2094        env.init_metric_region().await;
2095
2096        let logical_region_id = env.default_logical_region_id();
2097
2098        // Timestamp column is declared as String — the very payload that
2099        // caused #7990. It should surface a typed error rather than panic.
2100        let schema = vec![
2101            PbColumnSchema {
2102                column_name: greptime_timestamp().to_string(),
2103                datatype: ColumnDataType::String as i32,
2104                semantic_type: SemanticType::Timestamp as _,
2105                datatype_extension: None,
2106                options: None,
2107            },
2108            PbColumnSchema {
2109                column_name: greptime_value().to_string(),
2110                datatype: ColumnDataType::Float64 as i32,
2111                semantic_type: SemanticType::Field as _,
2112                datatype_extension: None,
2113                options: None,
2114            },
2115            PbColumnSchema {
2116                column_name: "job".to_string(),
2117                datatype: ColumnDataType::String as i32,
2118                semantic_type: SemanticType::Tag as _,
2119                datatype_extension: None,
2120                options: None,
2121            },
2122        ];
2123        let rows = vec![Row {
2124            values: vec![
2125                Value {
2126                    value_data: Some(ValueData::StringValue("not-a-timestamp".to_string())),
2127                },
2128                Value {
2129                    value_data: Some(ValueData::F64Value(1.0)),
2130                },
2131                Value {
2132                    value_data: Some(ValueData::StringValue("tag_0".to_string())),
2133                },
2134            ],
2135        }];
2136
2137        let err = env
2138            .metric()
2139            .handle_request(
2140                logical_region_id,
2141                RegionRequest::Put(RegionPutRequest {
2142                    skip_wal: false,
2143                    rows: Rows { schema, rows },
2144                    hint: None,
2145                    partition_expr_version: None,
2146                }),
2147            )
2148            .await
2149            .unwrap_err();
2150        assert_eq!(err.status_code(), StatusCode::InvalidArguments);
2151    }
2152
2153    /// The completeness check must reject requests that omit the time index
2154    /// column, since mito cannot default-fill a `TimeIndex` column and would
2155    /// previously panic on the empty builder.
2156    #[tokio::test]
2157    async fn test_verify_rows_rejects_missing_time_index() {
2158        use api::v1::{ColumnDataType, ColumnSchema as PbColumnSchema, SemanticType};
2159        use common_query::prelude::greptime_value;
2160
2161        let env = TestEnv::new().await;
2162        env.init_metric_region().await;
2163
2164        let logical_region_id = env.default_logical_region_id();
2165
2166        // Payload only carries the field and a tag — no timestamp column.
2167        let schema = vec![
2168            PbColumnSchema {
2169                column_name: greptime_value().to_string(),
2170                datatype: ColumnDataType::Float64 as i32,
2171                semantic_type: SemanticType::Field as _,
2172                datatype_extension: None,
2173                options: None,
2174            },
2175            PbColumnSchema {
2176                column_name: "job".to_string(),
2177                datatype: ColumnDataType::String as i32,
2178                semantic_type: SemanticType::Tag as _,
2179                datatype_extension: None,
2180                options: None,
2181            },
2182        ];
2183        let rows = vec![Row {
2184            values: vec![
2185                Value {
2186                    value_data: Some(api::v1::value::ValueData::F64Value(1.0)),
2187                },
2188                Value {
2189                    value_data: Some(api::v1::value::ValueData::StringValue("tag_0".to_string())),
2190                },
2191            ],
2192        }];
2193
2194        let err = env
2195            .metric()
2196            .handle_request(
2197                logical_region_id,
2198                RegionRequest::Put(RegionPutRequest {
2199                    skip_wal: false,
2200                    rows: Rows { schema, rows },
2201                    hint: None,
2202                    partition_expr_version: None,
2203                }),
2204            )
2205            .await
2206            .unwrap_err();
2207        assert_eq!(err.status_code(), StatusCode::InvalidArguments);
2208    }
2209
2210    #[tokio::test]
2211    async fn test_verify_rows_rejects_missing_field() {
2212        use api::v1::value::ValueData;
2213        use api::v1::{ColumnDataType, ColumnSchema as PbColumnSchema, SemanticType};
2214        use common_query::prelude::greptime_timestamp;
2215
2216        let env = TestEnv::new().await;
2217        env.init_metric_region().await;
2218
2219        let logical_region_id = env.default_logical_region_id();
2220
2221        // Schema has timestamp + tag but no field column.
2222        let schema = vec![
2223            PbColumnSchema {
2224                column_name: greptime_timestamp().to_string(),
2225                datatype: ColumnDataType::TimestampMillisecond as i32,
2226                semantic_type: SemanticType::Timestamp as _,
2227                datatype_extension: None,
2228                options: None,
2229            },
2230            PbColumnSchema {
2231                column_name: "job".to_string(),
2232                datatype: ColumnDataType::String as i32,
2233                semantic_type: SemanticType::Tag as _,
2234                datatype_extension: None,
2235                options: None,
2236            },
2237        ];
2238        let rows = vec![Row {
2239            values: vec![
2240                Value {
2241                    value_data: Some(ValueData::TimestampMillisecondValue(0)),
2242                },
2243                Value {
2244                    value_data: Some(ValueData::StringValue("tag_0".to_string())),
2245                },
2246            ],
2247        }];
2248
2249        let err = env
2250            .metric()
2251            .handle_request(
2252                logical_region_id,
2253                RegionRequest::Put(RegionPutRequest {
2254                    skip_wal: false,
2255                    rows: Rows { schema, rows },
2256                    hint: None,
2257                    partition_expr_version: None,
2258                }),
2259            )
2260            .await
2261            .unwrap_err();
2262        let message = err.to_string();
2263        assert!(
2264            message.contains("missing required field column"),
2265            "expected field-completeness rejection, got: {message}"
2266        );
2267        assert_eq!(err.status_code(), StatusCode::InvalidArguments);
2268    }
2269
2270    #[test]
2271    fn test_fill_missing_field_column_nullable_no_default() {
2272        let field_meta = ColumnMetadata {
2273            column_id: 1,
2274            semantic_type: SemanticType::Field,
2275            column_schema: ColumnSchema::new(
2276                "greptime_value".to_string(),
2277                ConcreteDataType::float64_datatype(),
2278                true, // nullable, no default
2279            ),
2280        };
2281        let mut rows = Rows {
2282            schema: vec![PbColumnSchema {
2283                column_name: "ts".to_string(),
2284                datatype: ColumnDataType::TimestampMillisecond as i32,
2285                semantic_type: SemanticType::Timestamp as _,
2286                datatype_extension: None,
2287                options: None,
2288            }],
2289            rows: vec![Row {
2290                values: vec![Value {
2291                    value_data: Some(ValueData::TimestampMillisecondValue(0)),
2292                }],
2293            }],
2294        };
2295
2296        MetricEngineInner::fill_missing_field_column(
2297            RegionId::new(1, 1),
2298            "greptime_value",
2299            &field_meta,
2300            &mut rows,
2301        )
2302        .unwrap();
2303
2304        assert_eq!(rows.schema.len(), 2);
2305        assert_eq!(rows.schema[1].column_name, "greptime_value");
2306        assert_eq!(rows.rows[0].values.len(), 2);
2307        assert!(
2308            rows.rows[0].values[1].value_data.is_none(),
2309            "missing nullable field should be filled with null"
2310        );
2311    }
2312
2313    #[test]
2314    fn test_fill_missing_field_column_rejects_impure_default() {
2315        let field_meta = ColumnMetadata {
2316            column_id: 1,
2317            semantic_type: SemanticType::Field,
2318            column_schema: ColumnSchema::new(
2319                "greptime_value".to_string(),
2320                ConcreteDataType::timestamp_millisecond_datatype(),
2321                false,
2322            )
2323            .with_default_constraint(Some(ColumnDefaultConstraint::Function("now()".to_string())))
2324            .unwrap(),
2325        };
2326        let mut rows = Rows {
2327            schema: vec![PbColumnSchema {
2328                column_name: "ts".to_string(),
2329                datatype: api::v1::ColumnDataType::TimestampMillisecond as i32,
2330                semantic_type: SemanticType::Timestamp as _,
2331                datatype_extension: None,
2332                options: None,
2333            }],
2334            rows: vec![Row {
2335                values: vec![Value {
2336                    value_data: Some(ValueData::TimestampMillisecondValue(0)),
2337                }],
2338            }],
2339        };
2340
2341        let err = MetricEngineInner::fill_missing_field_column(
2342            RegionId::new(1, 1),
2343            "greptime_value",
2344            &field_meta,
2345            &mut rows,
2346        )
2347        .unwrap_err();
2348        assert!(
2349            err.to_string().contains("impure default value"),
2350            "expected impure-default rejection, got: {err}"
2351        );
2352    }
2353}