Skip to main content

servers/batcher/
logical_table.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
15mod batch;
16mod batch_convert;
17mod flow_notifier;
18mod pending_worker;
19mod region_write;
20mod tables;
21#[cfg(test)]
22pub(crate) mod test_util;
23
24use std::collections::{HashMap, HashSet};
25use std::future::{Future, ready};
26use std::num::NonZeroUsize;
27use std::sync::Arc;
28use std::time::Duration;
29
30use api::v1::RowInsertRequests;
31use catalog::CatalogManagerRef;
32use common_batcher::flush_limiter::FlushLimiter;
33use common_batcher::flush_policy::timing::TimingFlushPolicy;
34use common_batcher::request_limiter::RequestLimiter;
35use common_batcher::worker_registry::WorkerRegistry;
36use common_error::ext::BoxedError;
37use common_meta::cache::TableFlownodeSetCacheRef;
38use common_meta::node_manager::NodeManagerRef;
39use common_query::prelude::GREPTIME_PHYSICAL_TABLE;
40use common_query::{Output, WriteCompletion, WriteProgress};
41use common_time::timestamp::TimeUnit;
42use meter_core::data::MeterRecord;
43use meter_macros::write_meter;
44use operator::write_dispatcher::WriteDispatcherRef;
45use partition::manager::PartitionRuleManagerRef;
46use session::context::QueryContextRef;
47use snafu::ResultExt;
48use store_api::metric_engine_consts::{LOGICAL_TABLE_METADATA_KEY, METRIC_ENGINE_NAME};
49use tokio::sync::{Semaphore, broadcast, mpsc, oneshot};
50
51use crate::batcher::flow_notifier::{FlowNotifier, start_flow_notification_worker};
52pub use crate::batcher::logical_table::batch::flush_batch_physical;
53pub use crate::batcher::logical_table::batch_convert::{RecordBatchWithTsIdx, TableBatch};
54use crate::batcher::logical_table::pending_worker::{
55    PendingWorker, WorkerCommand, remove_worker_if_same_channel, start_worker,
56};
57pub use crate::batcher::logical_table::region_write::{
58    PhysicalFlushCatalogProvider, PhysicalFlushPartitionProvider, PhysicalFlushWriteDispatcher,
59    PhysicalTableMetadata,
60};
61pub use crate::batcher::logical_table::tables::{
62    PendingRowsSchemaAlterer, PendingRowsSchemaAltererRef,
63};
64use crate::batcher::pending_rows_batch_sync_enabled;
65use crate::error;
66use crate::error::{Error, Result};
67use crate::metrics::{
68    FLOW_NOTIFICATION_DROPPED, PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED, PENDING_WORKERS,
69};
70
71const PHYSICAL_TABLE_KEY: &str = "physical_table";
72
73const WORKER_IDLE_TIMEOUT_MULTIPLIER: u32 = 3;
74
75#[derive(Debug, Clone, Hash, Eq, PartialEq)]
76pub(crate) struct BatchKey {
77    pub(crate) catalog: String,
78    pub(crate) schema: String,
79    pub(crate) physical_table: String,
80    pub(crate) skip_wal: bool,
81}
82
83// Requests can share a batch only when their write target and WAL policy match.
84pub(crate) fn batch_key_from_ctx(ctx: &QueryContextRef) -> BatchKey {
85    let physical_table = ctx
86        .extension(PHYSICAL_TABLE_KEY)
87        .unwrap_or(GREPTIME_PHYSICAL_TABLE)
88        .to_string();
89    BatchKey {
90        catalog: ctx.current_catalog().to_string(),
91        schema: ctx.current_schema(),
92        physical_table,
93        skip_wal: ctx.skip_wal(),
94    }
95}
96
97/// Prometheus remote write pending rows batcher.
98pub struct LogicalTablePendingRowsBatcher {
99    workers: Arc<WorkerRegistry<BatchKey, WorkerCommand>>,
100    flush_interval: Duration,
101    flush_policy: TimingFlushPolicy,
102    partition_manager: PartitionRuleManagerRef,
103    write_dispatcher: WriteDispatcherRef,
104    catalog_manager: CatalogManagerRef,
105    flow_notification_tx: FlowNotifier,
106    flush_limiter: FlushLimiter,
107    request_limiter: RequestLimiter,
108    worker_channel_capacity: usize,
109    prom_store_with_metric_engine: bool,
110    schema_alterer: PendingRowsSchemaAltererRef,
111    pending_rows_batch_sync: bool,
112    shutdown: broadcast::Sender<()>,
113}
114
115impl LogicalTablePendingRowsBatcher {
116    /// Creates the batcher when metric-engine batching is enabled.
117    #[allow(clippy::too_many_arguments)]
118    pub fn try_new(
119        partition_manager: PartitionRuleManagerRef,
120        node_manager: NodeManagerRef,
121        write_dispatcher: WriteDispatcherRef,
122        catalog_manager: CatalogManagerRef,
123        table_flownode_set_cache: TableFlownodeSetCacheRef,
124        prom_store_with_metric_engine: bool,
125        schema_alterer: PendingRowsSchemaAltererRef,
126        flush_interval: Duration,
127        max_batch_rows: usize,
128        max_concurrent_flushes: usize,
129        worker_channel_capacity: usize,
130        max_inflight_requests: usize,
131        flow_notification_queue_capacity: NonZeroUsize,
132    ) -> Option<Arc<Self>> {
133        if worker_channel_capacity == 0 || worker_channel_capacity > Semaphore::MAX_PERMITS {
134            return None;
135        }
136
137        let flush_policy = TimingFlushPolicy::try_new(flush_interval, max_batch_rows)?;
138        let flush_limiter = FlushLimiter::try_new(max_concurrent_flushes)?;
139
140        let request_limiter = RequestLimiter::try_new(max_inflight_requests)?;
141        let (flow_notification_tx, flow_notification_rx) = FlowNotifier::try_new(
142            flow_notification_queue_capacity.get(),
143            FLOW_NOTIFICATION_DROPPED.clone(),
144        )?;
145
146        let (shutdown, _) = broadcast::channel(1);
147        let pending_rows_batch_sync = pending_rows_batch_sync_enabled();
148        let workers = Arc::new(WorkerRegistry::new());
149        PENDING_WORKERS.set(0);
150        start_flow_notification_worker(
151            flow_notification_rx,
152            table_flownode_set_cache,
153            node_manager.clone(),
154        );
155
156        Some(Arc::new(Self {
157            workers,
158            flush_interval,
159            flush_policy,
160            partition_manager,
161            write_dispatcher,
162            catalog_manager,
163            flow_notification_tx,
164            prom_store_with_metric_engine,
165            schema_alterer,
166            flush_limiter,
167            request_limiter,
168            worker_channel_capacity,
169            pending_rows_batch_sync,
170            shutdown,
171        }))
172    }
173}
174
175impl LogicalTablePendingRowsBatcher {
176    pub(crate) fn is_synchronous(&self) -> bool {
177        self.pending_rows_batch_sync
178    }
179
180    pub async fn submit(&self, requests: RowInsertRequests, ctx: QueryContextRef) -> Result<u64> {
181        self.submit_with(requests, ctx, |_| ready(Ok(())))
182            .await
183            .map(|(rows, ())| rows)
184    }
185
186    /// Returns the physical metric table's time index unit resolved from
187    /// `ctx`, defaulting to millisecond when the table does not exist yet
188    /// (the schema alterer auto-creates it as millisecond).
189    async fn physical_time_index_unit_or_default(&self, ctx: &QueryContextRef) -> TimeUnit {
190        let key = batch_key_from_ctx(ctx);
191        let Ok(Some(table)) = self
192            .catalog_manager
193            .table(&key.catalog, &key.schema, &key.physical_table, None)
194            .await
195        else {
196            return TimeUnit::Millisecond;
197        };
198        table
199            .table_info()
200            .meta
201            .schema
202            .timestamp_column()
203            .and_then(|col| col.data_type.as_timestamp().map(|ts| ts.unit()))
204            .unwrap_or(TimeUnit::Millisecond)
205    }
206
207    /// Returns whether the bulk path can accept `batches`: every existing
208    /// destination table must be a metric logical table bound to the
209    /// physical table selected by its context. A destination bound to
210    /// another physical table would be flushed through the selected
211    /// physical's regions, silently misplacing its rows, so such requests
212    /// must stay on the ordinary insert path (which routes per destination).
213    /// New tables are always fine: they are created on the selected physical
214    /// table. Time index units need no check here — the bulk encode converts
215    /// each request to its destination's unit. Destinations are resolved
216    /// once per distinct (schema, table).
217    pub(crate) async fn accepts_bulk_destinations<'a>(
218        &self,
219        batches: impl Iterator<Item = (&'a QueryContextRef, &'a RowInsertRequests)>,
220    ) -> bool {
221        // One request can select different physical tables per batch (e.g.
222        // per-series physical-table labels), so the dedupe key includes the
223        // selected physical table: every distinct (schema, table, physical)
224        // triple is validated against the table's actual binding.
225        let mut checked = HashSet::new();
226        // For missing tables, one request must not select two different
227        // physical tables: the batcher would create the table through one
228        // selection and flush its rows through the other's regions.
229        let mut missing_selections: HashMap<(String, String), String> = HashMap::new();
230        for (ctx, requests) in batches {
231            let physical_table = batch_key_from_ctx(ctx).physical_table;
232            let schema = ctx.current_schema();
233            for request in &requests.inserts {
234                if !checked.insert((
235                    schema.clone(),
236                    request.table_name.clone(),
237                    physical_table.clone(),
238                )) {
239                    continue;
240                }
241                let Ok(Some(table)) = self
242                    .catalog_manager
243                    .table(ctx.current_catalog(), &schema, &request.table_name, None)
244                    .await
245                else {
246                    // New table: created on the selected physical table, but
247                    // a conflicting selection within the same request cannot
248                    // be batched.
249                    if missing_selections
250                        .insert(
251                            (schema.clone(), request.table_name.clone()),
252                            physical_table.clone(),
253                        )
254                        .is_some_and(|previous| previous != physical_table)
255                    {
256                        return false;
257                    }
258                    continue;
259                };
260                let info = table.table_info();
261                if info.meta.engine != METRIC_ENGINE_NAME
262                    || info
263                        .meta
264                        .options
265                        .extra_options
266                        .get(LOGICAL_TABLE_METADATA_KEY)
267                        .map(String::as_str)
268                        != Some(physical_table.as_str())
269                {
270                    return false;
271                }
272            }
273        }
274        true
275    }
276
277    /// Submits with request-level accounting after schema preparation and before
278    /// queue admission. Acknowledgement follows the global batching policy.
279    /// In synchronous mode, waits for storage even when confirmed writes are enabled.
280    /// Use [`Self::submit_output_with`] with confirmed writes to retain partial row
281    /// counts on failure.
282    pub async fn submit_with<T, F>(
283        &self,
284        requests: RowInsertRequests,
285        ctx: QueryContextRef,
286        after_prepare: impl FnOnce(RowInsertRequests) -> F,
287    ) -> Result<(u64, T)>
288    where
289        F: Future<Output = Result<T>> + Send,
290    {
291        let (output, prepared) = self
292            .submit_output_with(requests, ctx, after_prepare)
293            .await?;
294        let rows = output.extract_rows_and_cost().0 as u64;
295        if self.pending_rows_batch_sync {
296            output
297                .write_progress()
298                .await
299                .into_result()
300                .context(error::ExecuteGrpcQuerySnafu)?;
301        }
302        Ok((rows, prepared))
303    }
304
305    /// Retains completion even when the batching policy acknowledges queue admission.
306    /// With confirmed writes, callers must await [`Output::write_progress`] to observe
307    /// storage errors and acknowledged rows, including in synchronous batching mode.
308    pub async fn submit_output_with<T, F>(
309        &self,
310        requests: RowInsertRequests,
311        ctx: QueryContextRef,
312        after_prepare: impl FnOnce(RowInsertRequests) -> F,
313    ) -> Result<(Output, T)>
314    where
315        F: Future<Output = Result<T>> + Send,
316    {
317        let confirmed = ctx.confirmed_writes();
318        let (table_batches, total_rows) = {
319            let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
320                .with_label_values(&["submit_build_and_align"])
321                .start_timer();
322            self.build_and_align_table_batches(&requests, &ctx).await?
323        };
324        let prepared = after_prepare(requests).await?;
325        if total_rows == 0 {
326            return Ok((Output::new_with_affected_rows(0), prepared));
327        }
328
329        // Flushes dispatch directly to datanodes, so admit once before enqueueing.
330        write_meter!(MeterRecord::new(
331            ctx.current_catalog().to_string(),
332            ctx.current_schema(),
333            0,
334            ctx.write_rows_to_admit(
335                ctx.current_catalog(),
336                &ctx.current_schema(),
337                total_rows as u64
338            ),
339            ctx.channel() as u8,
340        ))
341        .await
342        .context(error::WriteRejectedSnafu)?;
343
344        let permit = {
345            let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
346                .with_label_values(&["submit_acquire_inflight_permit"])
347                .start_timer();
348            self.request_limiter
349                .acquire()
350                .await
351                .map_err(|_| error::BatcherChannelClosedSnafu.build())?
352        };
353
354        let (response_tx, response_rx) = oneshot::channel();
355        let progress = ctx.write_progress().cloned();
356
357        let batch_key = batch_key_from_ctx(&ctx);
358        let mut cmd = Some(WorkerCommand::Submit {
359            _completion: progress.as_ref().map(|progress| progress.start()),
360            table_batches,
361            total_rows,
362            ctx,
363            response_tx,
364            _permit: permit,
365        });
366
367        {
368            let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
369                .with_label_values(&["submit_send_to_worker"])
370                .start_timer();
371
372            for _ in 0..2 {
373                let worker = self.get_or_spawn_worker(batch_key.clone()).await;
374                let Some(worker_cmd) = cmd.take() else {
375                    break;
376                };
377
378                match worker.tx.send(worker_cmd).await {
379                    Ok(()) => break,
380                    Err(err) => {
381                        cmd = Some(err.0);
382                        remove_worker_if_same_channel(
383                            self.workers.as_ref(),
384                            &batch_key,
385                            &worker.tx,
386                        )
387                        .await;
388                    }
389                }
390            }
391
392            if cmd.is_some() {
393                return Err(Error::BatcherChannelClosed);
394            }
395        }
396
397        let completion = async move {
398            let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
399                .with_label_values(&["submit_wait_flush_result"])
400                .start_timer();
401            response_rx.await.unwrap_or_else(|_| {
402                WriteProgress::failed(BoxedError::new(error::BatcherChannelClosedSnafu.build()))
403            })
404        };
405        let mut output = Output::new_with_affected_rows(total_rows);
406        if self.pending_rows_batch_sync && !confirmed {
407            completion
408                .await
409                .into_result()
410                .context(error::ExecuteGrpcQuerySnafu)?;
411        } else {
412            if let Some(progress) = progress {
413                progress.mark_accepted();
414            }
415            output
416                .meta
417                .write_completions
418                .push(WriteCompletion::with_progress(completion));
419        }
420        Ok((output, prepared))
421    }
422}
423
424impl LogicalTablePendingRowsBatcher {
425    async fn get_or_spawn_worker(&self, key: BatchKey) -> PendingWorker {
426        let (tx, receiver) = self
427            .workers
428            .get_or_create(key.clone(), self.worker_channel_capacity)
429            .await;
430        if let Some(rx) = receiver {
431            self.spawn_worker(key, tx.clone(), rx);
432            PENDING_WORKERS.set(self.workers.len().await as i64);
433        }
434        PendingWorker { tx }
435    }
436}
437
438impl LogicalTablePendingRowsBatcher {
439    fn spawn_worker(
440        &self,
441        key: BatchKey,
442        tx: mpsc::Sender<WorkerCommand>,
443        rx: mpsc::Receiver<WorkerCommand>,
444    ) {
445        let worker_idle_timeout = self
446            .flush_interval
447            .checked_mul(WORKER_IDLE_TIMEOUT_MULTIPLIER)
448            .unwrap_or(self.flush_interval);
449
450        start_worker(
451            key,
452            tx,
453            self.workers.clone(),
454            rx,
455            self.shutdown.clone(),
456            self.partition_manager.clone(),
457            self.write_dispatcher.clone(),
458            self.catalog_manager.clone(),
459            self.flow_notification_tx.clone(),
460            worker_idle_timeout,
461            self.flush_policy,
462            self.flush_limiter.clone(),
463        );
464    }
465}
466
467impl Drop for LogicalTablePendingRowsBatcher {
468    fn drop(&mut self) {
469        let _ = self.shutdown.send(());
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use std::future::ready;
476    use std::sync::Arc;
477
478    use api::v1::value::ValueData;
479    use api::v1::{
480        ColumnDataType, ColumnSchema, Row, RowInsertRequest, RowInsertRequests, Rows, SemanticType,
481        Value,
482    };
483    use arrow::array::{StringArray, TimestampMillisecondArray};
484    use arrow::datatypes::{DataType as ArrowDataType, Field, Schema as ArrowSchema};
485    use arrow::record_batch::RecordBatch;
486    use common_error::ext::{BoxedError, ErrorExt};
487    use common_error::status_code::StatusCode;
488    use common_query::WriteProgress;
489    use common_query::prelude::greptime_timestamp;
490    use session::context::QueryContext;
491
492    use crate::batcher::logical_table::batch_convert::{RecordBatchWithTsIdx, TableBatch};
493    use crate::batcher::logical_table::flow_notifier::extract_timestamps;
494    use crate::batcher::logical_table::pending_worker::WorkerCommand;
495    use crate::batcher::logical_table::test_util::{
496        mock_aligned_tag_batch, mock_tag_batch, submission_batcher,
497    };
498    use crate::error;
499
500    fn submission_requests() -> RowInsertRequests {
501        RowInsertRequests {
502            inserts: vec![RowInsertRequest {
503                table_name: "cpu".to_string(),
504                rows: Some(Rows {
505                    schema: vec![ColumnSchema {
506                        column_name: "greptime_timestamp".to_string(),
507                        datatype: ColumnDataType::TimestampMillisecond as i32,
508                        semantic_type: SemanticType::Timestamp as i32,
509                        ..Default::default()
510                    }],
511                    rows: [1000, 2000]
512                        .into_iter()
513                        .map(|ts| Row {
514                            values: vec![Value {
515                                value_data: Some(ValueData::TimestampMillisecondValue(ts)),
516                            }],
517                        })
518                        .collect(),
519                }),
520            }],
521        }
522    }
523
524    fn flush_progress(affected_rows: usize) -> WriteProgress {
525        WriteProgress {
526            affected_rows,
527            error: (affected_rows < 2).then(|| {
528                Arc::new(BoxedError::new(
529                    error::InvalidQuerySnafu {
530                        reason: "storage write failed",
531                    }
532                    .build(),
533                ))
534            }),
535        }
536    }
537
538    #[tokio::test]
539    async fn test_submit_with_preserves_sync_acknowledgement() {
540        for confirmed in [false, true] {
541            for sync in [false, true] {
542                for affected_rows in [0, 1, 2] {
543                    let (batcher, mut receiver) = submission_batcher(sync).await;
544                    let ctx = QueryContext::arc();
545                    let ctx = if confirmed {
546                        Arc::new(ctx.with_confirmed_writes())
547                    } else {
548                        ctx
549                    };
550                    let submit = batcher.submit_with(submission_requests(), ctx, |_| ready(Ok(7)));
551                    tokio::pin!(submit);
552                    let mut early_result = None;
553                    // Poll submission first to detect early acknowledgement without timing assumptions.
554                    let command = tokio::select! {
555                        biased;
556                        result = &mut submit => {
557                            assert!(result.is_ok(), "{result:?}");
558                            early_result = Some(result);
559                            receiver.try_recv().unwrap()
560                        }
561                        command = receiver.recv() => command.unwrap(),
562                    };
563                    assert_eq!(early_result.is_none(), sync);
564                    let WorkerCommand::Submit { response_tx, .. } = command else {
565                        panic!("expected submission")
566                    };
567                    assert_eq!(
568                        response_tx.send(flush_progress(affected_rows)).is_ok(),
569                        sync
570                    );
571                    let result = match early_result {
572                        Some(result) => result,
573                        None => submit.await,
574                    };
575                    if sync && affected_rows < 2 {
576                        assert_eq!(
577                            result.unwrap_err().status_code(),
578                            StatusCode::InvalidArguments
579                        );
580                    } else {
581                        assert_eq!(result.unwrap(), (2, 7));
582                    }
583                }
584            }
585        }
586    }
587
588    #[tokio::test]
589    async fn test_submit_output_with_preserves_confirmed_partial_progress() {
590        for sync in [false, true] {
591            let (batcher, mut receiver) = submission_batcher(sync).await;
592            let ctx = Arc::new(QueryContext::arc().with_confirmed_writes());
593            let (output, prepared) = batcher
594                .submit_output_with(submission_requests(), ctx, |_| ready(Ok(7)))
595                .await
596                .unwrap();
597            assert_eq!(prepared, 7);
598            assert_eq!(output.meta.write_completions.len(), 1);
599            let WorkerCommand::Submit { response_tx, .. } = receiver.try_recv().unwrap() else {
600                panic!("expected submission")
601            };
602            let progress = output.write_progress();
603            tokio::pin!(progress);
604            assert!(futures::poll!(progress.as_mut()).is_pending());
605            response_tx.send(flush_progress(1)).unwrap();
606            let progress = progress.await;
607            assert_eq!(progress.affected_rows, 1);
608            assert_eq!(
609                progress.error.unwrap().status_code(),
610                StatusCode::InvalidArguments
611            );
612        }
613    }
614
615    #[test]
616    fn test_extract_timestamps_appends_non_null_batches_in_order() {
617        let table_batch = TableBatch {
618            table_name: "cpu".to_string(),
619            table_id: 42,
620            batches: vec![
621                mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0),
622                mock_aligned_tag_batch("tag1", "host-1", 2000, 2.0),
623            ],
624            row_count: 2,
625        };
626
627        assert_eq!(vec![1000, 2000], extract_timestamps(&table_batch));
628    }
629
630    #[test]
631    fn test_extract_timestamps_omits_nulls_and_retains_order() {
632        let table_batch = TableBatch {
633            table_name: "cpu".to_string(),
634            table_id: 42,
635            batches: vec![
636                mock_timestamp_batch(vec![Some(1000), None, Some(3000)]),
637                mock_timestamp_batch(vec![None, Some(5000)]),
638            ],
639            row_count: 5,
640        };
641
642        assert_eq!(vec![1000, 3000, 5000], extract_timestamps(&table_batch));
643    }
644
645    #[test]
646    fn test_record_batch_with_ts_idx_rejects_out_of_bounds_index() {
647        let batch = mock_tag_batch("tag1", "host-1", 1000, 1.0);
648
649        assert!(RecordBatchWithTsIdx::try_new(batch, 3).is_err());
650    }
651
652    #[test]
653    fn test_record_batch_with_ts_idx_rejects_non_timestamp_column() {
654        let batch = mock_tag_batch("tag1", "host-1", 1000, 1.0);
655
656        assert!(RecordBatchWithTsIdx::try_new(batch, 1).is_err());
657    }
658
659    #[test]
660    fn test_extract_timestamps_supports_per_batch_timestamp_indices() {
661        let timestamp_first = RecordBatch::try_new(
662            Arc::new(ArrowSchema::new(vec![
663                Field::new(
664                    "ts",
665                    ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
666                    false,
667                ),
668                Field::new("host", ArrowDataType::Utf8, true),
669            ])),
670            vec![
671                Arc::new(TimestampMillisecondArray::from(vec![1000, 2000])),
672                Arc::new(StringArray::from(vec!["host-1", "host-2"])),
673            ],
674        )
675        .unwrap();
676        let timestamp_second = RecordBatch::try_new(
677            Arc::new(ArrowSchema::new(vec![
678                Field::new("host", ArrowDataType::Utf8, true),
679                Field::new(
680                    "timestamp",
681                    ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
682                    false,
683                ),
684            ])),
685            vec![
686                Arc::new(StringArray::from(vec!["host-3", "host-4"])),
687                Arc::new(TimestampMillisecondArray::from(vec![3000, 4000])),
688            ],
689        )
690        .unwrap();
691        let table_batch = TableBatch {
692            table_name: "cpu".to_string(),
693            table_id: 42,
694            batches: vec![
695                RecordBatchWithTsIdx::try_new(timestamp_first, 0).unwrap(),
696                RecordBatchWithTsIdx::try_new(timestamp_second, 1).unwrap(),
697            ],
698            row_count: 4,
699        };
700
701        assert_eq!(
702            vec![1000, 2000, 3000, 4000],
703            extract_timestamps(&table_batch)
704        );
705    }
706
707    fn mock_timestamp_batch(timestamps: Vec<Option<i64>>) -> RecordBatchWithTsIdx {
708        let batch = RecordBatch::try_new(
709            Arc::new(ArrowSchema::new(vec![Field::new(
710                greptime_timestamp(),
711                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
712                true,
713            )])),
714            vec![Arc::new(TimestampMillisecondArray::from(timestamps))],
715        )
716        .unwrap();
717        RecordBatchWithTsIdx::try_new(batch, 0).unwrap()
718    }
719    #[test]
720    fn test_batch_key_groups_by_skip_wal() {
721        use std::collections::HashMap;
722
723        use crate::batcher::logical_table::batch_key_from_ctx;
724
725        let wal_ctx = session::context::QueryContext::arc();
726        let skip_wal_ctx = session::context::QueryContext::arc();
727        skip_wal_ctx.set_skip_wal(true);
728        let another_skip_wal_ctx = session::context::QueryContext::arc();
729        another_skip_wal_ctx.set_skip_wal(true);
730
731        let mut batches = HashMap::new();
732        *batches.entry(batch_key_from_ctx(&wal_ctx)).or_insert(0) += 1;
733        *batches
734            .entry(batch_key_from_ctx(&skip_wal_ctx))
735            .or_insert(0) += 1;
736        *batches
737            .entry(batch_key_from_ctx(&another_skip_wal_ctx))
738            .or_insert(0) += 1;
739        *batches
740            .entry(batch_key_from_ctx(&session::context::QueryContext::arc()))
741            .or_insert(0) += 1;
742
743        assert_eq!(batches.len(), 2);
744        assert_eq!(batches[&batch_key_from_ctx(&wal_ctx)], 2);
745        assert_eq!(batches[&batch_key_from_ctx(&skip_wal_ctx)], 2);
746    }
747}