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