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::{Float64Array, TimestampMillisecondArray};
795    use datatypes::prelude::ConcreteDataType;
796    use datatypes::schema::{ColumnDefaultConstraint, ColumnSchema};
797    use datatypes::value::Value as PartitionValue;
798    use partition::expr::col;
799    use store_api::metadata::ColumnMetadata;
800    use store_api::metric_engine_consts::{
801        DATA_SCHEMA_TABLE_ID_COLUMN_NAME, DATA_SCHEMA_TSID_COLUMN_NAME, METRIC_ENGINE_NAME,
802        PHYSICAL_TABLE_METADATA_KEY, PRIMARY_KEY_ENCODING,
803    };
804    use store_api::path_utils::table_dir;
805    use store_api::region_engine::RegionEngine;
806    use store_api::region_request::{
807        EnterStagingRequest, PathType, RegionCloseRequest, RegionOpenRequest, RegionRequest,
808        StagingPartitionDirective,
809    };
810    use store_api::storage::ScanRequest;
811    use store_api::storage::consts::PRIMARY_KEY_COLUMN_NAME;
812
813    use super::*;
814    use crate::engine::MetricEngine;
815    use crate::test_util::{self, TestEnv};
816
817    async fn scan_timestamp_values(engine: &MetricEngine, region_id: RegionId) -> Vec<(i64, f64)> {
818        let stream = engine
819            .scan_to_stream(region_id, ScanRequest::default())
820            .await
821            .unwrap();
822        let batches = RecordBatches::try_collect(stream).await.unwrap();
823        let mut rows = Vec::new();
824        for batch in batches.iter() {
825            let batch = batch.df_record_batch();
826            let timestamp_index = batch.schema().index_of(greptime_timestamp()).unwrap();
827            let value_index = batch.schema().index_of(greptime_value()).unwrap();
828            let timestamps = batch
829                .column(timestamp_index)
830                .as_any()
831                .downcast_ref::<TimestampMillisecondArray>()
832                .unwrap();
833            let values = batch
834                .column(value_index)
835                .as_any()
836                .downcast_ref::<Float64Array>()
837                .unwrap();
838            rows.extend(
839                timestamps
840                    .values()
841                    .iter()
842                    .copied()
843                    .zip(values.values().iter().copied()),
844            );
845        }
846        rows.sort_unstable_by_key(|(timestamp, _)| *timestamp);
847        rows
848    }
849
850    #[tokio::test]
851    async fn test_batch_partition_versions() {
852        check_batch_partition_versions("sparse").await;
853        check_batch_partition_versions("dense").await;
854    }
855
856    async fn check_batch_partition_versions(encoding: &str) {
857        let env = TestEnv::new().await;
858        let physical_region_id = env.default_physical_region_id();
859        let logical_region_id = env.default_logical_region_id();
860        env.create_physical_region(
861            physical_region_id,
862            &TestEnv::default_table_dir(),
863            vec![(PRIMARY_KEY_ENCODING.to_string(), encoding.to_string())],
864        )
865        .await;
866        create_logical_region_with_tags(&env, physical_region_id, logical_region_id, &["job"])
867            .await;
868        let build_requests = |versions: [Option<u64>; 3]| {
869            versions
870                .into_iter()
871                .map(|partition_expr_version| {
872                    (
873                        logical_region_id,
874                        RegionPutRequest {
875                            skip_wal: false,
876                            rows: Rows {
877                                schema: test_util::row_schema_with_tags(&["job"]),
878                                rows: test_util::build_rows(1, 1),
879                            },
880                            hint: None,
881                            partition_expr_version,
882                        },
883                    )
884                })
885                .collect::<Vec<_>>()
886        };
887        // Conflicting explicit versions must fail before any data is written.
888        let err = env
889            .metric()
890            .inner
891            .put_regions_batch_single_physical(
892                physical_region_id,
893                build_requests([None, Some(10), Some(11)]),
894            )
895            .await
896            .unwrap_err();
897        assert!(
898            err.to_string()
899                .contains("inconsistent partition expr version")
900        );
901        assert!(
902            scan_timestamp_values(&env.metric(), logical_region_id)
903                .await
904                .is_empty()
905        );
906
907        for (versions, expected) in [
908            ([None, None, None], None),
909            ([None, Some(7), None], Some(7)),
910            ([Some(7), None, Some(7)], Some(7)),
911        ] {
912            let mut requests = build_requests(versions);
913            let engine = env.metric();
914            engine
915                .inner
916                .validate_batch_requests(physical_region_id, &mut requests)
917                .await
918                .unwrap();
919            let (merged, _) = match encoding {
920                "sparse" => engine
921                    .inner
922                    .merge_sparse_batch(physical_region_id, requests),
923                "dense" => engine
924                    .inner
925                    .merge_dense_batch(to_data_region_id(physical_region_id), requests),
926                _ => unreachable!(),
927            }
928            .unwrap();
929            assert_eq!(merged.partition_expr_version, expected);
930        }
931    }
932
933    #[tokio::test]
934    async fn test_put_skip_wal_batch_recovery() {
935        check_put_skip_wal_batch_recovery("sparse", false).await;
936        check_put_skip_wal_batch_recovery("sparse", true).await;
937        check_put_skip_wal_batch_recovery("dense", false).await;
938        check_put_skip_wal_batch_recovery("dense", true).await;
939    }
940
941    async fn check_put_skip_wal_batch_recovery(encoding: &str, skip_wal: bool) {
942        let env = TestEnv::new().await;
943        let engine = env.metric();
944        engine.inner.flush_task.stop().await.unwrap();
945        let physical_region_id = env.default_physical_region_id();
946        let logical_region_id = env.default_logical_region_id();
947        env.create_physical_region(
948            physical_region_id,
949            &TestEnv::default_table_dir(),
950            vec![(PRIMARY_KEY_ENCODING.to_string(), encoding.to_string())],
951        )
952        .await;
953        create_logical_region_with_tags(&env, physical_region_id, logical_region_id, &["job"])
954            .await;
955        let metadata_before = engine.get_metadata(logical_region_id).await.unwrap();
956
957        let requests = [skip_wal; 3]
958            .into_iter()
959            .enumerate()
960            .map(|(index, skip_wal)| {
961                let timestamp = index as i64 + 1;
962                let value = timestamp as f64 * 10.0;
963                // Every request updates the same key at timestamp zero and
964                // also inserts a distinct key to verify merge order.
965                let rows = [0, timestamp]
966                    .into_iter()
967                    .map(|timestamp| Row {
968                        values: vec![
969                            Value {
970                                value_data: Some(ValueData::TimestampMillisecondValue(timestamp)),
971                            },
972                            Value {
973                                value_data: Some(ValueData::F64Value(value)),
974                            },
975                            Value {
976                                value_data: Some(ValueData::StringValue("tag_0".to_string())),
977                            },
978                        ],
979                    })
980                    .collect();
981                (
982                    logical_region_id,
983                    RegionPutRequest {
984                        rows: Rows {
985                            schema: test_util::row_schema_with_tags(&["job"]),
986                            rows,
987                        },
988                        hint: None,
989                        partition_expr_version: None,
990                        skip_wal,
991                    },
992                )
993            });
994        let affected_rows = engine.inner.put_regions_batch(requests).await.unwrap();
995        assert_eq!(affected_rows, 6);
996        assert_eq!(
997            scan_timestamp_values(&engine, logical_region_id).await,
998            vec![(0, 30.0), (1, 10.0), (2, 20.0), (3, 30.0)]
999        );
1000
1001        // Neither data nor metadata has an SST to hide missing WAL.
1002        for region_id in [
1003            to_data_region_id(physical_region_id),
1004            crate::utils::to_metadata_region_id(physical_region_id),
1005        ] {
1006            let stat = env.mito().region_statistic(region_id).unwrap();
1007            assert!(stat.memtable_size > 0);
1008            assert_eq!(stat.sst_num, 0);
1009        }
1010        engine
1011            .handle_request(
1012                physical_region_id,
1013                RegionRequest::Close(RegionCloseRequest {
1014                    flush_on_close: false,
1015                }),
1016            )
1017            .await
1018            .unwrap();
1019
1020        // Recreate the wrapper as well, discarding its metadata cache.
1021        let reopened = MetricEngine::try_new(env.mito(), Default::default()).unwrap();
1022        reopened.inner.flush_task.stop().await.unwrap();
1023        reopened
1024            .handle_request(
1025                physical_region_id,
1026                RegionRequest::Open(RegionOpenRequest {
1027                    engine: METRIC_ENGINE_NAME.to_string(),
1028                    table_dir: TestEnv::default_table_dir(),
1029                    path_type: PathType::Bare,
1030                    options: [
1031                        (PHYSICAL_TABLE_METADATA_KEY.to_string(), String::new()),
1032                        (PRIMARY_KEY_ENCODING.to_string(), encoding.to_string()),
1033                    ]
1034                    .into_iter()
1035                    .collect(),
1036                    skip_wal_replay: false,
1037                    checkpoint: None,
1038                    requirements: Default::default(),
1039                }),
1040            )
1041            .await
1042            .unwrap();
1043        let recovered_metadata = reopened.get_metadata(logical_region_id).await.unwrap();
1044        assert_eq!(
1045            metadata_before.column_metadatas,
1046            recovered_metadata.column_metadatas
1047        );
1048        let expected = if skip_wal {
1049            vec![]
1050        } else {
1051            vec![(0, 30.0), (1, 10.0), (2, 20.0), (3, 30.0)]
1052        };
1053        assert_eq!(
1054            scan_timestamp_values(&reopened, logical_region_id).await,
1055            expected,
1056            "encoding={encoding}, skip_wal={skip_wal}"
1057        );
1058    }
1059
1060    fn assert_merged_schema(rows: &Rows, expect_sparse: bool) {
1061        let column_names: HashSet<String> = rows
1062            .schema
1063            .iter()
1064            .map(|col| col.column_name.clone())
1065            .collect();
1066
1067        if expect_sparse {
1068            assert!(
1069                column_names.contains(PRIMARY_KEY_COLUMN_NAME),
1070                "sparse encoding should include primary key column"
1071            );
1072            assert!(
1073                !column_names.contains(DATA_SCHEMA_TABLE_ID_COLUMN_NAME),
1074                "sparse encoding should not include table id column"
1075            );
1076            assert!(
1077                !column_names.contains(DATA_SCHEMA_TSID_COLUMN_NAME),
1078                "sparse encoding should not include tsid column"
1079            );
1080            assert!(
1081                !column_names.contains("job"),
1082                "sparse encoding should not include tag columns"
1083            );
1084            assert!(
1085                !column_names.contains("instance"),
1086                "sparse encoding should not include tag columns"
1087            );
1088        } else {
1089            assert!(
1090                !column_names.contains(PRIMARY_KEY_COLUMN_NAME),
1091                "dense encoding should not include primary key column"
1092            );
1093            assert!(
1094                column_names.contains(DATA_SCHEMA_TABLE_ID_COLUMN_NAME),
1095                "dense encoding should include table id column"
1096            );
1097            assert!(
1098                column_names.contains(DATA_SCHEMA_TSID_COLUMN_NAME),
1099                "dense encoding should include tsid column"
1100            );
1101            assert!(
1102                column_names.contains("job"),
1103                "dense encoding should keep tag columns"
1104            );
1105            assert!(
1106                column_names.contains("instance"),
1107                "dense encoding should keep tag columns"
1108            );
1109        }
1110    }
1111
1112    fn job_partition_expr_json() -> String {
1113        let expr = col("job")
1114            .gt_eq(PartitionValue::String("job-0".into()))
1115            .and(col("job").lt(PartitionValue::String("job-9".into())));
1116        expr.as_json_str().unwrap()
1117    }
1118
1119    async fn create_logical_region_with_tags(
1120        env: &TestEnv,
1121        physical_region_id: RegionId,
1122        logical_region_id: RegionId,
1123        tags: &[&str],
1124    ) {
1125        let region_create_request = test_util::create_logical_region_request(
1126            tags,
1127            physical_region_id,
1128            &table_dir("test", logical_region_id.table_id()),
1129        );
1130        env.metric()
1131            .handle_request(
1132                logical_region_id,
1133                RegionRequest::Create(region_create_request),
1134            )
1135            .await
1136            .unwrap();
1137    }
1138
1139    fn column_index(rows: &Rows, name: &str) -> usize {
1140        rows.schema
1141            .iter()
1142            .position(|col| col.column_name == name)
1143            .unwrap()
1144    }
1145
1146    fn check_batch_merge_wal_policy(
1147        env: &TestEnv,
1148        physical_region_id: RegionId,
1149        mut requests: Vec<(RegionId, RegionPutRequest)>,
1150        expect_sparse: bool,
1151        skip_wal: bool,
1152    ) {
1153        for (_, request) in &mut requests {
1154            request.skip_wal = skip_wal;
1155        }
1156        let (merged_request, affected_rows) = if expect_sparse {
1157            let (merged_request, affected_rows) = env
1158                .metric()
1159                .inner
1160                .merge_sparse_batch(physical_region_id, requests)
1161                .unwrap();
1162            let hint = merged_request
1163                .hint
1164                .as_ref()
1165                .expect("missing sparse write hint");
1166            assert_eq!(
1167                hint.primary_key_encoding,
1168                PrimaryKeyEncodingProto::Sparse as i32
1169            );
1170            (merged_request, affected_rows)
1171        } else {
1172            let (merged_request, affected_rows) = env
1173                .metric()
1174                .inner
1175                .merge_dense_batch(to_data_region_id(physical_region_id), requests)
1176                .unwrap();
1177            assert!(merged_request.hint.is_none());
1178            (merged_request, affected_rows)
1179        };
1180        assert_merged_schema(&merged_request.rows, expect_sparse);
1181        assert_eq!(merged_request.skip_wal, skip_wal);
1182        assert_eq!(affected_rows, 5);
1183    }
1184
1185    async fn run_batch_write_with_schema_variants(
1186        env: &TestEnv,
1187        physical_region_id: RegionId,
1188        options: Vec<(String, String)>,
1189        expect_sparse: bool,
1190    ) {
1191        env.create_physical_region(physical_region_id, &TestEnv::default_table_dir(), options)
1192            .await;
1193
1194        let logical_region_1 = env.default_logical_region_id();
1195        let logical_region_2 = RegionId::new(1024, 1);
1196
1197        create_logical_region_with_tags(env, physical_region_id, logical_region_1, &["job"]).await;
1198        create_logical_region_with_tags(
1199            env,
1200            physical_region_id,
1201            logical_region_2,
1202            &["job", "instance"],
1203        )
1204        .await;
1205
1206        let schema_1 = test_util::row_schema_with_tags(&["job"]);
1207        let schema_2 = test_util::row_schema_with_tags(&["job", "instance"]);
1208
1209        let data_region_id = RegionId::new(physical_region_id.table_id(), 2);
1210        let primary_key_encoding = env
1211            .metric()
1212            .inner
1213            .get_primary_key_encoding(data_region_id)
1214            .unwrap();
1215        assert_eq!(
1216            primary_key_encoding,
1217            if expect_sparse {
1218                PrimaryKeyEncoding::Sparse
1219            } else {
1220                PrimaryKeyEncoding::Dense
1221            }
1222        );
1223
1224        let build_requests = || {
1225            let rows_1 = test_util::build_rows(1, 3);
1226            let rows_2 = test_util::build_rows(2, 2);
1227
1228            vec![
1229                (
1230                    logical_region_1,
1231                    RegionPutRequest {
1232                        skip_wal: false,
1233                        rows: Rows {
1234                            schema: schema_1.clone(),
1235                            rows: rows_1,
1236                        },
1237                        hint: None,
1238                        partition_expr_version: None,
1239                    },
1240                ),
1241                (
1242                    logical_region_2,
1243                    RegionPutRequest {
1244                        skip_wal: false,
1245                        rows: Rows {
1246                            schema: schema_2.clone(),
1247                            rows: rows_2,
1248                        },
1249                        hint: None,
1250                        partition_expr_version: None,
1251                    },
1252                ),
1253            ]
1254        };
1255
1256        check_batch_merge_wal_policy(
1257            env,
1258            physical_region_id,
1259            build_requests(),
1260            expect_sparse,
1261            false,
1262        );
1263        check_batch_merge_wal_policy(
1264            env,
1265            physical_region_id,
1266            build_requests(),
1267            expect_sparse,
1268            true,
1269        );
1270
1271        for policies in [[false, true], [true, false]] {
1272            let mut mixed_requests = build_requests();
1273            for ((_, request), skip_wal) in mixed_requests.iter_mut().zip(policies) {
1274                request.skip_wal = skip_wal;
1275            }
1276            let err = env
1277                .metric()
1278                .inner
1279                .put_regions_batch(mixed_requests.into_iter())
1280                .await
1281                .unwrap_err();
1282            assert!(err.to_string().contains("inconsistent WAL policy in batch"));
1283            for logical_region_id in [logical_region_1, logical_region_2] {
1284                assert!(
1285                    scan_timestamp_values(&env.metric(), logical_region_id)
1286                        .await
1287                        .is_empty()
1288                );
1289            }
1290        }
1291
1292        let affected_rows = env
1293            .metric()
1294            .inner
1295            .put_regions_batch(build_requests().into_iter())
1296            .await
1297            .unwrap();
1298        assert_eq!(affected_rows, 5);
1299
1300        let request = ScanRequest::default();
1301        let stream = env
1302            .mito()
1303            .scan_to_stream(data_region_id, request)
1304            .await
1305            .unwrap();
1306        let batches = RecordBatches::try_collect(stream).await.unwrap();
1307
1308        assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 5);
1309    }
1310
1311    #[test]
1312    fn test_sparse_batch_aligns_mixed_field_order() {
1313        let primary_key = PbColumnSchema {
1314            column_name: PRIMARY_KEY_COLUMN_NAME.to_string(),
1315            datatype: ColumnDataType::Binary as i32,
1316            semantic_type: SemanticType::Tag as _,
1317            datatype_extension: None,
1318            options: None,
1319        };
1320        let timestamp = PbColumnSchema {
1321            column_name: greptime_timestamp().to_string(),
1322            datatype: ColumnDataType::TimestampMillisecond as i32,
1323            semantic_type: SemanticType::Timestamp as _,
1324            datatype_extension: None,
1325            options: None,
1326        };
1327        let value = PbColumnSchema {
1328            column_name: greptime_value().to_string(),
1329            datatype: ColumnDataType::Float64 as i32,
1330            semantic_type: SemanticType::Field as _,
1331            datatype_extension: None,
1332            options: None,
1333        };
1334        let histogram = PbColumnSchema {
1335            column_name: greptime_native_histogram().to_string(),
1336            datatype: ColumnDataType::Struct as i32,
1337            semantic_type: SemanticType::Field as _,
1338            datatype_extension: None,
1339            options: None,
1340        };
1341
1342        let sample_rows = Rows {
1343            schema: vec![
1344                primary_key.clone(),
1345                timestamp.clone(),
1346                value.clone(),
1347                histogram.clone(),
1348            ],
1349            rows: vec![Row {
1350                values: vec![
1351                    ValueData::BinaryValue(vec![1]).into(),
1352                    ValueData::TimestampMillisecondValue(0).into(),
1353                    ValueData::F64Value(1.0).into(),
1354                    Value { value_data: None },
1355                ],
1356            }],
1357        };
1358        let histogram_rows = Rows {
1359            schema: vec![primary_key, timestamp, histogram, value],
1360            rows: vec![Row {
1361                values: vec![
1362                    ValueData::BinaryValue(vec![2]).into(),
1363                    ValueData::TimestampMillisecondValue(0).into(),
1364                    ValueData::StructValue(api::v1::StructValue { items: vec![] }).into(),
1365                    Value { value_data: None },
1366                ],
1367            }],
1368        };
1369
1370        let schema = MetricEngineInner::build_union_schema([
1371            sample_rows.schema.as_slice(),
1372            histogram_rows.schema.as_slice(),
1373        ]);
1374        let merged_rows = MetricEngineInner::align_rows_to_schema(sample_rows, &schema)
1375            .into_iter()
1376            .chain(MetricEngineInner::align_rows_to_schema(
1377                histogram_rows,
1378                &schema,
1379            ))
1380            .collect();
1381        let merged_request = Rows {
1382            schema,
1383            rows: merged_rows,
1384        };
1385
1386        let value_idx = column_index(&merged_request, greptime_value());
1387        let histogram_idx = column_index(&merged_request, greptime_native_histogram());
1388        assert!(matches!(
1389            merged_request.rows[0].values[value_idx].value_data,
1390            Some(ValueData::F64Value(_))
1391        ));
1392        assert!(
1393            merged_request.rows[0].values[histogram_idx]
1394                .value_data
1395                .is_none()
1396        );
1397        assert!(
1398            merged_request.rows[1].values[value_idx]
1399                .value_data
1400                .is_none()
1401        );
1402        assert!(matches!(
1403            merged_request.rows[1].values[histogram_idx].value_data,
1404            Some(ValueData::StructValue(_))
1405        ));
1406    }
1407
1408    #[tokio::test]
1409    async fn test_write_logical_region() {
1410        let env = TestEnv::new().await;
1411        env.init_metric_region().await;
1412
1413        // prepare data
1414        let schema = test_util::row_schema_with_tags(&["job"]);
1415        let rows = test_util::build_rows(1, 5);
1416        let request = RegionRequest::Put(RegionPutRequest {
1417            skip_wal: false,
1418            rows: Rows { schema, rows },
1419            hint: None,
1420            partition_expr_version: None,
1421        });
1422
1423        // write data
1424        let logical_region_id = env.default_logical_region_id();
1425        let result = env
1426            .metric()
1427            .handle_request(logical_region_id, request)
1428            .await
1429            .unwrap();
1430        assert_eq!(result.affected_rows, 5);
1431
1432        // read data from physical region
1433        let physical_region_id = env.default_physical_region_id();
1434        let request = ScanRequest::default();
1435        let stream = env
1436            .metric()
1437            .scan_to_stream(physical_region_id, request)
1438            .await
1439            .unwrap();
1440        let batches = RecordBatches::try_collect(stream).await.unwrap();
1441        let expected = "\
1442+-------------------------+----------------+------------+---------------------+-------+
1443| greptime_timestamp      | greptime_value | __table_id | __tsid              | job   |
1444+-------------------------+----------------+------------+---------------------+-------+
1445| 1970-01-01T00:00:00     | 0.0            | 3          | 2955007454552897459 | tag_0 |
1446| 1970-01-01T00:00:00.001 | 1.0            | 3          | 2955007454552897459 | tag_0 |
1447| 1970-01-01T00:00:00.002 | 2.0            | 3          | 2955007454552897459 | tag_0 |
1448| 1970-01-01T00:00:00.003 | 3.0            | 3          | 2955007454552897459 | tag_0 |
1449| 1970-01-01T00:00:00.004 | 4.0            | 3          | 2955007454552897459 | tag_0 |
1450+-------------------------+----------------+------------+---------------------+-------+";
1451        assert_eq!(expected, batches.pretty_print().unwrap(), "physical region");
1452
1453        // read data from logical region
1454        let request = ScanRequest::default();
1455        let stream = env
1456            .metric()
1457            .scan_to_stream(logical_region_id, request)
1458            .await
1459            .unwrap();
1460        let batches = RecordBatches::try_collect(stream).await.unwrap();
1461        let expected = "\
1462+-------------------------+----------------+-------+
1463| greptime_timestamp      | greptime_value | job   |
1464+-------------------------+----------------+-------+
1465| 1970-01-01T00:00:00     | 0.0            | tag_0 |
1466| 1970-01-01T00:00:00.001 | 1.0            | tag_0 |
1467| 1970-01-01T00:00:00.002 | 2.0            | tag_0 |
1468| 1970-01-01T00:00:00.003 | 3.0            | tag_0 |
1469| 1970-01-01T00:00:00.004 | 4.0            | tag_0 |
1470+-------------------------+----------------+-------+";
1471        assert_eq!(expected, batches.pretty_print().unwrap(), "logical region");
1472    }
1473
1474    #[tokio::test]
1475    async fn test_write_logical_region_row_count() {
1476        let env = TestEnv::new().await;
1477        env.init_metric_region().await;
1478        let engine = env.metric();
1479
1480        // add columns
1481        let logical_region_id = env.default_logical_region_id();
1482        let columns = &["odd", "even", "Ev_En"];
1483        let alter_request = test_util::alter_logical_region_add_tag_columns(123456, columns);
1484        engine
1485            .handle_request(logical_region_id, RegionRequest::Alter(alter_request))
1486            .await
1487            .unwrap();
1488
1489        // prepare data
1490        let schema = test_util::row_schema_with_tags(columns);
1491        let rows = test_util::build_rows(3, 100);
1492        let request = RegionRequest::Put(RegionPutRequest {
1493            skip_wal: false,
1494            rows: Rows { schema, rows },
1495            hint: None,
1496            partition_expr_version: None,
1497        });
1498
1499        // write data
1500        let result = engine
1501            .handle_request(logical_region_id, request)
1502            .await
1503            .unwrap();
1504        assert_eq!(100, result.affected_rows);
1505    }
1506
1507    #[tokio::test]
1508    async fn test_write_physical_region() {
1509        let env = TestEnv::new().await;
1510        env.init_metric_region().await;
1511        let engine = env.metric();
1512
1513        let physical_region_id = env.default_physical_region_id();
1514        let schema = test_util::row_schema_with_tags(&["abc"]);
1515        let rows = test_util::build_rows(1, 100);
1516        let request = RegionRequest::Put(RegionPutRequest {
1517            skip_wal: false,
1518            rows: Rows { schema, rows },
1519            hint: None,
1520            partition_expr_version: None,
1521        });
1522
1523        engine
1524            .handle_request(physical_region_id, request)
1525            .await
1526            .unwrap_err();
1527    }
1528
1529    #[tokio::test]
1530    async fn test_write_nonexist_logical_region() {
1531        let env = TestEnv::new().await;
1532        env.init_metric_region().await;
1533        let engine = env.metric();
1534
1535        let logical_region_id = RegionId::new(175, 8345);
1536        let schema = test_util::row_schema_with_tags(&["def"]);
1537        let rows = test_util::build_rows(1, 100);
1538        let request = RegionRequest::Put(RegionPutRequest {
1539            skip_wal: false,
1540            rows: Rows { schema, rows },
1541            hint: None,
1542            partition_expr_version: None,
1543        });
1544
1545        engine
1546            .handle_request(logical_region_id, request)
1547            .await
1548            .unwrap_err();
1549    }
1550
1551    #[tokio::test]
1552    async fn test_batch_write_multiple_logical_regions() {
1553        let env = TestEnv::new().await;
1554        env.init_metric_region().await;
1555        let engine = env.metric();
1556
1557        // Create two additional logical regions
1558        let physical_region_id = env.default_physical_region_id();
1559        let logical_region_1 = env.default_logical_region_id();
1560        let logical_region_2 = RegionId::new(1024, 1);
1561        let logical_region_3 = RegionId::new(1024, 2);
1562
1563        env.create_logical_region(physical_region_id, logical_region_2)
1564            .await;
1565        env.create_logical_region(physical_region_id, logical_region_3)
1566            .await;
1567
1568        // Prepare batch requests with non-overlapping timestamps
1569        let schema = test_util::row_schema_with_tags(&["job"]);
1570
1571        // Use build_rows_with_ts to create non-overlapping timestamps
1572        // logical_region_1: ts 0, 1, 2
1573        // logical_region_2: ts 10, 11  (offset to avoid overlap)
1574        // logical_region_3: ts 20, 21, 22, 23, 24  (offset to avoid overlap)
1575        let rows1 = test_util::build_rows(1, 3);
1576        let mut rows2 = test_util::build_rows(1, 2);
1577        let mut rows3 = test_util::build_rows(1, 5);
1578
1579        // Adjust timestamps to avoid conflicts
1580        use api::v1::value::ValueData;
1581        for (i, row) in rows2.iter_mut().enumerate() {
1582            if let Some(ValueData::TimestampMillisecondValue(ts)) =
1583                row.values.get_mut(0).and_then(|v| v.value_data.as_mut())
1584            {
1585                *ts = (10 + i) as i64;
1586            }
1587        }
1588        for (i, row) in rows3.iter_mut().enumerate() {
1589            if let Some(ValueData::TimestampMillisecondValue(ts)) =
1590                row.values.get_mut(0).and_then(|v| v.value_data.as_mut())
1591            {
1592                *ts = (20 + i) as i64;
1593            }
1594        }
1595
1596        let requests = vec![
1597            (
1598                logical_region_1,
1599                RegionPutRequest {
1600                    skip_wal: false,
1601                    rows: Rows {
1602                        schema: schema.clone(),
1603                        rows: rows1,
1604                    },
1605                    hint: None,
1606                    partition_expr_version: None,
1607                },
1608            ),
1609            (
1610                logical_region_2,
1611                RegionPutRequest {
1612                    skip_wal: false,
1613                    rows: Rows {
1614                        schema: schema.clone(),
1615                        rows: rows2,
1616                    },
1617                    hint: None,
1618                    partition_expr_version: None,
1619                },
1620            ),
1621            (
1622                logical_region_3,
1623                RegionPutRequest {
1624                    skip_wal: false,
1625                    rows: Rows {
1626                        schema: schema.clone(),
1627                        rows: rows3,
1628                    },
1629                    hint: None,
1630                    partition_expr_version: None,
1631                },
1632            ),
1633        ];
1634
1635        // Batch write
1636        let affected_rows = engine
1637            .inner
1638            .put_regions_batch(requests.into_iter())
1639            .await
1640            .unwrap();
1641        assert_eq!(affected_rows, 10);
1642
1643        // Verify physical region contains data from all logical regions
1644        let request = ScanRequest::default();
1645        let stream = env
1646            .metric()
1647            .scan_to_stream(physical_region_id, request)
1648            .await
1649            .unwrap();
1650        let batches = RecordBatches::try_collect(stream).await.unwrap();
1651
1652        // Should have 3 + 2 + 5 = 10 rows total
1653        assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 10);
1654    }
1655
1656    #[tokio::test]
1657    async fn test_batch_write_with_partial_failure() {
1658        let env = TestEnv::new().await;
1659        env.init_metric_region().await;
1660        let engine = env.metric();
1661
1662        let physical_region_id = env.default_physical_region_id();
1663        let logical_region_1 = env.default_logical_region_id();
1664        let logical_region_2 = RegionId::new(1024, 1);
1665        let nonexistent_region = RegionId::new(9999, 9999);
1666
1667        env.create_logical_region(physical_region_id, logical_region_2)
1668            .await;
1669
1670        // Prepare batch with one invalid region
1671        let schema = test_util::row_schema_with_tags(&["job"]);
1672        let requests = vec![
1673            (
1674                logical_region_1,
1675                RegionPutRequest {
1676                    skip_wal: false,
1677                    rows: Rows {
1678                        schema: schema.clone(),
1679                        rows: test_util::build_rows(1, 3),
1680                    },
1681                    hint: None,
1682                    partition_expr_version: None,
1683                },
1684            ),
1685            (
1686                nonexistent_region,
1687                RegionPutRequest {
1688                    skip_wal: false,
1689                    rows: Rows {
1690                        schema: schema.clone(),
1691                        rows: test_util::build_rows(1, 2),
1692                    },
1693                    hint: None,
1694                    partition_expr_version: None,
1695                },
1696            ),
1697            (
1698                logical_region_2,
1699                RegionPutRequest {
1700                    skip_wal: false,
1701                    rows: Rows {
1702                        schema: schema.clone(),
1703                        rows: test_util::build_rows(1, 5),
1704                    },
1705                    hint: None,
1706                    partition_expr_version: None,
1707                },
1708            ),
1709        ];
1710
1711        // Batch write
1712        let result = engine.inner.put_regions_batch(requests.into_iter()).await;
1713        assert!(result.is_err());
1714
1715        // Invalid region is detected before any write, so the physical region remains empty.
1716        // Fail-fast is per physical-region group; cross-group partial success is possible.
1717        let request = ScanRequest::default();
1718        let stream = env
1719            .metric()
1720            .scan_to_stream(physical_region_id, request)
1721            .await
1722            .unwrap();
1723        let batches = RecordBatches::try_collect(stream).await.unwrap();
1724
1725        assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 0);
1726    }
1727
1728    #[tokio::test]
1729    async fn test_batch_write_single_physical_region_forbidden() {
1730        let env = TestEnv::new().await;
1731        env.init_metric_region().await;
1732        let engine = env.metric();
1733
1734        let physical_region_id = env.default_physical_region_id();
1735        let schema = test_util::row_schema_with_tags(&["job"]);
1736        let requests = vec![(
1737            physical_region_id,
1738            RegionPutRequest {
1739                skip_wal: false,
1740                rows: Rows {
1741                    schema,
1742                    rows: test_util::build_rows(1, 1),
1743                },
1744                hint: None,
1745                partition_expr_version: None,
1746            },
1747        )];
1748
1749        let err = engine
1750            .inner
1751            .put_regions_batch(requests.into_iter())
1752            .await
1753            .unwrap_err();
1754
1755        assert!(matches!(
1756            err,
1757            crate::error::Error::ForbiddenPhysicalWrite { .. }
1758        ));
1759    }
1760
1761    #[tokio::test]
1762    async fn test_batch_write_physical_region_forbidden() {
1763        let env = TestEnv::new().await;
1764        env.init_metric_region().await;
1765        let engine = env.metric();
1766
1767        let physical_region_id = env.default_physical_region_id();
1768        let logical_region_id = env.default_logical_region_id();
1769        let schema = test_util::row_schema_with_tags(&["job"]);
1770        let requests = vec![
1771            (
1772                logical_region_id,
1773                RegionPutRequest {
1774                    skip_wal: false,
1775                    rows: Rows {
1776                        schema: schema.clone(),
1777                        rows: test_util::build_rows(1, 1),
1778                    },
1779                    hint: None,
1780                    partition_expr_version: None,
1781                },
1782            ),
1783            (
1784                physical_region_id,
1785                RegionPutRequest {
1786                    skip_wal: false,
1787                    rows: Rows {
1788                        schema,
1789                        rows: test_util::build_rows(1, 1),
1790                    },
1791                    hint: None,
1792                    partition_expr_version: None,
1793                },
1794            ),
1795        ];
1796
1797        let err = engine
1798            .inner
1799            .put_regions_batch(requests.into_iter())
1800            .await
1801            .unwrap_err();
1802
1803        assert!(matches!(
1804            err,
1805            crate::error::Error::ForbiddenPhysicalWrite { .. }
1806        ));
1807    }
1808
1809    #[tokio::test]
1810    async fn test_batch_write_single_request_fast_path() {
1811        let env = TestEnv::new().await;
1812        env.init_metric_region().await;
1813        let engine = env.metric();
1814
1815        let logical_region_id = env.default_logical_region_id();
1816        let schema = test_util::row_schema_with_tags(&["job"]);
1817
1818        // Single request should use fast path
1819        let requests = vec![(
1820            logical_region_id,
1821            RegionPutRequest {
1822                skip_wal: false,
1823                rows: Rows {
1824                    schema,
1825                    rows: test_util::build_rows(1, 5),
1826                },
1827                hint: None,
1828                partition_expr_version: None,
1829            },
1830        )];
1831
1832        let affected_rows = engine
1833            .inner
1834            .put_regions_batch(requests.into_iter())
1835            .await
1836            .unwrap();
1837        assert_eq!(affected_rows, 5);
1838    }
1839
1840    #[tokio::test]
1841    async fn test_batch_write_empty_requests() {
1842        let env = TestEnv::new().await;
1843        env.init_metric_region().await;
1844        let engine = env.metric();
1845
1846        // Empty batch should return zero affected rows
1847        let requests = vec![];
1848        let affected_rows = engine
1849            .inner
1850            .put_regions_batch(requests.into_iter())
1851            .await
1852            .unwrap();
1853
1854        assert_eq!(affected_rows, 0);
1855    }
1856
1857    #[tokio::test]
1858    async fn test_batch_write_sparse_encoding() {
1859        let env = TestEnv::new().await;
1860        let physical_region_id = env.default_physical_region_id();
1861
1862        run_batch_write_with_schema_variants(
1863            &env,
1864            physical_region_id,
1865            vec![(PRIMARY_KEY_ENCODING.to_string(), "sparse".to_string())],
1866            true,
1867        )
1868        .await;
1869    }
1870
1871    #[tokio::test]
1872    async fn test_batch_write_dense_encoding() {
1873        let env = TestEnv::new().await;
1874        let physical_region_id = env.default_physical_region_id();
1875
1876        run_batch_write_with_schema_variants(
1877            &env,
1878            physical_region_id,
1879            vec![(PRIMARY_KEY_ENCODING.to_string(), "dense".to_string())],
1880            false,
1881        )
1882        .await;
1883    }
1884
1885    #[tokio::test]
1886    async fn test_metric_put_rejects_bad_partition_expr_version() {
1887        let env = TestEnv::new().await;
1888        env.init_metric_region().await;
1889
1890        let logical_region_id = env.default_logical_region_id();
1891        let rows = Rows {
1892            schema: test_util::row_schema_with_tags(&["job"]),
1893            rows: test_util::build_rows(1, 3),
1894        };
1895
1896        let err = env
1897            .metric()
1898            .handle_request(
1899                logical_region_id,
1900                RegionRequest::Put(RegionPutRequest {
1901                    skip_wal: false,
1902                    rows,
1903                    hint: None,
1904                    partition_expr_version: Some(1),
1905                }),
1906            )
1907            .await
1908            .unwrap_err();
1909
1910        assert_eq!(err.status_code(), StatusCode::InvalidArguments);
1911    }
1912
1913    #[tokio::test]
1914    async fn test_metric_put_respects_staging_partition_expr_version() {
1915        let env = TestEnv::new().await;
1916        env.init_metric_region().await;
1917
1918        let logical_region_id = env.default_logical_region_id();
1919        let physical_region_id = env.default_physical_region_id();
1920        let partition_expr = job_partition_expr_json();
1921        env.metric()
1922            .handle_request(
1923                physical_region_id,
1924                RegionRequest::EnterStaging(EnterStagingRequest {
1925                    partition_directive: StagingPartitionDirective::UpdatePartitionExpr(
1926                        partition_expr.clone(),
1927                    ),
1928                }),
1929            )
1930            .await
1931            .unwrap();
1932
1933        let expected_version = partition_expr_version(Some(&partition_expr));
1934        let rows = Rows {
1935            schema: test_util::row_schema_with_tags(&["job"]),
1936            rows: test_util::build_rows(1, 3),
1937        };
1938
1939        let err = env
1940            .metric()
1941            .handle_request(
1942                logical_region_id,
1943                RegionRequest::Put(RegionPutRequest {
1944                    skip_wal: false,
1945                    rows: rows.clone(),
1946                    hint: None,
1947                    partition_expr_version: Some(expected_version.wrapping_add(1)),
1948                }),
1949            )
1950            .await
1951            .unwrap_err();
1952        assert_eq!(err.status_code(), StatusCode::InvalidArguments);
1953
1954        let response = env
1955            .metric()
1956            .handle_request(
1957                logical_region_id,
1958                RegionRequest::Put(RegionPutRequest {
1959                    skip_wal: false,
1960                    rows: rows.clone(),
1961                    hint: None,
1962                    partition_expr_version: None,
1963                }),
1964            )
1965            .await
1966            .unwrap();
1967        assert_eq!(response.affected_rows, 3);
1968
1969        let response = env
1970            .metric()
1971            .handle_request(
1972                logical_region_id,
1973                RegionRequest::Put(RegionPutRequest {
1974                    skip_wal: false,
1975                    rows,
1976                    hint: None,
1977                    partition_expr_version: Some(expected_version),
1978                }),
1979            )
1980            .await
1981            .unwrap();
1982        assert_eq!(response.affected_rows, 3);
1983    }
1984
1985    /// Regression test for issue #7990: the metric engine must reject a row
1986    /// whose timestamp column carries a non-timestamp datatype, rather than
1987    /// letting it panic inside mito's `ValueBuilder::push`.
1988    #[tokio::test]
1989    async fn test_verify_rows_rejects_wrong_type() {
1990        use api::v1::value::ValueData;
1991        use api::v1::{ColumnDataType, ColumnSchema as PbColumnSchema, SemanticType};
1992        use common_query::prelude::{greptime_timestamp, greptime_value};
1993
1994        let env = TestEnv::new().await;
1995        env.init_metric_region().await;
1996
1997        let logical_region_id = env.default_logical_region_id();
1998
1999        // Timestamp column is declared as String — the very payload that
2000        // caused #7990. It should surface a typed error rather than panic.
2001        let schema = vec![
2002            PbColumnSchema {
2003                column_name: greptime_timestamp().to_string(),
2004                datatype: ColumnDataType::String as i32,
2005                semantic_type: SemanticType::Timestamp as _,
2006                datatype_extension: None,
2007                options: None,
2008            },
2009            PbColumnSchema {
2010                column_name: greptime_value().to_string(),
2011                datatype: ColumnDataType::Float64 as i32,
2012                semantic_type: SemanticType::Field as _,
2013                datatype_extension: None,
2014                options: None,
2015            },
2016            PbColumnSchema {
2017                column_name: "job".to_string(),
2018                datatype: ColumnDataType::String as i32,
2019                semantic_type: SemanticType::Tag as _,
2020                datatype_extension: None,
2021                options: None,
2022            },
2023        ];
2024        let rows = vec![Row {
2025            values: vec![
2026                Value {
2027                    value_data: Some(ValueData::StringValue("not-a-timestamp".to_string())),
2028                },
2029                Value {
2030                    value_data: Some(ValueData::F64Value(1.0)),
2031                },
2032                Value {
2033                    value_data: Some(ValueData::StringValue("tag_0".to_string())),
2034                },
2035            ],
2036        }];
2037
2038        let err = env
2039            .metric()
2040            .handle_request(
2041                logical_region_id,
2042                RegionRequest::Put(RegionPutRequest {
2043                    skip_wal: false,
2044                    rows: Rows { schema, rows },
2045                    hint: None,
2046                    partition_expr_version: None,
2047                }),
2048            )
2049            .await
2050            .unwrap_err();
2051        assert_eq!(err.status_code(), StatusCode::InvalidArguments);
2052    }
2053
2054    /// The completeness check must reject requests that omit the time index
2055    /// column, since mito cannot default-fill a `TimeIndex` column and would
2056    /// previously panic on the empty builder.
2057    #[tokio::test]
2058    async fn test_verify_rows_rejects_missing_time_index() {
2059        use api::v1::{ColumnDataType, ColumnSchema as PbColumnSchema, SemanticType};
2060        use common_query::prelude::greptime_value;
2061
2062        let env = TestEnv::new().await;
2063        env.init_metric_region().await;
2064
2065        let logical_region_id = env.default_logical_region_id();
2066
2067        // Payload only carries the field and a tag — no timestamp column.
2068        let schema = vec![
2069            PbColumnSchema {
2070                column_name: greptime_value().to_string(),
2071                datatype: ColumnDataType::Float64 as i32,
2072                semantic_type: SemanticType::Field as _,
2073                datatype_extension: None,
2074                options: None,
2075            },
2076            PbColumnSchema {
2077                column_name: "job".to_string(),
2078                datatype: ColumnDataType::String as i32,
2079                semantic_type: SemanticType::Tag as _,
2080                datatype_extension: None,
2081                options: None,
2082            },
2083        ];
2084        let rows = vec![Row {
2085            values: vec![
2086                Value {
2087                    value_data: Some(api::v1::value::ValueData::F64Value(1.0)),
2088                },
2089                Value {
2090                    value_data: Some(api::v1::value::ValueData::StringValue("tag_0".to_string())),
2091                },
2092            ],
2093        }];
2094
2095        let err = env
2096            .metric()
2097            .handle_request(
2098                logical_region_id,
2099                RegionRequest::Put(RegionPutRequest {
2100                    skip_wal: false,
2101                    rows: Rows { schema, rows },
2102                    hint: None,
2103                    partition_expr_version: None,
2104                }),
2105            )
2106            .await
2107            .unwrap_err();
2108        assert_eq!(err.status_code(), StatusCode::InvalidArguments);
2109    }
2110
2111    #[tokio::test]
2112    async fn test_verify_rows_rejects_missing_field() {
2113        use api::v1::value::ValueData;
2114        use api::v1::{ColumnDataType, ColumnSchema as PbColumnSchema, SemanticType};
2115        use common_query::prelude::greptime_timestamp;
2116
2117        let env = TestEnv::new().await;
2118        env.init_metric_region().await;
2119
2120        let logical_region_id = env.default_logical_region_id();
2121
2122        // Schema has timestamp + tag but no field column.
2123        let schema = vec![
2124            PbColumnSchema {
2125                column_name: greptime_timestamp().to_string(),
2126                datatype: ColumnDataType::TimestampMillisecond as i32,
2127                semantic_type: SemanticType::Timestamp as _,
2128                datatype_extension: None,
2129                options: None,
2130            },
2131            PbColumnSchema {
2132                column_name: "job".to_string(),
2133                datatype: ColumnDataType::String as i32,
2134                semantic_type: SemanticType::Tag as _,
2135                datatype_extension: None,
2136                options: None,
2137            },
2138        ];
2139        let rows = vec![Row {
2140            values: vec![
2141                Value {
2142                    value_data: Some(ValueData::TimestampMillisecondValue(0)),
2143                },
2144                Value {
2145                    value_data: Some(ValueData::StringValue("tag_0".to_string())),
2146                },
2147            ],
2148        }];
2149
2150        let err = env
2151            .metric()
2152            .handle_request(
2153                logical_region_id,
2154                RegionRequest::Put(RegionPutRequest {
2155                    skip_wal: false,
2156                    rows: Rows { schema, rows },
2157                    hint: None,
2158                    partition_expr_version: None,
2159                }),
2160            )
2161            .await
2162            .unwrap_err();
2163        let message = err.to_string();
2164        assert!(
2165            message.contains("missing required field column"),
2166            "expected field-completeness rejection, got: {message}"
2167        );
2168        assert_eq!(err.status_code(), StatusCode::InvalidArguments);
2169    }
2170
2171    #[test]
2172    fn test_fill_missing_field_column_nullable_no_default() {
2173        let field_meta = ColumnMetadata {
2174            column_id: 1,
2175            semantic_type: SemanticType::Field,
2176            column_schema: ColumnSchema::new(
2177                "greptime_value".to_string(),
2178                ConcreteDataType::float64_datatype(),
2179                true, // nullable, no default
2180            ),
2181        };
2182        let mut rows = Rows {
2183            schema: vec![PbColumnSchema {
2184                column_name: "ts".to_string(),
2185                datatype: ColumnDataType::TimestampMillisecond as i32,
2186                semantic_type: SemanticType::Timestamp as _,
2187                datatype_extension: None,
2188                options: None,
2189            }],
2190            rows: vec![Row {
2191                values: vec![Value {
2192                    value_data: Some(ValueData::TimestampMillisecondValue(0)),
2193                }],
2194            }],
2195        };
2196
2197        MetricEngineInner::fill_missing_field_column(
2198            RegionId::new(1, 1),
2199            "greptime_value",
2200            &field_meta,
2201            &mut rows,
2202        )
2203        .unwrap();
2204
2205        assert_eq!(rows.schema.len(), 2);
2206        assert_eq!(rows.schema[1].column_name, "greptime_value");
2207        assert_eq!(rows.rows[0].values.len(), 2);
2208        assert!(
2209            rows.rows[0].values[1].value_data.is_none(),
2210            "missing nullable field should be filled with null"
2211        );
2212    }
2213
2214    #[test]
2215    fn test_fill_missing_field_column_rejects_impure_default() {
2216        let field_meta = ColumnMetadata {
2217            column_id: 1,
2218            semantic_type: SemanticType::Field,
2219            column_schema: ColumnSchema::new(
2220                "greptime_value".to_string(),
2221                ConcreteDataType::timestamp_millisecond_datatype(),
2222                false,
2223            )
2224            .with_default_constraint(Some(ColumnDefaultConstraint::Function("now()".to_string())))
2225            .unwrap(),
2226        };
2227        let mut rows = Rows {
2228            schema: vec![PbColumnSchema {
2229                column_name: "ts".to_string(),
2230                datatype: api::v1::ColumnDataType::TimestampMillisecond as i32,
2231                semantic_type: SemanticType::Timestamp as _,
2232                datatype_extension: None,
2233                options: None,
2234            }],
2235            rows: vec![Row {
2236                values: vec![Value {
2237                    value_data: Some(ValueData::TimestampMillisecondValue(0)),
2238                }],
2239            }],
2240        };
2241
2242        let err = MetricEngineInner::fill_missing_field_column(
2243            RegionId::new(1, 1),
2244            "greptime_value",
2245            &field_meta,
2246            &mut rows,
2247        )
2248        .unwrap_err();
2249        assert!(
2250            err.to_string().contains("impure default value"),
2251            "expected impure-default rejection, got: {err}"
2252        );
2253    }
2254}