Skip to main content

servers/
pending_rows_batcher.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, HashSet};
16use std::num::NonZeroUsize;
17use std::sync::Arc;
18use std::time::{Duration, Instant};
19
20use api::v1::flow::{DirtyWindowRequest, DirtyWindowRequests};
21use api::v1::meta::Peer;
22use api::v1::region::{
23    BulkInsertRequest, RegionRequest, RegionRequestHeader, bulk_insert_request, region_request,
24};
25use api::v1::{ArrowIpc, ColumnSchema, RowInsertRequests, Rows};
26use arrow::array::Array;
27use arrow::compute::{concat_batches, filter_record_batch};
28use arrow::datatypes::{DataType as ArrowDataType, Schema as ArrowSchema, TimeUnit};
29use arrow::record_batch::RecordBatch;
30use async_trait::async_trait;
31use bytes::Bytes;
32use catalog::CatalogManagerRef;
33use common_grpc::flight::{FlightEncoder, FlightMessage};
34use common_meta::cache::TableFlownodeSetCacheRef;
35use common_meta::node_manager::NodeManagerRef;
36use common_query::prelude::{GREPTIME_PHYSICAL_TABLE, greptime_timestamp, greptime_value};
37use common_telemetry::tracing_context::TracingContext;
38use common_telemetry::{debug, error, warn};
39use dashmap::DashMap;
40use dashmap::mapref::entry::Entry;
41use futures::StreamExt;
42use metric_engine::batch_modifier::{TagColumnInfo, modify_batch_sparse};
43use partition::manager::PartitionRuleManagerRef;
44use partition::partition::PartitionRuleRef;
45use session::context::QueryContextRef;
46use smallvec::SmallVec;
47use snafu::{OptionExt, ResultExt, ensure};
48use store_api::storage::{RegionId, TableId};
49use table::metadata::{TableInfo, TableInfoRef};
50use tokio::sync::{OwnedSemaphorePermit, Semaphore, broadcast, mpsc, oneshot};
51
52use crate::error;
53use crate::error::{Error, Result};
54use crate::metrics::{
55    FLOW_NOTIFICATION_DROPPED, FLUSH_DROPPED_ROWS, FLUSH_ELAPSED, FLUSH_FAILURES, FLUSH_ROWS,
56    FLUSH_TOTAL, PENDING_BATCHES, PENDING_ROWS, PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED,
57    PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED, PENDING_WORKERS,
58};
59use crate::prom_row_builder::{
60    build_prom_create_table_schema_from_proto, identify_missing_columns_from_proto,
61    rows_to_aligned_record_batch,
62};
63
64const PHYSICAL_TABLE_KEY: &str = "physical_table";
65/// Whether wait for ingestion result before reply to client.
66const PENDING_ROWS_BATCH_SYNC_ENV: &str = "PENDING_ROWS_BATCH_SYNC";
67
68/// Returns whether pending-row batch submissions wait for the flush result
69/// before replying to the client (synchronous mode), controlled by the
70/// `PENDING_ROWS_BATCH_SYNC` environment variable and defaulting to `true`.
71///
72/// Callers that reason about how long a remote write request may block (e.g.
73/// the frontend HTTP timeout fallback) must consult this instead of
74/// duplicating the env lookup.
75pub fn pending_rows_batch_sync_enabled() -> bool {
76    std::env::var(PENDING_ROWS_BATCH_SYNC_ENV)
77        .ok()
78        .as_deref()
79        .and_then(|v| v.parse::<bool>().ok())
80        .unwrap_or(true)
81}
82const WORKER_IDLE_TIMEOUT_MULTIPLIER: u32 = 3;
83const PHYSICAL_REGION_ESSENTIAL_COLUMN_COUNT: usize = 3;
84const MAX_CONCURRENT_FLOW_NOTIFICATIONS: usize = 8;
85#[async_trait]
86pub trait PendingRowsSchemaAlterer: Send + Sync {
87    /// Batch-create multiple logical tables that are missing.
88    /// Each entry is `(table_name, request_schema)`.
89    async fn create_tables_if_missing_batch(
90        &self,
91        catalog: &str,
92        schema: &str,
93        tables: &[(&str, &[ColumnSchema])],
94        with_metric_engine: bool,
95        ctx: QueryContextRef,
96    ) -> Result<()>;
97
98    /// Batch-alter multiple logical tables to add missing tag columns.
99    /// Each entry is `(table_name, missing_column_names)`.
100    async fn add_missing_prom_tag_columns_batch(
101        &self,
102        catalog: &str,
103        schema: &str,
104        tables: &[(&str, &[String])],
105        ctx: QueryContextRef,
106    ) -> Result<()>;
107}
108
109pub type PendingRowsSchemaAltererRef = Arc<dyn PendingRowsSchemaAlterer>;
110
111#[derive(Clone)]
112pub struct PhysicalTableMetadata {
113    pub table_info: TableInfoRef,
114    /// Mapping from column name to column id
115    pub col_name_to_ids: Option<HashMap<String, u32>>,
116}
117
118#[async_trait]
119pub trait PhysicalFlushCatalogProvider: Send + Sync {
120    async fn physical_table(
121        &self,
122        catalog: &str,
123        schema: &str,
124        table_name: &str,
125        query_ctx: &session::context::QueryContext,
126    ) -> catalog::error::Result<Option<PhysicalTableMetadata>>;
127}
128
129#[async_trait]
130pub trait PhysicalFlushPartitionProvider: Send + Sync {
131    async fn find_table_partition_rule(
132        &self,
133        table_info: &TableInfo,
134    ) -> partition::error::Result<PartitionRuleRef>;
135
136    async fn find_region_leader(&self, region_id: RegionId) -> Result<Peer>;
137}
138
139#[async_trait]
140pub trait PhysicalFlushNodeRequester: Send + Sync {
141    async fn handle(
142        &self,
143        peer: &Peer,
144        request: RegionRequest,
145    ) -> Result<api::region::RegionResponse>;
146}
147
148#[derive(Clone)]
149struct CatalogManagerPhysicalFlushAdapter {
150    catalog_manager: CatalogManagerRef,
151}
152
153#[async_trait]
154impl PhysicalFlushCatalogProvider for CatalogManagerPhysicalFlushAdapter {
155    async fn physical_table(
156        &self,
157        catalog: &str,
158        schema: &str,
159        table_name: &str,
160        query_ctx: &session::context::QueryContext,
161    ) -> catalog::error::Result<Option<PhysicalTableMetadata>> {
162        self.catalog_manager
163            .table(catalog, schema, table_name, Some(query_ctx))
164            .await
165            .map(|table| {
166                table.map(|table| {
167                    let table_info = table.table_info();
168                    let name_to_ids = table_info.name_to_ids();
169                    PhysicalTableMetadata {
170                        table_info,
171                        col_name_to_ids: name_to_ids,
172                    }
173                })
174            })
175    }
176}
177
178#[derive(Clone)]
179struct PartitionManagerPhysicalFlushAdapter {
180    partition_manager: PartitionRuleManagerRef,
181}
182
183#[async_trait]
184impl PhysicalFlushPartitionProvider for PartitionManagerPhysicalFlushAdapter {
185    async fn find_table_partition_rule(
186        &self,
187        table_info: &TableInfo,
188    ) -> partition::error::Result<PartitionRuleRef> {
189        self.partition_manager
190            .find_table_partition_rule(table_info)
191            .await
192            .map(|(rule, _)| rule)
193    }
194
195    async fn find_region_leader(&self, region_id: RegionId) -> Result<Peer> {
196        let peer = self.partition_manager.find_region_leader(region_id).await?;
197        Ok(peer)
198    }
199}
200
201#[derive(Clone)]
202struct NodeManagerPhysicalFlushAdapter {
203    node_manager: NodeManagerRef,
204}
205
206#[async_trait]
207impl PhysicalFlushNodeRequester for NodeManagerPhysicalFlushAdapter {
208    async fn handle(
209        &self,
210        peer: &Peer,
211        request: RegionRequest,
212    ) -> error::Result<api::region::RegionResponse> {
213        let datanode = self.node_manager.datanode(peer).await;
214        datanode
215            .handle(request)
216            .await
217            .context(error::CommonMetaSnafu)
218    }
219}
220
221#[derive(Debug, Clone, Hash, Eq, PartialEq)]
222struct BatchKey {
223    catalog: String,
224    schema: String,
225    physical_table: String,
226}
227
228/// An aligned logical record batch and its timestamp column index.
229#[derive(Debug, Clone)]
230pub struct RecordBatchWithTsIdx {
231    /// The aligned logical record batch.
232    batch: RecordBatch,
233    /// The timestamp column index in `batch`.
234    timestamp_index: usize,
235}
236
237impl RecordBatchWithTsIdx {
238    /// Creates a record batch with a validated timestamp column index.
239    pub fn try_new(batch: RecordBatch, timestamp_index: usize) -> Result<Self> {
240        let schema = batch.schema();
241        let timestamp_field = schema.fields().get(timestamp_index).with_context(|| {
242            error::InvalidPromRemoteRequestSnafu {
243                msg: format!(
244                    "Timestamp column index {} is out of bounds for record batch with {} columns",
245                    timestamp_index,
246                    batch.num_columns()
247                ),
248            }
249        })?;
250        ensure!(
251            matches!(timestamp_field.data_type(), ArrowDataType::Timestamp(_, _)),
252            error::InvalidPromRemoteRequestSnafu {
253                msg: format!(
254                    "Column at index {} is not a timestamp column: {:?}",
255                    timestamp_index,
256                    timestamp_field.data_type()
257                ),
258            }
259        );
260
261        Ok(Self {
262            batch,
263            timestamp_index,
264        })
265    }
266
267    #[cfg(test)]
268    pub(crate) fn into_parts(self) -> (RecordBatch, usize) {
269        (self.batch, self.timestamp_index)
270    }
271}
272
273#[derive(Debug, Clone)]
274pub struct TableBatch {
275    pub table_name: String,
276    pub table_id: TableId,
277    pub batches: Vec<RecordBatchWithTsIdx>,
278    pub row_count: usize,
279}
280
281/// Intermediate planning state for resolving and preparing logical tables
282/// before row-to-batch alignment.
283struct TableResolutionPlan {
284    /// Resolved table schema and table id by logical table name.
285    region_schemas: HashMap<String, (Arc<ArrowSchema>, u32)>,
286    /// Missing tables that need to be created before alignment.
287    tables_to_create: Vec<(String, Vec<ColumnSchema>)>,
288    /// Existing tables that need tag-column schema evolution.
289    tables_to_alter: Vec<(String, Vec<String>)>,
290}
291
292struct PendingBatch {
293    tables: HashMap<TableId, TableBatch>,
294    total_row_count: usize,
295    db_string: String,
296    ctx: QueryContextRef,
297    waiters: Vec<FlushWaiter>,
298}
299
300struct FlushWaiter {
301    response_tx: oneshot::Sender<std::result::Result<(), Arc<Error>>>,
302    _permit: OwnedSemaphorePermit,
303}
304
305struct FlushBatch {
306    table_batches: Vec<TableBatch>,
307    total_row_count: usize,
308    db_string: String,
309    ctx: QueryContextRef,
310    waiters: Vec<FlushWaiter>,
311}
312
313#[derive(Clone)]
314struct PendingWorker {
315    tx: mpsc::Sender<WorkerCommand>,
316}
317
318enum WorkerCommand {
319    Submit {
320        table_batches: Vec<(String, u32, RecordBatchWithTsIdx)>,
321        total_rows: usize,
322        ctx: QueryContextRef,
323        response_tx: oneshot::Sender<std::result::Result<(), Arc<Error>>>,
324        _permit: OwnedSemaphorePermit,
325    },
326    #[cfg(test)]
327    Ack { ack_tx: oneshot::Sender<()> },
328}
329
330// Batch key is derived from QueryContext; it assumes catalog/schema/physical_table fully
331// define the write target and must remain consistent across the batch.
332fn batch_key_from_ctx(ctx: &QueryContextRef) -> BatchKey {
333    let physical_table = ctx
334        .extension(PHYSICAL_TABLE_KEY)
335        .unwrap_or(GREPTIME_PHYSICAL_TABLE)
336        .to_string();
337    BatchKey {
338        catalog: ctx.current_catalog().to_string(),
339        schema: ctx.current_schema(),
340        physical_table,
341    }
342}
343
344/// Prometheus remote write pending rows batcher.
345pub struct PendingRowsBatcher {
346    workers: Arc<DashMap<BatchKey, PendingWorker>>,
347    flush_interval: Duration,
348    max_batch_rows: usize,
349    partition_manager: PartitionRuleManagerRef,
350    node_manager: NodeManagerRef,
351    catalog_manager: CatalogManagerRef,
352    flow_notification_tx: mpsc::Sender<FlowNotification>,
353    flush_semaphore: Arc<Semaphore>,
354    inflight_semaphore: Arc<Semaphore>,
355    worker_channel_capacity: usize,
356    prom_store_with_metric_engine: bool,
357    schema_alterer: PendingRowsSchemaAltererRef,
358    pending_rows_batch_sync: bool,
359    shutdown: broadcast::Sender<()>,
360}
361
362impl PendingRowsBatcher {
363    #[allow(clippy::too_many_arguments)]
364    pub fn try_new(
365        partition_manager: PartitionRuleManagerRef,
366        node_manager: NodeManagerRef,
367        catalog_manager: CatalogManagerRef,
368        table_flownode_set_cache: TableFlownodeSetCacheRef,
369        prom_store_with_metric_engine: bool,
370        schema_alterer: PendingRowsSchemaAltererRef,
371        flush_interval: Duration,
372        max_batch_rows: usize,
373        max_concurrent_flushes: usize,
374        worker_channel_capacity: usize,
375        max_inflight_requests: usize,
376        flow_notification_queue_capacity: NonZeroUsize,
377    ) -> Option<Arc<Self>> {
378        // Disable the batcher if flush is disabled or configuration is invalid.
379        // Zero values for these knobs either cause panics (e.g., zero-capacity channels)
380        // or deadlocks (e.g., semaphores with no permits).
381        if flush_interval.is_zero()
382            || max_batch_rows == 0
383            || max_concurrent_flushes == 0
384            || worker_channel_capacity == 0
385            || max_inflight_requests == 0
386        {
387            return None;
388        }
389
390        let (shutdown, _) = broadcast::channel(1);
391        let pending_rows_batch_sync = pending_rows_batch_sync_enabled();
392        let workers = Arc::new(DashMap::new());
393        PENDING_WORKERS.set(workers.len() as i64);
394        let (flow_notification_tx, flow_notification_rx) =
395            mpsc::channel(flow_notification_queue_capacity.get());
396        start_flow_notification_worker(
397            flow_notification_rx,
398            table_flownode_set_cache,
399            node_manager.clone(),
400        );
401
402        Some(Arc::new(Self {
403            workers,
404            flush_interval,
405            max_batch_rows,
406            partition_manager,
407            node_manager,
408            catalog_manager,
409            flow_notification_tx,
410            prom_store_with_metric_engine,
411            schema_alterer,
412            flush_semaphore: Arc::new(Semaphore::new(max_concurrent_flushes)),
413            inflight_semaphore: Arc::new(Semaphore::new(max_inflight_requests)),
414            worker_channel_capacity,
415            pending_rows_batch_sync,
416            shutdown,
417        }))
418    }
419
420    pub async fn submit(&self, requests: RowInsertRequests, ctx: QueryContextRef) -> Result<u64> {
421        let (table_batches, total_rows) = {
422            let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
423                .with_label_values(&["submit_build_and_align"])
424                .start_timer();
425            self.build_and_align_table_batches(requests, &ctx).await?
426        };
427        if total_rows == 0 {
428            return Ok(0);
429        }
430
431        let permit = {
432            let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
433                .with_label_values(&["submit_acquire_inflight_permit"])
434                .start_timer();
435            self.inflight_semaphore
436                .clone()
437                .acquire_owned()
438                .await
439                .map_err(|_| error::BatcherChannelClosedSnafu.build())?
440        };
441
442        let (response_tx, response_rx) = oneshot::channel();
443
444        let batch_key = batch_key_from_ctx(&ctx);
445        let mut cmd = Some(WorkerCommand::Submit {
446            table_batches,
447            total_rows,
448            ctx,
449            response_tx,
450            _permit: permit,
451        });
452
453        {
454            let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
455                .with_label_values(&["submit_send_to_worker"])
456                .start_timer();
457
458            for _ in 0..2 {
459                let worker = self.get_or_spawn_worker(batch_key.clone());
460                let Some(worker_cmd) = cmd.take() else {
461                    break;
462                };
463
464                match worker.tx.send(worker_cmd).await {
465                    Ok(()) => break,
466                    Err(err) => {
467                        cmd = Some(err.0);
468                        remove_worker_if_same_channel(
469                            self.workers.as_ref(),
470                            &batch_key,
471                            &worker.tx,
472                        );
473                    }
474                }
475            }
476
477            if cmd.is_some() {
478                return Err(Error::BatcherChannelClosed);
479            }
480        }
481
482        if self.pending_rows_batch_sync {
483            let result = {
484                let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
485                    .with_label_values(&["submit_wait_flush_result"])
486                    .start_timer();
487                response_rx
488                    .await
489                    .map_err(|_| error::BatcherChannelClosedSnafu.build())?
490            };
491            result
492                .context(error::SubmitBatchSnafu)
493                .map(|()| total_rows as u64)
494        } else {
495            Ok(total_rows as u64)
496        }
497    }
498
499    /// Converts proto `RowInsertRequests` directly into aligned `RecordBatch`es
500    /// in a single pass, handling table creation, schema alteration, column
501    /// renaming, reordering, and null-filling without building intermediate
502    /// RecordBatches.
503    async fn build_and_align_table_batches(
504        &self,
505        requests: RowInsertRequests,
506        ctx: &QueryContextRef,
507    ) -> Result<(Vec<(String, u32, RecordBatchWithTsIdx)>, usize)> {
508        let catalog = ctx.current_catalog().to_string();
509        let schema = ctx.current_schema();
510
511        let (table_rows, total_rows) = Self::collect_non_empty_table_rows(requests);
512        if total_rows == 0 {
513            return Ok((Vec::new(), 0));
514        }
515
516        let unique_tables = Self::collect_unique_table_schemas(&table_rows)?;
517        let mut plan = self
518            .plan_table_resolution(&catalog, &schema, ctx, &unique_tables)
519            .await?;
520
521        self.create_missing_tables_and_refresh_schemas(
522            &catalog,
523            &schema,
524            ctx,
525            &table_rows,
526            &mut plan,
527        )
528        .await?;
529
530        self.alter_tables_and_refresh_schemas(&catalog, &schema, ctx, &mut plan)
531            .await?;
532
533        let aligned_batches = Self::build_aligned_batches(&table_rows, &plan.region_schemas)?;
534
535        Ok((aligned_batches, total_rows))
536    }
537
538    /// Extracts non-empty `(table_name, rows)` pairs and computes total row
539    /// count across the retained entries.
540    fn collect_non_empty_table_rows(requests: RowInsertRequests) -> (Vec<(String, Rows)>, usize) {
541        let mut table_rows: Vec<(String, Rows)> = Vec::with_capacity(requests.inserts.len());
542        let mut total_rows = 0;
543
544        for request in requests.inserts {
545            let Some(rows) = request.rows else {
546                continue;
547            };
548            if rows.rows.is_empty() {
549                continue;
550            }
551
552            total_rows += rows.rows.len();
553            table_rows.push((request.table_name, rows));
554        }
555
556        (table_rows, total_rows)
557    }
558
559    /// Returns unique `(table_name, proto_schema)` pairs while keeping the
560    /// first-seen schema for duplicate table names.
561    fn collect_unique_table_schemas(
562        table_rows: &[(String, Rows)],
563    ) -> Result<Vec<(&str, &[ColumnSchema])>> {
564        let mut unique_tables: Vec<(&str, &[ColumnSchema])> = Vec::with_capacity(table_rows.len());
565        let mut seen = HashSet::new();
566
567        for (table_name, rows) in table_rows {
568            if seen.insert(table_name.as_str()) {
569                unique_tables.push((table_name.as_str(), &rows.schema));
570            } else {
571                // table_rows should group rows by table name.
572                return error::InvalidPromRemoteRequestSnafu {
573                    msg: format!(
574                        "Found duplicated table name in RowInsertRequest: {}",
575                        table_name
576                    ),
577                }
578                .fail();
579            }
580        }
581
582        Ok(unique_tables)
583    }
584
585    /// Resolves table metadata and classifies each table into existing,
586    /// to-create, and to-alter groups used by subsequent DDL steps.
587    async fn plan_table_resolution(
588        &self,
589        catalog: &str,
590        schema: &str,
591        ctx: &QueryContextRef,
592        unique_tables: &[(&str, &[ColumnSchema])],
593    ) -> Result<TableResolutionPlan> {
594        let mut plan = TableResolutionPlan {
595            region_schemas: HashMap::with_capacity(unique_tables.len()),
596            tables_to_create: Vec::new(),
597            tables_to_alter: Vec::new(),
598        };
599
600        let resolved_tables = {
601            let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
602                .with_label_values(&["align_resolve_table"])
603                .start_timer();
604            futures::future::join_all(unique_tables.iter().map(|(table_name, _)| {
605                self.catalog_manager
606                    .table(catalog, schema, table_name, Some(ctx.as_ref()))
607            }))
608            .await
609        };
610
611        for ((table_name, rows_schema), table_result) in unique_tables.iter().zip(resolved_tables) {
612            let table = table_result?;
613
614            if let Some(table) = table {
615                let table_info = table.table_info();
616                let table_id = table_info.ident.table_id;
617                let region_schema = table_info.meta.schema.arrow_schema().clone();
618
619                let missing_columns = {
620                    let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
621                        .with_label_values(&["align_identify_missing_columns"])
622                        .start_timer();
623                    identify_missing_columns_from_proto(rows_schema, region_schema.as_ref())?
624                };
625                if !missing_columns.is_empty() {
626                    plan.tables_to_alter
627                        .push(((*table_name).to_string(), missing_columns));
628                }
629                plan.region_schemas
630                    .insert((*table_name).to_string(), (region_schema, table_id));
631            } else {
632                let request_schema = {
633                    let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
634                        .with_label_values(&["align_build_create_table_schema"])
635                        .start_timer();
636                    build_prom_create_table_schema_from_proto(rows_schema)?
637                };
638                plan.tables_to_create
639                    .push(((*table_name).to_string(), request_schema));
640            }
641        }
642
643        Ok(plan)
644    }
645
646    /// Batch-creates missing tables, refreshes their schema metadata, and
647    /// enqueues follow-up alters for extra tag columns discovered in later rows.
648    async fn create_missing_tables_and_refresh_schemas(
649        &self,
650        catalog: &str,
651        schema: &str,
652        ctx: &QueryContextRef,
653        table_rows: &[(String, Rows)],
654        plan: &mut TableResolutionPlan,
655    ) -> Result<()> {
656        if plan.tables_to_create.is_empty() {
657            return Ok(());
658        }
659
660        let create_refs: Vec<(&str, &[ColumnSchema])> = plan
661            .tables_to_create
662            .iter()
663            .map(|(name, schema)| (name.as_str(), schema.as_slice()))
664            .collect();
665
666        {
667            let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
668                .with_label_values(&["align_batch_create_tables"])
669                .start_timer();
670            self.schema_alterer
671                .create_tables_if_missing_batch(
672                    catalog,
673                    schema,
674                    &create_refs,
675                    self.prom_store_with_metric_engine,
676                    ctx.clone(),
677                )
678                .await?;
679        }
680
681        let created_table_results = {
682            let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
683                .with_label_values(&["align_resolve_table_after_create"])
684                .start_timer();
685            futures::future::join_all(plan.tables_to_create.iter().map(|(table_name, _)| {
686                self.catalog_manager
687                    .table(catalog, schema, table_name, Some(ctx.as_ref()))
688            }))
689            .await
690        };
691
692        for ((table_name, _), table_result) in
693            plan.tables_to_create.iter().zip(created_table_results)
694        {
695            let table = table_result?.with_context(|| error::UnexpectedResultSnafu {
696                reason: format!(
697                    "Table not found after pending batch create attempt: {}",
698                    table_name
699                ),
700            })?;
701            let table_info = table.table_info();
702            let table_id = table_info.ident.table_id;
703            let region_schema = table_info.meta.schema.arrow_schema().clone();
704            plan.region_schemas
705                .insert(table_name.clone(), (region_schema, table_id));
706        }
707
708        Self::enqueue_alter_for_new_tables(table_rows, plan)?;
709
710        Ok(())
711    }
712
713    /// For newly created tables, re-checks all row schemas and appends alter
714    /// operations when additional tag columns are still missing.
715    fn enqueue_alter_for_new_tables(
716        table_rows: &[(String, Rows)],
717        plan: &mut TableResolutionPlan,
718    ) -> Result<()> {
719        let created_tables: HashSet<&str> = plan
720            .tables_to_create
721            .iter()
722            .map(|(table_name, _)| table_name.as_str())
723            .collect();
724
725        for (table_name, rows) in table_rows {
726            if !created_tables.contains(table_name.as_str()) {
727                continue;
728            }
729
730            let Some((region_schema, _)) = plan.region_schemas.get(table_name) else {
731                continue;
732            };
733
734            let missing_columns = identify_missing_columns_from_proto(&rows.schema, region_schema)?;
735            if missing_columns.is_empty()
736                || plan
737                    .tables_to_alter
738                    .iter()
739                    .any(|(existing_name, _)| existing_name == table_name)
740            {
741                continue;
742            }
743
744            plan.tables_to_alter
745                .push((table_name.clone(), missing_columns));
746        }
747
748        Ok(())
749    }
750
751    /// Batch-alters tables that have missing tag columns and refreshes the
752    /// in-memory schema map used for row alignment.
753    async fn alter_tables_and_refresh_schemas(
754        &self,
755        catalog: &str,
756        schema: &str,
757        ctx: &QueryContextRef,
758        plan: &mut TableResolutionPlan,
759    ) -> Result<()> {
760        if plan.tables_to_alter.is_empty() {
761            return Ok(());
762        }
763
764        let alter_refs: Vec<(&str, &[String])> = plan
765            .tables_to_alter
766            .iter()
767            .map(|(name, cols)| (name.as_str(), cols.as_slice()))
768            .collect();
769        {
770            let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
771                .with_label_values(&["align_batch_add_missing_columns"])
772                .start_timer();
773            self.schema_alterer
774                .add_missing_prom_tag_columns_batch(catalog, schema, &alter_refs, ctx.clone())
775                .await?;
776        }
777
778        let altered_table_results = {
779            let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
780                .with_label_values(&["align_resolve_table_after_schema_alter"])
781                .start_timer();
782            futures::future::join_all(plan.tables_to_alter.iter().map(|(table_name, _)| {
783                self.catalog_manager
784                    .table(catalog, schema, table_name, Some(ctx.as_ref()))
785            }))
786            .await
787        };
788
789        for ((table_name, _), table_result) in
790            plan.tables_to_alter.iter().zip(altered_table_results)
791        {
792            let table = table_result?.with_context(|| error::UnexpectedResultSnafu {
793                reason: format!(
794                    "Table not found after pending batch schema alter: {}",
795                    table_name
796                ),
797            })?;
798            let table_info = table.table_info();
799            let table_id = table_info.ident.table_id;
800            let refreshed_region_schema = table_info.meta.schema.arrow_schema().clone();
801            plan.region_schemas
802                .insert(table_name.clone(), (refreshed_region_schema, table_id));
803        }
804
805        Ok(())
806    }
807
808    /// Converts proto rows to `RecordBatch` values aligned to resolved region
809    /// schemas and returns `(table_name, table_id, batch)` tuples.
810    fn build_aligned_batches(
811        table_rows: &[(String, Rows)],
812        region_schemas: &HashMap<String, (Arc<ArrowSchema>, u32)>,
813    ) -> Result<Vec<(String, u32, RecordBatchWithTsIdx)>> {
814        let mut aligned_batches = Vec::with_capacity(table_rows.len());
815        for (table_name, rows) in table_rows {
816            let (region_schema, table_id) =
817                region_schemas.get(table_name).cloned().with_context(|| {
818                    error::UnexpectedResultSnafu {
819                        reason: format!("Region schema not resolved for table: {}", table_name),
820                    }
821                })?;
822
823            let record_batch = {
824                let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
825                    .with_label_values(&["align_rows_to_record_batch"])
826                    .start_timer();
827                rows_to_aligned_record_batch(rows, region_schema.as_ref())?
828            };
829            aligned_batches.push((table_name.clone(), table_id, record_batch));
830        }
831
832        Ok(aligned_batches)
833    }
834
835    fn get_or_spawn_worker(&self, key: BatchKey) -> PendingWorker {
836        if let Some(worker) = self.workers.get(&key)
837            && !worker.tx.is_closed()
838        {
839            return worker.clone();
840        }
841
842        let entry = self.workers.entry(key.clone());
843        match entry {
844            Entry::Occupied(mut worker) => {
845                if worker.get().tx.is_closed() {
846                    let new_worker = self.spawn_worker(key);
847                    worker.insert(new_worker.clone());
848                    PENDING_WORKERS.set(self.workers.len() as i64);
849                    new_worker
850                } else {
851                    worker.get().clone()
852                }
853            }
854            Entry::Vacant(vacant) => {
855                let worker = self.spawn_worker(key);
856
857                vacant.insert(worker.clone());
858                PENDING_WORKERS.set(self.workers.len() as i64);
859                worker
860            }
861        }
862    }
863
864    fn spawn_worker(&self, key: BatchKey) -> PendingWorker {
865        let (tx, rx) = mpsc::channel(self.worker_channel_capacity);
866        let worker = PendingWorker { tx: tx.clone() };
867        let worker_idle_timeout = self
868            .flush_interval
869            .checked_mul(WORKER_IDLE_TIMEOUT_MULTIPLIER)
870            .unwrap_or(self.flush_interval);
871
872        start_worker(
873            key,
874            worker.tx.clone(),
875            self.workers.clone(),
876            rx,
877            self.shutdown.clone(),
878            self.partition_manager.clone(),
879            self.node_manager.clone(),
880            self.catalog_manager.clone(),
881            self.flow_notification_tx.clone(),
882            self.flush_interval,
883            worker_idle_timeout,
884            self.max_batch_rows,
885            self.flush_semaphore.clone(),
886        );
887
888        worker
889    }
890}
891
892impl Drop for PendingRowsBatcher {
893    fn drop(&mut self) {
894        let _ = self.shutdown.send(());
895    }
896}
897
898impl PendingBatch {
899    fn new(ctx: QueryContextRef) -> Self {
900        let db_string = ctx.get_db_string();
901        Self {
902            tables: HashMap::new(),
903            total_row_count: 0,
904            db_string,
905            ctx,
906            waiters: Vec::new(),
907        }
908    }
909
910    fn add_table_batch(
911        &mut self,
912        table_name: String,
913        table_id: TableId,
914        record_batch: RecordBatchWithTsIdx,
915    ) {
916        let entry = self.tables.entry(table_id).or_insert_with(|| TableBatch {
917            table_name,
918            table_id,
919            batches: Vec::new(),
920            row_count: 0,
921        });
922        entry.row_count += record_batch.batch.num_rows();
923        entry.batches.push(record_batch);
924    }
925}
926
927#[allow(clippy::too_many_arguments)]
928fn start_worker(
929    key: BatchKey,
930    worker_tx: mpsc::Sender<WorkerCommand>,
931    workers: Arc<DashMap<BatchKey, PendingWorker>>,
932    mut rx: mpsc::Receiver<WorkerCommand>,
933    shutdown: broadcast::Sender<()>,
934    partition_manager: PartitionRuleManagerRef,
935    node_manager: NodeManagerRef,
936    catalog_manager: CatalogManagerRef,
937    flow_notification_tx: mpsc::Sender<FlowNotification>,
938    flush_interval: Duration,
939    worker_idle_timeout: Duration,
940    max_batch_rows: usize,
941    flush_semaphore: Arc<Semaphore>,
942) {
943    tokio::spawn(async move {
944        let mut batch = None;
945        let flush_timer = tokio::time::sleep(flush_interval);
946        tokio::pin!(flush_timer);
947        let mut shutdown_rx = shutdown.subscribe();
948        let idle_deadline = tokio::time::Instant::now() + worker_idle_timeout;
949        let idle_timer = tokio::time::sleep_until(idle_deadline);
950        tokio::pin!(idle_timer);
951
952        loop {
953            tokio::select! {
954                cmd = rx.recv() => {
955                    match cmd {
956                        Some(WorkerCommand::Submit { table_batches, total_rows, ctx, response_tx, _permit }) => {
957                            idle_timer.as_mut().reset(tokio::time::Instant::now() + worker_idle_timeout);
958
959                            if batch.is_none() {
960                                // Anchor the flush deadline to this batch's first submission,
961                                // rather than to the worker's creation time.
962                                flush_timer
963                                    .as_mut()
964                                    .reset(tokio::time::Instant::now() + flush_interval);
965                            }
966                            let pending_batch = batch.get_or_insert_with(||{
967                                PENDING_BATCHES.inc();
968                                PendingBatch::new(ctx)
969                            });
970
971                            pending_batch.waiters.push(FlushWaiter { response_tx, _permit });
972
973                            for (table_name, table_id, record_batch) in table_batches {
974                                pending_batch.add_table_batch(table_name, table_id, record_batch);
975                            }
976
977                            pending_batch.total_row_count += total_rows;
978                            PENDING_ROWS.add(total_rows as i64);
979
980                            if pending_batch.total_row_count >= max_batch_rows
981                                && let Some(flush) = drain_batch(&mut batch) {
982                                    spawn_flush(
983                                        flush,
984                                        partition_manager.clone(),
985                                        node_manager.clone(),
986                                        catalog_manager.clone(),
987                                        flow_notification_tx.clone(),
988                                        flush_semaphore.clone(),
989                                    ).await;
990                            }
991                        }
992                        None => {
993                            if let Some(flush) = drain_batch(&mut batch) {
994                                flush_batch_with_managers(
995                                    flush,
996                                    partition_manager.clone(),
997                                    node_manager.clone(),
998                                    catalog_manager.clone(),
999                                    flow_notification_tx.clone(),
1000                                ).await;
1001                            }
1002                            break;
1003                        }
1004                        #[cfg(test)]
1005                        Some(WorkerCommand::Ack { ack_tx }) => {
1006                            let _ = ack_tx.send(());
1007                        }
1008                    }
1009                }
1010                _ = &mut idle_timer => {
1011                    if !should_close_worker_on_idle_timeout(
1012                        batch.as_ref().map_or(0, |batch| batch.total_row_count),
1013                        rx.len(),
1014                    ) {
1015                        idle_timer
1016                            .as_mut()
1017                            .reset(tokio::time::Instant::now() + worker_idle_timeout);
1018                        continue;
1019                    }
1020
1021                    debug!(
1022                        "Closing idle pending rows worker due to timeout: catalog={}, schema={}, physical_table={}",
1023                        key.catalog,
1024                        key.schema,
1025                        key.physical_table
1026                    );
1027                    break;
1028                }
1029                _ = &mut flush_timer, if batch.is_some() => {
1030                    if let Some(flush) = drain_batch(&mut batch) {
1031                        spawn_flush(
1032                            flush,
1033                            partition_manager.clone(),
1034                            node_manager.clone(),
1035                            catalog_manager.clone(),
1036                            flow_notification_tx.clone(),
1037                            flush_semaphore.clone(),
1038                        ).await;
1039                    }
1040                }
1041                _ = shutdown_rx.recv() => {
1042                    if let Some(flush) = drain_batch(&mut batch) {
1043                        flush_batch_with_managers(
1044                            flush,
1045                            partition_manager.clone(),
1046                            node_manager.clone(),
1047                            catalog_manager.clone(),
1048                            flow_notification_tx.clone(),
1049                        ).await;
1050                    }
1051                    break;
1052                }
1053            }
1054        }
1055
1056        remove_worker_if_same_channel(workers.as_ref(), &key, &worker_tx);
1057    });
1058}
1059
1060fn remove_worker_if_same_channel(
1061    workers: &DashMap<BatchKey, PendingWorker>,
1062    key: &BatchKey,
1063    worker_tx: &mpsc::Sender<WorkerCommand>,
1064) -> bool {
1065    if let Some(worker) = workers.get(key)
1066        && worker.tx.same_channel(worker_tx)
1067    {
1068        drop(worker);
1069        workers.remove(key);
1070        PENDING_WORKERS.set(workers.len() as i64);
1071        return true;
1072    }
1073
1074    false
1075}
1076
1077fn should_close_worker_on_idle_timeout(total_row_count: usize, queued_requests: usize) -> bool {
1078    total_row_count == 0 && queued_requests == 0
1079}
1080
1081fn drain_batch(batch: &mut Option<PendingBatch>) -> Option<FlushBatch> {
1082    let batch = batch.take()?;
1083    let total_row_count = batch.total_row_count;
1084
1085    if total_row_count == 0 {
1086        return None;
1087    }
1088
1089    let table_batches = batch.tables.into_values().collect();
1090    let waiters = batch.waiters;
1091
1092    PENDING_ROWS.sub(total_row_count as i64);
1093    PENDING_BATCHES.dec();
1094
1095    Some(FlushBatch {
1096        table_batches,
1097        total_row_count,
1098        db_string: batch.db_string,
1099        ctx: batch.ctx,
1100        waiters,
1101    })
1102}
1103
1104async fn spawn_flush(
1105    flush: FlushBatch,
1106    partition_manager: PartitionRuleManagerRef,
1107    node_manager: NodeManagerRef,
1108    catalog_manager: CatalogManagerRef,
1109    flow_notification_tx: mpsc::Sender<FlowNotification>,
1110    semaphore: Arc<Semaphore>,
1111) {
1112    match semaphore.acquire_owned().await {
1113        Ok(permit) => {
1114            tokio::spawn(async move {
1115                let _permit = permit;
1116                flush_batch_with_managers(
1117                    flush,
1118                    partition_manager,
1119                    node_manager,
1120                    catalog_manager,
1121                    flow_notification_tx,
1122                )
1123                .await;
1124            });
1125        }
1126        Err(err) => {
1127            warn!(err; "Flush semaphore closed, flushing inline");
1128            flush_batch_with_managers(
1129                flush,
1130                partition_manager,
1131                node_manager,
1132                catalog_manager,
1133                flow_notification_tx,
1134            )
1135            .await;
1136        }
1137    }
1138}
1139
1140struct FlushRegionWrite {
1141    datanode: Peer,
1142    request: RegionRequest,
1143}
1144
1145struct PlannedRegionBatch {
1146    region_id: RegionId,
1147    batch: RecordBatch,
1148}
1149
1150#[cfg(test)]
1151impl PlannedRegionBatch {
1152    fn num_rows(&self) -> usize {
1153        self.batch.num_rows()
1154    }
1155}
1156
1157struct ResolvedRegionBatch {
1158    planned: PlannedRegionBatch,
1159    datanode: Peer,
1160}
1161
1162fn should_dispatch_concurrently(region_write_count: usize) -> bool {
1163    region_write_count > 1
1164}
1165
1166/// Classifies columns in a logical-table batch for sparse primary-key conversion.
1167///
1168/// Returns:
1169/// - `Vec<TagColumnInfo>`: all Utf8 tag columns sorted by tag name, used for
1170///   TSID and sparse primary-key encoding.
1171/// - `SmallVec<[usize; 3]>`: indices of columns copied into the physical batch
1172///   after `__primary_key`, ordered as `[greptime_timestamp, greptime_value,
1173///   partition_tag_columns...]`.
1174fn columns_taxonomy(
1175    batch_schema: &Arc<ArrowSchema>,
1176    table_name: &str,
1177    name_to_ids: &HashMap<String, u32>,
1178    partition_columns: &HashSet<&str>,
1179) -> Result<(Vec<TagColumnInfo>, SmallVec<[usize; 3]>)> {
1180    let mut tag_columns = Vec::new();
1181    let mut essential_column_indices =
1182        SmallVec::<[usize; 3]>::with_capacity(2 + partition_columns.len());
1183    // Placeholder for greptime_timestamp and greptime_value
1184    essential_column_indices.push(0);
1185    essential_column_indices.push(0);
1186
1187    let mut timestamp_index = None;
1188    let mut value_index = None;
1189
1190    for (index, field) in batch_schema.fields().iter().enumerate() {
1191        match field.data_type() {
1192            ArrowDataType::Utf8 => {
1193                let column_id = name_to_ids.get(field.name()).copied().with_context(|| {
1194                    error::InvalidPromRemoteRequestSnafu {
1195                        msg: format!(
1196                            "Column '{}' from logical table '{}' not found in physical table column IDs",
1197                            field.name(),
1198                            table_name
1199                        ),
1200                    }
1201                })?;
1202                tag_columns.push(TagColumnInfo {
1203                    name: field.name().clone(),
1204                    index,
1205                    column_id,
1206                });
1207
1208                if partition_columns.contains(field.name().as_str()) {
1209                    essential_column_indices.push(index);
1210                }
1211            }
1212            ArrowDataType::Timestamp(TimeUnit::Millisecond, _) => {
1213                ensure!(
1214                    timestamp_index.replace(index).is_none(),
1215                    error::InvalidPromRemoteRequestSnafu {
1216                        msg: format!(
1217                            "Duplicated timestamp column in logical table '{}' batch schema",
1218                            table_name
1219                        ),
1220                    }
1221                );
1222            }
1223            ArrowDataType::Float64 => {
1224                ensure!(
1225                    value_index.replace(index).is_none(),
1226                    error::InvalidPromRemoteRequestSnafu {
1227                        msg: format!(
1228                            "Duplicated value column in logical table '{}' batch schema",
1229                            table_name
1230                        ),
1231                    }
1232                );
1233            }
1234            datatype => {
1235                return error::InvalidPromRemoteRequestSnafu {
1236                    msg: format!(
1237                        "Unexpected data type '{datatype:?}' in logical table '{}' batch schema",
1238                        table_name
1239                    ),
1240                }
1241                .fail();
1242            }
1243        }
1244    }
1245
1246    let timestamp_index =
1247        timestamp_index.with_context(|| error::InvalidPromRemoteRequestSnafu {
1248            msg: format!(
1249                "Missing essential column '{}' in logical table '{}' batch schema",
1250                greptime_timestamp(),
1251                table_name
1252            ),
1253        })?;
1254    let value_index = value_index.with_context(|| error::InvalidPromRemoteRequestSnafu {
1255        msg: format!(
1256            "Missing essential column '{}' in logical table '{}' batch schema",
1257            greptime_value(),
1258            table_name
1259        ),
1260    })?;
1261
1262    tag_columns.sort_by(|a, b| a.name.cmp(&b.name));
1263
1264    essential_column_indices[0] = timestamp_index;
1265    essential_column_indices[1] = value_index;
1266
1267    Ok((tag_columns, essential_column_indices))
1268}
1269
1270fn strip_partition_columns_from_batch(batch: RecordBatch) -> Result<RecordBatch> {
1271    ensure!(
1272        batch.num_columns() >= PHYSICAL_REGION_ESSENTIAL_COLUMN_COUNT,
1273        error::InternalSnafu {
1274            err_msg: format!(
1275                "Expected at least {} columns in physical batch, got {}",
1276                PHYSICAL_REGION_ESSENTIAL_COLUMN_COUNT,
1277                batch.num_columns()
1278            ),
1279        }
1280    );
1281    let essential_indices: Vec<usize> = (0..PHYSICAL_REGION_ESSENTIAL_COLUMN_COUNT).collect();
1282    batch.project(&essential_indices).context(error::ArrowSnafu)
1283}
1284
1285async fn flush_region_writes_concurrently(
1286    node_manager: &(impl PhysicalFlushNodeRequester + ?Sized),
1287    writes: Vec<FlushRegionWrite>,
1288) -> Result<usize> {
1289    let mut affected_rows = 0;
1290    if !should_dispatch_concurrently(writes.len()) {
1291        for write in writes {
1292            let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1293                .with_label_values(&["flush_write_region"])
1294                .start_timer();
1295            affected_rows += node_manager
1296                .handle(&write.datanode, write.request)
1297                .await?
1298                .affected_rows;
1299        }
1300        return Ok(affected_rows);
1301    }
1302
1303    let write_futures = writes.into_iter().map(|write| async move {
1304        let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1305            .with_label_values(&["flush_write_region"])
1306            .start_timer();
1307
1308        let response = node_manager.handle(&write.datanode, write.request).await?;
1309        Ok::<_, Error>(response.affected_rows)
1310    });
1311
1312    // todo(hl): should be bounded.
1313    let affected_rows = futures::future::try_join_all(write_futures)
1314        .await?
1315        .into_iter()
1316        .sum();
1317    Ok(affected_rows)
1318}
1319
1320async fn flush_batch_with_managers(
1321    flush: FlushBatch,
1322    partition_manager: PartitionRuleManagerRef,
1323    node_manager: NodeManagerRef,
1324    catalog_manager: CatalogManagerRef,
1325    flow_notification_tx: mpsc::Sender<FlowNotification>,
1326) {
1327    let partition_provider = PartitionManagerPhysicalFlushAdapter { partition_manager };
1328    let node_requester = NodeManagerPhysicalFlushAdapter {
1329        node_manager: node_manager.clone(),
1330    };
1331    let catalog_provider = CatalogManagerPhysicalFlushAdapter { catalog_manager };
1332    flush_batch(
1333        flush,
1334        &partition_provider,
1335        &node_requester,
1336        &catalog_provider,
1337        flow_notification_tx,
1338    )
1339    .await;
1340}
1341
1342async fn flush_batch(
1343    flush: FlushBatch,
1344    partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
1345    node_manager: &(impl PhysicalFlushNodeRequester + ?Sized),
1346    catalog_manager: &(impl PhysicalFlushCatalogProvider + ?Sized),
1347    flow_notification_tx: mpsc::Sender<FlowNotification>,
1348) {
1349    let FlushBatch {
1350        table_batches,
1351        total_row_count,
1352        db_string,
1353        ctx,
1354        waiters,
1355    } = flush;
1356    let start = Instant::now();
1357
1358    // Physical-table-level flush: transform all logical table batches
1359    // into physical format and write them together.
1360    let physical_table_name = ctx
1361        .extension(PHYSICAL_TABLE_KEY)
1362        .unwrap_or(GREPTIME_PHYSICAL_TABLE)
1363        .to_string();
1364    let result = flush_batch_physical(
1365        &table_batches,
1366        &physical_table_name,
1367        &ctx,
1368        partition_manager,
1369        node_manager,
1370        catalog_manager,
1371    )
1372    .await;
1373
1374    let elapsed = start.elapsed().as_secs_f64();
1375    FLUSH_ELAPSED.observe(elapsed);
1376
1377    debug!(
1378        "Pending rows batch flushed, total rows: {}, elapsed time: {}s",
1379        total_row_count, elapsed
1380    );
1381
1382    match result {
1383        Ok(affected_rows) => {
1384            FLUSH_TOTAL.inc();
1385            FLUSH_ROWS.observe(total_row_count as f64);
1386            operator::metrics::DIST_INGEST_ROW_COUNT
1387                .with_label_values(&[db_string.as_str()])
1388                .inc_by(affected_rows as u64);
1389
1390            notify_waiters(waiters, Ok(()));
1391            enqueue_flow_notifications(table_batches, &flow_notification_tx);
1392        }
1393        Err(err) => {
1394            FLUSH_FAILURES.inc();
1395            FLUSH_DROPPED_ROWS.inc_by(total_row_count as u64);
1396            notify_waiters(waiters, Err(err));
1397        }
1398    }
1399}
1400
1401fn extract_timestamps(table_batch: &TableBatch) -> Vec<i64> {
1402    let mut timestamps = Vec::with_capacity(table_batch.row_count);
1403    for batch in &table_batch.batches {
1404        let timestamp_column = batch.batch.column(batch.timestamp_index);
1405        let Some((timestamp_values, _)) =
1406            datatypes::timestamp::timestamp_array_to_primitive(timestamp_column)
1407        else {
1408            error!(
1409                "Failed to extract timestamps from record batch, table_id: {}, timestamp_index: {}",
1410                table_batch.table_id, batch.timestamp_index
1411            );
1412            continue;
1413        };
1414
1415        if timestamp_values.null_count() == 0 {
1416            timestamps.extend_from_slice(timestamp_values.values());
1417        } else {
1418            timestamps.extend(timestamp_values.iter().flatten());
1419        }
1420    }
1421    timestamps
1422}
1423
1424struct FlowNotification {
1425    table_id: TableId,
1426    timestamps: Vec<i64>,
1427}
1428
1429fn try_enqueue_flow_notification(
1430    tx: &mpsc::Sender<FlowNotification>,
1431    notification: FlowNotification,
1432) -> bool {
1433    match tx.try_send(notification) {
1434        Ok(()) => true,
1435        Err(mpsc::error::TrySendError::Full(notification)) => {
1436            FLOW_NOTIFICATION_DROPPED.with_label_values(&["full"]).inc();
1437            warn!(
1438                "Dropping flow notification because queue is full, table_id: {}, queue_capacity: {}",
1439                notification.table_id,
1440                tx.max_capacity()
1441            );
1442            false
1443        }
1444        Err(mpsc::error::TrySendError::Closed(notification)) => {
1445            FLOW_NOTIFICATION_DROPPED
1446                .with_label_values(&["closed"])
1447                .inc();
1448            error!(
1449                "Dropping flow notification because queue is closed, table_id: {}, queue_capacity: {}",
1450                notification.table_id,
1451                tx.max_capacity()
1452            );
1453            false
1454        }
1455    }
1456}
1457
1458fn enqueue_flow_notifications(table_batches: Vec<TableBatch>, tx: &mpsc::Sender<FlowNotification>) {
1459    for table_batch in table_batches {
1460        let timestamps = extract_timestamps(&table_batch);
1461        if timestamps.is_empty() {
1462            continue;
1463        }
1464        try_enqueue_flow_notification(
1465            tx,
1466            FlowNotification {
1467                table_id: table_batch.table_id,
1468                timestamps,
1469            },
1470        );
1471    }
1472}
1473
1474async fn handle_flow_notification(
1475    notification: FlowNotification,
1476    table_flownode_set_cache: TableFlownodeSetCacheRef,
1477    node_manager: NodeManagerRef,
1478) {
1479    let table_id = notification.table_id;
1480    let flownodes = match table_flownode_set_cache.get(table_id).await {
1481        Ok(Some(flownodes)) => flownodes,
1482        Ok(None) => return,
1483        Err(e) => {
1484            error!(e; "Failed to get flownodes for table id: {}", table_id);
1485            return;
1486        }
1487    };
1488    let peers = flownodes.values().cloned().collect::<HashSet<_>>();
1489
1490    for peer in peers {
1491        if let Err(e) = node_manager
1492            .flownode(&peer)
1493            .await
1494            .handle_mark_window_dirty(DirtyWindowRequests {
1495                requests: vec![DirtyWindowRequest {
1496                    table_id,
1497                    timestamps: notification.timestamps.clone(),
1498                    time_ranges: Vec::new(),
1499                }],
1500            })
1501            .await
1502        {
1503            error!(
1504                e;
1505                "Failed to mark timestamps as dirty, table_id: {}, peer_id: {}, peer_addr: {}",
1506                table_id,
1507                peer.id,
1508                peer.addr
1509            );
1510        }
1511    }
1512}
1513
1514fn start_flow_notification_worker(
1515    notification_rx: mpsc::Receiver<FlowNotification>,
1516    table_flownode_set_cache: TableFlownodeSetCacheRef,
1517    node_manager: NodeManagerRef,
1518) {
1519    common_runtime::spawn_global(async move {
1520        tokio_stream::wrappers::ReceiverStream::new(notification_rx)
1521            .for_each_concurrent(MAX_CONCURRENT_FLOW_NOTIFICATIONS, |notification| {
1522                let table_flownode_set_cache = table_flownode_set_cache.clone();
1523                let node_manager = node_manager.clone();
1524                handle_flow_notification(notification, table_flownode_set_cache, node_manager)
1525            })
1526            .await;
1527    });
1528}
1529
1530#[cfg(test)]
1531fn notify_flow_dirty_windows_after_flush(
1532    table_batches: Vec<TableBatch>,
1533    table_flownode_set_cache: TableFlownodeSetCacheRef,
1534    node_manager: NodeManagerRef,
1535) {
1536    let (tx, rx) = mpsc::channel(table_batches.len().max(1));
1537    start_flow_notification_worker(rx, table_flownode_set_cache, node_manager);
1538    enqueue_flow_notifications(table_batches, &tx);
1539}
1540
1541/// Flushes a batch of logical table rows by transforming them into the physical table format
1542/// and writing them to the appropriate datanode regions.
1543///
1544/// This function performs the end-to-end physical flush pipeline:
1545/// 1. Resolves the physical table metadata and column ID mapping.
1546/// 2. Fetches the physical table's partition rule.
1547/// 3. Transforms each logical table batch into the physical (sparse primary key) format.
1548/// 4. Concatenates all transformed batches into a single combined batch.
1549/// 5. Splits the combined batch by partition rule and sends region write requests
1550///    concurrently to the target datanodes.
1551pub async fn flush_batch_physical(
1552    table_batches: &[TableBatch],
1553    physical_table_name: &str,
1554    ctx: &QueryContextRef,
1555    partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
1556    node_manager: &(impl PhysicalFlushNodeRequester + ?Sized),
1557    catalog_manager: &(impl PhysicalFlushCatalogProvider + ?Sized),
1558) -> Result<usize> {
1559    // 1. Resolve the physical table and get column ID mapping
1560    let physical_table = {
1561        let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1562            .with_label_values(&["flush_physical_resolve_table"])
1563            .start_timer();
1564        catalog_manager
1565            .physical_table(
1566                ctx.current_catalog(),
1567                &ctx.current_schema(),
1568                physical_table_name,
1569                ctx.as_ref(),
1570            )
1571            .await?
1572            .with_context(|| error::InternalSnafu {
1573                err_msg: format!(
1574                    "Physical table '{}' not found during pending flush",
1575                    physical_table_name
1576                ),
1577            })?
1578    };
1579
1580    let physical_table_info = physical_table.table_info;
1581    let name_to_ids = physical_table
1582        .col_name_to_ids
1583        .with_context(|| error::InternalSnafu {
1584            err_msg: format!(
1585                "Physical table '{}' has no column IDs for pending flush",
1586                physical_table_name
1587            ),
1588        })?;
1589
1590    // 2. Get the physical table's partition rule (one lookup instead of N)
1591    let partition_rule = {
1592        let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1593            .with_label_values(&["flush_physical_fetch_partition_rule"])
1594            .start_timer();
1595        partition_manager
1596            .find_table_partition_rule(physical_table_info.as_ref())
1597            .await?
1598    };
1599    let partition_columns = partition_rule.partition_columns();
1600    let partition_columns_set: HashSet<&str> =
1601        partition_columns.iter().map(String::as_str).collect();
1602
1603    // 3. Transform each logical table batch into physical format
1604    let modified_batches =
1605        transform_logical_batches_to_physical(table_batches, &name_to_ids, &partition_columns_set)?;
1606
1607    // 4. Concatenate all modified batches (all share the same physical schema)
1608    let combined_batch = concat_modified_batches(&modified_batches)?;
1609
1610    // 5. Split by physical partition rule and send to regions
1611    let physical_table_id = physical_table_info.table_id();
1612    let planned_batches = plan_region_batches(
1613        combined_batch,
1614        physical_table_id,
1615        partition_rule.as_ref(),
1616        partition_columns,
1617    )?;
1618
1619    let resolved_batches = resolve_region_targets(planned_batches, partition_manager).await?;
1620    let region_writes = encode_region_write_requests(resolved_batches)?;
1621    flush_region_writes_concurrently(node_manager, region_writes).await
1622}
1623
1624/// Transforms logical table batches into physical format (sparse primary key encoding).
1625///
1626/// It identifies tag columns and essential columns (timestamp, value) for each logical batch
1627/// and applies sparse primary key modification.
1628fn transform_logical_batches_to_physical(
1629    table_batches: &[TableBatch],
1630    name_to_ids: &HashMap<String, u32>,
1631    partition_columns_set: &HashSet<&str>,
1632) -> Result<Vec<RecordBatch>> {
1633    let mut modified_batches: Vec<RecordBatch> =
1634        Vec::with_capacity(table_batches.iter().map(|b| b.batches.len()).sum());
1635
1636    let mut modify_elapsed = Duration::ZERO;
1637    let mut columns_taxonomy_elapsed = Duration::ZERO;
1638
1639    for table_batch in table_batches {
1640        let table_id = table_batch.table_id;
1641
1642        for batch in &table_batch.batches {
1643            let batch = &batch.batch;
1644            let batch_schema = batch.schema();
1645            let start = Instant::now();
1646            let (tag_columns, essential_col_indices) = columns_taxonomy(
1647                &batch_schema,
1648                &table_batch.table_name,
1649                name_to_ids,
1650                partition_columns_set,
1651            )?;
1652
1653            columns_taxonomy_elapsed += start.elapsed();
1654            if tag_columns.is_empty() && essential_col_indices.is_empty() {
1655                continue;
1656            }
1657
1658            let modified = {
1659                let start = Instant::now();
1660                // The schema of modified batch is: __primary_key, timestamp, value, other partition columns...
1661                let batch = modify_batch_sparse(
1662                    batch.clone(),
1663                    table_id,
1664                    &tag_columns,
1665                    &essential_col_indices,
1666                )?;
1667                modify_elapsed += start.elapsed();
1668                batch
1669            };
1670
1671            modified_batches.push(modified);
1672        }
1673    }
1674
1675    PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1676        .with_label_values(&["flush_physical_modify_batch"])
1677        .observe(modify_elapsed.as_secs_f64());
1678    PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1679        .with_label_values(&["flush_physical_columns_taxonomy"])
1680        .observe(columns_taxonomy_elapsed.as_secs_f64());
1681
1682    ensure!(
1683        !modified_batches.is_empty(),
1684        error::InternalSnafu {
1685            err_msg: "No batches can be transformed during pending flush",
1686        }
1687    );
1688    Ok(modified_batches)
1689}
1690
1691/// Concatenates all modified batches into a single large batch.
1692///
1693/// All modified batches share the same physical schema.
1694fn concat_modified_batches(modified_batches: &[RecordBatch]) -> Result<RecordBatch> {
1695    let combined_schema = modified_batches[0].schema();
1696    let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1697        .with_label_values(&["flush_physical_concat_all"])
1698        .start_timer();
1699    concat_batches(&combined_schema, modified_batches).context(error::ArrowSnafu)
1700}
1701
1702fn split_combined_batch_by_region(
1703    combined_batch: &RecordBatch,
1704    partition_rule: &dyn partition::partition::PartitionRule,
1705) -> Result<HashMap<u32, partition::partition::RegionMask>> {
1706    let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1707        .with_label_values(&["flush_physical_split_record_batch"])
1708        .start_timer();
1709    let map = partition_rule.split_record_batch(combined_batch)?;
1710    Ok(map)
1711}
1712
1713fn prepare_physical_region_routing_batch(
1714    combined_batch: RecordBatch,
1715    partition_columns: &[String],
1716) -> Result<RecordBatch> {
1717    if partition_columns.is_empty() {
1718        return Ok(combined_batch);
1719    }
1720    strip_partition_columns_from_batch(combined_batch)
1721}
1722
1723fn plan_region_batch(
1724    stripped_batch: &RecordBatch,
1725    physical_table_id: TableId,
1726    region_number: u32,
1727    mask: &partition::partition::RegionMask,
1728) -> Result<Option<PlannedRegionBatch>> {
1729    if mask.select_none() {
1730        return Ok(None);
1731    }
1732
1733    let region_batch = if mask.select_all() {
1734        stripped_batch.clone()
1735    } else {
1736        let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1737            .with_label_values(&["flush_physical_filter_record_batch"])
1738            .start_timer();
1739        filter_record_batch(stripped_batch, mask.array()).context(error::ArrowSnafu)?
1740    };
1741
1742    let row_count = region_batch.num_rows();
1743    if row_count == 0 {
1744        return Ok(None);
1745    }
1746
1747    Ok(Some(PlannedRegionBatch {
1748        region_id: RegionId::new(physical_table_id, region_number),
1749        batch: region_batch,
1750    }))
1751}
1752
1753fn plan_region_batches(
1754    combined_batch: RecordBatch,
1755    physical_table_id: TableId,
1756    partition_rule: &dyn partition::partition::PartitionRule,
1757    partition_columns: &[String],
1758) -> Result<Vec<PlannedRegionBatch>> {
1759    let region_masks = split_combined_batch_by_region(&combined_batch, partition_rule)?;
1760    let stripped_batch = prepare_physical_region_routing_batch(combined_batch, partition_columns)?;
1761
1762    let mut planned_batches = Vec::new();
1763    for (region_number, mask) in region_masks {
1764        if let Some(planned_batch) =
1765            plan_region_batch(&stripped_batch, physical_table_id, region_number, &mask)?
1766        {
1767            planned_batches.push(planned_batch);
1768        }
1769    }
1770
1771    Ok(planned_batches)
1772}
1773
1774async fn resolve_region_targets(
1775    planned_batches: Vec<PlannedRegionBatch>,
1776    partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
1777) -> Result<Vec<ResolvedRegionBatch>> {
1778    let mut resolved_batches = Vec::with_capacity(planned_batches.len());
1779    for planned in planned_batches {
1780        let datanode = {
1781            let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1782                .with_label_values(&["flush_physical_resolve_region_leader"])
1783                .start_timer();
1784            partition_manager
1785                .find_region_leader(planned.region_id)
1786                .await?
1787        };
1788
1789        resolved_batches.push(ResolvedRegionBatch { planned, datanode });
1790    }
1791
1792    Ok(resolved_batches)
1793}
1794
1795fn encode_region_write_requests(
1796    resolved_batches: Vec<ResolvedRegionBatch>,
1797) -> Result<Vec<FlushRegionWrite>> {
1798    let mut region_writes = Vec::with_capacity(resolved_batches.len());
1799    for resolved in resolved_batches {
1800        let region_id = resolved.planned.region_id;
1801        let (schema_bytes, data_header, payload) = {
1802            let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1803                .with_label_values(&["flush_physical_encode_ipc"])
1804                .start_timer();
1805            record_batch_to_ipc(resolved.planned.batch)?
1806        };
1807
1808        let request = RegionRequest {
1809            header: Some(RegionRequestHeader {
1810                tracing_context: TracingContext::from_current_span().to_w3c(),
1811                ..Default::default()
1812            }),
1813            body: Some(region_request::Body::BulkInsert(BulkInsertRequest {
1814                region_id: region_id.as_u64(),
1815                partition_expr_version: None,
1816                // Set aligned_schema_version to None so that datanode will check the batch schema again to see if any
1817                // column is missing.
1818                aligned_schema_version: None,
1819                body: Some(bulk_insert_request::Body::ArrowIpc(ArrowIpc {
1820                    schema: schema_bytes,
1821                    data_header,
1822                    payload,
1823                })),
1824            })),
1825        };
1826
1827        region_writes.push(FlushRegionWrite {
1828            datanode: resolved.datanode,
1829            request,
1830        });
1831    }
1832
1833    Ok(region_writes)
1834}
1835
1836fn notify_waiters(waiters: Vec<FlushWaiter>, result: Result<()>) {
1837    let shared_result = result.map_err(Arc::new);
1838    for waiter in waiters {
1839        let _ = waiter.response_tx.send(match &shared_result {
1840            Ok(()) => Ok(()),
1841            Err(error) => Err(Arc::clone(error)),
1842        });
1843        // waiter._permit is dropped here, releasing the inflight semaphore slot
1844    }
1845}
1846
1847fn record_batch_to_ipc(record_batch: RecordBatch) -> Result<(Bytes, Bytes, Bytes)> {
1848    let mut encoder = FlightEncoder::default();
1849    let schema = encoder.encode_schema(record_batch.schema().as_ref());
1850    let mut iter = encoder
1851        .encode(FlightMessage::RecordBatch(record_batch))
1852        .into_iter();
1853    let Some(flight_data) = iter.next() else {
1854        return Err(Error::Internal {
1855            err_msg: "Failed to encode empty flight data".to_string(),
1856        });
1857    };
1858    if iter.next().is_some() {
1859        return Err(Error::NotSupported {
1860            feat: "bulk insert RecordBatch with dictionary arrays".to_string(),
1861        });
1862    }
1863
1864    Ok((
1865        schema.data_header,
1866        flight_data.data_header,
1867        flight_data.data_body,
1868    ))
1869}
1870
1871#[cfg(test)]
1872mod tests {
1873    use std::any::Any;
1874    use std::collections::{HashMap, HashSet};
1875    use std::sync::atomic::{AtomicUsize, Ordering};
1876    use std::sync::{Arc, Mutex};
1877    use std::time::Duration;
1878
1879    use api::region::RegionResponse;
1880    use api::v1::flow::{DirtyWindowRequests, FlowRequest, FlowResponse};
1881    use api::v1::meta::Peer;
1882    use api::v1::region::{InsertRequests, RegionRequest, region_request};
1883    use api::v1::value::ValueData;
1884    use api::v1::{
1885        ColumnDataType, ColumnSchema, Row, RowInsertRequest, RowInsertRequests, Rows, SemanticType,
1886        Value,
1887    };
1888    use arrow::array::{BinaryArray, BooleanArray, StringArray, TimestampMillisecondArray};
1889    use arrow::datatypes::{DataType as ArrowDataType, Field, Schema as ArrowSchema};
1890    use arrow::record_batch::RecordBatch;
1891    use async_trait::async_trait;
1892    use catalog::error::Result as CatalogResult;
1893    use catalog::memory::MemoryCatalogManager;
1894    use common_meta::cache::{
1895        TableFlownodeSetCacheRef, new_table_flownode_set_cache, new_table_route_cache,
1896    };
1897    use common_meta::error::Result as MetaResult;
1898    use common_meta::instruction::{CacheIdent, CreateFlow};
1899    use common_meta::kv_backend::memory::MemoryKvBackend;
1900    use common_meta::kv_backend::{KvBackend, TxnService};
1901    use common_meta::node_manager::{
1902        Datanode, DatanodeManager, DatanodeRef, Flownode, FlownodeManager, FlownodeRef,
1903        NodeManagerRef,
1904    };
1905    use common_meta::rpc::store::{
1906        BatchDeleteRequest, BatchDeleteResponse, BatchGetRequest, BatchGetResponse,
1907        BatchPutRequest, BatchPutResponse, DeleteRangeRequest, DeleteRangeResponse, PutRequest,
1908        PutResponse, RangeRequest, RangeResponse,
1909    };
1910    use common_query::request::QueryRequest;
1911    use common_recordbatch::SendableRecordBatchStream;
1912    use dashmap::DashMap;
1913    use datatypes::schema::{ColumnSchema as DtColumnSchema, Schema as DtSchema};
1914    use moka::future::CacheBuilder;
1915    use partition::cache::new_partition_info_cache;
1916    use partition::error::Result as PartitionResult;
1917    use partition::manager::PartitionRuleManager;
1918    use partition::partition::{PartitionRule, PartitionRuleRef, RegionMask};
1919    use smallvec::SmallVec;
1920    use snafu::ResultExt;
1921    use store_api::storage::RegionId;
1922    use table::metadata::TableId;
1923    use table::test_util::table_info::test_table_info;
1924    use tokio::sync::{Notify, Semaphore, broadcast, mpsc, oneshot};
1925    use tokio::time::{advance, sleep};
1926
1927    use super::{
1928        BatchKey, Error, FlushBatch, FlushRegionWrite, FlushWaiter, PendingBatch,
1929        PendingRowsBatcher, PendingWorker, PhysicalFlushCatalogProvider,
1930        PhysicalFlushNodeRequester, PhysicalFlushPartitionProvider, PhysicalTableMetadata,
1931        PlannedRegionBatch, RecordBatchWithTsIdx, ResolvedRegionBatch, TableBatch, WorkerCommand,
1932        columns_taxonomy, drain_batch, encode_region_write_requests, extract_timestamps,
1933        flush_batch, flush_batch_physical, flush_region_writes_concurrently, greptime_timestamp,
1934        notify_flow_dirty_windows_after_flush, plan_region_batches, remove_worker_if_same_channel,
1935        should_close_worker_on_idle_timeout, should_dispatch_concurrently,
1936        start_flow_notification_worker, start_worker, strip_partition_columns_from_batch,
1937        transform_logical_batches_to_physical, try_enqueue_flow_notification,
1938    };
1939    use crate::error;
1940    use crate::metrics::FLOW_NOTIFICATION_DROPPED;
1941    use crate::prom_row_builder::rows_to_aligned_record_batch;
1942
1943    fn mock_rows(row_count: usize, schema_name: &str) -> Rows {
1944        Rows {
1945            schema: vec![ColumnSchema {
1946                column_name: schema_name.to_string(),
1947                ..Default::default()
1948            }],
1949            rows: (0..row_count).map(|_| Row { values: vec![] }).collect(),
1950        }
1951    }
1952
1953    fn mock_tag_batch(tag_name: &str, tag_value: &str, ts: i64, val: f64) -> RecordBatch {
1954        let schema = Arc::new(ArrowSchema::new(vec![
1955            Field::new(
1956                "greptime_timestamp",
1957                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
1958                false,
1959            ),
1960            Field::new("greptime_value", ArrowDataType::Float64, true),
1961            Field::new(tag_name, ArrowDataType::Utf8, true),
1962        ]));
1963
1964        RecordBatch::try_new(
1965            schema,
1966            vec![
1967                Arc::new(TimestampMillisecondArray::from(vec![ts])),
1968                Arc::new(arrow::array::Float64Array::from(vec![val])),
1969                Arc::new(StringArray::from(vec![tag_value])),
1970            ],
1971        )
1972        .unwrap()
1973    }
1974
1975    fn mock_aligned_tag_batch(
1976        tag_name: &str,
1977        tag_value: &str,
1978        ts: i64,
1979        val: f64,
1980    ) -> RecordBatchWithTsIdx {
1981        RecordBatchWithTsIdx::try_new(mock_tag_batch(tag_name, tag_value, ts, val), 0).unwrap()
1982    }
1983
1984    fn mock_timestamp_batch(timestamps: Vec<Option<i64>>) -> RecordBatchWithTsIdx {
1985        let batch = RecordBatch::try_new(
1986            Arc::new(ArrowSchema::new(vec![Field::new(
1987                greptime_timestamp(),
1988                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
1989                true,
1990            )])),
1991            vec![Arc::new(TimestampMillisecondArray::from(timestamps))],
1992        )
1993        .unwrap();
1994        RecordBatchWithTsIdx::try_new(batch, 0).unwrap()
1995    }
1996
1997    #[test]
1998    fn test_extract_timestamps_appends_non_null_batches_in_order() {
1999        let table_batch = TableBatch {
2000            table_name: "cpu".to_string(),
2001            table_id: 42,
2002            batches: vec![
2003                mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0),
2004                mock_aligned_tag_batch("tag1", "host-1", 2000, 2.0),
2005            ],
2006            row_count: 2,
2007        };
2008
2009        assert_eq!(vec![1000, 2000], extract_timestamps(&table_batch));
2010    }
2011
2012    #[test]
2013    fn test_extract_timestamps_omits_nulls_and_retains_order() {
2014        let table_batch = TableBatch {
2015            table_name: "cpu".to_string(),
2016            table_id: 42,
2017            batches: vec![
2018                mock_timestamp_batch(vec![Some(1000), None, Some(3000)]),
2019                mock_timestamp_batch(vec![None, Some(5000)]),
2020            ],
2021            row_count: 5,
2022        };
2023
2024        assert_eq!(vec![1000, 3000, 5000], extract_timestamps(&table_batch));
2025    }
2026
2027    #[test]
2028    fn test_record_batch_with_ts_idx_rejects_out_of_bounds_index() {
2029        let batch = mock_tag_batch("tag1", "host-1", 1000, 1.0);
2030
2031        assert!(RecordBatchWithTsIdx::try_new(batch, 3).is_err());
2032    }
2033
2034    #[test]
2035    fn test_record_batch_with_ts_idx_rejects_non_timestamp_column() {
2036        let batch = mock_tag_batch("tag1", "host-1", 1000, 1.0);
2037
2038        assert!(RecordBatchWithTsIdx::try_new(batch, 1).is_err());
2039    }
2040
2041    #[test]
2042    fn test_extract_timestamps_supports_per_batch_timestamp_indices() {
2043        let timestamp_first = RecordBatch::try_new(
2044            Arc::new(ArrowSchema::new(vec![
2045                Field::new(
2046                    "ts",
2047                    ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
2048                    false,
2049                ),
2050                Field::new("host", ArrowDataType::Utf8, true),
2051            ])),
2052            vec![
2053                Arc::new(TimestampMillisecondArray::from(vec![1000, 2000])),
2054                Arc::new(StringArray::from(vec!["host-1", "host-2"])),
2055            ],
2056        )
2057        .unwrap();
2058        let timestamp_second = RecordBatch::try_new(
2059            Arc::new(ArrowSchema::new(vec![
2060                Field::new("host", ArrowDataType::Utf8, true),
2061                Field::new(
2062                    "timestamp",
2063                    ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
2064                    false,
2065                ),
2066            ])),
2067            vec![
2068                Arc::new(StringArray::from(vec!["host-3", "host-4"])),
2069                Arc::new(TimestampMillisecondArray::from(vec![3000, 4000])),
2070            ],
2071        )
2072        .unwrap();
2073        let table_batch = TableBatch {
2074            table_name: "cpu".to_string(),
2075            table_id: 42,
2076            batches: vec![
2077                RecordBatchWithTsIdx::try_new(timestamp_first, 0).unwrap(),
2078                RecordBatchWithTsIdx::try_new(timestamp_second, 1).unwrap(),
2079            ],
2080            row_count: 4,
2081        };
2082
2083        assert_eq!(
2084            vec![1000, 2000, 3000, 4000],
2085            extract_timestamps(&table_batch)
2086        );
2087    }
2088
2089    #[test]
2090    fn test_extract_timestamps_uses_aligned_custom_timestamp_index() {
2091        let rows = Rows {
2092            schema: vec![
2093                ColumnSchema {
2094                    column_name: greptime_timestamp().to_string(),
2095                    datatype: ColumnDataType::TimestampMillisecond as i32,
2096                    semantic_type: SemanticType::Timestamp as i32,
2097                    ..Default::default()
2098                },
2099                ColumnSchema {
2100                    column_name: "host".to_string(),
2101                    datatype: ColumnDataType::String as i32,
2102                    semantic_type: SemanticType::Tag as i32,
2103                    ..Default::default()
2104                },
2105                ColumnSchema {
2106                    column_name: "greptime_value".to_string(),
2107                    datatype: ColumnDataType::Float64 as i32,
2108                    semantic_type: SemanticType::Field as i32,
2109                    ..Default::default()
2110                },
2111            ],
2112            rows: vec![
2113                Row {
2114                    values: vec![
2115                        Value {
2116                            value_data: Some(ValueData::TimestampMillisecondValue(1000)),
2117                        },
2118                        Value {
2119                            value_data: Some(ValueData::StringValue("host-1".to_string())),
2120                        },
2121                        Value {
2122                            value_data: Some(ValueData::F64Value(1.0)),
2123                        },
2124                    ],
2125                },
2126                Row {
2127                    values: vec![
2128                        Value {
2129                            value_data: Some(ValueData::TimestampMillisecondValue(2000)),
2130                        },
2131                        Value {
2132                            value_data: Some(ValueData::StringValue("host-2".to_string())),
2133                        },
2134                        Value {
2135                            value_data: Some(ValueData::F64Value(2.0)),
2136                        },
2137                    ],
2138                },
2139            ],
2140        };
2141        let target_schema = ArrowSchema::new(vec![
2142            Field::new("host", ArrowDataType::Utf8, true),
2143            Field::new(
2144                "timestamp",
2145                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
2146                false,
2147            ),
2148            Field::new("greptime_value", ArrowDataType::Float64, true),
2149        ]);
2150        let batch = rows_to_aligned_record_batch(&rows, &target_schema).unwrap();
2151        assert_eq!(1, batch.timestamp_index);
2152        let table_batch = TableBatch {
2153            table_name: "cpu".to_string(),
2154            table_id: 42,
2155            row_count: batch.batch.num_rows(),
2156            batches: vec![batch],
2157        };
2158
2159        assert_eq!(vec![1000, 2000], extract_timestamps(&table_batch));
2160    }
2161
2162    #[test]
2163    fn test_flow_notification_queue_drops_when_full() {
2164        let (tx, mut rx) = mpsc::channel(1);
2165        let notification = |table_id| super::FlowNotification {
2166            table_id,
2167            timestamps: vec![table_id as i64],
2168        };
2169        let dropped = FLOW_NOTIFICATION_DROPPED.with_label_values(&["full"]);
2170        let dropped_before = dropped.get();
2171
2172        assert!(try_enqueue_flow_notification(&tx, notification(1)));
2173        assert!(!try_enqueue_flow_notification(&tx, notification(2)));
2174
2175        assert_eq!(1, rx.try_recv().unwrap().table_id);
2176        assert_eq!(dropped_before + 1, dropped.get());
2177    }
2178
2179    fn mock_physical_table_metadata(table_id: TableId) -> PhysicalTableMetadata {
2180        let schema = Arc::new(
2181            DtSchema::try_new(vec![
2182                DtColumnSchema::new(
2183                    "__primary_key",
2184                    datatypes::prelude::ConcreteDataType::binary_datatype(),
2185                    false,
2186                ),
2187                DtColumnSchema::new(
2188                    "greptime_timestamp",
2189                    datatypes::prelude::ConcreteDataType::timestamp_millisecond_datatype(),
2190                    false,
2191                ),
2192                DtColumnSchema::new(
2193                    "greptime_value",
2194                    datatypes::prelude::ConcreteDataType::float64_datatype(),
2195                    true,
2196                ),
2197                DtColumnSchema::new(
2198                    "tag1",
2199                    datatypes::prelude::ConcreteDataType::string_datatype(),
2200                    true,
2201                ),
2202            ])
2203            .unwrap(),
2204        );
2205        let mut table_info = test_table_info(table_id, "phy", "public", "greptime", schema);
2206        table_info.meta.column_ids = vec![0, 1, 2, 3];
2207
2208        PhysicalTableMetadata {
2209            table_info: Arc::new(table_info),
2210            col_name_to_ids: Some(HashMap::from([("tag1".to_string(), 3)])),
2211        }
2212    }
2213
2214    struct MockFlushCatalogProvider {
2215        table: Option<PhysicalTableMetadata>,
2216    }
2217
2218    #[async_trait]
2219    impl PhysicalFlushCatalogProvider for MockFlushCatalogProvider {
2220        async fn physical_table(
2221            &self,
2222            _catalog: &str,
2223            _schema: &str,
2224            _table_name: &str,
2225            _query_ctx: &session::context::QueryContext,
2226        ) -> CatalogResult<Option<PhysicalTableMetadata>> {
2227            Ok(self.table.clone())
2228        }
2229    }
2230
2231    struct SingleRegionPartitionRule;
2232
2233    impl PartitionRule for SingleRegionPartitionRule {
2234        fn as_any(&self) -> &dyn std::any::Any {
2235            self
2236        }
2237
2238        fn partition_columns(&self) -> &[String] {
2239            &[]
2240        }
2241
2242        fn find_region(
2243            &self,
2244            _values: &[datatypes::prelude::Value],
2245        ) -> partition::error::Result<store_api::storage::RegionNumber> {
2246            unimplemented!()
2247        }
2248
2249        fn split_record_batch(
2250            &self,
2251            record_batch: &RecordBatch,
2252        ) -> partition::error::Result<HashMap<store_api::storage::RegionNumber, RegionMask>>
2253        {
2254            Ok(HashMap::from([(
2255                1,
2256                RegionMask::new(
2257                    arrow::array::BooleanArray::from(vec![true; record_batch.num_rows()]),
2258                    record_batch.num_rows(),
2259                ),
2260            )]))
2261        }
2262    }
2263
2264    struct TwoRegionPartitionRule {
2265        partition_columns: Vec<String>,
2266    }
2267
2268    impl PartitionRule for TwoRegionPartitionRule {
2269        fn as_any(&self) -> &dyn std::any::Any {
2270            self
2271        }
2272
2273        fn partition_columns(&self) -> &[String] {
2274            &self.partition_columns
2275        }
2276
2277        fn find_region(
2278            &self,
2279            _values: &[datatypes::prelude::Value],
2280        ) -> partition::error::Result<store_api::storage::RegionNumber> {
2281            unimplemented!()
2282        }
2283
2284        fn split_record_batch(
2285            &self,
2286            _record_batch: &RecordBatch,
2287        ) -> partition::error::Result<HashMap<store_api::storage::RegionNumber, RegionMask>>
2288        {
2289            Ok(HashMap::from([
2290                (1, RegionMask::new(BooleanArray::from(vec![true, false]), 1)),
2291                (2, RegionMask::new(BooleanArray::from(vec![false, true]), 1)),
2292                (
2293                    3,
2294                    RegionMask::new(BooleanArray::from(vec![false, false]), 0),
2295                ),
2296            ]))
2297        }
2298    }
2299
2300    struct MockFlushPartitionProvider {
2301        partition_rule_calls: Arc<AtomicUsize>,
2302        region_leader_calls: Arc<AtomicUsize>,
2303    }
2304
2305    #[async_trait]
2306    impl PhysicalFlushPartitionProvider for MockFlushPartitionProvider {
2307        async fn find_table_partition_rule(
2308            &self,
2309            _table_info: &table::metadata::TableInfo,
2310        ) -> PartitionResult<PartitionRuleRef> {
2311            self.partition_rule_calls.fetch_add(1, Ordering::SeqCst);
2312            Ok(Arc::new(SingleRegionPartitionRule))
2313        }
2314
2315        async fn find_region_leader(&self, _region_id: RegionId) -> error::Result<Peer> {
2316            self.region_leader_calls.fetch_add(1, Ordering::SeqCst);
2317            Ok(Peer {
2318                id: 1,
2319                addr: "node-1".to_string(),
2320            })
2321        }
2322    }
2323
2324    #[derive(Default)]
2325    struct MockFlushNodeRequester {
2326        writes: Arc<AtomicUsize>,
2327        fail: bool,
2328    }
2329
2330    #[async_trait]
2331    impl PhysicalFlushNodeRequester for MockFlushNodeRequester {
2332        async fn handle(
2333            &self,
2334            _peer: &Peer,
2335            _request: RegionRequest,
2336        ) -> error::Result<RegionResponse> {
2337            self.writes.fetch_add(1, Ordering::SeqCst);
2338            if self.fail {
2339                return Err(Error::Internal {
2340                    err_msg: "physical write failed".to_string(),
2341                });
2342            }
2343            Ok(RegionResponse::new(0))
2344        }
2345    }
2346
2347    #[test]
2348    fn test_collect_non_empty_table_rows_filters_empty_payloads() {
2349        let requests = RowInsertRequests {
2350            inserts: vec![
2351                RowInsertRequest {
2352                    table_name: "cpu".to_string(),
2353                    rows: Some(mock_rows(2, "host")),
2354                },
2355                RowInsertRequest {
2356                    table_name: "mem".to_string(),
2357                    rows: Some(mock_rows(0, "host")),
2358                },
2359                RowInsertRequest {
2360                    table_name: "disk".to_string(),
2361                    rows: None,
2362                },
2363            ],
2364        };
2365
2366        let (table_rows, total_rows) = PendingRowsBatcher::collect_non_empty_table_rows(requests);
2367
2368        assert_eq!(2, total_rows);
2369        assert_eq!(1, table_rows.len());
2370        assert_eq!("cpu", table_rows[0].0);
2371        assert_eq!(2, table_rows[0].1.rows.len());
2372    }
2373
2374    #[test]
2375    fn test_drain_batch_takes_initialized_pending_batch_from_option() {
2376        let ctx = session::context::QueryContext::arc();
2377        let (response_tx, _response_rx) = oneshot::channel();
2378        let permit = Arc::new(Semaphore::new(1)).try_acquire_owned().unwrap();
2379        let mut batch = Some(PendingBatch {
2380            tables: HashMap::from([(
2381                42,
2382                TableBatch {
2383                    table_name: "cpu".to_string(),
2384                    table_id: 42,
2385                    batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
2386                    row_count: 1,
2387                },
2388            )]),
2389            total_row_count: 1,
2390            db_string: ctx.get_db_string(),
2391            ctx: ctx.clone(),
2392            waiters: vec![FlushWaiter {
2393                response_tx,
2394                _permit: permit,
2395            }],
2396        });
2397
2398        let flush = drain_batch(&mut batch).unwrap();
2399
2400        assert!(batch.is_none());
2401        assert_eq!(1, flush.total_row_count);
2402        assert_eq!(1, flush.table_batches.len());
2403        assert_eq!(ctx.get_db_string(), flush.db_string);
2404        assert_eq!(ctx.current_catalog(), flush.ctx.current_catalog());
2405    }
2406
2407    #[test]
2408    fn test_pending_batch_keeps_same_name_batches_with_distinct_table_ids() {
2409        let ctx = session::context::QueryContext::arc();
2410        let mut pending_batch = PendingBatch::new(ctx);
2411
2412        pending_batch.add_table_batch(
2413            "cpu".to_string(),
2414            42,
2415            mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0),
2416        );
2417        pending_batch.add_table_batch(
2418            "cpu".to_string(),
2419            43,
2420            mock_aligned_tag_batch("tag1", "host-1", 2000, 2.0),
2421        );
2422
2423        assert_eq!(2, pending_batch.tables.len());
2424        assert_eq!(42, pending_batch.tables[&42].table_id);
2425        assert_eq!(43, pending_batch.tables[&43].table_id);
2426        assert_eq!("cpu", pending_batch.tables[&42].table_name);
2427        assert_eq!("cpu", pending_batch.tables[&43].table_name);
2428    }
2429
2430    #[derive(Clone)]
2431    struct ConcurrentMockDatanode {
2432        delay: Duration,
2433        inflight: Arc<AtomicUsize>,
2434        max_inflight: Arc<AtomicUsize>,
2435    }
2436
2437    #[async_trait]
2438    impl Datanode for ConcurrentMockDatanode {
2439        async fn handle(&self, _request: RegionRequest) -> MetaResult<RegionResponse> {
2440            let now = self.inflight.fetch_add(1, Ordering::SeqCst) + 1;
2441            loop {
2442                let max = self.max_inflight.load(Ordering::SeqCst);
2443                if now <= max {
2444                    break;
2445                }
2446                if self
2447                    .max_inflight
2448                    .compare_exchange(max, now, Ordering::SeqCst, Ordering::SeqCst)
2449                    .is_ok()
2450                {
2451                    break;
2452                }
2453            }
2454
2455            sleep(self.delay).await;
2456            self.inflight.fetch_sub(1, Ordering::SeqCst);
2457            Ok(RegionResponse::new(0))
2458        }
2459
2460        async fn handle_query(
2461            &self,
2462            _request: QueryRequest,
2463        ) -> MetaResult<SendableRecordBatchStream> {
2464            unimplemented!()
2465        }
2466    }
2467
2468    #[derive(Clone)]
2469    struct ConcurrentMockNodeManager {
2470        datanodes: Arc<HashMap<u64, DatanodeRef>>,
2471    }
2472
2473    #[async_trait]
2474    impl DatanodeManager for ConcurrentMockNodeManager {
2475        async fn datanode(&self, node: &Peer) -> DatanodeRef {
2476            self.datanodes
2477                .get(&node.id)
2478                .expect("datanode not found")
2479                .clone()
2480        }
2481    }
2482
2483    struct NoopFlownode;
2484
2485    #[async_trait]
2486    impl Flownode for NoopFlownode {
2487        async fn handle(&self, _request: FlowRequest) -> MetaResult<FlowResponse> {
2488            unimplemented!()
2489        }
2490
2491        async fn handle_inserts(&self, _request: InsertRequests) -> MetaResult<FlowResponse> {
2492            unimplemented!()
2493        }
2494
2495        async fn handle_mark_window_dirty(
2496            &self,
2497            _req: DirtyWindowRequests,
2498        ) -> MetaResult<FlowResponse> {
2499            unimplemented!()
2500        }
2501    }
2502
2503    #[async_trait]
2504    impl FlownodeManager for ConcurrentMockNodeManager {
2505        async fn flownode(&self, _node: &Peer) -> FlownodeRef {
2506            Arc::new(NoopFlownode)
2507        }
2508    }
2509
2510    struct RecordingFlownode {
2511        requests_tx: mpsc::UnboundedSender<DirtyWindowRequests>,
2512    }
2513
2514    #[async_trait]
2515    impl Flownode for RecordingFlownode {
2516        async fn handle(&self, _request: FlowRequest) -> MetaResult<FlowResponse> {
2517            unimplemented!()
2518        }
2519
2520        async fn handle_inserts(&self, _request: InsertRequests) -> MetaResult<FlowResponse> {
2521            unimplemented!()
2522        }
2523
2524        async fn handle_mark_window_dirty(
2525            &self,
2526            req: DirtyWindowRequests,
2527        ) -> MetaResult<FlowResponse> {
2528            self.requests_tx.send(req).unwrap();
2529            Ok(FlowResponse::default())
2530        }
2531    }
2532
2533    struct FlowNotificationMockNodeManager {
2534        flownode: FlownodeRef,
2535    }
2536
2537    #[async_trait]
2538    impl DatanodeManager for FlowNotificationMockNodeManager {
2539        async fn datanode(&self, _node: &Peer) -> DatanodeRef {
2540            unimplemented!()
2541        }
2542    }
2543
2544    #[async_trait]
2545    impl FlownodeManager for FlowNotificationMockNodeManager {
2546        async fn flownode(&self, _node: &Peer) -> FlownodeRef {
2547            self.flownode.clone()
2548        }
2549    }
2550
2551    async fn mock_table_flownode_cache(table_id: TableId, peer: Peer) -> TableFlownodeSetCacheRef {
2552        let cache = Arc::new(new_table_flownode_set_cache(
2553            "test".to_string(),
2554            CacheBuilder::new(1).build(),
2555            Arc::new(MemoryKvBackend::default()),
2556        ));
2557        cache
2558            .invalidate(&[CacheIdent::CreateFlow(CreateFlow {
2559                flow_id: 1,
2560                source_table_ids: vec![table_id],
2561                partition_to_peer_mapping: vec![(0, peer.clone()), (1, peer)],
2562            })])
2563            .await
2564            .unwrap();
2565        cache
2566    }
2567
2568    fn mock_flow_notification_sender(
2569        cache: TableFlownodeSetCacheRef,
2570        node_manager: NodeManagerRef,
2571    ) -> mpsc::Sender<super::FlowNotification> {
2572        let (tx, rx) = mpsc::channel(16);
2573        start_flow_notification_worker(rx, cache, node_manager);
2574        tx
2575    }
2576
2577    struct BlockingRangeKvBackend {
2578        range_started: Mutex<Option<oneshot::Sender<()>>>,
2579        range_release: Arc<Notify>,
2580    }
2581
2582    impl TxnService for BlockingRangeKvBackend {
2583        type Error = common_meta::error::Error;
2584    }
2585
2586    #[async_trait]
2587    impl KvBackend for BlockingRangeKvBackend {
2588        fn name(&self) -> &str {
2589            "blocking_range"
2590        }
2591
2592        fn as_any(&self) -> &dyn Any {
2593            self
2594        }
2595
2596        async fn range(&self, _req: RangeRequest) -> MetaResult<RangeResponse> {
2597            let range_started = self.range_started.lock().unwrap().take();
2598            if let Some(range_started) = range_started {
2599                let _ = range_started.send(());
2600                self.range_release.notified().await;
2601            }
2602            Ok(RangeResponse {
2603                kvs: Vec::new(),
2604                more: false,
2605            })
2606        }
2607
2608        async fn put(&self, _req: PutRequest) -> MetaResult<PutResponse> {
2609            unimplemented!()
2610        }
2611
2612        async fn batch_put(&self, _req: BatchPutRequest) -> MetaResult<BatchPutResponse> {
2613            unimplemented!()
2614        }
2615
2616        async fn batch_get(&self, _req: BatchGetRequest) -> MetaResult<BatchGetResponse> {
2617            unimplemented!()
2618        }
2619
2620        async fn delete_range(&self, _req: DeleteRangeRequest) -> MetaResult<DeleteRangeResponse> {
2621            unimplemented!()
2622        }
2623
2624        async fn batch_delete(&self, _req: BatchDeleteRequest) -> MetaResult<BatchDeleteResponse> {
2625            unimplemented!()
2626        }
2627    }
2628
2629    #[tokio::test]
2630    async fn test_flow_notifications_do_not_block_on_previous_table_cache_lookup() {
2631        let blocked_table_id = 41;
2632        let cached_table_id = 42;
2633        let peer = Peer {
2634            id: 7,
2635            addr: "flow-7".to_string(),
2636        };
2637        let (range_started_tx, range_started_rx) = oneshot::channel();
2638        let range_release = Arc::new(Notify::new());
2639        let cache = Arc::new(new_table_flownode_set_cache(
2640            "test".to_string(),
2641            CacheBuilder::new(2).build(),
2642            Arc::new(BlockingRangeKvBackend {
2643                range_started: Mutex::new(Some(range_started_tx)),
2644                range_release: range_release.clone(),
2645            }),
2646        ));
2647        cache
2648            .invalidate(&[CacheIdent::CreateFlow(CreateFlow {
2649                flow_id: 1,
2650                source_table_ids: vec![cached_table_id],
2651                partition_to_peer_mapping: vec![(0, peer)],
2652            })])
2653            .await
2654            .unwrap();
2655        let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
2656        let _requests_tx = requests_tx.clone();
2657        let node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
2658            flownode: Arc::new(RecordingFlownode { requests_tx }),
2659        });
2660        let table_batches = vec![
2661            TableBatch {
2662                table_name: "blocked".to_string(),
2663                table_id: blocked_table_id,
2664                batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
2665                row_count: 1,
2666            },
2667            TableBatch {
2668                table_name: "cached".to_string(),
2669                table_id: cached_table_id,
2670                batches: vec![mock_aligned_tag_batch("tag1", "host-2", 2000, 2.0)],
2671                row_count: 1,
2672            },
2673        ];
2674
2675        notify_flow_dirty_windows_after_flush(table_batches, cache, node_manager);
2676
2677        tokio::time::timeout(Duration::from_secs(1), range_started_rx)
2678            .await
2679            .unwrap()
2680            .unwrap();
2681        let requests = tokio::time::timeout(Duration::from_secs(1), requests_rx.recv())
2682            .await
2683            .unwrap()
2684            .unwrap();
2685        assert_eq!(cached_table_id, requests.requests[0].table_id);
2686        range_release.notify_one();
2687    }
2688
2689    #[tokio::test]
2690    async fn test_successful_flush_notifies_flownode_with_logical_table_timestamps() {
2691        let table_id = 42;
2692        let peer = Peer {
2693            id: 7,
2694            addr: "flow-7".to_string(),
2695        };
2696        let cache = mock_table_flownode_cache(table_id, peer).await;
2697        let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
2698        let _requests_tx = requests_tx.clone();
2699        let node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
2700            flownode: Arc::new(RecordingFlownode { requests_tx }),
2701        });
2702        let table_batches = vec![TableBatch {
2703            table_name: "cpu".to_string(),
2704            table_id,
2705            batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
2706            row_count: 1,
2707        }];
2708
2709        notify_flow_dirty_windows_after_flush(table_batches, cache, node_manager);
2710
2711        let requests = tokio::time::timeout(Duration::from_secs(1), requests_rx.recv())
2712            .await
2713            .unwrap()
2714            .unwrap();
2715        assert_eq!(
2716            vec![api::v1::flow::DirtyWindowRequest {
2717                table_id,
2718                timestamps: vec![1000],
2719                time_ranges: Vec::new(),
2720            }],
2721            requests.requests
2722        );
2723        assert!(
2724            tokio::time::timeout(Duration::from_millis(50), requests_rx.recv())
2725                .await
2726                .is_err()
2727        );
2728    }
2729
2730    #[tokio::test]
2731    async fn test_successful_flush_coalesces_logical_batches_per_flownode() {
2732        let table_id = 42;
2733        let peer = Peer {
2734            id: 7,
2735            addr: "flow-7".to_string(),
2736        };
2737        let cache = mock_table_flownode_cache(table_id, peer).await;
2738        let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
2739        let _requests_tx = requests_tx.clone();
2740        let node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
2741            flownode: Arc::new(RecordingFlownode { requests_tx }),
2742        });
2743        let table_batches = vec![TableBatch {
2744            table_name: "cpu".to_string(),
2745            table_id,
2746            batches: vec![
2747                mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0),
2748                mock_aligned_tag_batch("tag1", "host-1", 2000, 2.0),
2749            ],
2750            row_count: 2,
2751        }];
2752
2753        notify_flow_dirty_windows_after_flush(table_batches, cache, node_manager);
2754
2755        let requests = tokio::time::timeout(Duration::from_secs(1), requests_rx.recv())
2756            .await
2757            .unwrap()
2758            .unwrap();
2759        assert_eq!(
2760            vec![api::v1::flow::DirtyWindowRequest {
2761                table_id,
2762                timestamps: vec![1000, 2000],
2763                time_ranges: Vec::new(),
2764            }],
2765            requests.requests
2766        );
2767        assert!(
2768            tokio::time::timeout(Duration::from_millis(50), requests_rx.recv())
2769                .await
2770                .is_err()
2771        );
2772    }
2773
2774    #[tokio::test]
2775    async fn test_flush_batch_notifies_flownode_after_successful_physical_write() {
2776        let table_id = 42;
2777        let peer = Peer {
2778            id: 7,
2779            addr: "flow-7".to_string(),
2780        };
2781        let cache = mock_table_flownode_cache(table_id, peer).await;
2782        let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
2783        let _requests_tx = requests_tx.clone();
2784        let flow_node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
2785            flownode: Arc::new(RecordingFlownode { requests_tx }),
2786        });
2787        let flow_notification_tx = mock_flow_notification_sender(cache, flow_node_manager.clone());
2788        let ctx = session::context::QueryContext::arc();
2789        let table_batches = vec![TableBatch {
2790            table_name: "cpu".to_string(),
2791            table_id,
2792            batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
2793            row_count: 1,
2794        }];
2795        let writes = Arc::new(AtomicUsize::new(0));
2796
2797        flush_batch(
2798            FlushBatch {
2799                table_batches,
2800                total_row_count: 1,
2801                db_string: ctx.get_db_string(),
2802                ctx,
2803                waiters: Vec::new(),
2804            },
2805            &MockFlushPartitionProvider {
2806                partition_rule_calls: Arc::new(AtomicUsize::new(0)),
2807                region_leader_calls: Arc::new(AtomicUsize::new(0)),
2808            },
2809            &MockFlushNodeRequester {
2810                writes: writes.clone(),
2811                fail: false,
2812            },
2813            &MockFlushCatalogProvider {
2814                table: Some(mock_physical_table_metadata(1024)),
2815            },
2816            flow_notification_tx,
2817        )
2818        .await;
2819
2820        assert_eq!(1, writes.load(Ordering::SeqCst));
2821        let requests = tokio::time::timeout(Duration::from_secs(1), requests_rx.recv())
2822            .await
2823            .unwrap()
2824            .unwrap();
2825        assert_eq!(
2826            vec![api::v1::flow::DirtyWindowRequest {
2827                table_id,
2828                timestamps: vec![1000],
2829                time_ranges: Vec::new(),
2830            }],
2831            requests.requests
2832        );
2833    }
2834
2835    #[tokio::test]
2836    async fn test_flush_batch_does_not_notify_flownode_after_physical_write_error() {
2837        let table_id = 42;
2838        let peer = Peer {
2839            id: 7,
2840            addr: "flow-7".to_string(),
2841        };
2842        let cache = mock_table_flownode_cache(table_id, peer).await;
2843        let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
2844        let _requests_tx = requests_tx.clone();
2845        let flow_node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
2846            flownode: Arc::new(RecordingFlownode { requests_tx }),
2847        });
2848        let flow_notification_tx = mock_flow_notification_sender(cache, flow_node_manager.clone());
2849        let ctx = session::context::QueryContext::arc();
2850        let table_batches = vec![TableBatch {
2851            table_name: "cpu".to_string(),
2852            table_id,
2853            batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
2854            row_count: 1,
2855        }];
2856        let writes = Arc::new(AtomicUsize::new(0));
2857
2858        flush_batch(
2859            FlushBatch {
2860                table_batches,
2861                total_row_count: 1,
2862                db_string: ctx.get_db_string(),
2863                ctx,
2864                waiters: Vec::new(),
2865            },
2866            &MockFlushPartitionProvider {
2867                partition_rule_calls: Arc::new(AtomicUsize::new(0)),
2868                region_leader_calls: Arc::new(AtomicUsize::new(0)),
2869            },
2870            &MockFlushNodeRequester {
2871                writes: writes.clone(),
2872                fail: true,
2873            },
2874            &MockFlushCatalogProvider {
2875                table: Some(mock_physical_table_metadata(1024)),
2876            },
2877            flow_notification_tx,
2878        )
2879        .await;
2880
2881        assert_eq!(1, writes.load(Ordering::SeqCst));
2882        assert!(
2883            tokio::time::timeout(Duration::from_millis(50), requests_rx.recv())
2884                .await
2885                .is_err()
2886        );
2887    }
2888
2889    #[async_trait]
2890    impl PhysicalFlushNodeRequester for ConcurrentMockNodeManager {
2891        async fn handle(
2892            &self,
2893            peer: &Peer,
2894            request: RegionRequest,
2895        ) -> error::Result<RegionResponse> {
2896            let datanode = self.datanode(peer).await;
2897            datanode
2898                .handle(request)
2899                .await
2900                .context(error::CommonMetaSnafu)
2901        }
2902    }
2903
2904    #[test]
2905    fn test_remove_worker_if_same_channel_removes_matching_entry() {
2906        let workers = DashMap::new();
2907        let key = BatchKey {
2908            catalog: "greptime".to_string(),
2909            schema: "public".to_string(),
2910            physical_table: "phy".to_string(),
2911        };
2912
2913        let (tx, _rx) = mpsc::channel::<WorkerCommand>(1);
2914        workers.insert(key.clone(), PendingWorker { tx: tx.clone() });
2915
2916        assert!(remove_worker_if_same_channel(&workers, &key, &tx));
2917        assert!(!workers.contains_key(&key));
2918    }
2919
2920    #[test]
2921    fn test_remove_worker_if_same_channel_keeps_newer_entry() {
2922        let workers = DashMap::new();
2923        let key = BatchKey {
2924            catalog: "greptime".to_string(),
2925            schema: "public".to_string(),
2926            physical_table: "phy".to_string(),
2927        };
2928
2929        let (stale_tx, _stale_rx) = mpsc::channel::<WorkerCommand>(1);
2930        let (fresh_tx, _fresh_rx) = mpsc::channel::<WorkerCommand>(1);
2931        workers.insert(
2932            key.clone(),
2933            PendingWorker {
2934                tx: fresh_tx.clone(),
2935            },
2936        );
2937
2938        assert!(!remove_worker_if_same_channel(&workers, &key, &stale_tx));
2939        assert!(workers.contains_key(&key));
2940        assert!(workers.get(&key).unwrap().tx.same_channel(&fresh_tx));
2941    }
2942
2943    #[test]
2944    fn test_worker_idle_timeout_close_decision() {
2945        assert!(should_close_worker_on_idle_timeout(0, 0));
2946        assert!(!should_close_worker_on_idle_timeout(1, 0));
2947        assert!(!should_close_worker_on_idle_timeout(0, 1));
2948    }
2949
2950    const WORKER_TEST_TIMEOUT: Duration = Duration::from_secs(30);
2951
2952    async fn submit_mock_worker_batch(
2953        worker_tx: &mpsc::Sender<WorkerCommand>,
2954        total_rows: usize,
2955        timestamp: i64,
2956    ) -> oneshot::Receiver<std::result::Result<(), Arc<Error>>> {
2957        let (response_tx, response_rx) = oneshot::channel();
2958        let permit = Arc::new(Semaphore::new(1)).acquire_owned().await.unwrap();
2959        worker_tx
2960            .send(WorkerCommand::Submit {
2961                table_batches: vec![(
2962                    "cpu".to_string(),
2963                    42,
2964                    mock_aligned_tag_batch("tag1", "host-1", timestamp, 1.0),
2965                )],
2966                total_rows,
2967                ctx: session::context::QueryContext::arc(),
2968                response_tx,
2969                _permit: permit,
2970            })
2971            .await
2972            .unwrap();
2973
2974        // The channel is FIFO, so the ack proves the worker has dequeued and
2975        // processed the submission (anchoring the flush deadline) before the
2976        // caller advances virtual time.
2977        let (ack_tx, ack_rx) = oneshot::channel();
2978        worker_tx.send(WorkerCommand::Ack { ack_tx }).await.unwrap();
2979        ack_rx
2980            .await
2981            .expect("worker exited before acking the submitted batch");
2982
2983        response_rx
2984    }
2985
2986    async fn receive_mock_flush_result(
2987        response_rx: oneshot::Receiver<std::result::Result<(), Arc<Error>>>,
2988        context: &str,
2989    ) -> std::result::Result<(), Arc<Error>> {
2990        // Under paused time the timeout auto-advances the clock and fires
2991        // deterministically if the flush never completes.
2992        tokio::time::timeout(WORKER_TEST_TIMEOUT, response_rx)
2993            .await
2994            .unwrap_or_else(|_| panic!("{context}"))
2995            .expect("flush result channel closed without a result")
2996    }
2997
2998    fn assert_missing_physical_table(result: std::result::Result<(), Arc<Error>>) {
2999        let err = result.expect_err("the empty catalog should make the flush fail");
3000        assert!(
3001            matches!(
3002                err.as_ref(),
3003                Error::Internal { err_msg }
3004                    if err_msg.contains("not found during pending flush")
3005            ),
3006            "unexpected flush error: {err}"
3007        );
3008    }
3009
3010    #[tokio::test(start_paused = true)]
3011    async fn test_worker_rearms_creation_relative_deadline_after_size_flush() {
3012        let flush_interval = Duration::from_secs(10);
3013        let worker_idle_timeout = Duration::from_secs(30);
3014        let key = BatchKey {
3015            catalog: "greptime".to_string(),
3016            schema: "public".to_string(),
3017            physical_table: "phy".to_string(),
3018        };
3019        let workers = Arc::new(DashMap::new());
3020        let (worker_tx, worker_rx) = mpsc::channel(1);
3021        workers.insert(
3022            key.clone(),
3023            PendingWorker {
3024                tx: worker_tx.clone(),
3025            },
3026        );
3027
3028        let backend = Arc::new(MemoryKvBackend::default());
3029        let table_route_cache = Arc::new(new_table_route_cache(
3030            "pending-rows-flush-deadline-routes".to_string(),
3031            CacheBuilder::new(1).build(),
3032            backend.clone(),
3033        ));
3034        let partition_info_cache = Arc::new(new_partition_info_cache(
3035            "pending-rows-flush-deadline-partitions".to_string(),
3036            CacheBuilder::new(1).build(),
3037            table_route_cache.clone(),
3038        ));
3039        let partition_manager = Arc::new(PartitionRuleManager::new(
3040            backend,
3041            table_route_cache,
3042            partition_info_cache,
3043        ));
3044        let node_manager: NodeManagerRef = Arc::new(ConcurrentMockNodeManager {
3045            datanodes: Arc::new(HashMap::new()),
3046        });
3047        let catalog_manager = MemoryCatalogManager::with_default_setup();
3048        let (flow_notification_tx, _flow_notification_rx) = mpsc::channel(1);
3049        let (shutdown, _) = broadcast::channel(1);
3050
3051        start_worker(
3052            key.clone(),
3053            worker_tx.clone(),
3054            workers.clone(),
3055            worker_rx,
3056            shutdown.clone(),
3057            partition_manager,
3058            node_manager,
3059            catalog_manager,
3060            flow_notification_tx,
3061            flush_interval,
3062            worker_idle_timeout,
3063            2,
3064            Arc::new(Semaphore::new(1)),
3065        );
3066
3067        // Start the worker, then size-flush a batch halfway to the first
3068        // worker-aligned interval boundary. This arms the reusable timer and
3069        // drains the batch before that deadline is reached.
3070        tokio::task::yield_now().await;
3071        advance(flush_interval / 2).await;
3072        let size_flush_rx = submit_mock_worker_batch(&worker_tx, 2, 1000).await;
3073        let size_flush_result =
3074            receive_mock_flush_result(size_flush_rx, "row threshold did not flush the first batch")
3075                .await;
3076        assert_missing_physical_table(size_flush_result);
3077
3078        // Submit a low-volume batch before the first batch's old timer would
3079        // expire. It must receive a fresh full interval.
3080        advance(flush_interval / 5).await;
3081        let mut timed_flush_rx = submit_mock_worker_batch(&worker_tx, 1, 2000).await;
3082
3083        advance(flush_interval - Duration::from_millis(1)).await;
3084        for _ in 0..10 {
3085            tokio::task::yield_now().await;
3086        }
3087        assert!(matches!(
3088            timed_flush_rx.try_recv(),
3089            Err(oneshot::error::TryRecvError::Empty)
3090        ));
3091
3092        advance(Duration::from_millis(1)).await;
3093        let timed_flush_result = receive_mock_flush_result(
3094            timed_flush_rx,
3095            "batch was not flushed one interval after its creation",
3096        )
3097        .await;
3098        assert_missing_physical_table(timed_flush_result);
3099
3100        let _ = shutdown.send(());
3101        for _ in 0..10 {
3102            if !workers.contains_key(&key) {
3103                break;
3104            }
3105            tokio::task::yield_now().await;
3106        }
3107        assert!(
3108            !workers.contains_key(&key),
3109            "worker did not exit after shutdown"
3110        );
3111    }
3112
3113    #[tokio::test]
3114    async fn test_flush_region_writes_concurrently_dispatches_multiple_datanodes() {
3115        let inflight = Arc::new(AtomicUsize::new(0));
3116        let max_inflight = Arc::new(AtomicUsize::new(0));
3117        let datanode1: DatanodeRef = Arc::new(ConcurrentMockDatanode {
3118            delay: Duration::from_millis(100),
3119            inflight: inflight.clone(),
3120            max_inflight: max_inflight.clone(),
3121        });
3122        let datanode2: DatanodeRef = Arc::new(ConcurrentMockDatanode {
3123            delay: Duration::from_millis(100),
3124            inflight,
3125            max_inflight: max_inflight.clone(),
3126        });
3127
3128        let mut datanodes = HashMap::new();
3129        datanodes.insert(1, datanode1);
3130        datanodes.insert(2, datanode2);
3131        let node_manager = Arc::new(ConcurrentMockNodeManager {
3132            datanodes: Arc::new(datanodes),
3133        });
3134
3135        let writes = vec![
3136            FlushRegionWrite {
3137                datanode: Peer {
3138                    id: 1,
3139                    addr: "node1".to_string(),
3140                },
3141                request: RegionRequest::default(),
3142            },
3143            FlushRegionWrite {
3144                datanode: Peer {
3145                    id: 2,
3146                    addr: "node2".to_string(),
3147                },
3148                request: RegionRequest::default(),
3149            },
3150        ];
3151
3152        flush_region_writes_concurrently(node_manager.as_ref(), writes)
3153            .await
3154            .unwrap();
3155        assert!(max_inflight.load(Ordering::SeqCst) >= 2);
3156    }
3157
3158    #[test]
3159    fn test_should_dispatch_concurrently_by_region_count() {
3160        assert!(!should_dispatch_concurrently(0));
3161        assert!(!should_dispatch_concurrently(1));
3162        assert!(should_dispatch_concurrently(2));
3163    }
3164
3165    #[test]
3166    fn test_strip_partition_columns_from_batch_removes_partition_tags() {
3167        let batch = RecordBatch::try_new(
3168            Arc::new(ArrowSchema::new(vec![
3169                Field::new("__primary_key", ArrowDataType::Binary, false),
3170                Field::new(
3171                    "greptime_timestamp",
3172                    ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3173                    false,
3174                ),
3175                Field::new("greptime_value", ArrowDataType::Float64, true),
3176                Field::new("host", ArrowDataType::Utf8, true),
3177            ])),
3178            vec![
3179                Arc::new(BinaryArray::from(vec![b"k1".as_slice()])),
3180                Arc::new(TimestampMillisecondArray::from(vec![1000_i64])),
3181                Arc::new(arrow::array::Float64Array::from(vec![42.0_f64])),
3182                Arc::new(StringArray::from(vec!["node-1"])),
3183            ],
3184        )
3185        .unwrap();
3186
3187        let stripped = strip_partition_columns_from_batch(batch).unwrap();
3188
3189        assert_eq!(3, stripped.num_columns());
3190        assert_eq!("__primary_key", stripped.schema().field(0).name());
3191        assert_eq!("greptime_timestamp", stripped.schema().field(1).name());
3192        assert_eq!("greptime_value", stripped.schema().field(2).name());
3193    }
3194
3195    #[test]
3196    fn test_strip_partition_columns_from_batch_projects_essential_columns_without_lookup() {
3197        let batch = RecordBatch::try_new(
3198            Arc::new(ArrowSchema::new(vec![
3199                Field::new("__primary_key", ArrowDataType::Binary, false),
3200                Field::new(
3201                    "greptime_timestamp",
3202                    ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3203                    false,
3204                ),
3205                Field::new("greptime_value", ArrowDataType::Float64, true),
3206                Field::new("host", ArrowDataType::Utf8, true),
3207            ])),
3208            vec![
3209                Arc::new(BinaryArray::from(vec![b"k1".as_slice()])),
3210                Arc::new(TimestampMillisecondArray::from(vec![1000_i64])),
3211                Arc::new(arrow::array::Float64Array::from(vec![42.0_f64])),
3212                Arc::new(StringArray::from(vec!["node-1"])),
3213            ],
3214        )
3215        .unwrap();
3216
3217        let stripped = strip_partition_columns_from_batch(batch).unwrap();
3218
3219        assert_eq!(3, stripped.num_columns());
3220        assert_eq!("__primary_key", stripped.schema().field(0).name());
3221        assert_eq!("greptime_timestamp", stripped.schema().field(1).name());
3222        assert_eq!("greptime_value", stripped.schema().field(2).name());
3223    }
3224
3225    #[test]
3226    fn test_collect_tag_columns_and_non_tag_indices_keeps_partition_tag_column() {
3227        let schema = Arc::new(ArrowSchema::new(vec![
3228            Field::new(
3229                "greptime_timestamp",
3230                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3231                false,
3232            ),
3233            Field::new("greptime_value", ArrowDataType::Float64, true),
3234            Field::new("host", ArrowDataType::Utf8, true),
3235            Field::new("region", ArrowDataType::Utf8, true),
3236        ]));
3237        let name_to_ids =
3238            HashMap::from([("host".to_string(), 1_u32), ("region".to_string(), 2_u32)]);
3239        let partition_columns = HashSet::from(["host"]);
3240
3241        let (tag_columns, non_tag_indices) =
3242            columns_taxonomy(&schema, "cpu", &name_to_ids, &partition_columns).unwrap();
3243
3244        assert_eq!(2, tag_columns.len());
3245        assert_eq!(&[0, 1, 2], non_tag_indices.as_slice());
3246    }
3247
3248    #[test]
3249    fn test_collect_tag_columns_and_non_tag_indices_prioritizes_essential_columns() {
3250        let schema = Arc::new(ArrowSchema::new(vec![
3251            Field::new("host", ArrowDataType::Utf8, true),
3252            Field::new("greptime_value", ArrowDataType::Float64, true),
3253            Field::new(
3254                "greptime_timestamp",
3255                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3256                false,
3257            ),
3258            Field::new("region", ArrowDataType::Utf8, true),
3259        ]));
3260        let name_to_ids =
3261            HashMap::from([("host".to_string(), 1_u32), ("region".to_string(), 2_u32)]);
3262        let partition_columns = HashSet::from(["host", "region"]);
3263
3264        let (_tag_columns, non_tag_indices): (_, SmallVec<[usize; 3]>) =
3265            columns_taxonomy(&schema, "cpu", &name_to_ids, &partition_columns).unwrap();
3266
3267        assert_eq!(&[2, 1, 0, 3], non_tag_indices.as_slice());
3268    }
3269
3270    #[test]
3271    fn test_collect_tag_columns_and_non_tag_indices_rejects_unexpected_data_type() {
3272        let schema = Arc::new(ArrowSchema::new(vec![
3273            Field::new(
3274                "greptime_timestamp",
3275                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3276                false,
3277            ),
3278            Field::new("greptime_value", ArrowDataType::Float64, true),
3279            Field::new("host", ArrowDataType::Utf8, true),
3280            Field::new("invalid", ArrowDataType::Boolean, true),
3281        ]));
3282        let name_to_ids = HashMap::from([("host".to_string(), 1_u32)]);
3283        let partition_columns = HashSet::from(["host"]);
3284
3285        let result = columns_taxonomy(&schema, "cpu", &name_to_ids, &partition_columns);
3286
3287        assert!(matches!(
3288            result,
3289            Err(Error::InvalidPromRemoteRequest { .. })
3290        ));
3291    }
3292
3293    #[test]
3294    fn test_collect_tag_columns_and_non_tag_indices_rejects_int64_timestamp_column() {
3295        let schema = Arc::new(ArrowSchema::new(vec![
3296            Field::new("greptime_timestamp", ArrowDataType::Int64, false),
3297            Field::new("greptime_value", ArrowDataType::Float64, true),
3298            Field::new("host", ArrowDataType::Utf8, true),
3299        ]));
3300        let name_to_ids = HashMap::from([("host".to_string(), 1_u32)]);
3301        let partition_columns = HashSet::from(["host"]);
3302
3303        let result = columns_taxonomy(&schema, "cpu", &name_to_ids, &partition_columns);
3304
3305        assert!(matches!(
3306            result,
3307            Err(Error::InvalidPromRemoteRequest { .. })
3308        ));
3309    }
3310
3311    #[test]
3312    fn test_collect_tag_columns_and_non_tag_indices_rejects_duplicated_timestamp_column() {
3313        let schema = Arc::new(ArrowSchema::new(vec![
3314            Field::new(
3315                "ts1",
3316                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3317                false,
3318            ),
3319            Field::new(
3320                "ts2",
3321                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3322                false,
3323            ),
3324            Field::new("greptime_value", ArrowDataType::Float64, true),
3325            Field::new("host", ArrowDataType::Utf8, true),
3326        ]));
3327        let name_to_ids = HashMap::from([("host".to_string(), 1_u32)]);
3328        let partition_columns = HashSet::from(["host"]);
3329
3330        let result = columns_taxonomy(&schema, "cpu", &name_to_ids, &partition_columns);
3331
3332        assert!(matches!(
3333            result,
3334            Err(Error::InvalidPromRemoteRequest { .. })
3335        ));
3336    }
3337
3338    #[test]
3339    fn test_collect_tag_columns_and_non_tag_indices_rejects_duplicated_value_column() {
3340        let schema = Arc::new(ArrowSchema::new(vec![
3341            Field::new(
3342                "greptime_timestamp",
3343                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3344                false,
3345            ),
3346            Field::new("value1", ArrowDataType::Float64, true),
3347            Field::new("value2", ArrowDataType::Float64, true),
3348            Field::new("host", ArrowDataType::Utf8, true),
3349        ]));
3350        let name_to_ids = HashMap::from([("host".to_string(), 1_u32)]);
3351        let partition_columns = HashSet::from(["host"]);
3352
3353        let result = columns_taxonomy(&schema, "cpu", &name_to_ids, &partition_columns);
3354
3355        assert!(matches!(
3356            result,
3357            Err(Error::InvalidPromRemoteRequest { .. })
3358        ));
3359    }
3360
3361    #[test]
3362    fn test_modify_batch_sparse_with_taxonomy_per_batch() {
3363        use arrow::array::BinaryArray;
3364        use metric_engine::batch_modifier::modify_batch_sparse;
3365
3366        let schema1 = Arc::new(ArrowSchema::new(vec![
3367            Field::new(
3368                "greptime_timestamp",
3369                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3370                false,
3371            ),
3372            Field::new("greptime_value", ArrowDataType::Float64, true),
3373            Field::new("tag1", ArrowDataType::Utf8, true),
3374        ]));
3375
3376        let schema2 = Arc::new(ArrowSchema::new(vec![
3377            Field::new(
3378                "greptime_timestamp",
3379                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3380                false,
3381            ),
3382            Field::new("greptime_value", ArrowDataType::Float64, true),
3383            Field::new("tag1", ArrowDataType::Utf8, true),
3384            Field::new("tag2", ArrowDataType::Utf8, true),
3385        ]));
3386        let batch2 = RecordBatch::try_new(
3387            schema2.clone(),
3388            vec![
3389                Arc::new(TimestampMillisecondArray::from(vec![2000])),
3390                Arc::new(arrow::array::Float64Array::from(vec![2.0])),
3391                Arc::new(StringArray::from(vec!["v1"])),
3392                Arc::new(StringArray::from(vec!["v2"])),
3393            ],
3394        )
3395        .unwrap();
3396
3397        let name_to_ids = HashMap::from([("tag1".to_string(), 1), ("tag2".to_string(), 2)]);
3398        let partition_columns = HashSet::new();
3399
3400        // A batch that only has tag1, same values as batch2 for ts and val.
3401        let batch3 = RecordBatch::try_new(
3402            schema1.clone(),
3403            vec![
3404                Arc::new(TimestampMillisecondArray::from(vec![2000])),
3405                Arc::new(arrow::array::Float64Array::from(vec![2.0])),
3406                Arc::new(StringArray::from(vec!["v1"])),
3407            ],
3408        )
3409        .unwrap();
3410
3411        // Simulate the new loop logic in flush_batch_physical:
3412        // Resolve taxonomy FOR EACH BATCH.
3413        let (tag_columns2, indices2) =
3414            columns_taxonomy(&batch2.schema(), "table", &name_to_ids, &partition_columns).unwrap();
3415        let modified2 = modify_batch_sparse(batch2, 123, &tag_columns2, &indices2).unwrap();
3416
3417        let (tag_columns3, indices3) =
3418            columns_taxonomy(&batch3.schema(), "table", &name_to_ids, &partition_columns).unwrap();
3419        let modified3 = modify_batch_sparse(batch3, 123, &tag_columns3, &indices3).unwrap();
3420
3421        let pk2 = modified2
3422            .column(0)
3423            .as_any()
3424            .downcast_ref::<BinaryArray>()
3425            .unwrap();
3426        let pk3 = modified3
3427            .column(0)
3428            .as_any()
3429            .downcast_ref::<BinaryArray>()
3430            .unwrap();
3431
3432        // Now they SHOULD be different because tag2 is included in pk2 but not in pk3.
3433        assert_ne!(
3434            pk2.value(0),
3435            pk3.value(0),
3436            "PK should be different because batch2 has tag2!"
3437        );
3438    }
3439
3440    #[test]
3441    fn test_transform_logical_batches_to_physical_success() {
3442        let batch = mock_aligned_tag_batch("tag1", "v1", 1000, 1.0);
3443
3444        let table_batches = vec![TableBatch {
3445            table_name: "t1".to_string(),
3446            table_id: 1,
3447            batches: vec![batch],
3448            row_count: 1,
3449        }];
3450
3451        let name_to_ids = HashMap::from([("tag1".to_string(), 1)]);
3452        let partition_columns = HashSet::new();
3453        let modified =
3454            transform_logical_batches_to_physical(&table_batches, &name_to_ids, &partition_columns)
3455                .unwrap();
3456
3457        assert_eq!(1, modified.len());
3458        assert_eq!(3, modified[0].num_columns());
3459        assert_eq!("__primary_key", modified[0].schema().field(0).name());
3460        assert_eq!("greptime_timestamp", modified[0].schema().field(1).name());
3461        assert_eq!("greptime_value", modified[0].schema().field(2).name());
3462    }
3463
3464    #[test]
3465    fn test_transform_logical_batches_to_physical_taxonomy_failure() {
3466        let batch = mock_aligned_tag_batch("tag1", "v1", 1000, 1.0);
3467
3468        let table_batches = vec![TableBatch {
3469            table_name: "t1".to_string(),
3470            table_id: 1,
3471            batches: vec![batch],
3472            row_count: 1,
3473        }];
3474
3475        // tag1 is missing from name_to_ids, causing columns_taxonomy to fail.
3476        let name_to_ids = HashMap::new();
3477        let partition_columns = HashSet::new();
3478        let err =
3479            transform_logical_batches_to_physical(&table_batches, &name_to_ids, &partition_columns)
3480                .unwrap_err();
3481
3482        assert!(
3483            err.to_string()
3484                .contains("not found in physical table column IDs")
3485        );
3486    }
3487
3488    #[test]
3489    fn test_transform_logical_batches_to_physical_multiple_batches() {
3490        let batch1 = mock_aligned_tag_batch("tag1", "v1", 1000, 1.0);
3491        let batch2 = mock_aligned_tag_batch("tag2", "v2", 2000, 2.0);
3492
3493        let table_batches = vec![
3494            TableBatch {
3495                table_name: "t1".to_string(),
3496                table_id: 1,
3497                batches: vec![batch1],
3498                row_count: 1,
3499            },
3500            TableBatch {
3501                table_name: "t2".to_string(),
3502                table_id: 2,
3503                batches: vec![batch2],
3504                row_count: 1,
3505            },
3506        ];
3507
3508        let name_to_ids = HashMap::from([("tag1".to_string(), 1), ("tag2".to_string(), 2)]);
3509        let partition_columns = HashSet::new();
3510        let modified =
3511            transform_logical_batches_to_physical(&table_batches, &name_to_ids, &partition_columns)
3512                .unwrap();
3513
3514        assert_eq!(2, modified.len());
3515    }
3516
3517    #[test]
3518    fn test_transform_logical_batches_to_physical_mixed_success_failure() {
3519        let batch1 = mock_aligned_tag_batch("tag1", "v1", 1000, 1.0);
3520        let batch2 = mock_aligned_tag_batch("tag2", "v2", 2000, 2.0);
3521
3522        let table_batches = vec![
3523            TableBatch {
3524                table_name: "t1".to_string(),
3525                table_id: 1,
3526                batches: vec![batch1],
3527                row_count: 1,
3528            },
3529            TableBatch {
3530                table_name: "t2".to_string(),
3531                table_id: 2,
3532                batches: vec![batch2],
3533                row_count: 1,
3534            },
3535        ];
3536
3537        // tag1 is missing from name_to_ids, causing batch1 to fail.
3538        let name_to_ids = HashMap::from([("tag2".to_string(), 2)]);
3539        let partition_columns = HashSet::new();
3540        let err =
3541            transform_logical_batches_to_physical(&table_batches, &name_to_ids, &partition_columns)
3542                .unwrap_err();
3543
3544        assert!(err.to_string().contains("tag1"));
3545    }
3546
3547    #[tokio::test]
3548    async fn test_flush_batch_physical_uses_mockable_trait_dependencies() {
3549        let table_batches = vec![TableBatch {
3550            table_name: "t1".to_string(),
3551            table_id: 11,
3552            batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
3553            row_count: 1,
3554        }];
3555        let partition_calls = Arc::new(AtomicUsize::new(0));
3556        let leader_calls = Arc::new(AtomicUsize::new(0));
3557        let node = MockFlushNodeRequester::default();
3558        let ctx = session::context::QueryContext::arc();
3559
3560        flush_batch_physical(
3561            &table_batches,
3562            "phy",
3563            &ctx,
3564            &MockFlushPartitionProvider {
3565                partition_rule_calls: partition_calls.clone(),
3566                region_leader_calls: leader_calls.clone(),
3567            },
3568            &node,
3569            &MockFlushCatalogProvider {
3570                table: Some(mock_physical_table_metadata(1024)),
3571            },
3572        )
3573        .await
3574        .unwrap();
3575
3576        assert_eq!(1, partition_calls.load(Ordering::SeqCst));
3577        assert_eq!(1, leader_calls.load(Ordering::SeqCst));
3578        assert_eq!(1, node.writes.load(Ordering::SeqCst));
3579    }
3580
3581    #[derive(Default)]
3582    struct AffectedRowsFlushNodeRequester {
3583        affected_rows: usize,
3584    }
3585
3586    #[async_trait]
3587    impl PhysicalFlushNodeRequester for AffectedRowsFlushNodeRequester {
3588        async fn handle(
3589            &self,
3590            _peer: &Peer,
3591            _request: RegionRequest,
3592        ) -> error::Result<RegionResponse> {
3593            Ok(RegionResponse::new(self.affected_rows))
3594        }
3595    }
3596
3597    #[tokio::test]
3598    async fn test_flush_batch_physical_returns_actual_affected_rows() {
3599        let table_batches = vec![TableBatch {
3600            table_name: "t1".to_string(),
3601            table_id: 11,
3602            batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
3603            row_count: 1,
3604        }];
3605        let ctx = session::context::QueryContext::arc();
3606
3607        let affected_rows = flush_batch_physical(
3608            &table_batches,
3609            "phy",
3610            &ctx,
3611            &MockFlushPartitionProvider {
3612                partition_rule_calls: Arc::new(AtomicUsize::new(0)),
3613                region_leader_calls: Arc::new(AtomicUsize::new(0)),
3614            },
3615            &AffectedRowsFlushNodeRequester { affected_rows: 7 },
3616            &MockFlushCatalogProvider {
3617                table: Some(mock_physical_table_metadata(1024)),
3618            },
3619        )
3620        .await
3621        .unwrap();
3622
3623        assert_eq!(7, affected_rows);
3624    }
3625
3626    #[tokio::test]
3627    async fn test_flush_batch_physical_stops_before_partition_and_node_when_table_missing() {
3628        let table_batches = vec![TableBatch {
3629            table_name: "t1".to_string(),
3630            table_id: 11,
3631            batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
3632            row_count: 1,
3633        }];
3634        let partition_calls = Arc::new(AtomicUsize::new(0));
3635        let leader_calls = Arc::new(AtomicUsize::new(0));
3636        let node = MockFlushNodeRequester::default();
3637        let ctx = session::context::QueryContext::arc();
3638
3639        let err = flush_batch_physical(
3640            &table_batches,
3641            "missing_phy",
3642            &ctx,
3643            &MockFlushPartitionProvider {
3644                partition_rule_calls: partition_calls.clone(),
3645                region_leader_calls: leader_calls.clone(),
3646            },
3647            &node,
3648            &MockFlushCatalogProvider { table: None },
3649        )
3650        .await
3651        .unwrap_err();
3652
3653        assert!(
3654            err.to_string()
3655                .contains("Physical table 'missing_phy' not found")
3656        );
3657        assert_eq!(0, partition_calls.load(Ordering::SeqCst));
3658        assert_eq!(0, leader_calls.load(Ordering::SeqCst));
3659        assert_eq!(0, node.writes.load(Ordering::SeqCst));
3660    }
3661
3662    #[tokio::test]
3663    async fn test_flush_batch_physical_aborts_immediately_on_transform_error() {
3664        let table_batches = vec![
3665            TableBatch {
3666                table_name: "broken".to_string(),
3667                table_id: 11,
3668                batches: vec![mock_aligned_tag_batch("unknown_tag", "host-1", 1000, 1.0)],
3669                row_count: 1,
3670            },
3671            TableBatch {
3672                table_name: "healthy".to_string(),
3673                table_id: 12,
3674                batches: vec![mock_aligned_tag_batch("tag1", "host-2", 2000, 2.0)],
3675                row_count: 1,
3676            },
3677        ];
3678        let partition_calls = Arc::new(AtomicUsize::new(0));
3679        let leader_calls = Arc::new(AtomicUsize::new(0));
3680        let node = MockFlushNodeRequester::default();
3681        let ctx = session::context::QueryContext::arc();
3682
3683        let err = flush_batch_physical(
3684            &table_batches,
3685            "phy",
3686            &ctx,
3687            &MockFlushPartitionProvider {
3688                partition_rule_calls: partition_calls.clone(),
3689                region_leader_calls: leader_calls.clone(),
3690            },
3691            &node,
3692            &MockFlushCatalogProvider {
3693                table: Some(mock_physical_table_metadata(1024)),
3694            },
3695        )
3696        .await
3697        .unwrap_err();
3698
3699        assert!(err.to_string().contains("unknown_tag"));
3700        assert_eq!(1, partition_calls.load(Ordering::SeqCst));
3701        assert_eq!(0, leader_calls.load(Ordering::SeqCst));
3702        assert_eq!(0, node.writes.load(Ordering::SeqCst));
3703    }
3704
3705    #[test]
3706    fn test_plan_region_batches_splits_and_strips_partition_columns() {
3707        let combined_batch = RecordBatch::try_new(
3708            Arc::new(ArrowSchema::new(vec![
3709                Field::new("__primary_key", ArrowDataType::Binary, false),
3710                Field::new(
3711                    "greptime_timestamp",
3712                    ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3713                    false,
3714                ),
3715                Field::new("greptime_value", ArrowDataType::Float64, true),
3716                Field::new("host", ArrowDataType::Utf8, true),
3717            ])),
3718            vec![
3719                Arc::new(BinaryArray::from(vec![b"k1".as_slice(), b"k2".as_slice()])),
3720                Arc::new(TimestampMillisecondArray::from(vec![1000_i64, 2000_i64])),
3721                Arc::new(arrow::array::Float64Array::from(vec![1.0_f64, 2.0_f64])),
3722                Arc::new(StringArray::from(vec!["node-1", "node-2"])),
3723            ],
3724        )
3725        .unwrap();
3726        let mut planned_batches = plan_region_batches(
3727            combined_batch,
3728            1024,
3729            &TwoRegionPartitionRule {
3730                partition_columns: vec!["host".to_string()],
3731            },
3732            &["host".to_string()],
3733        )
3734        .unwrap();
3735        planned_batches.sort_by_key(|planned| planned.region_id.region_number());
3736
3737        assert_eq!(2, planned_batches.len());
3738        assert_eq!(RegionId::new(1024, 1), planned_batches[0].region_id);
3739        assert_eq!(1, planned_batches[0].num_rows());
3740        assert_eq!(3, planned_batches[0].batch.num_columns());
3741        assert_eq!(RegionId::new(1024, 2), planned_batches[1].region_id);
3742        assert_eq!(1, planned_batches[1].num_rows());
3743        assert_eq!(3, planned_batches[1].batch.num_columns());
3744    }
3745
3746    #[test]
3747    fn test_encode_region_write_requests_builds_bulk_insert_requests() {
3748        let planned_batch = PlannedRegionBatch {
3749            region_id: RegionId::new(1024, 1),
3750            batch: RecordBatch::try_new(
3751                Arc::new(ArrowSchema::new(vec![
3752                    Field::new("__primary_key", ArrowDataType::Binary, false),
3753                    Field::new(
3754                        "greptime_timestamp",
3755                        ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3756                        false,
3757                    ),
3758                    Field::new("greptime_value", ArrowDataType::Float64, true),
3759                ])),
3760                vec![
3761                    Arc::new(BinaryArray::from(vec![b"k1".as_slice()])),
3762                    Arc::new(TimestampMillisecondArray::from(vec![1000_i64])),
3763                    Arc::new(arrow::array::Float64Array::from(vec![1.0_f64])),
3764                ],
3765            )
3766            .unwrap(),
3767        };
3768        let resolved_batch = ResolvedRegionBatch {
3769            planned: planned_batch,
3770            datanode: Peer {
3771                id: 1,
3772                addr: "node-1".to_string(),
3773            },
3774        };
3775        let writes = encode_region_write_requests(vec![resolved_batch]).unwrap();
3776
3777        assert_eq!(1, writes.len());
3778        assert_eq!(1, writes[0].datanode.id);
3779        let Some(region_request::Body::BulkInsert(request)) = &writes[0].request.body else {
3780            panic!("expected bulk insert request");
3781        };
3782        assert_eq!(RegionId::new(1024, 1).as_u64(), request.region_id);
3783    }
3784}