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