Skip to main content

metric_engine/engine/
bulk_insert.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::HashSet;
16
17use api::v1::{ArrowIpc, SemanticType};
18use bytes::Bytes;
19use common_error::ext::BoxedError;
20use common_grpc::flight::{FlightEncoder, FlightMessage};
21use datatypes::arrow::record_batch::RecordBatch;
22use mito2::request::check_bulk_insert_batch;
23use snafu::{OptionExt, ResultExt, ensure};
24use store_api::codec::PrimaryKeyEncoding;
25use store_api::metadata::RegionMetadataRef;
26use store_api::region_request::{AffectedRows, RegionBulkInsertsRequest, RegionRequest};
27use store_api::storage::RegionId;
28
29use crate::batch_modifier::{TagColumnInfo, modify_batch_sparse};
30use crate::engine::MetricEngineInner;
31use crate::error;
32use crate::error::Result;
33use crate::metrics::MITO_OPERATION_ELAPSED;
34
35impl MetricEngineInner {
36    /// Bulk-inserts rows into a metric region.
37    ///
38    /// **Logical region path:** The request payload is a logical `RecordBatch`
39    /// (timestamp, value and tag columns). It is transformed to physical format
40    /// via `modify_batch_sparse`, encoded to Arrow IPC, and forwarded as a
41    /// `BulkInserts` request to the data region.
42    ///
43    /// **Physical region path:** The request payload is already in physical format
44    /// (produced by the batcher's `flush_batch_physical`). It is forwarded directly
45    /// to the data region with no transformation.
46    ///
47    /// Returns the number of affected rows, or `0` if the input batch is empty.
48    pub async fn bulk_insert_region(
49        &self,
50        region_id: RegionId,
51        request: RegionBulkInsertsRequest,
52    ) -> Result<AffectedRows> {
53        if request.payload.num_rows() == 0 {
54            return Ok(0);
55        }
56        if self.is_physical_region(region_id) {
57            let _timer = MITO_OPERATION_ELAPSED
58                .with_label_values(&["bulk_insert_physical"])
59                .start_timer();
60            return self.bulk_insert_physical_region(region_id, request).await;
61        }
62
63        let _timer = MITO_OPERATION_ELAPSED
64            .with_label_values(&["bulk_insert_logical"])
65            .start_timer();
66        self.bulk_insert_logical_region(region_id, request).await
67    }
68
69    /// Passthrough for bulk inserts targeting a physical data region.
70    ///
71    /// The batch is already in physical format (with `__primary_key`, timestamp,
72    /// value columns), so no logical-to-physical transformation is needed.
73    async fn bulk_insert_physical_region(
74        &self,
75        region_id: RegionId,
76        mut request: RegionBulkInsertsRequest,
77    ) -> Result<AffectedRows> {
78        // Other logical tables can add fields to the shared physical schema.
79        // Let Mito fill fields absent from this batch before writing it.
80        request.aligned_schema_version = None;
81        self.data_region
82            .write_data(region_id, RegionRequest::BulkInserts(request))
83            .await
84    }
85
86    /// Bulk-inserts logical rows, transforming them to physical format first.
87    async fn bulk_insert_logical_region(
88        &self,
89        region_id: RegionId,
90        request: RegionBulkInsertsRequest,
91    ) -> Result<AffectedRows> {
92        let (physical_region_id, data_region_id, primary_key_encoding) =
93            self.find_data_region_meta(region_id)?;
94
95        if primary_key_encoding != PrimaryKeyEncoding::Sparse {
96            return error::UnsupportedRegionRequestSnafu {
97                request: RegionRequest::BulkInserts(request),
98            }
99            .fail();
100        }
101
102        let batch = request.payload;
103        if batch.num_rows() == 0 {
104            return Ok(0);
105        }
106
107        let logical_metadata = self
108            .logical_region_metadata(physical_region_id, region_id)
109            .await?;
110        let (tag_columns, non_tag_indices) = self.resolve_tag_columns_from_metadata(
111            region_id,
112            data_region_id,
113            &batch,
114            &logical_metadata,
115        )?;
116        // The rewrite below copies columns without checking them, and would encode a
117        // duplicated tag into the primary key twice.
118        check_bulk_insert_batch(&logical_metadata, &batch)
119            .map_err(BoxedError::new)
120            .context(error::MitoWriteOperationSnafu)?;
121        let modified_batch = modify_batch_sparse(
122            batch.clone(),
123            region_id.table_id(),
124            &tag_columns,
125            &non_tag_indices,
126        )?;
127        let (schema, data_header, payload) = record_batch_to_ipc(&modified_batch)?;
128
129        let partition_expr_version = request.partition_expr_version;
130
131        let request = RegionBulkInsertsRequest {
132            skip_wal: request.skip_wal,
133            region_id: data_region_id,
134            payload: modified_batch,
135            raw_data: ArrowIpc {
136                schema,
137                data_header,
138                payload,
139            },
140            partition_expr_version,
141            aligned_schema_version: None,
142        };
143        self.data_region
144            .write_data(data_region_id, RegionRequest::BulkInserts(request))
145            .await
146    }
147
148    fn resolve_tag_columns_from_metadata(
149        &self,
150        logical_region_id: RegionId,
151        data_region_id: RegionId,
152        batch: &RecordBatch,
153        logical_metadata: &RegionMetadataRef,
154    ) -> Result<(Vec<TagColumnInfo>, Vec<usize>)> {
155        let tag_names: HashSet<&str> = logical_metadata
156            .column_metadatas
157            .iter()
158            .filter_map(|column| {
159                if column.semantic_type == SemanticType::Tag {
160                    Some(column.column_schema.name.as_str())
161                } else {
162                    None
163                }
164            })
165            .collect();
166
167        let mut tag_columns = Vec::new();
168        let mut non_tag_indices = Vec::new();
169        {
170            let state = self.state.read().unwrap();
171            let physical_columns = state
172                .physical_region_states()
173                .get(&data_region_id)
174                .context(error::PhysicalRegionNotFoundSnafu {
175                    region_id: data_region_id,
176                })?
177                .physical_columns();
178
179            for (index, field) in batch.schema().fields().iter().enumerate() {
180                let name = field.name();
181                let column_id = physical_columns
182                    .get(name)
183                    .map(|info| info.column_id)
184                    .with_context(|| error::ColumnNotFoundSnafu {
185                        name: name.clone(),
186                        region_id: logical_region_id,
187                    })?;
188                if tag_names.contains(name.as_str()) {
189                    tag_columns.push(TagColumnInfo {
190                        name: name.clone(),
191                        index,
192                        column_id,
193                    });
194                } else {
195                    non_tag_indices.push(index);
196                }
197            }
198        }
199
200        tag_columns.sort_by(|a, b| a.name.cmp(&b.name));
201        Ok((tag_columns, non_tag_indices))
202    }
203}
204
205fn record_batch_to_ipc(record_batch: &RecordBatch) -> Result<(Bytes, Bytes, Bytes)> {
206    let mut encoder = FlightEncoder::default();
207    let schema = encoder.encode_schema(record_batch.schema().as_ref());
208    let mut iter = encoder
209        .encode(FlightMessage::RecordBatch(record_batch.clone()))
210        .into_iter();
211
212    let Some(flight_data) = iter.next() else {
213        return error::UnexpectedRequestSnafu {
214            reason: "Failed to encode empty flight data",
215        }
216        .fail();
217    };
218    ensure!(
219        iter.next().is_none(),
220        error::UnexpectedRequestSnafu {
221            reason: "Bulk insert RecordBatch with dictionary arrays is unsupported".to_string(),
222        }
223    );
224
225    Ok((
226        schema.data_header,
227        flight_data.data_header,
228        flight_data.data_body,
229    ))
230}
231
232#[cfg(test)]
233mod tests {
234    use std::assert_matches;
235    use std::sync::Arc;
236
237    use api::v1::ArrowIpc;
238    use common_error::ext::ErrorExt;
239    use common_query::prelude::{greptime_timestamp, greptime_value};
240    use common_recordbatch::RecordBatches;
241    use datafusion::parquet::basic::Encoding;
242    use datatypes::arrow::array::{Float64Array, StringArray, TimestampMillisecondArray};
243    use datatypes::arrow::datatypes::{DataType, Field, Schema as ArrowSchema, TimeUnit};
244    use datatypes::arrow::record_batch::RecordBatch;
245    use mito2::config::MitoConfig;
246    use mito2::sst::parquet::metadata::MetadataLoader;
247    use mito2::sst::parquet::reader::MetadataCacheMetrics;
248    use store_api::metric_engine_consts::{
249        METRIC_ENGINE_NAME, PHYSICAL_TABLE_METADATA_KEY, PRIMARY_KEY_ENCODING,
250    };
251    use store_api::mito_engine_options::EXPERIMENTAL_SST_FLOAT_FIELD_ENCODING;
252    use store_api::path_utils::table_dir;
253    use store_api::region_engine::RegionEngine;
254    use store_api::region_request::{
255        PathType, RegionBulkInsertsRequest, RegionCloseRequest, RegionFlushRequest,
256        RegionOpenRequest, RegionPutRequest, RegionRequest,
257    };
258    use store_api::storage::{RegionId, ScanRequest};
259
260    use super::record_batch_to_ipc;
261    use crate::batch_modifier::{TagColumnInfo, modify_batch_sparse};
262    use crate::engine::MetricEngine;
263    use crate::error::Error;
264    use crate::test_util::{self, TestEnv};
265
266    fn build_logical_batch(start: usize, rows: usize) -> RecordBatch {
267        let schema = Arc::new(ArrowSchema::new(vec![
268            Field::new(
269                greptime_timestamp(),
270                DataType::Timestamp(TimeUnit::Millisecond, None),
271                false,
272            ),
273            Field::new(greptime_value(), DataType::Float64, true),
274            Field::new("job", DataType::Utf8, true),
275        ]));
276
277        let mut ts = Vec::with_capacity(rows);
278        let mut values = Vec::with_capacity(rows);
279        let mut tags = Vec::with_capacity(rows);
280        for i in start..start + rows {
281            ts.push(i as i64);
282            values.push(i as f64);
283            tags.push("tag_0".to_string());
284        }
285
286        RecordBatch::try_new(
287            schema,
288            vec![
289                Arc::new(TimestampMillisecondArray::from(ts)),
290                Arc::new(Float64Array::from(values)),
291                Arc::new(StringArray::from(tags)),
292            ],
293        )
294        .unwrap()
295    }
296
297    fn build_bulk_request(
298        logical_region_id: RegionId,
299        batch: RecordBatch,
300        skip_wal: bool,
301    ) -> RegionRequest {
302        let (schema, data_header, payload) = record_batch_to_ipc(&batch).unwrap();
303        RegionRequest::BulkInserts(RegionBulkInsertsRequest {
304            skip_wal,
305            region_id: logical_region_id,
306            payload: batch,
307            raw_data: ArrowIpc {
308                schema,
309                data_header,
310                payload,
311            },
312            partition_expr_version: None,
313            aligned_schema_version: None,
314        })
315    }
316
317    #[tokio::test]
318    async fn test_bulk_insert_rejects_duplicate_tag() {
319        use common_error::status_code::StatusCode;
320
321        let env = TestEnv::new().await;
322        env.init_metric_region().await;
323        let logical_region_id = env.default_logical_region_id();
324
325        let batch = build_logical_batch(0, 2);
326        let mut fields = batch.schema().fields().to_vec();
327        let mut columns = batch.columns().to_vec();
328        fields.push(fields[2].clone());
329        columns.push(columns[2].clone());
330        let batch = RecordBatch::try_new(Arc::new(ArrowSchema::new(fields)), columns).unwrap();
331        let err = env
332            .metric()
333            .handle_request(
334                logical_region_id,
335                build_bulk_request(logical_region_id, batch, false),
336            )
337            .await
338            .unwrap_err();
339        assert_eq!(err.status_code(), StatusCode::InvalidArguments, "{err:?}");
340    }
341
342    #[tokio::test]
343    async fn test_bulk_insert_physical_region_rejects_malformed_primary_key() {
344        use common_error::status_code::StatusCode;
345        use datatypes::arrow::array::{ArrayRef, BinaryArray, Int64Array};
346        use store_api::storage::consts::PRIMARY_KEY_COLUMN_NAME;
347
348        let env = TestEnv::new().await;
349        env.init_metric_region().await;
350        let physical_region_id = env.default_physical_region_id();
351        let logical_region_id = env.default_logical_region_id();
352        let tag_columns = vec![TagColumnInfo {
353            name: "job".to_string(),
354            index: 2,
355            column_id: 2,
356        }];
357        let batch = modify_batch_sparse(
358            build_logical_batch(0, 2),
359            logical_region_id.table_id(),
360            &tag_columns,
361            &[0, 1],
362        )
363        .unwrap();
364        let primary_key_index = batch.schema().index_of(PRIMARY_KEY_COLUMN_NAME).unwrap();
365        let with_primary_key = |primary_key: Option<ArrayRef>, extra: Option<ArrayRef>| {
366            let mut fields = batch.schema().fields().to_vec();
367            let mut columns = batch.columns().to_vec();
368            fields.remove(primary_key_index);
369            columns.remove(primary_key_index);
370            for array in [primary_key, extra].into_iter().flatten() {
371                fields.push(Arc::new(Field::new(
372                    PRIMARY_KEY_COLUMN_NAME,
373                    array.data_type().clone(),
374                    true,
375                )));
376                columns.push(array);
377            }
378            RecordBatch::try_new(Arc::new(ArrowSchema::new(fields)), columns).unwrap()
379        };
380        let encoded = batch.column(primary_key_index).clone();
381        let rejected = [
382            with_primary_key(None, None),
383            with_primary_key(Some(Arc::new(Int64Array::from(vec![1, 2]))), None),
384            with_primary_key(
385                Some(Arc::new(BinaryArray::from_opt_vec(vec![Some(b"a"), None]))),
386                None,
387            ),
388            with_primary_key(Some(encoded.clone()), Some(encoded)),
389        ];
390        for batch in rejected {
391            let err = env
392                .metric()
393                .handle_request(
394                    physical_region_id,
395                    build_bulk_request(physical_region_id, batch, false),
396                )
397                .await
398                .unwrap_err();
399            assert_eq!(err.status_code(), StatusCode::InvalidArguments, "{err:?}");
400        }
401    }
402
403    async fn init_dense_metric_region(env: &TestEnv) -> RegionId {
404        let physical_region_id = env.default_physical_region_id();
405        env.create_physical_region(
406            physical_region_id,
407            &TestEnv::default_table_dir(),
408            vec![(PRIMARY_KEY_ENCODING.to_string(), "dense".to_string())],
409        )
410        .await;
411
412        let logical_region_id = env.default_logical_region_id();
413        let request = test_util::create_logical_region_request(
414            &["job"],
415            physical_region_id,
416            &table_dir("test", logical_region_id.table_id()),
417        );
418        env.metric()
419            .handle_request(logical_region_id, RegionRequest::Create(request))
420            .await
421            .unwrap();
422        logical_region_id
423    }
424
425    #[tokio::test]
426    async fn test_bulk_insert_empty_batch_returns_zero() {
427        let env = TestEnv::new().await;
428        env.init_metric_region().await;
429        let logical_region_id = env.default_logical_region_id();
430
431        let batch = build_logical_batch(0, 0);
432        let request = RegionRequest::BulkInserts(RegionBulkInsertsRequest {
433            skip_wal: false,
434            region_id: logical_region_id,
435            payload: batch,
436            raw_data: ArrowIpc::default(),
437            partition_expr_version: None,
438            aligned_schema_version: None,
439        });
440        let response = env
441            .metric()
442            .handle_request(logical_region_id, request)
443            .await
444            .unwrap();
445        assert_eq!(response.affected_rows, 0);
446    }
447
448    #[tokio::test]
449    async fn test_bulk_insert_physical_region_passthrough() {
450        check_bulk_insert_physical_region_passthrough(false, false, false).await;
451        check_bulk_insert_physical_region_passthrough(true, false, false).await;
452    }
453
454    #[tokio::test]
455    async fn test_bulk_insert_physical_region_close_recovery() {
456        check_bulk_insert_physical_region_passthrough(false, true, false).await;
457        check_bulk_insert_physical_region_passthrough(true, true, false).await;
458    }
459
460    #[tokio::test]
461    async fn test_physical_bulk_partition_columns_preserve_stored_rows() {
462        for skip_wal in [false, true] {
463            let stripped =
464                check_bulk_insert_physical_region_passthrough(skip_wal, true, false).await;
465            let retained =
466                check_bulk_insert_physical_region_passthrough(skip_wal, true, true).await;
467            assert_eq!(retained, stripped, "skip_wal={skip_wal}");
468        }
469    }
470
471    async fn check_bulk_insert_physical_region_passthrough(
472        skip_wal: bool,
473        close: bool,
474        retain_partition_columns: bool,
475    ) -> String {
476        // Use flat format so that BulkMemtable is used (supports write_bulk).
477        let mito_config = MitoConfig {
478            default_flat_format: true,
479            ..Default::default()
480        };
481        let mut env = TestEnv::with_mito_config("", mito_config, Default::default()).await;
482        env.init_metric_region().await;
483        env.metric().inner.flush_task.stop().await.unwrap();
484        let physical_region_id = env.default_physical_region_id();
485        let logical_region_id = env.default_logical_region_id();
486
487        // First, do a normal logical bulk insert so we can compare results.
488        let logical_batch = build_logical_batch(0, 3);
489        let logical_request = build_bulk_request(logical_region_id, logical_batch, skip_wal);
490        let response = env
491            .metric()
492            .handle_request(logical_region_id, logical_request)
493            .await
494            .unwrap();
495        assert_eq!(response.affected_rows, 3);
496
497        // Now build a physical-format batch using modify_batch_sparse (simulating
498        // what the batcher's flush_batch_physical does) and send it directly to
499        // the physical region.
500        let tag_columns = vec![TagColumnInfo {
501            name: "job".to_string(),
502            index: 2,
503            column_id: 2, // column_id for "job" in the physical table
504        }];
505        let non_tag_indices = if retain_partition_columns {
506            vec![0, 1, 2] // timestamp, value, and the job partition column
507        } else {
508            vec![0, 1] // timestamp, value
509        };
510        let second_batch = build_logical_batch(3, 3);
511        let physical_batch = modify_batch_sparse(
512            second_batch,
513            logical_region_id.table_id(),
514            &tag_columns,
515            &non_tag_indices,
516        )
517        .unwrap();
518        assert_eq!(
519            physical_batch.column_by_name("job").is_some(),
520            retain_partition_columns
521        );
522        let request = build_bulk_request(physical_region_id, physical_batch, skip_wal);
523        let response = env
524            .metric()
525            .handle_request(physical_region_id, request)
526            .await
527            .unwrap();
528        assert_eq!(response.affected_rows, 3);
529
530        // Verify all 6 rows are readable from the logical region.
531        let stream = env
532            .metric()
533            .scan_to_stream(logical_region_id, ScanRequest::default())
534            .await
535            .unwrap();
536        let batches = RecordBatches::try_collect(stream).await.unwrap();
537        assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 6);
538
539        let stat = env.mito().region_statistic(physical_region_id).unwrap();
540        assert_eq!(stat.sst_num, 0);
541        let reopened = if close {
542            env.metric()
543                .handle_request(
544                    physical_region_id,
545                    RegionRequest::Close(RegionCloseRequest::default()),
546                )
547                .await
548                .unwrap();
549            // Recreating the wrapper prevents its metadata cache from hiding data loss.
550            MetricEngine::try_new(env.mito(), Default::default()).unwrap()
551        } else {
552            // Stopping workers without Close exercises recovery from WAL alone.
553            env.reopen_engine(Default::default()).await;
554            env.metric()
555        };
556        reopened.inner.flush_task.stop().await.unwrap();
557        reopened
558            .handle_request(
559                physical_region_id,
560                RegionRequest::Open(RegionOpenRequest {
561                    engine: METRIC_ENGINE_NAME.to_string(),
562                    table_dir: TestEnv::default_table_dir(),
563                    path_type: PathType::Bare,
564                    options: [(PHYSICAL_TABLE_METADATA_KEY.to_string(), String::new())]
565                        .into_iter()
566                        .collect(),
567                    skip_wal_replay: false,
568                    checkpoint: None,
569                    requirements: Default::default(),
570                }),
571            )
572            .await
573            .unwrap();
574        let stat = env
575            .mito()
576            .region_statistic(crate::utils::to_data_region_id(physical_region_id))
577            .unwrap();
578        assert_eq!(stat.sst_num > 0, skip_wal && close);
579        let stream = reopened
580            .scan_to_stream(logical_region_id, ScanRequest::default())
581            .await
582            .unwrap();
583        let batches = RecordBatches::try_collect(stream).await.unwrap();
584        assert_eq!(
585            batches.iter().map(|b| b.num_rows()).sum::<usize>(),
586            if skip_wal && !close { 0 } else { 6 },
587        );
588        batches.pretty_print().unwrap()
589    }
590
591    #[tokio::test]
592    async fn test_bulk_insert_physical_region_empty_batch() {
593        // Use flat format so that BulkMemtable is used (supports write_bulk).
594        let mito_config = MitoConfig {
595            default_flat_format: true,
596            ..Default::default()
597        };
598        let env = TestEnv::with_mito_config("", mito_config, Default::default()).await;
599        env.init_metric_region().await;
600        let physical_region_id = env.default_physical_region_id();
601
602        let batch = build_logical_batch(0, 0);
603        let request = build_bulk_request(physical_region_id, batch, false);
604        let response = env
605            .metric()
606            .handle_request(physical_region_id, request)
607            .await
608            .unwrap();
609        assert_eq!(response.affected_rows, 0);
610    }
611
612    #[tokio::test]
613    async fn test_bulk_insert_unknown_column_errors() {
614        let env = TestEnv::new().await;
615        env.init_metric_region().await;
616        let logical_region_id = env.default_logical_region_id();
617
618        let schema = Arc::new(ArrowSchema::new(vec![
619            Field::new(
620                greptime_timestamp(),
621                DataType::Timestamp(TimeUnit::Millisecond, None),
622                false,
623            ),
624            Field::new(greptime_value(), DataType::Float64, true),
625            Field::new("nonexistent_column", DataType::Utf8, true),
626        ]));
627        let batch = RecordBatch::try_new(
628            schema,
629            vec![
630                Arc::new(TimestampMillisecondArray::from(vec![0i64])),
631                Arc::new(Float64Array::from(vec![1.0])),
632                Arc::new(StringArray::from(vec!["val"])),
633            ],
634        )
635        .unwrap();
636
637        let request = build_bulk_request(logical_region_id, batch, false);
638        let err = env
639            .metric()
640            .handle_request(logical_region_id, request)
641            .await
642            .unwrap_err();
643        let Some(err) = err.as_any().downcast_ref::<Error>() else {
644            panic!("unexpected error type");
645        };
646        assert_matches!(err, Error::ColumnNotFound { .. });
647    }
648
649    #[tokio::test]
650    async fn test_bulk_insert_multiple_tag_columns() {
651        let env = TestEnv::new().await;
652        let physical_region_id = env.default_physical_region_id();
653        env.create_physical_region(physical_region_id, &TestEnv::default_table_dir(), vec![])
654            .await;
655        let logical_region_id = env.default_logical_region_id();
656        let request = test_util::create_logical_region_request(
657            &["host", "region"],
658            physical_region_id,
659            &table_dir("test", logical_region_id.table_id()),
660        );
661        env.metric()
662            .handle_request(logical_region_id, RegionRequest::Create(request))
663            .await
664            .unwrap();
665
666        let schema = Arc::new(ArrowSchema::new(vec![
667            Field::new(
668                greptime_timestamp(),
669                DataType::Timestamp(TimeUnit::Millisecond, None),
670                false,
671            ),
672            Field::new(greptime_value(), DataType::Float64, true),
673            Field::new("host", DataType::Utf8, true),
674            Field::new("region", DataType::Utf8, true),
675        ]));
676        let batch = RecordBatch::try_new(
677            schema,
678            vec![
679                Arc::new(TimestampMillisecondArray::from(vec![0i64, 1, 2])),
680                Arc::new(Float64Array::from(vec![10.0, 20.0, 30.0])),
681                Arc::new(StringArray::from(vec!["h1", "h2", "h1"])),
682                Arc::new(StringArray::from(vec!["us-east", "us-west", "eu-west"])),
683            ],
684        )
685        .unwrap();
686
687        let request = build_bulk_request(logical_region_id, batch, false);
688        let response = env
689            .metric()
690            .handle_request(logical_region_id, request)
691            .await
692            .unwrap();
693        assert_eq!(response.affected_rows, 3);
694
695        let stream = env
696            .metric()
697            .scan_to_stream(logical_region_id, ScanRequest::default())
698            .await
699            .unwrap();
700        let batches = RecordBatches::try_collect(stream).await.unwrap();
701        assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 3);
702    }
703
704    #[tokio::test]
705    async fn test_bulk_insert_accumulates_rows() {
706        let env = TestEnv::new().await;
707        env.init_metric_region().await;
708        let logical_region_id = env.default_logical_region_id();
709
710        let request = build_bulk_request(logical_region_id, build_logical_batch(0, 3), false);
711        let response = env
712            .metric()
713            .handle_request(logical_region_id, request)
714            .await
715            .unwrap();
716        assert_eq!(response.affected_rows, 3);
717
718        let request = build_bulk_request(logical_region_id, build_logical_batch(3, 5), false);
719        let response = env
720            .metric()
721            .handle_request(logical_region_id, request)
722            .await
723            .unwrap();
724        assert_eq!(response.affected_rows, 5);
725
726        let stream = env
727            .metric()
728            .scan_to_stream(logical_region_id, ScanRequest::default())
729            .await
730            .unwrap();
731        let batches = RecordBatches::try_collect(stream).await.unwrap();
732        assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 8);
733    }
734
735    #[tokio::test]
736    async fn test_bulk_insert_sparse_encoding() {
737        let env = TestEnv::new().await;
738        let physical_region_id = env.default_physical_region_id();
739        env.create_physical_region(
740            physical_region_id,
741            &TestEnv::default_table_dir(),
742            vec![(
743                EXPERIMENTAL_SST_FLOAT_FIELD_ENCODING.to_string(),
744                "byte_stream_split".to_string(),
745            )],
746        )
747        .await;
748        let logical_region_id = env.default_logical_region_id();
749        env.create_logical_region(physical_region_id, logical_region_id)
750            .await;
751
752        let rows = 4;
753        let request = build_bulk_request(logical_region_id, build_logical_batch(0, rows), false);
754        let response = env
755            .metric()
756            .handle_request(logical_region_id, request)
757            .await
758            .unwrap();
759        assert_eq!(response.affected_rows, rows);
760
761        let data_region_id = crate::utils::to_data_region_id(physical_region_id);
762        env.mito()
763            .handle_request(
764                data_region_id,
765                RegionRequest::Flush(RegionFlushRequest::default()),
766            )
767            .await
768            .unwrap();
769
770        let region = env.mito().find_region(data_region_id).unwrap();
771        let entry = region
772            .manifest_sst_entries()
773            .await
774            .into_iter()
775            .find(|entry| entry.visible && entry.file_path.ends_with(".parquet"))
776            .unwrap();
777        let mut cache_metrics = MetadataCacheMetrics::default();
778        let footer = MetadataLoader::new(
779            region.access_layer().object_store().clone(),
780            &entry.file_path,
781            entry.file_size,
782        )
783        .load(&mut cache_metrics)
784        .await
785        .unwrap();
786        let field_column_index = footer
787            .file_metadata()
788            .schema_descr()
789            .columns()
790            .iter()
791            .position(|column| column.name() == greptime_value())
792            .unwrap();
793        assert!(!footer.row_groups().is_empty());
794        assert!(footer.row_groups().iter().all(|row_group| {
795            row_group
796                .column(field_column_index)
797                .encodings()
798                .any(|encoding| encoding == Encoding::BYTE_STREAM_SPLIT)
799        }));
800
801        let stream = env
802            .metric()
803            .scan_to_stream(logical_region_id, ScanRequest::default())
804            .await
805            .unwrap();
806        let batches = RecordBatches::try_collect(stream).await.unwrap();
807        assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), rows);
808    }
809
810    #[tokio::test]
811    async fn test_bulk_insert_dense_encoding_rejected() {
812        let env = TestEnv::new().await;
813        let logical_region_id = init_dense_metric_region(&env).await;
814
815        let request = build_bulk_request(logical_region_id, build_logical_batch(0, 2), false);
816        let err = env
817            .metric()
818            .handle_request(logical_region_id, request)
819            .await
820            .unwrap_err();
821        let Some(err) = err.as_any().downcast_ref::<Error>() else {
822            panic!("unexpected error type");
823        };
824        assert_matches!(err, Error::UnsupportedRegionRequest { .. });
825    }
826
827    #[tokio::test]
828    async fn test_bulk_insert_matches_put() {
829        let env_put = TestEnv::new().await;
830        env_put.init_metric_region().await;
831        let logical_region_id = env_put.default_logical_region_id();
832        let schema = test_util::row_schema_with_tags(&["job"]);
833        let rows = test_util::build_rows(1, 5);
834        env_put
835            .metric()
836            .handle_request(
837                logical_region_id,
838                RegionRequest::Put(RegionPutRequest {
839                    skip_wal: false,
840                    rows: api::v1::Rows { schema, rows },
841                    hint: None,
842                    partition_expr_version: None,
843                }),
844            )
845            .await
846            .unwrap();
847        let put_stream = env_put
848            .metric()
849            .scan_to_stream(logical_region_id, ScanRequest::default())
850            .await
851            .unwrap();
852        let put_batches = RecordBatches::try_collect(put_stream).await.unwrap();
853        let put_output = put_batches.pretty_print().unwrap();
854
855        let env_bulk = TestEnv::new().await;
856        env_bulk.init_metric_region().await;
857        let request = build_bulk_request(logical_region_id, build_logical_batch(0, 5), false);
858        env_bulk
859            .metric()
860            .handle_request(logical_region_id, request)
861            .await
862            .unwrap();
863        let bulk_stream = env_bulk
864            .metric()
865            .scan_to_stream(logical_region_id, ScanRequest::default())
866            .await
867            .unwrap();
868        let bulk_batches = RecordBatches::try_collect(bulk_stream).await.unwrap();
869        let bulk_output = bulk_batches.pretty_print().unwrap();
870
871        assert_eq!(put_output, bulk_output);
872    }
873}