Skip to main content

operator/
insert.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::BTreeMap;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::sync::{Arc, LazyLock};
18use std::time::{Duration, Instant};
19
20use ahash::{HashMap, HashMapExt, HashSet, HashSetExt};
21use api::v1::alter_table_expr::Kind;
22use api::v1::column_def::{options_from_skipping, try_as_column_def};
23use api::v1::region::{
24    InsertRequest as RegionInsertRequest, InsertRequests as RegionInsertRequests,
25    RegionRequestHeader,
26};
27use api::v1::value::ValueData;
28use api::v1::{
29    AlterTableExpr, ColumnDataType, ColumnSchema, CreateTableExpr, InsertRequests,
30    RowInsertRequest, RowInsertRequests, Rows, SemanticType,
31};
32use arrow::datatypes::{DataType as ArrowDataType, Schema as ArrowSchema};
33use catalog::CatalogManagerRef;
34use client::{OutputData, OutputMeta};
35use common_catalog::consts::{
36    DEFAULT_PRIVATE_SCHEMA_NAME, PARENT_SPAN_ID_COLUMN, SERVICE_NAME_COLUMN, TRACE_ID_COLUMN,
37    TRACE_TABLE_NAME, TRACE_TABLE_NAME_SESSION_KEY, default_engine, is_ddl_reserved_table,
38    trace_operations_table_name, trace_services_table_name,
39};
40use common_event_recorder::DEFAULT_EVENTS_TABLE_NAME;
41use common_frontend::slow_query_event::SLOW_QUERY_TABLE_NAME;
42use common_grpc_expr::util::ColumnExpr;
43use common_meta::cache::TableFlownodeSetCacheRef;
44use common_meta::datanode::REGION_STATS_HISTORY_TABLE_NAME;
45use common_meta::node_manager::NodeManagerRef;
46use common_meta::peer::Peer;
47use common_meta::rpc::ddl::TriggerReason;
48use common_query::Output;
49use common_query::native_histogram::{is_native_histogram_value_type, native_histogram_value_type};
50use common_query::prelude::{greptime_timestamp, greptime_value};
51use common_telemetry::tracing_context::TracingContext;
52use common_telemetry::{debug, error, warn};
53use common_time::Timestamp;
54use common_time::timestamp::TimeUnit;
55use datatypes::schema::SkippingIndexOptions;
56use futures_util::future;
57use meter_core::data::MeterRecord;
58use meter_macros::write_meter;
59use partition::manager::PartitionRuleManagerRef;
60use session::context::QueryContextRef;
61use snafu::ResultExt;
62use snafu::prelude::*;
63use sql::partition::partition_rule_for_hexstring;
64use sql::statements::create::Partitions;
65use sql::statements::insert::Insert;
66use store_api::metric_engine_consts::{
67    LOGICAL_TABLE_METADATA_KEY, METRIC_ENGINE_NAME, PHYSICAL_TABLE_METADATA_KEY,
68};
69use store_api::mito_engine_options::{
70    APPEND_MODE_KEY, COMPACTION_TYPE, COMPACTION_TYPE_TWCS, MERGE_MODE_KEY, TTL_KEY,
71    TWCS_TIME_WINDOW,
72};
73use store_api::storage::{RegionId, TableId};
74use table::TableRef;
75use table::metadata::{TableInfo, TableInfoRef};
76use table::requests::{
77    AUTO_CREATE_TABLE_KEY, InsertRequest as TableInsertRequest, SEMANTIC_PER_TABLE_INDEX_KEY,
78    SEMANTIC_PIPELINE, TABLE_DATA_MODEL, TABLE_DATA_MODEL_TRACE_V1, TABLE_DATA_MODEL_TRACE_V2,
79    TRACE_TABLE_PARTITIONS_HINT_KEY, VALID_TABLE_OPTION_KEYS, is_semantic_option_key,
80    validate_semantic_option,
81};
82use table::table_reference::TableReference;
83
84use crate::batcher::PendingRowsBatcher;
85use crate::error::{
86    CatalogSnafu, ColumnOptionsSnafu, CreatePartitionRulesSnafu, InvalidInsertRequestSnafu,
87    JoinTaskSnafu, RequestInsertsSnafu, Result, TableNotFoundSnafu, WriteRejectedSnafu,
88};
89use crate::expr_helper;
90use crate::region_req_factory::RegionRequestFactory;
91use crate::req_convert::common::preprocess_row_insert_requests;
92use crate::req_convert::insert::{
93    ColumnToRow, ImpureDefaultFiller, RowToRegion, StatementToRegion, TableToRegion,
94    fill_reqs_with_impure_default, rows_to_record_batch,
95};
96use crate::statement::StatementExecutor;
97use crate::write_dispatcher::{WriteDispatcher, WriteDispatcherRef};
98
99pub struct Inserter {
100    catalog_manager: CatalogManagerRef,
101    pub(crate) partition_manager: PartitionRuleManagerRef,
102    pub(crate) node_manager: NodeManagerRef,
103    pub(crate) write_dispatcher: WriteDispatcherRef,
104    pub(crate) table_flownode_set_cache: TableFlownodeSetCacheRef,
105    /// Server-side upper bound for auto table creation on write.
106    /// When `false`, missing tables are never auto-created regardless of the
107    /// per-request `auto_create_table` hint. When `true`, the hint still applies.
108    auto_create_table: bool,
109    pending_rows_batcher: Option<Arc<dyn PendingRowsBatcher>>,
110    /// Rows handed to detached flow mirror tasks that have not finished yet.
111    /// Bounds how much row data in-flight mirror writes may keep alive, see
112    /// [`MAX_MIRROR_PENDING_ROWS`].
113    mirror_pending_rows: Arc<AtomicU64>,
114}
115
116pub type InserterRef = Arc<Inserter>;
117
118/// Hint for the table type to create automatically.
119#[derive(Clone)]
120pub enum AutoCreateTableType {
121    /// A logical table with the physical table name.
122    Logical(String),
123    /// A physical table.
124    Physical,
125    /// A log table which is append-only.
126    Log,
127    /// A table that merges rows by `last_non_null` strategy.
128    LastNonNull,
129    /// Create table that build index and default partition rules on trace_id
130    Trace { alter_existing: bool },
131}
132
133impl AutoCreateTableType {
134    pub fn as_str(&self) -> &'static str {
135        match self {
136            AutoCreateTableType::Logical(_) => "logical",
137            AutoCreateTableType::Physical => "physical",
138            AutoCreateTableType::Log => "log",
139            AutoCreateTableType::LastNonNull => "last_non_null",
140            AutoCreateTableType::Trace { .. } => "trace",
141        }
142    }
143
144    fn alter_existing(&self) -> bool {
145        !matches!(
146            self,
147            Self::Trace {
148                alter_existing: false
149            }
150        )
151    }
152}
153
154/// Split insert requests into normal and instant requests.
155///
156/// Where instant requests are requests with ttl=instant,
157/// and normal requests are requests with ttl set to other values.
158///
159/// This is used to split requests for different processing.
160#[derive(Clone)]
161pub struct InstantAndNormalInsertRequests {
162    /// Requests with normal ttl.
163    pub normal_requests: RegionInsertRequests,
164    /// Requests with ttl=instant.
165    /// Will be discarded immediately at frontend, wouldn't even insert into memtable, and only sent to flow node if needed.
166    pub instant_requests: RegionInsertRequests,
167}
168
169impl Inserter {
170    /// Checks the assumptions of the logical bulk path without changing tables.
171    /// Unsupported requests retain ordinary insertion, including schema policy,
172    /// defaults, instant TTL and row-based Flow delivery.
173    pub async fn can_batch_metric_rows(
174        &self,
175        requests: &RowInsertRequests,
176        ctx: &QueryContextRef,
177        physical_table: &str,
178    ) -> Result<bool> {
179        if self.auto_create_disabled_reason(ctx)?.is_some() || ctx.extension(TTL_KEY).is_some() {
180            return Ok(false);
181        }
182        for request in &requests.inserts {
183            // The logical bulk encoder only supports scalar metric schemas;
184            // any time index unit is accepted — requests are converted to the
185            // destination table's unit during batch alignment.
186            // Check new tables too, before catalog lookup or schema changes.
187            if request.rows.as_ref().is_some_and(|rows| {
188                rows.schema.iter().any(|column| {
189                    column.datatype_extension.is_some()
190                        || !matches!(
191                            ColumnDataType::try_from(column.datatype),
192                            Ok(ColumnDataType::TimestampSecond
193                                | ColumnDataType::TimestampMillisecond
194                                | ColumnDataType::TimestampMicrosecond
195                                | ColumnDataType::TimestampNanosecond
196                                | ColumnDataType::Float64
197                                | ColumnDataType::String)
198                        )
199                })
200            }) {
201                return Ok(false);
202            }
203
204            let Some(table) = self
205                .get_table(
206                    ctx.current_catalog(),
207                    &ctx.current_schema(),
208                    &request.table_name,
209                )
210                .await?
211            else {
212                continue;
213            };
214            let info = table.table_info();
215            if info.meta.engine != METRIC_ENGINE_NAME
216                || info.is_ttl_instant_table()
217                || info
218                    .meta
219                    .options
220                    .extra_options
221                    .get(LOGICAL_TABLE_METADATA_KEY)
222                    .map(String::as_str)
223                    != Some(physical_table)
224                || info
225                    .meta
226                    .schema
227                    .column_schemas()
228                    .iter()
229                    .any(|column| column.default_constraint().is_some())
230            {
231                return Ok(false);
232            }
233            // Physical metric tags are nullable even when their logical schema
234            // is not. Arrow alignment is stricter than ordinary metric insertion.
235            if info
236                .meta
237                .primary_key_indices
238                .iter()
239                .any(|&index| !info.meta.schema.column_schemas()[index].is_nullable())
240            {
241                return Ok(false);
242            }
243            // The current Flow cache does not distinguish streaming and batch
244            // flows. Keep all Flow sources on the row-based delivery path.
245            match self.table_flownode_set_cache.get(info.table_id()).await {
246                Ok(None) => {}
247                Ok(Some(flows)) if flows.is_empty() => {}
248                _ => return Ok(false),
249            }
250        }
251        Ok(true)
252    }
253
254    /// Meters an original logical-table request before bulk routing, without
255    /// cloning its rows or changing the request boundary used for accounting.
256    pub async fn meter_row_inserts(
257        requests: &mut RowInsertRequests,
258        ctx: &QueryContextRef,
259    ) -> Result<u64> {
260        let metered = InstantAndNormalInsertRequests {
261            normal_requests: RegionInsertRequests {
262                requests: requests
263                    .inserts
264                    .iter_mut()
265                    .map(|request| RegionInsertRequest {
266                        rows: request.rows.take(),
267                        ..Default::default()
268                    })
269                    .collect(),
270            },
271            instant_requests: RegionInsertRequests::default(),
272        };
273        let cost = write_meter!(
274            ctx.current_catalog(),
275            ctx.current_schema(),
276            metered,
277            ctx.write_rows_to_admit(
278                ctx.current_catalog(),
279                &ctx.current_schema(),
280                count_insert_rows(&metered)?
281            ),
282            ctx.channel() as u8
283        )
284        .await
285        .context(WriteRejectedSnafu);
286        for (request, region) in requests
287            .inserts
288            .iter_mut()
289            .zip(metered.normal_requests.requests)
290        {
291            request.rows = region.rows;
292        }
293        cost
294    }
295
296    /// Creates a row inserter with the default write dispatcher.
297    pub fn new(
298        catalog_manager: CatalogManagerRef,
299        partition_manager: PartitionRuleManagerRef,
300        node_manager: NodeManagerRef,
301        table_flownode_set_cache: TableFlownodeSetCacheRef,
302        auto_create_table: bool,
303    ) -> Self {
304        Self {
305            write_dispatcher: Arc::new(WriteDispatcher::new(
306                node_manager.clone(),
307                partition_manager.clone(),
308            )),
309            catalog_manager,
310            partition_manager,
311            node_manager,
312            table_flownode_set_cache,
313            auto_create_table,
314            pending_rows_batcher: None,
315            mirror_pending_rows: Arc::new(AtomicU64::new(0)),
316        }
317    }
318
319    /// Installs the shared dispatcher used by direct writes and batch flushes.
320    pub fn with_write_dispatcher(mut self, dispatcher: WriteDispatcherRef) -> Self {
321        self.write_dispatcher = dispatcher;
322        self
323    }
324
325    /// Installs the shared batcher; callers explicitly select its ingestion entry point.
326    pub fn with_pending_rows_batcher(
327        mut self,
328        batcher: Option<Arc<dyn PendingRowsBatcher>>,
329    ) -> Self {
330        self.pending_rows_batcher = batcher;
331        self
332    }
333
334    pub async fn handle_column_inserts(
335        &self,
336        requests: InsertRequests,
337        ctx: QueryContextRef,
338        statement_executor: &StatementExecutor,
339    ) -> Result<Output> {
340        let row_inserts = ColumnToRow::convert(requests)?;
341        self.handle_row_inserts(row_inserts, ctx, statement_executor, false, false)
342            .await
343    }
344
345    /// Handles row inserts request and creates a physical table on demand.
346    pub async fn handle_row_inserts(
347        &self,
348        mut requests: RowInsertRequests,
349        ctx: QueryContextRef,
350        statement_executor: &StatementExecutor,
351        accommodate_existing_schema: bool,
352        is_single_value: bool,
353    ) -> Result<Output> {
354        preprocess_row_insert_requests(&mut requests.inserts)?;
355        self.handle_row_inserts_with_create_type(
356            requests,
357            ctx,
358            statement_executor,
359            AutoCreateTableType::Physical,
360            accommodate_existing_schema,
361            is_single_value,
362        )
363        .await
364    }
365
366    /// Handles row inserts request and creates a log table on demand.
367    pub async fn handle_log_inserts(
368        &self,
369        requests: RowInsertRequests,
370        ctx: QueryContextRef,
371        statement_executor: &StatementExecutor,
372    ) -> Result<Output> {
373        self.handle_row_inserts_with_create_type(
374            requests,
375            ctx,
376            statement_executor,
377            AutoCreateTableType::Log,
378            false,
379            false,
380        )
381        .await
382    }
383
384    pub async fn handle_trace_inserts(
385        &self,
386        requests: RowInsertRequests,
387        ctx: QueryContextRef,
388        statement_executor: &StatementExecutor,
389    ) -> Result<Output> {
390        self.handle_row_inserts_with_create_type(
391            requests,
392            ctx,
393            statement_executor,
394            AutoCreateTableType::Trace {
395                alter_existing: true,
396            },
397            false,
398            false,
399        )
400        .await
401    }
402
403    /// Handles row inserts request and creates a table with `last_non_null` merge mode on demand.
404    pub async fn handle_last_non_null_inserts(
405        &self,
406        requests: RowInsertRequests,
407        ctx: QueryContextRef,
408        statement_executor: &StatementExecutor,
409        accommodate_existing_schema: bool,
410        is_single_value: bool,
411    ) -> Result<Output> {
412        self.handle_row_inserts_with_create_type(
413            requests,
414            ctx,
415            statement_executor,
416            AutoCreateTableType::LastNonNull,
417            accommodate_existing_schema,
418            is_single_value,
419        )
420        .await
421    }
422
423    /// Handles row inserts request with specified [AutoCreateTableType].
424    async fn handle_row_inserts_with_create_type(
425        &self,
426        mut requests: RowInsertRequests,
427        ctx: QueryContextRef,
428        statement_executor: &StatementExecutor,
429        create_type: AutoCreateTableType,
430        accommodate_existing_schema: bool,
431        is_single_value: bool,
432    ) -> Result<Output> {
433        let skip_wal = ctx.skip_wal();
434
435        let batcher = self
436            .pending_rows_batcher
437            .as_ref()
438            .filter(|_| ctx.batching_enabled());
439
440        // remove empty requests
441        requests.inserts.retain(|req| {
442            req.rows
443                .as_ref()
444                .map(|r| !r.rows.is_empty())
445                .unwrap_or_default()
446        });
447        validate_column_count_match(&requests)?;
448
449        let CreateAlterTableResult {
450            instant_table_ids,
451            table_infos,
452        } = self
453            .create_or_alter_tables_on_demand(
454                &mut requests,
455                &ctx,
456                create_type,
457                statement_executor,
458                accommodate_existing_schema,
459                is_single_value,
460                None,
461            )
462            .await?;
463
464        // Instant tables have no persisted data for dirty-window Flow to read.
465        // Metric tables keep their existing dedicated ingestion path.
466        if let Some(batcher) = batcher
467            && instant_table_ids.is_empty()
468            && table_infos
469                .values()
470                .all(|info| info.meta.engine == default_engine())
471        {
472            return self
473                .submit_pending_rows(requests, table_infos, ctx, batcher)
474                .await;
475        }
476
477        let name_to_info = table_infos
478            .values()
479            .map(|info| (info.name.clone(), info.clone()))
480            .collect::<HashMap<_, _>>();
481        let inserts = RowToRegion::new(
482            name_to_info,
483            instant_table_ids,
484            self.partition_manager.as_ref(),
485        )
486        .convert(requests, skip_wal)
487        .await?;
488
489        self.do_request(inserts, &table_infos, &ctx).await
490    }
491
492    async fn submit_pending_rows(
493        &self,
494        mut requests: RowInsertRequests,
495        table_infos: HashMap<TableId, Arc<TableInfo>>,
496        ctx: QueryContextRef,
497        batcher: &Arc<dyn PendingRowsBatcher>,
498    ) -> Result<Output> {
499        // All entry points, including single-table and SQL writes, skip empty input
500        // before evaluating defaults or converting prepared rows.
501        requests.inserts.retain(|request| {
502            request
503                .rows
504                .as_ref()
505                .is_some_and(|rows| !rows.rows.is_empty())
506        });
507        let by_name = table_infos
508            .values()
509            .map(|info| (info.name.as_str(), info))
510            .collect::<HashMap<_, _>>();
511        let mut prepared = Vec::with_capacity(requests.inserts.len());
512        for request in &mut requests.inserts {
513            let table_info =
514                by_name
515                    .get(request.table_name.as_str())
516                    .context(TableNotFoundSnafu {
517                        table_name: &request.table_name,
518                    })?;
519            let Some(rows) = &mut request.rows else {
520                continue;
521            };
522            ImpureDefaultFiller::new((*table_info).clone())?.fill_rows(rows);
523            let batch = rows_to_record_batch(rows, table_info)?;
524            prepared.push(((*table_info).clone(), batch));
525        }
526        // Preserve the existing meter input and original request boundary. These
527        // envelopes are only for accounting; routing happens after batching.
528        let metered = InstantAndNormalInsertRequests {
529            normal_requests: RegionInsertRequests {
530                requests: requests
531                    .inserts
532                    .into_iter()
533                    .map(|request| RegionInsertRequest {
534                        rows: request.rows,
535                        ..Default::default()
536                    })
537                    .collect(),
538            },
539            instant_requests: RegionInsertRequests::default(),
540        };
541        let table_info = table_infos.values().next();
542        let catalog = table_info.map_or(ctx.current_catalog(), |info| info.catalog_name.as_str());
543        let schema =
544            table_info.map_or_else(|| ctx.current_schema(), |info| info.schema_name.clone());
545        let write_cost = write_meter!(
546            catalog,
547            &schema,
548            metered,
549            ctx.write_rows_to_admit(catalog, &schema, count_insert_rows(&metered)?),
550            ctx.channel() as u8
551        )
552        .await
553        .context(WriteRejectedSnafu)?;
554        prepared.retain(|(_, batch)| batch.num_rows() != 0);
555        let results = if prepared.is_empty() {
556            Vec::new()
557        } else {
558            // One original request shares admission across all table submissions.
559            let permit = batcher.acquire().await?;
560            let submissions = prepared.into_iter().map(|(info, batch)| {
561                // Route to the same target database used for admission above.
562                let mut target_ctx = ctx.fork();
563                target_ctx.set_current_catalog(&info.catalog_name);
564                target_ctx.set_current_schema(&info.schema_name);
565                batcher.submit(info, batch, Arc::new(target_ctx), permit.clone())
566            });
567            // Observe every table completion even when another table fails.
568            future::join_all(submissions).await
569        };
570        let mut affected_rows = 0;
571        let mut meta = OutputMeta::new_with_cost(write_cost as _);
572        for result in results {
573            let output = result?;
574            affected_rows += output.extract_rows_and_cost().0;
575            meta.write_completions.extend(output.meta.write_completions);
576        }
577        Ok(Output::new(OutputData::AffectedRows(affected_rows), meta))
578    }
579
580    /// Handles row inserts request with metric engine.
581    pub async fn handle_metric_row_inserts(
582        &self,
583        mut requests: RowInsertRequests,
584        ctx: QueryContextRef,
585        statement_executor: &StatementExecutor,
586        physical_table: String,
587    ) -> Result<Output> {
588        let skip_wal = ctx.skip_wal();
589
590        // remove empty requests
591        requests.inserts.retain(|req| {
592            req.rows
593                .as_ref()
594                .map(|r| !r.rows.is_empty())
595                .unwrap_or_default()
596        });
597        validate_column_count_match(&requests)?;
598
599        // check and create physical table
600        let physical_table_ref = self
601            .create_physical_table_on_demand(&ctx, physical_table.clone(), statement_executor)
602            .await?;
603
604        // check and create logical tables; `create_or_alter_tables_on_demand`
605        // aligns each request's time index unit with the unit of the table it
606        // targets, inside its existing table lookups: existing tables keep
607        // their own unit (which matches the physical table they are bound
608        // to), and new tables use the selected physical table's unit (from
609        // `physical_table_ref`). Ingestion endpoints encode timestamps in a
610        // fixed unit (prometheus remote write always uses millisecond; OTLP
611        // keeps nanosecond precision on the metric engine path), and the
612        // metric engine requires each logical table's requests to match its
613        // time index unit. Narrowing conversions truncate the sub-unit part
614        // (floor), following `Timestamp::convert_to`.
615        let CreateAlterTableResult {
616            instant_table_ids,
617            table_infos,
618        } = self
619            .create_or_alter_tables_on_demand(
620                &mut requests,
621                &ctx,
622                AutoCreateTableType::Logical(physical_table.clone()),
623                statement_executor,
624                true,
625                true,
626                table_time_index_unit(&physical_table_ref),
627            )
628            .await?;
629        let name_to_info = table_infos
630            .values()
631            .map(|info| (info.name.clone(), info.clone()))
632            .collect::<HashMap<_, _>>();
633        let inserts = RowToRegion::new(name_to_info, instant_table_ids, &self.partition_manager)
634            .convert(requests, skip_wal)
635            .await?;
636
637        self.do_request(inserts, &table_infos, &ctx).await
638    }
639
640    fn table_batcher(
641        &self,
642        table_info: &TableInfoRef,
643        ctx: &QueryContextRef,
644    ) -> Option<&Arc<dyn PendingRowsBatcher>> {
645        self.pending_rows_batcher.as_ref().filter(|_| {
646            ctx.batching_enabled()
647                && !table_info.is_ttl_instant_table()
648                && table_info.meta.engine == default_engine()
649        })
650    }
651
652    async fn submit_table_rows(
653        &self,
654        rows: Rows,
655        table_info: TableInfoRef,
656        ctx: QueryContextRef,
657        batcher: &Arc<dyn PendingRowsBatcher>,
658    ) -> Result<Output> {
659        let requests = RowInsertRequests {
660            inserts: vec![RowInsertRequest {
661                table_name: table_info.name.clone(),
662                rows: Some(rows),
663            }],
664        };
665        let table_infos = HashMap::from_iter([(table_info.table_id(), table_info)]);
666        self.submit_pending_rows(requests, table_infos, ctx, batcher)
667            .await
668    }
669
670    pub async fn handle_table_insert(
671        &self,
672        request: TableInsertRequest,
673        ctx: QueryContextRef,
674    ) -> Result<Output> {
675        let catalog = request.catalog_name.as_str();
676        let schema = request.schema_name.as_str();
677        let table_name = request.table_name.as_str();
678        let table = self.get_table(catalog, schema, table_name).await?;
679        let table = table.with_context(|| TableNotFoundSnafu {
680            table_name: common_catalog::format_full_table_name(catalog, schema, table_name),
681        })?;
682        let table_info = table.table_info();
683
684        let converter = TableToRegion::new(&table_info, &self.partition_manager);
685        let skip_wal = request.skip_wal;
686        let rows = converter.prepare(request)?;
687        if let Some(batcher) = self.table_batcher(&table_info, &ctx) {
688            return self.submit_table_rows(rows, table_info, ctx, batcher).await;
689        }
690        let inserts = converter.partition(rows, skip_wal).await?;
691
692        let table_infos = HashMap::from_iter([(table_info.table_id(), table_info.clone())]);
693
694        self.do_request(inserts, &table_infos, &ctx).await
695    }
696
697    pub async fn handle_statement_insert(
698        &self,
699        insert: &Insert,
700        ctx: &QueryContextRef,
701    ) -> Result<Output> {
702        let converter =
703            StatementToRegion::new(self.catalog_manager.as_ref(), &self.partition_manager, ctx);
704        let (rows, table_info) = converter.prepare(insert, ctx).await?;
705        if let Some(batcher) = self.table_batcher(&table_info, ctx) {
706            return self
707                .submit_table_rows(rows, table_info, ctx.clone(), batcher)
708                .await;
709        }
710        let inserts = converter.partition(rows, table_info.clone(), ctx).await?;
711
712        let table_infos = HashMap::from_iter([(table_info.table_id(), table_info.clone())]);
713
714        self.do_request(inserts, &table_infos, ctx).await
715    }
716}
717
718/// Admits a finite request before it is split into internal writes.
719/// The returned context preserves accounting while preventing a second row debit.
720pub async fn admit_write(rows: u64, ctx: &QueryContextRef) -> Result<QueryContextRef> {
721    // The zero value is WCU: this record only admits rows. Actual inserts retain
722    // their existing WCU accounting, so charging here would count it twice.
723    write_meter!(MeterRecord::new(
724        ctx.current_catalog().to_string(),
725        ctx.current_schema(),
726        0,
727        ctx.write_rows_to_admit(ctx.current_catalog(), &ctx.current_schema(), rows),
728        ctx.channel() as u8,
729    ))
730    .await
731    .context(WriteRejectedSnafu)?;
732    Ok(Arc::new(ctx.with_write_admission()))
733}
734
735/// Admits all database totals before dispatching any batch of a finite request.
736/// Each batch keeps its own protocol options and target database.
737pub async fn admit_row_insert_batches(
738    batches: &mut [(QueryContextRef, RowInsertRequests)],
739) -> Result<()> {
740    let mut totals = BTreeMap::<_, (QueryContextRef, u64)>::new();
741    for (ctx, requests) in batches.iter() {
742        let catalog = ctx.current_catalog();
743        let schema = ctx.current_schema();
744        if ctx.write_rows_to_admit(catalog, &schema, 1) == 0 {
745            continue;
746        }
747        let (_, total) = totals
748            .entry((catalog.to_string(), schema.clone()))
749            .or_insert_with(|| (ctx.clone(), 0));
750        for rows in requests.inserts.iter().filter_map(|r| r.rows.as_ref()) {
751            *total =
752                total
753                    .checked_add(rows.rows.len() as u64)
754                    .context(InvalidInsertRequestSnafu {
755                        reason: "Insert row count exceeds u64::MAX",
756                    })?;
757        }
758    }
759    for (ctx, rows) in totals.values() {
760        admit_write(*rows, ctx).await?;
761    }
762    for (ctx, _) in batches {
763        *ctx = Arc::new(ctx.with_write_admission());
764    }
765    Ok(())
766}
767
768fn count_insert_rows(requests: &InstantAndNormalInsertRequests) -> Result<u64> {
769    requests
770        .normal_requests
771        .requests
772        .iter()
773        .chain(&requests.instant_requests.requests)
774        .filter_map(|request| request.rows.as_ref())
775        .try_fold(0u64, |total, rows| {
776            total
777                .checked_add(rows.rows.len() as u64)
778                .context(InvalidInsertRequestSnafu {
779                    reason: "Insert row count exceeds u64::MAX",
780                })
781        })
782}
783
784impl Inserter {
785    async fn do_request(
786        &self,
787        requests: InstantAndNormalInsertRequests,
788        table_infos: &HashMap<TableId, Arc<TableInfo>>,
789        ctx: &QueryContextRef,
790    ) -> Result<Output> {
791        // Fill impure default values in the request
792        let requests = fill_reqs_with_impure_default(table_infos, requests)?;
793
794        // All tables in a batch resolve to the same database. Qualified SQL
795        // inserts may target a different database than the session's current one.
796        let table_info = table_infos.values().next();
797        let catalog = table_info.map_or(ctx.current_catalog(), |info| info.catalog_name.as_str());
798        let schema =
799            table_info.map_or_else(|| ctx.current_schema(), |info| info.schema_name.clone());
800        let write_cost = write_meter!(
801            catalog,
802            schema.clone(),
803            requests,
804            ctx.write_rows_to_admit(catalog, &schema, count_insert_rows(&requests)?),
805            ctx.channel() as u8
806        )
807        .await
808        .context(WriteRejectedSnafu)?;
809        let request_factory = RegionRequestFactory::new(RegionRequestHeader {
810            tracing_context: TracingContext::from_current_span().to_w3c(),
811            dbname: ctx.get_db_string(),
812            ..Default::default()
813        });
814
815        let InstantAndNormalInsertRequests {
816            normal_requests,
817            instant_requests,
818        } = requests;
819
820        // Mirror requests for source table to flownode asynchronously
821        let flow_mirror_task = FlowMirrorTask::new(
822            &self.table_flownode_set_cache,
823            normal_requests
824                .requests
825                .iter()
826                .chain(instant_requests.requests.iter()),
827        )
828        .await?;
829        let has_instant_rows = instant_requests.requests.iter().any(|request| {
830            request
831                .rows
832                .as_ref()
833                .is_some_and(|rows| !rows.rows.is_empty())
834        });
835        flow_mirror_task.detach(
836            self.node_manager.clone(),
837            self.mirror_pending_rows.clone(),
838            has_instant_rows,
839        )?;
840
841        let dispatcher = self.write_dispatcher.clone();
842        let request = request_factory.build_insert(normal_requests);
843        let writes = dispatcher.prepare(vec![request]).await?;
844        let affected_rows =
845            common_runtime::spawn_global(async move { dispatcher.execute(writes, None).await })
846                .await
847                .context(JoinTaskSnafu)?
848                .context(RequestInsertsSnafu)?
849                .affected_rows;
850        crate::metrics::DIST_INGEST_ROW_COUNT
851            .with_label_values(&[ctx.get_db_string().as_str()])
852            .inc_by(affected_rows as u64);
853        Ok(Output::new(
854            OutputData::AffectedRows(affected_rows),
855            OutputMeta::new_with_cost(write_cost as _),
856        ))
857    }
858
859    /// Returns `Some(reason)` if the config or request hint disables automatic
860    /// table creation. Exempt private system tables are handled by
861    /// [`Self::is_auto_create_exempt_private_table`].
862    fn auto_create_disabled_reason(&self, ctx: &QueryContextRef) -> Result<Option<&'static str>> {
863        let auto_create_table_hint = ctx
864            .extension(AUTO_CREATE_TABLE_KEY)
865            .map(|v| v.parse::<bool>())
866            .transpose()
867            .map_err(|_| {
868                InvalidInsertRequestSnafu {
869                    reason: "`auto_create_table` hint must be a boolean",
870                }
871                .build()
872            })?
873            .unwrap_or(true);
874        Ok(if !self.auto_create_table {
875            Some("auto-create table is disabled by frontend config")
876        } else if !auto_create_table_hint {
877            Some("`auto_create_table` hint is disabled")
878        } else {
879            None
880        })
881    }
882
883    /// Returns whether a private system table may infer and reconcile its schema
884    /// even when automatic table creation is disabled.
885    fn is_auto_create_exempt_private_table(schema: &str, table: &str) -> bool {
886        schema == DEFAULT_PRIVATE_SCHEMA_NAME
887            && matches!(
888                table,
889                DEFAULT_EVENTS_TABLE_NAME | SLOW_QUERY_TABLE_NAME | REGION_STATS_HISTORY_TABLE_NAME
890            )
891    }
892
893    /// Adds missing columns from a bulk stream's schema and returns the refreshed table.
894    /// Call once when initializing the stream, before writing its first batch.
895    /// Does not infer new nested or dictionary columns.
896    pub async fn ensure_bulk_insert_schema(
897        &self,
898        table: TableRef,
899        request_schema: &ArrowSchema,
900        ctx: &QueryContextRef,
901        statement_executor: &StatementExecutor,
902    ) -> Result<TableRef> {
903        let table_info = table.table_info();
904        if self.auto_create_disabled_reason(ctx)?.is_some()
905            && !Self::is_auto_create_exempt_private_table(&table_info.schema_name, &table_info.name)
906        {
907            return Ok(table);
908        }
909
910        let table_schema = table.schema();
911        let schema = request_schema
912            .fields()
913            .iter()
914            .filter(|field| table_schema.column_schema_by_name(field.name()).is_none())
915            .map(|field| {
916                let data_type = field.data_type();
917                // Dictionary values can reach the same infallible child-type conversion
918                // as nested types, even when Arrow's is_nested() returns false.
919                ensure!(
920                    !data_type.is_nested() && !matches!(data_type, ArrowDataType::Dictionary(..)),
921                    crate::error::NotSupportedSnafu {
922                        feat: format!(
923                            "automatically adding bulk insert column '{}' with type {:?}",
924                            field.name(),
925                            data_type
926                        ),
927                    }
928                );
929                let column = datatypes::schema::ColumnSchema::try_from(field.as_ref())
930                    .context(crate::error::ConvertSchemaSnafu)?;
931                // Arrow fields do not carry primary-key semantics. New columns are
932                // fields, unless explicitly marked as a time index.
933                let column_def =
934                    try_as_column_def(&column, false).context(crate::error::ColumnDataTypeSnafu)?;
935                Ok(ColumnSchema {
936                    column_name: column_def.name,
937                    datatype: column_def.data_type,
938                    semantic_type: column_def.semantic_type,
939                    datatype_extension: column_def.datatype_extension,
940                    options: column_def.options,
941                })
942            })
943            .collect::<Result<Vec<_>>>()?;
944        let mut request = RowInsertRequest {
945            table_name: table_info.name.clone(),
946            rows: Some(Rows {
947                schema,
948                rows: Vec::new(),
949            }),
950        };
951        let Some(alter_expr) =
952            self.get_alter_table_expr_on_demand(&mut request, &table, ctx, false, false, true)?
953        else {
954            return Ok(table);
955        };
956
957        statement_executor
958            .alter_table_inner(alter_expr, ctx.clone(), TriggerReason::AutoAlter)
959            .await?;
960        self.get_table(
961            &table_info.catalog_name,
962            &table_info.schema_name,
963            &table_info.name,
964        )
965        .await?
966        .with_context(|| TableNotFoundSnafu {
967            table_name: table_info.full_table_name(),
968        })
969    }
970
971    /// Ensures a trace table has the request-global schema without requiring a
972    /// padded data row to drive on-demand creation or alteration. When
973    /// `alter_existing` is false, a table created after planning is left for the
974    /// caller to re-plan.
975    pub async fn ensure_trace_table_on_demand(
976        &self,
977        table_name: &str,
978        request_schema: Vec<ColumnSchema>,
979        alter_existing: bool,
980        ctx: &QueryContextRef,
981        statement_executor: &StatementExecutor,
982    ) -> Result<()> {
983        let mut requests = RowInsertRequests {
984            inserts: vec![RowInsertRequest {
985                table_name: table_name.to_string(),
986                rows: Some(api::v1::Rows {
987                    schema: request_schema,
988                    rows: Vec::new(),
989                }),
990            }],
991        };
992        self.create_or_alter_tables_on_demand(
993            &mut requests,
994            ctx,
995            AutoCreateTableType::Trace { alter_existing },
996            statement_executor,
997            false,
998            false,
999            None,
1000        )
1001        .await?;
1002        Ok(())
1003    }
1004
1005    /// Creates or alter tables on demand:
1006    /// - if table does not exist, create table by inferred CreateExpr
1007    /// - if table exist, check if schema matches. If any new column found, alter table by inferred `AlterExpr`
1008    ///
1009    /// Returns a mapping from table name to table id, where table name is the table name involved in the requests.
1010    /// This mapping is used in the conversion of RowToRegion.
1011    ///
1012    /// `accommodate_existing_schema` is used to determine if the existing schema should override the new schema.
1013    /// It only works for TIME_INDEX and single VALUE columns. This is for the case where the user creates a table with
1014    /// custom schema, and then inserts data with endpoints that have default schema setting, like prometheus
1015    /// remote write. This will modify the `RowInsertRequests` in place.
1016    /// `is_single_value` indicates whether the default schema only contains single value column so we can accommodate it.
1017    ///
1018    /// `align_time_index_unit` is the selected physical metric table's time
1019    /// index unit; passing `Some` (metric engine path only) rewrites each
1020    /// request's time index column, inside this function's existing table
1021    /// lookups (no extra catalog access): existing destination tables are
1022    /// converted to their own unit, and new tables to the given unit.
1023    #[allow(clippy::too_many_arguments)]
1024    async fn create_or_alter_tables_on_demand(
1025        &self,
1026        requests: &mut RowInsertRequests,
1027        ctx: &QueryContextRef,
1028        auto_create_table_type: AutoCreateTableType,
1029        statement_executor: &StatementExecutor,
1030        accommodate_existing_schema: bool,
1031        is_single_value: bool,
1032        align_time_index_unit: Option<TimeUnit>,
1033    ) -> Result<CreateAlterTableResult> {
1034        let _timer = crate::metrics::CREATE_ALTER_ON_DEMAND
1035            .with_label_values(&[auto_create_table_type.as_str()])
1036            .start_timer();
1037        let catalog = ctx.current_catalog();
1038        let schema = ctx.current_schema();
1039
1040        let auto_create_disabled_reason = self.auto_create_disabled_reason(ctx)?;
1041        // Enabled batches permit every table, so only disabled batches need a whitelist scan.
1042        let has_auto_create_exempt_table = auto_create_disabled_reason.is_some()
1043            && requests
1044                .inserts
1045                .iter()
1046                .any(|req| Self::is_auto_create_exempt_private_table(&schema, &req.table_name));
1047        let mut table_infos = HashMap::new();
1048        // Without exempt tables, verify existing tables and reject missing ones without inferring schemas.
1049        if let Some(disabled_reason) = auto_create_disabled_reason
1050            && !has_auto_create_exempt_table
1051        {
1052            let mut instant_table_ids = HashSet::new();
1053            for req in &mut requests.inserts {
1054                let table = match self.get_table(catalog, &schema, &req.table_name).await? {
1055                    Some(table) => table,
1056                    // System-defined table: created canonically by the system,
1057                    // so the auto-create config/hint does not apply.
1058                    None if is_ddl_reserved_table(&schema, &req.table_name) => {
1059                        statement_executor
1060                            .create_declared_relationships_table(catalog, ctx.clone())
1061                            .await?
1062                    }
1063                    None => {
1064                        return InvalidInsertRequestSnafu {
1065                            reason: format!(
1066                                "Table `{}` does not exist, and {}",
1067                                req.table_name, disabled_reason
1068                            ),
1069                        }
1070                        .fail();
1071                    }
1072                };
1073                // Metric path: an existing destination table keeps its own
1074                // time index unit (it may be bound to another physical
1075                // table than the one selected by this request).
1076                if align_time_index_unit.is_some()
1077                    && let Some(rows) = req.rows.as_mut()
1078                    && let Some(target_unit) = table_time_index_unit(&table)
1079                {
1080                    convert_rows_time_unit(rows, target_unit)?;
1081                }
1082                let table_info = table.table_info();
1083                if matches!(auto_create_table_type, AutoCreateTableType::Trace { .. }) {
1084                    validate_trace_table_model(&table_info, ctx)?;
1085                }
1086                if table_info.is_ttl_instant_table() {
1087                    instant_table_ids.insert(table_info.table_id());
1088                }
1089                table_infos.insert(table_info.table_id(), table.table_info());
1090            }
1091            let ret = CreateAlterTableResult {
1092                instant_table_ids,
1093                table_infos,
1094            };
1095            return Ok(ret);
1096        }
1097
1098        let mut create_tables = vec![];
1099        let mut alter_tables = vec![];
1100        let mut need_refresh_table_infos = HashSet::new();
1101        let mut instant_table_ids = HashSet::new();
1102        let mut per_table_semantics: Option<Option<PerTableSemanticIndex>> = None;
1103
1104        for req in &mut requests.inserts {
1105            // Mixed batches need a per-table decision so an exempt table cannot authorize others.
1106            let auto_create_allowed = auto_create_disabled_reason.is_none()
1107                || Self::is_auto_create_exempt_private_table(&schema, &req.table_name);
1108            match self.get_table(catalog, &schema, &req.table_name).await? {
1109                Some(table) => {
1110                    let table_info = table.table_info();
1111                    if matches!(auto_create_table_type, AutoCreateTableType::Trace { .. }) {
1112                        validate_trace_table_model(&table_info, ctx)?;
1113                    }
1114                    if table_info.is_ttl_instant_table() {
1115                        instant_table_ids.insert(table_info.table_id());
1116                    }
1117                    // Metric path: an existing destination table keeps its
1118                    // own time index unit (it may be bound to another
1119                    // physical table than the one selected by this request).
1120                    if align_time_index_unit.is_some()
1121                        && let Some(rows) = req.rows.as_mut()
1122                        && let Some(target_unit) = table_time_index_unit(&table)
1123                    {
1124                        convert_rows_time_unit(rows, target_unit)?;
1125                    }
1126                    if auto_create_allowed
1127                        && let Some(alter_expr) = self.get_alter_table_expr_on_demand(
1128                            req,
1129                            &table,
1130                            ctx,
1131                            accommodate_existing_schema,
1132                            is_single_value,
1133                            auto_create_table_type.alter_existing(),
1134                        )?
1135                    {
1136                        alter_tables.push(alter_expr);
1137                        need_refresh_table_infos.insert((
1138                            catalog.to_string(),
1139                            schema.clone(),
1140                            req.table_name.clone(),
1141                        ));
1142                    } else {
1143                        table_infos.insert(table_info.table_id(), table.table_info());
1144                    }
1145                }
1146                // A DDL-reserved table's definition never derives from the
1147                // write request; the system creates it canonically, below the
1148                // user-DDL guard that rejects the generic create path.
1149                None if is_ddl_reserved_table(&schema, &req.table_name) => {
1150                    let table = statement_executor
1151                        .create_declared_relationships_table(catalog, ctx.clone())
1152                        .await?;
1153                    let table_info = table.table_info();
1154                    if table_info.is_ttl_instant_table() {
1155                        instant_table_ids.insert(table_info.table_id());
1156                    }
1157                    table_infos.insert(table_info.table_id(), table_info);
1158                }
1159                None if !auto_create_allowed
1160                    && let Some(disabled_reason) = auto_create_disabled_reason =>
1161                {
1162                    return InvalidInsertRequestSnafu {
1163                        reason: format!(
1164                            "Table `{}` does not exist, and {}",
1165                            req.table_name, disabled_reason,
1166                        ),
1167                    }
1168                    .fail();
1169                }
1170                None => {
1171                    // Metric path: a new table uses the selected physical
1172                    // table's unit; convert before the create expression is
1173                    // derived from the request schema.
1174                    if let Some(physical_unit) = align_time_index_unit
1175                        && let Some(rows) = req.rows.as_mut()
1176                    {
1177                        convert_rows_time_unit(rows, physical_unit)?;
1178                    }
1179                    let semantic_index = per_table_semantics
1180                        .get_or_insert_with(|| parse_per_table_semantic_index(ctx))
1181                        .as_ref();
1182                    let create_expr = self.get_create_table_expr_on_demand(
1183                        req,
1184                        &auto_create_table_type,
1185                        ctx,
1186                        semantic_index,
1187                    )?;
1188                    create_tables.push(create_expr);
1189                }
1190            }
1191        }
1192
1193        match auto_create_table_type {
1194            AutoCreateTableType::Logical(_) => {
1195                if !create_tables.is_empty() {
1196                    // Creates logical tables in batch.
1197                    let tables = self
1198                        .create_logical_tables(create_tables, ctx, statement_executor)
1199                        .await?;
1200
1201                    for table in tables {
1202                        let table_info = table.table_info();
1203                        if table_info.is_ttl_instant_table() {
1204                            instant_table_ids.insert(table_info.table_id());
1205                        }
1206                        table_infos.insert(table_info.table_id(), table.table_info());
1207                    }
1208                }
1209                if !alter_tables.is_empty() {
1210                    // Alter logical tables in batch.
1211                    statement_executor
1212                        .alter_logical_tables(alter_tables, ctx.clone(), TriggerReason::AutoAlter)
1213                        .await?;
1214                }
1215            }
1216            AutoCreateTableType::Physical
1217            | AutoCreateTableType::Log
1218            | AutoCreateTableType::LastNonNull => {
1219                // note that auto create table shouldn't be ttl instant table
1220                // for it's a very unexpected behavior and should be set by user explicitly
1221                for create_table in create_tables {
1222                    let table = self
1223                        .create_physical_table(create_table, None, ctx, statement_executor)
1224                        .await?;
1225                    let table_info = table.table_info();
1226                    if table_info.is_ttl_instant_table() {
1227                        instant_table_ids.insert(table_info.table_id());
1228                    }
1229                    table_infos.insert(table_info.table_id(), table.table_info());
1230                }
1231                for alter_expr in alter_tables.into_iter() {
1232                    statement_executor
1233                        .alter_table_inner(alter_expr, ctx.clone(), TriggerReason::AutoAlter)
1234                        .await?;
1235                }
1236            }
1237
1238            AutoCreateTableType::Trace { .. } => {
1239                let trace_table_name = ctx
1240                    .extension(TRACE_TABLE_NAME_SESSION_KEY)
1241                    .unwrap_or(TRACE_TABLE_NAME);
1242
1243                let trace_table_partitions = if let Some(trace_table_partitions) =
1244                    ctx.extension(TRACE_TABLE_PARTITIONS_HINT_KEY)
1245                {
1246                    let p = trace_table_partitions.parse::<u32>().map_err(|_| {
1247                        InvalidInsertRequestSnafu {
1248                            reason: format!(
1249                                "Failed to parse trace_table_partitions: {}",
1250                                trace_table_partitions
1251                            ),
1252                        }
1253                        .build()
1254                    })?;
1255                    Some(p)
1256                } else {
1257                    None
1258                };
1259
1260                // note that auto create table shouldn't be ttl instant table
1261                // for it's a very unexpected behavior and should be set by user explicitly
1262                for mut create_table in create_tables {
1263                    if create_table.table_name == trace_services_table_name(trace_table_name)
1264                        || create_table.table_name == trace_operations_table_name(trace_table_name)
1265                    {
1266                        // Disable append mode for auxiliary tables (services/operations) since they require upsert behavior.
1267                        create_table
1268                            .table_options
1269                            .insert(APPEND_MODE_KEY.to_string(), "false".to_string());
1270                        // Remove `ttl` key from table options if it exists
1271                        create_table.table_options.remove(TTL_KEY);
1272
1273                        let table = self
1274                            .create_physical_table(create_table, None, ctx, statement_executor)
1275                            .await?;
1276                        let table_info = table.table_info();
1277                        if table_info.is_ttl_instant_table() {
1278                            instant_table_ids.insert(table_info.table_id());
1279                        }
1280                        table_infos.insert(table_info.table_id(), table.table_info());
1281                    } else {
1282                        // prebuilt partition rules for uuid data: see the function
1283                        // for more information
1284                        let partitions = if matches!(trace_table_partitions, Some(0) | Some(1)) {
1285                            // disable partitions
1286                            None
1287                        } else {
1288                            let p = partition_rule_for_hexstring(
1289                                TRACE_ID_COLUMN,
1290                                trace_table_partitions,
1291                            )
1292                            .context(CreatePartitionRulesSnafu)?;
1293                            Some(p)
1294                        };
1295
1296                        // add skip index to
1297                        // - trace_id: when searching by trace id
1298                        // - parent_span_id: when searching root span
1299                        // - span_name: when searching certain types of span
1300                        let index_columns =
1301                            [TRACE_ID_COLUMN, PARENT_SPAN_ID_COLUMN, SERVICE_NAME_COLUMN];
1302                        for index_column in index_columns {
1303                            if let Some(col) = create_table
1304                                .column_defs
1305                                .iter_mut()
1306                                .find(|c| c.name == index_column)
1307                            {
1308                                col.options =
1309                                    options_from_skipping(&SkippingIndexOptions::default())
1310                                        .context(ColumnOptionsSnafu)?;
1311                            } else {
1312                                warn!(
1313                                    "Column {} not found when creating index for trace table: {}.",
1314                                    index_column, create_table.table_name
1315                                );
1316                            }
1317                        }
1318
1319                        // use table_options to mark table model version
1320                        create_table.table_options.insert(
1321                            TABLE_DATA_MODEL.to_string(),
1322                            ctx.extension(SEMANTIC_PIPELINE)
1323                                .unwrap_or(TABLE_DATA_MODEL_TRACE_V1)
1324                                .to_string(),
1325                        );
1326
1327                        let table = self
1328                            .create_physical_table(
1329                                create_table,
1330                                partitions,
1331                                ctx,
1332                                statement_executor,
1333                            )
1334                            .await?;
1335                        let table_info = table.table_info();
1336                        if table_info.is_ttl_instant_table() {
1337                            instant_table_ids.insert(table_info.table_id());
1338                        }
1339                        table_infos.insert(table_info.table_id(), table.table_info());
1340                    }
1341                }
1342                for alter_expr in alter_tables.into_iter() {
1343                    statement_executor
1344                        .alter_table_inner(alter_expr, ctx.clone(), TriggerReason::AutoAlter)
1345                        .await?;
1346                }
1347            }
1348        }
1349
1350        // refresh table infos for altered tables
1351        for (catalog, schema, table_name) in need_refresh_table_infos {
1352            let table = self
1353                .get_table(&catalog, &schema, &table_name)
1354                .await?
1355                .context(TableNotFoundSnafu {
1356                    table_name: common_catalog::format_full_table_name(
1357                        &catalog,
1358                        &schema,
1359                        &table_name,
1360                    ),
1361                })?;
1362            let table_info = table.table_info();
1363            table_infos.insert(table_info.table_id(), table.table_info());
1364        }
1365
1366        Ok(CreateAlterTableResult {
1367            instant_table_ids,
1368            table_infos,
1369        })
1370    }
1371
1372    async fn create_physical_table_on_demand(
1373        &self,
1374        ctx: &QueryContextRef,
1375        physical_table: String,
1376        statement_executor: &StatementExecutor,
1377    ) -> Result<TableRef> {
1378        let catalog_name = ctx.current_catalog();
1379        let schema_name = ctx.current_schema();
1380
1381        // check if exist
1382        if let Some(table) = self
1383            .get_table(catalog_name, &schema_name, &physical_table)
1384            .await?
1385        {
1386            return Ok(table);
1387        }
1388
1389        // Gate here too, otherwise a disabled switch would still leak the physical table.
1390        if let Some(disabled_reason) = self.auto_create_disabled_reason(ctx)? {
1391            return InvalidInsertRequestSnafu {
1392                reason: format!(
1393                    "Physical table `{physical_table}` does not exist, and {disabled_reason}"
1394                ),
1395            }
1396            .fail();
1397        }
1398
1399        let table_reference = TableReference::full(catalog_name, &schema_name, &physical_table);
1400        debug!("Ensuring physical metric table `{table_reference}` exists for insert");
1401
1402        // schema with timestamp and field column
1403        let default_schema = vec![
1404            ColumnSchema {
1405                column_name: greptime_timestamp().to_string(),
1406                datatype: ColumnDataType::TimestampMillisecond as _,
1407                semantic_type: SemanticType::Timestamp as _,
1408                datatype_extension: None,
1409                options: None,
1410            },
1411            ColumnSchema {
1412                column_name: greptime_value().to_string(),
1413                datatype: ColumnDataType::Float64 as _,
1414                semantic_type: SemanticType::Field as _,
1415                datatype_extension: None,
1416                options: None,
1417            },
1418        ];
1419        let create_table_expr =
1420            &mut build_create_table_expr(&table_reference, &default_schema, default_engine())?;
1421
1422        create_table_expr.engine = METRIC_ENGINE_NAME.to_string();
1423        create_table_expr
1424            .table_options
1425            .insert(PHYSICAL_TABLE_METADATA_KEY.to_string(), "true".to_string());
1426
1427        // create physical table
1428        let res = statement_executor
1429            .create_table_inner(
1430                create_table_expr,
1431                None,
1432                ctx.clone(),
1433                TriggerReason::AutoCreate,
1434            )
1435            .await;
1436
1437        match res {
1438            Ok(table) => Ok(table),
1439            Err(err) => {
1440                error!(err; "Failed to create table {table_reference}");
1441                Err(err)
1442            }
1443        }
1444    }
1445
1446    async fn get_table(
1447        &self,
1448        catalog: &str,
1449        schema: &str,
1450        table: &str,
1451    ) -> Result<Option<TableRef>> {
1452        self.catalog_manager
1453            .table(catalog, schema, table, None)
1454            .await
1455            .context(CatalogSnafu)
1456    }
1457
1458    fn get_create_table_expr_on_demand(
1459        &self,
1460        req: &RowInsertRequest,
1461        create_type: &AutoCreateTableType,
1462        ctx: &QueryContextRef,
1463        semantic_index: Option<&PerTableSemanticIndex>,
1464    ) -> Result<CreateTableExpr> {
1465        let schema = ctx.current_schema();
1466        let mut table_options = std::collections::HashMap::with_capacity(4);
1467        fill_table_options_for_create(&mut table_options, create_type, ctx);
1468        apply_per_table_semantic_options(
1469            &mut table_options,
1470            semantic_index,
1471            ctx.current_schema().as_str(),
1472            &req.table_name,
1473        );
1474
1475        let engine_name = if let AutoCreateTableType::Logical(_) = create_type {
1476            // engine should be metric engine when creating logical tables.
1477            METRIC_ENGINE_NAME
1478        } else {
1479            default_engine()
1480        };
1481
1482        let table_ref = TableReference::full(ctx.current_catalog(), &schema, &req.table_name);
1483        // SAFETY: `req.rows` is guaranteed to be `Some` by `handle_row_inserts_with_create_type()`.
1484        let request_schema = req.rows.as_ref().unwrap().schema.as_slice();
1485        let mut create_table_expr =
1486            build_create_table_expr(&table_ref, request_schema, engine_name)?;
1487
1488        // extension set by the Splunk HEC handler for identity path
1489        if ctx.extension(SPLUNK_PK_METADATA_ORDER_KEY).is_some() {
1490            reorder_splunk_primary_keys(&mut create_table_expr.primary_keys);
1491        }
1492
1493        debug!("Ensuring table `{table_ref}` exists for insert");
1494        create_table_expr.table_options.extend(table_options);
1495        Ok(create_table_expr)
1496    }
1497
1498    /// Returns an alter table expression if it finds new columns in the request.
1499    /// When `accommodate_existing_schema` is false, it always adds columns if not exist.
1500    /// When `accommodate_existing_schema` is true, it may modify the input `req` to
1501    /// accommodate it with existing schema. See [`create_or_alter_tables_on_demand`](Self::create_or_alter_tables_on_demand)
1502    /// for more details.
1503    /// When `is_single_value` is true, it also rejects native-histogram/float kind changes.
1504    /// When both options are true, it considers fields when modifying the input `req`.
1505    fn get_alter_table_expr_on_demand(
1506        &self,
1507        req: &mut RowInsertRequest,
1508        table: &TableRef,
1509        ctx: &QueryContextRef,
1510        accommodate_existing_schema: bool,
1511        is_single_value: bool,
1512        alter_existing: bool,
1513    ) -> Result<Option<AlterTableExpr>> {
1514        if !alter_existing {
1515            return Ok(None);
1516        }
1517
1518        let catalog_name = ctx.current_catalog();
1519        let schema_name = ctx.current_schema();
1520        let table_name = table.table_info().name.clone();
1521
1522        // Never auto-alter a system-defined table to fit a write; a request
1523        // with unknown columns fails instead.
1524        if is_ddl_reserved_table(&schema_name, &table_name) {
1525            return Ok(None);
1526        }
1527
1528        let request_schema = req.rows.as_ref().unwrap().schema.as_slice();
1529        let request_field_count = request_schema
1530            .iter()
1531            .filter(|col| col.semantic_type == SemanticType::Field as i32)
1532            .count();
1533        let column_exprs = ColumnExpr::from_column_schemas(request_schema);
1534        let add_columns = expr_helper::extract_add_columns_expr(&table.schema(), column_exprs)?;
1535        let Some(mut add_columns) = add_columns else {
1536            return Ok(None);
1537        };
1538
1539        if is_single_value {
1540            let request_is_native_histogram = request_is_native_histogram(request_schema);
1541            let table_is_native_histogram = table_is_native_histogram(table);
1542            ensure!(
1543                request_is_native_histogram == table_is_native_histogram,
1544                InvalidInsertRequestSnafu {
1545                    reason: format!(
1546                        "Table `{table_name}` cannot mix native histogram and float sample fields"
1547                    ),
1548                }
1549            );
1550        }
1551
1552        // If accommodate_existing_schema is true, update request schema for Timestamp/Field columns
1553        if accommodate_existing_schema {
1554            let table_schema = table.schema();
1555            // Find timestamp column name
1556            let ts_col_name = table_schema.timestamp_column().map(|c| c.name.clone());
1557            // Find field column name if there is only one and `is_single_value` is true.
1558            let mut field_col_name = None;
1559            if is_single_value && request_field_count <= 1 {
1560                let mut multiple_field_cols = false;
1561                table.field_columns().for_each(|col| {
1562                    if field_col_name.is_none() {
1563                        field_col_name = Some(col.name.clone());
1564                    } else {
1565                        multiple_field_cols = true;
1566                    }
1567                });
1568                if multiple_field_cols {
1569                    field_col_name = None;
1570                }
1571            }
1572
1573            // Update column name in request schema for Timestamp/Field columns
1574            if let Some(rows) = req.rows.as_mut() {
1575                for col in &mut rows.schema {
1576                    match col.semantic_type {
1577                        x if x == SemanticType::Timestamp as i32 => {
1578                            if let Some(ref ts_name) = ts_col_name
1579                                && col.column_name != *ts_name
1580                            {
1581                                col.column_name = ts_name.clone();
1582                            }
1583                        }
1584                        x if x == SemanticType::Field as i32 => {
1585                            if let Some(ref field_name) = field_col_name
1586                                && col.column_name != *field_name
1587                            {
1588                                col.column_name = field_name.clone();
1589                            }
1590                        }
1591                        _ => {}
1592                    }
1593                }
1594            }
1595
1596            // Only keep columns that are tags or non-single field.
1597            add_columns.add_columns.retain(|col| {
1598                let def = col.column_def.as_ref().unwrap();
1599                def.semantic_type == SemanticType::Tag as i32
1600                    || (def.semantic_type == SemanticType::Field as i32 && field_col_name.is_none())
1601            });
1602
1603            if add_columns.add_columns.is_empty() {
1604                return Ok(None);
1605            }
1606        }
1607
1608        Ok(Some(AlterTableExpr {
1609            catalog_name: catalog_name.to_string(),
1610            schema_name: schema_name.clone(),
1611            table_name: table_name.clone(),
1612            kind: Some(Kind::AddColumns(add_columns)),
1613        }))
1614    }
1615
1616    /// Creates a table with options.
1617    async fn create_physical_table(
1618        &self,
1619        mut create_table_expr: CreateTableExpr,
1620        partitions: Option<Partitions>,
1621        ctx: &QueryContextRef,
1622        statement_executor: &StatementExecutor,
1623    ) -> Result<TableRef> {
1624        let res = statement_executor
1625            .create_table_inner(
1626                &mut create_table_expr,
1627                partitions,
1628                ctx.clone(),
1629                TriggerReason::AutoCreate,
1630            )
1631            .await;
1632
1633        let table_ref = TableReference::full(
1634            &create_table_expr.catalog_name,
1635            &create_table_expr.schema_name,
1636            &create_table_expr.table_name,
1637        );
1638
1639        match res {
1640            Ok(table) => {
1641                validate_trace_table_model(&table.table_info(), ctx)?;
1642                Ok(table)
1643            }
1644            Err(err) => {
1645                error!(err; "Failed to create table {}", table_ref);
1646                Err(err)
1647            }
1648        }
1649    }
1650
1651    async fn create_logical_tables(
1652        &self,
1653        create_table_exprs: Vec<CreateTableExpr>,
1654        ctx: &QueryContextRef,
1655        statement_executor: &StatementExecutor,
1656    ) -> Result<Vec<TableRef>> {
1657        let res = statement_executor
1658            .create_logical_tables(&create_table_exprs, ctx.clone(), TriggerReason::AutoCreate)
1659            .await;
1660
1661        match res {
1662            Ok(res) => Ok(res),
1663            Err(err) => {
1664                let failed_tables = create_table_exprs
1665                    .into_iter()
1666                    .map(|expr| {
1667                        format!(
1668                            "{}.{}.{}",
1669                            expr.catalog_name, expr.schema_name, expr.table_name
1670                        )
1671                    })
1672                    .collect::<Vec<_>>();
1673                error!(
1674                    err;
1675                    "Failed to create logical tables {:?}",
1676                    failed_tables
1677                );
1678                Err(err)
1679            }
1680        }
1681    }
1682
1683    /// Returns the dispatcher shared with batching execution paths.
1684    pub fn write_dispatcher(&self) -> &WriteDispatcherRef {
1685        &self.write_dispatcher
1686    }
1687
1688    pub fn node_manager(&self) -> &NodeManagerRef {
1689        &self.node_manager
1690    }
1691
1692    pub fn partition_manager(&self) -> &PartitionRuleManagerRef {
1693        &self.partition_manager
1694    }
1695
1696    pub fn table_flownode_set_cache(&self) -> &TableFlownodeSetCacheRef {
1697        &self.table_flownode_set_cache
1698    }
1699}
1700
1701fn request_is_native_histogram(request_schema: &[ColumnSchema]) -> bool {
1702    let mut fields = request_schema
1703        .iter()
1704        .filter(|col| col.semantic_type == SemanticType::Field as i32);
1705    let Some(col) = fields.next() else {
1706        return false;
1707    };
1708
1709    fields.next().is_none()
1710        && api::helper::is_column_type_value_eq(
1711            col.datatype,
1712            col.datatype_extension.clone(),
1713            native_histogram_value_type(),
1714        )
1715}
1716
1717/// Returns the table's time index unit, if any. A metric table without a
1718/// timestamp column is left alone by the unit alignment: the metric engine
1719/// rejects it anyway.
1720fn table_time_index_unit(table: &TableRef) -> Option<TimeUnit> {
1721    table
1722        .table_info()
1723        .meta
1724        .schema
1725        .timestamp_column()
1726        .and_then(|col| col.data_type.as_timestamp().map(|ts| ts.unit()))
1727}
1728
1729fn convert_rows_time_unit(rows: &mut Rows, target_unit: TimeUnit) -> Result<()> {
1730    let Some(ts_index) = rows
1731        .schema
1732        .iter()
1733        .position(|col| col.semantic_type == SemanticType::Timestamp as i32)
1734    else {
1735        return Ok(());
1736    };
1737    let Some(source_unit) = ColumnDataType::try_from(rows.schema[ts_index].datatype)
1738        .ok()
1739        .and_then(api::helper::timestamp_unit)
1740    else {
1741        return Ok(());
1742    };
1743    if source_unit == target_unit {
1744        return Ok(());
1745    }
1746
1747    rows.schema[ts_index].datatype = api::helper::timestamp_datatype(target_unit) as i32;
1748    // Timestamp columns never carry a datatype extension.
1749    rows.schema[ts_index].datatype_extension = None;
1750
1751    // Note: the schema is rewritten before the rows are converted, so an
1752    // overflow error mid-batch leaves this request half-converted. That is
1753    // harmless: the error aborts the whole insert request.
1754    //
1755    // `validate_column_count_match` guarantees every row carries exactly one
1756    // value per schema column, so the time index position is directly in
1757    // bounds; no per-value search is needed.
1758    for row in &mut rows.rows {
1759        debug_assert_eq!(row.values.len(), rows.schema.len());
1760        let value = &mut row.values[ts_index];
1761        let Some(value_data) = value.value_data.take() else {
1762            continue;
1763        };
1764        value.value_data =
1765            convert_timestamp_value_data(value_data, source_unit, target_unit, ts_index)?;
1766    }
1767    Ok(())
1768}
1769
1770fn convert_timestamp_value_data(
1771    value_data: ValueData,
1772    source_unit: TimeUnit,
1773    target_unit: TimeUnit,
1774    column_index: usize,
1775) -> Result<Option<ValueData>> {
1776    let timestamp = match value_data {
1777        ValueData::TimestampSecondValue(v) => Timestamp::new_second(v),
1778        ValueData::TimestampMillisecondValue(v) => Timestamp::new_millisecond(v),
1779        ValueData::TimestampMicrosecondValue(v) => Timestamp::new_microsecond(v),
1780        ValueData::TimestampNanosecondValue(v) => Timestamp::new_nanosecond(v),
1781        // Null or non-timestamp value; nothing to convert.
1782        other => return Ok(Some(other)),
1783    };
1784    let converted = timestamp
1785        .convert_to(target_unit)
1786        .with_context(|| InvalidInsertRequestSnafu {
1787            reason: format!(
1788                "timestamp column {column_index} value {} in unit {source_unit:?} overflows when converting to unit {target_unit:?}",
1789                timestamp.value()
1790            ),
1791        })?;
1792    Ok(api::helper::to_grpc_value(datatypes::value::Value::Timestamp(converted)).value_data)
1793}
1794
1795fn table_is_native_histogram(table: &TableRef) -> bool {
1796    let mut fields = table.field_columns();
1797    let Some(col) = fields.next() else {
1798        return false;
1799    };
1800
1801    fields.next().is_none() && is_native_histogram_value_type(&col.data_type)
1802}
1803
1804fn validate_column_count_match(requests: &RowInsertRequests) -> Result<()> {
1805    for request in &requests.inserts {
1806        let rows = request.rows.as_ref().unwrap();
1807        let column_count = rows.schema.len();
1808        rows.rows.iter().try_for_each(|r| {
1809            ensure!(
1810                r.values.len() == column_count,
1811                InvalidInsertRequestSnafu {
1812                    reason: format!(
1813                        "column count mismatch, columns: {}, values: {}",
1814                        column_count,
1815                        r.values.len()
1816                    )
1817                }
1818            );
1819            Ok(())
1820        })?;
1821    }
1822    Ok(())
1823}
1824
1825/// Rejects writes from a different built-in trace model before schema mutation.
1826/// Unstamped, explicitly created tables remain subject to normal schema validation.
1827pub fn validate_trace_table_model(table_info: &TableInfo, ctx: &QueryContextRef) -> Result<()> {
1828    let Some(expected @ (TABLE_DATA_MODEL_TRACE_V1 | TABLE_DATA_MODEL_TRACE_V2)) =
1829        ctx.extension(SEMANTIC_PIPELINE)
1830    else {
1831        return Ok(());
1832    };
1833    if let Some(actual) = table_info.meta.options.data_model() {
1834        ensure!(
1835            actual == expected,
1836            InvalidInsertRequestSnafu {
1837                reason: format!(
1838                    "Trace table `{}` uses {actual}, but the request uses {expected}",
1839                    table_info.name,
1840                ),
1841            }
1842        );
1843    }
1844    Ok(())
1845}
1846
1847/// Fill table options for a new table by create type.
1848pub fn fill_table_options_for_create(
1849    table_options: &mut std::collections::HashMap<String, String>,
1850    create_type: &AutoCreateTableType,
1851    ctx: &QueryContextRef,
1852) {
1853    for key in VALID_TABLE_OPTION_KEYS {
1854        if let Some(value) = ctx.extension(key) {
1855            table_options.insert(key.to_string(), value.to_string());
1856        }
1857    }
1858
1859    // Semantic keys use their own vocabulary instead of the fixed option list.
1860    for (key, value) in ctx.extensions() {
1861        if is_semantic_option_key(&key) && validate_semantic_option(&key, &value) {
1862            table_options.insert(key, value);
1863        }
1864    }
1865
1866    match create_type {
1867        AutoCreateTableType::Logical(physical_table) => {
1868            table_options.insert(
1869                LOGICAL_TABLE_METADATA_KEY.to_string(),
1870                physical_table.clone(),
1871            );
1872        }
1873        AutoCreateTableType::Physical => {
1874            if let Some(append_mode) = ctx.extension(APPEND_MODE_KEY) {
1875                table_options.insert(APPEND_MODE_KEY.to_string(), append_mode.to_string());
1876            }
1877            if let Some(merge_mode) = ctx.extension(MERGE_MODE_KEY) {
1878                table_options.insert(MERGE_MODE_KEY.to_string(), merge_mode.to_string());
1879            }
1880            if let Some(time_window) = ctx.extension(TWCS_TIME_WINDOW) {
1881                table_options.insert(TWCS_TIME_WINDOW.to_string(), time_window.to_string());
1882                // We need to set the compaction type explicitly.
1883                table_options.insert(
1884                    COMPACTION_TYPE.to_string(),
1885                    COMPACTION_TYPE_TWCS.to_string(),
1886                );
1887            }
1888        }
1889        // Set append_mode to true for log table.
1890        // because log tables should keep rows with the same ts and tags.
1891        AutoCreateTableType::Log => {
1892            table_options.insert(APPEND_MODE_KEY.to_string(), "true".to_string());
1893        }
1894        AutoCreateTableType::LastNonNull => {
1895            if ctx
1896                .extension(APPEND_MODE_KEY)
1897                .is_some_and(|value| value.eq_ignore_ascii_case("true"))
1898            {
1899                table_options.insert(APPEND_MODE_KEY.to_string(), "true".to_string());
1900                table_options.insert(MERGE_MODE_KEY.to_string(), "last_row".to_string());
1901            } else if let Some(merge_mode) = ctx.extension(MERGE_MODE_KEY) {
1902                table_options.insert(MERGE_MODE_KEY.to_string(), merge_mode.to_string());
1903            } else {
1904                table_options.insert(MERGE_MODE_KEY.to_string(), "last_non_null".to_string());
1905            }
1906        }
1907        AutoCreateTableType::Trace { .. } => {
1908            table_options.insert(APPEND_MODE_KEY.to_string(), "true".to_string());
1909        }
1910    }
1911}
1912
1913/// The parsed per-table semantic index: `{schema -> {table -> {key -> value}}}`,
1914/// produced by the OTLP metrics encode path (where one metric can fan out into
1915/// several tables with distinct keys) and the Prometheus remote write v2 path
1916/// (where per-series metadata declares type/unit, and a series may override its
1917/// target schema).
1918pub type PerTableSemanticIndex = BTreeMap<String, BTreeMap<String, BTreeMap<String, String>>>;
1919
1920/// Parses the per-table semantic index off the context extension. Call once per
1921/// create-planning round: a first write creating N tables would otherwise
1922/// re-parse the whole index N times. `None` when the request carries no index
1923/// (logs, traces, Prom RW v1) or it fails to parse.
1924pub fn parse_per_table_semantic_index(ctx: &QueryContextRef) -> Option<PerTableSemanticIndex> {
1925    let raw = ctx.extension(SEMANTIC_PER_TABLE_INDEX_KEY)?;
1926    match serde_json::from_str(raw) {
1927        Ok(index) => Some(index),
1928        Err(_) => {
1929            warn!("failed to parse semantic per-table index, skipping per-table options");
1930            None
1931        }
1932    }
1933}
1934
1935/// Folds the semantic keys of the table being created into `table_options`.
1936///
1937/// Common keys shared by every table in a request travel as plain semantic
1938/// extensions and are handled by [`fill_table_options_for_create`]; this
1939/// carries only the per-table tail and is applied after it, so a per-table
1940/// value (e.g. `declared` quality) wins. Keys are re-checked against the
1941/// vocabulary defensively.
1942pub fn apply_per_table_semantic_options(
1943    table_options: &mut std::collections::HashMap<String, String>,
1944    index: Option<&PerTableSemanticIndex>,
1945    schema: &str,
1946    table_name: &str,
1947) {
1948    let Some(entry) = index
1949        .and_then(|index| index.get(schema))
1950        .and_then(|tables| tables.get(table_name))
1951    else {
1952        return;
1953    };
1954    for (key, value) in entry {
1955        if is_semantic_option_key(key) && validate_semantic_option(key, value) {
1956            table_options.insert(key.clone(), value.clone());
1957        }
1958    }
1959}
1960
1961pub fn build_create_table_expr(
1962    table: &TableReference,
1963    request_schema: &[ColumnSchema],
1964    engine: &str,
1965) -> Result<CreateTableExpr> {
1966    expr_helper::create_table_expr_by_column_schemas(table, request_schema, engine, None)
1967}
1968
1969/// `QueryContext` extension key the Splunk HEC handler sets (to `"true"`) on its identity
1970/// path to request metadata-first primary-key ordering at table creation. It is absent for
1971/// user-supplied pipelines, so their primary-key order is left untouched.
1972pub const SPLUNK_PK_METADATA_ORDER_KEY: &str = "splunk_pk_metadata_order";
1973
1974/// Moves Splunk's metadata tags (`host`, `source`, `sourcetype`) to the front of the
1975/// primary key, keeping the relative order of the remaining tags.
1976fn reorder_splunk_primary_keys(primary_keys: &mut [String]) {
1977    const LEAD: [&str; 3] = ["host", "source", "sourcetype"];
1978    // Stable sort: `LEAD` columns move to the front in `host`/`source`/`sourcetype` order;
1979    // every other column keeps its existing relative position.
1980    primary_keys.sort_by_key(|name| {
1981        LEAD.iter()
1982            .position(|&lead| lead == name.as_str())
1983            .unwrap_or(LEAD.len())
1984    });
1985}
1986
1987/// Result of `create_or_alter_tables_on_demand`.
1988struct CreateAlterTableResult {
1989    /// table ids of ttl=instant tables.
1990    instant_table_ids: HashSet<TableId>,
1991    /// Table Info of the created tables.
1992    table_infos: HashMap<TableId, Arc<TableInfo>>,
1993}
1994
1995/// Upper bound of rows buffered by detached flow mirror tasks on this frontend.
1996///
1997/// Rows retained by in-flight mirror RPCs count against this bound. If ordinary
1998/// persisted source-table mirroring exceeds it, that mirror batch is best-effort
1999/// dropped while its datanode write continues. Instant-TTL batches fail retryably
2000/// on temporary saturation; oversized batches fail non-retryably so the client
2001/// can reduce the batch size.
2002const MAX_MIRROR_PENDING_ROWS: usize = 1_000_000;
2003
2004/// How often at most each mirror event is reported.
2005const MIRROR_LOG_INTERVAL: Duration = Duration::from_secs(10);
2006const MIRROR_LOG_NEVER_REPORTED: u64 = u64::MAX;
2007
2008struct MirrorLog {
2009    last_log_millis: AtomicU64,
2010    events: AtomicU64,
2011}
2012
2013impl MirrorLog {
2014    const fn new() -> Self {
2015        Self {
2016            last_log_millis: AtomicU64::new(MIRROR_LOG_NEVER_REPORTED),
2017            events: AtomicU64::new(0),
2018        }
2019    }
2020
2021    /// Returns the count since the previous report, including this event, to
2022    /// the single caller that claims the interval.
2023    fn claim_report(&self, now_millis: u64) -> Option<u64> {
2024        self.events.fetch_add(1, Ordering::Relaxed);
2025        let last = self.last_log_millis.load(Ordering::Relaxed);
2026        if last != MIRROR_LOG_NEVER_REPORTED
2027            && now_millis.saturating_sub(last) < MIRROR_LOG_INTERVAL.as_millis() as u64
2028        {
2029            return None;
2030        }
2031        if self
2032            .last_log_millis
2033            .compare_exchange(last, now_millis, Ordering::Relaxed, Ordering::Relaxed)
2034            .is_err()
2035        {
2036            return None;
2037        }
2038        Some(self.events.swap(0, Ordering::Relaxed))
2039    }
2040}
2041
2042static MIRROR_DROP_LOG: MirrorLog = MirrorLog::new();
2043static MIRROR_FAILURE_LOG: MirrorLog = MirrorLog::new();
2044
2045/// Milliseconds since the first call, so the drop log is rate limited on a
2046/// monotonic clock instead of the wall clock, which can jump.
2047fn mirror_drop_log_millis() -> u64 {
2048    static START: LazyLock<Instant> = LazyLock::new(Instant::now);
2049    START.elapsed().as_millis() as u64
2050}
2051
2052struct FlowMirrorTask {
2053    requests: HashMap<Peer, RegionInsertRequests>,
2054}
2055
2056impl FlowMirrorTask {
2057    async fn new(
2058        cache: &TableFlownodeSetCacheRef,
2059        requests: impl Iterator<Item = &RegionInsertRequest>,
2060    ) -> Result<Self> {
2061        let mut src_table_reqs: HashMap<TableId, Option<(Vec<Peer>, RegionInsertRequests)>> =
2062            HashMap::new();
2063
2064        for req in requests {
2065            let table_id = RegionId::from_u64(req.region_id).table_id();
2066            match src_table_reqs.get_mut(&table_id) {
2067                Some(Some((_peers, reqs))) => reqs.requests.push(req.clone()),
2068                // already know this is not source table
2069                Some(None) => continue,
2070                _ => {
2071                    // dedup peers
2072                    let peers = cache
2073                        .get(table_id)
2074                        .await
2075                        .context(RequestInsertsSnafu)?
2076                        .unwrap_or_default()
2077                        .values()
2078                        .cloned()
2079                        .collect::<HashSet<_>>()
2080                        .into_iter()
2081                        .collect::<Vec<_>>();
2082
2083                    if !peers.is_empty() {
2084                        let mut reqs = RegionInsertRequests::default();
2085                        reqs.requests.push(req.clone());
2086                        src_table_reqs.insert(table_id, Some((peers, reqs)));
2087                    } else {
2088                        // insert a empty entry to avoid repeat query
2089                        src_table_reqs.insert(table_id, None);
2090                    }
2091                }
2092            }
2093        }
2094
2095        let mut inserts: HashMap<Peer, RegionInsertRequests> = HashMap::new();
2096
2097        for (_table_id, (peers, reqs)) in src_table_reqs
2098            .into_iter()
2099            .filter_map(|(k, v)| v.map(|v| (k, v)))
2100        {
2101            if peers.len() == 1 {
2102                // fast path, zero copy
2103                inserts
2104                    .entry(peers[0].clone())
2105                    .or_default()
2106                    .requests
2107                    .extend(reqs.requests);
2108                continue;
2109            } else {
2110                // TODO(discord9): need to split requests to multiple flownodes
2111                for flownode in peers {
2112                    inserts
2113                        .entry(flownode.clone())
2114                        .or_default()
2115                        .requests
2116                        .extend(reqs.requests.clone());
2117                }
2118            }
2119        }
2120
2121        Ok(Self { requests: inserts })
2122    }
2123
2124    /// Rows of row data this task keeps alive once detached: the payload it
2125    /// spawns per peer, not the source requests it was built from.
2126    ///
2127    /// A source table mapped to several flownodes clones its requests per peer
2128    /// and every clone owns its own copy of the rows, so reserving from
2129    /// `self.requests` is what the per-peer shares released by [`Self::detach`]
2130    /// sum to. Counting the source requests instead under-reserves whenever a
2131    /// batch holds several requests for the same source table.
2132    fn pending_rows(&self) -> u64 {
2133        self.requests.values().map(region_inserts_rows).sum()
2134    }
2135
2136    fn detach(
2137        self,
2138        node_manager: NodeManagerRef,
2139        mirror_pending_rows: Arc<AtomicU64>,
2140        has_instant_rows: bool,
2141    ) -> Result<()> {
2142        // Reserve the cloned per-peer payload before spawning. A full budget
2143        // drops normal mirrors or rejects instant writes before datanode dispatch.
2144        let num_rows = self.pending_rows();
2145        if num_rows == 0 {
2146            return Ok(());
2147        }
2148        if has_instant_rows && num_rows > MAX_MIRROR_PENDING_ROWS as u64 {
2149            return InvalidInsertRequestSnafu {
2150                reason: format!(
2151                    "flow mirror batch has {num_rows} peer-fan-out rows, exceeding the limit of {}; reduce the batch size",
2152                    MAX_MIRROR_PENDING_ROWS
2153                ),
2154            }
2155            .fail();
2156        }
2157
2158        let pending = reserve_mirror_pending_rows(&mirror_pending_rows, num_rows);
2159        if pending > MAX_MIRROR_PENDING_ROWS as u64 {
2160            release_mirror_pending_rows(&mirror_pending_rows, num_rows);
2161            if has_instant_rows {
2162                return Err(meter_core::collect::WriteRejected::new(
2163                    "flow mirror pending rows limit exceeded for instant-TTL insert",
2164                ))
2165                .context(WriteRejectedSnafu);
2166            }
2167            crate::metrics::DIST_MIRROR_DROPPED_ROW_COUNT.inc_by(num_rows);
2168            if let Some(events) = MIRROR_DROP_LOG.claim_report(mirror_drop_log_millis()) {
2169                warn!(
2170                    "Flow mirror write dropped: {} pending rows exceeds limit {}, {} drop events since last report",
2171                    pending, MAX_MIRROR_PENDING_ROWS, events
2172                );
2173            }
2174            return Ok(());
2175        }
2176
2177        for (peer, inserts) in self.requests {
2178            // Each spawned task releases exactly its own share of the
2179            // reservation. The shares are computed over the same per-peer
2180            // payload as `pending_rows`, so they sum to the reservation.
2181            let peer_rows = region_inserts_rows(&inserts);
2182            let node_manager = node_manager.clone();
2183            let mirror_pending_rows = mirror_pending_rows.clone();
2184            common_runtime::spawn_global(async move {
2185                let result = node_manager
2186                    .flownode(&peer)
2187                    .await
2188                    .handle_inserts(inserts)
2189                    .await
2190                    .context(RequestInsertsSnafu);
2191
2192                match result {
2193                    Ok(resp) => {
2194                        let affected_rows = resp.affected_rows;
2195                        crate::metrics::DIST_MIRROR_ROW_COUNT.inc_by(affected_rows);
2196                    }
2197                    Err(err) => {
2198                        if let Some(events) =
2199                            MIRROR_FAILURE_LOG.claim_report(mirror_drop_log_millis())
2200                        {
2201                            error!(err; flownode_id = peer.id, flownode_addr = %peer.addr, "Failed to insert data into flownode ({} total mirror failures across peers since last report)", events);
2202                        }
2203                    }
2204                }
2205                // Release the reservation on both the success and the failure
2206                // path, otherwise a broken flownode would fill the budget forever.
2207                release_mirror_pending_rows(&mirror_pending_rows, peer_rows);
2208            });
2209        }
2210
2211        Ok(())
2212    }
2213}
2214
2215/// Rows of row data carried by `inserts`. A request without a `rows` payload
2216/// carries nothing to keep buffered and contributes zero.
2217fn region_inserts_rows(inserts: &RegionInsertRequests) -> u64 {
2218    inserts
2219        .requests
2220        .iter()
2221        .filter_map(|req| req.rows.as_ref())
2222        .map(|rows| rows.rows.len() as u64)
2223        .sum()
2224}
2225
2226/// Reserves `rows` of the pending mirror budget and returns the resulting
2227/// pending row count. The caller must [`release_mirror_pending_rows`] exactly
2228/// these rows once they are no longer buffered, whether it spawns the batch or
2229/// drops it.
2230fn reserve_mirror_pending_rows(mirror_pending_rows: &AtomicU64, rows: u64) -> u64 {
2231    let pending = mirror_pending_rows.fetch_add(rows, Ordering::Relaxed) + rows;
2232    // Keep the gauge in step by applying the same increment, never by setting an
2233    // absolute value: another task may move the budget in between.
2234    crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.add(rows as i64);
2235    pending
2236}
2237
2238/// Releases `rows` of the pending mirror budget.
2239///
2240/// Every release mirrors the reservation it hands back, so a well-behaved
2241/// release never subtracts more than is pending; the saturating update only
2242/// keeps a duplicated release from wrapping the shared budget around.
2243fn release_mirror_pending_rows(mirror_pending_rows: &AtomicU64, rows: u64) {
2244    let prev = mirror_pending_rows
2245        .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |pending| {
2246            Some(pending.saturating_sub(rows))
2247        })
2248        .unwrap_or_else(|prev| prev);
2249    // Subtract from the gauge with the opposite sign of
2250    // [`reserve_mirror_pending_rows`]; reading the budget here and `set`-ting it
2251    // would publish a stale value whenever another task releases concurrently.
2252    let released = rows.min(prev);
2253    if released > 0 {
2254        crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.sub(released as i64);
2255    }
2256}
2257
2258#[cfg(test)]
2259mod tests {
2260    use std::sync::Arc;
2261    use std::sync::atomic::{AtomicU64, Ordering};
2262
2263    use api::helper::ColumnDataTypeWrapper;
2264    use api::v1::flow::FlowResponse;
2265    use api::v1::helper::{field_column_schema, time_index_column_schema};
2266    use api::v1::{Row, RowInsertRequest, Rows, Value};
2267    use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
2268    use common_meta::cache::new_table_flownode_set_cache;
2269    use common_meta::ddl::test_util::datanode_handler::NaiveDatanodeHandler;
2270    use common_meta::test_util::{MockDatanodeManager, MockFlownodeHandler, MockFlownodeManager};
2271    use common_query::native_histogram::NATIVE_HISTOGRAM_FIELD;
2272    use common_query::prelude::{greptime_native_histogram, set_default_prefix};
2273    use datatypes::data_type::ConcreteDataType;
2274    use datatypes::schema::ColumnSchema;
2275    use moka::future::Cache;
2276    use session::context::QueryContext;
2277    use table::TableRef;
2278    use table::dist_table::DummyDataSource;
2279    use table::metadata::{TableInfoBuilder, TableMetaBuilder, TableType};
2280
2281    use crate::insert::*;
2282    use crate::test_util::{
2283        create_partition_rule_manager, new_test_table_info, prepare_mocked_backend,
2284    };
2285
2286    fn make_table_ref_with_schema(
2287        ts_name: &str,
2288        field_name: &str,
2289        field_type: ConcreteDataType,
2290    ) -> TableRef {
2291        let schema = datatypes::schema::SchemaBuilder::try_from_columns(vec![
2292            ColumnSchema::new(
2293                ts_name,
2294                ConcreteDataType::timestamp_millisecond_datatype(),
2295                false,
2296            )
2297            .with_time_index(true),
2298            ColumnSchema::new(field_name, field_type, true),
2299        ])
2300        .unwrap()
2301        .build()
2302        .unwrap();
2303        let meta = TableMetaBuilder::empty()
2304            .schema(Arc::new(schema))
2305            .primary_key_indices(vec![])
2306            .value_indices(vec![1])
2307            .engine("mito")
2308            .next_column_id(0)
2309            .options(Default::default())
2310            .created_on(Default::default())
2311            .build()
2312            .unwrap();
2313        let info = Arc::new(
2314            TableInfoBuilder::default()
2315                .table_id(1)
2316                .table_version(0)
2317                .name("test_table")
2318                .schema_name(DEFAULT_SCHEMA_NAME)
2319                .catalog_name(DEFAULT_CATALOG_NAME)
2320                .desc(None)
2321                .table_type(TableType::Base)
2322                .meta(meta)
2323                .build()
2324                .unwrap(),
2325        );
2326        Arc::new(table::Table::new(
2327            info,
2328            table::metadata::FilterPushDownType::Unsupported,
2329            Arc::new(DummyDataSource),
2330        ))
2331    }
2332
2333    fn make_metric_physical_table_ref_with_time_unit(unit: TimeUnit) -> TableRef {
2334        let schema = datatypes::schema::SchemaBuilder::try_from_columns(vec![
2335            ColumnSchema::new(
2336                greptime_timestamp(),
2337                ConcreteDataType::timestamp_datatype(unit),
2338                false,
2339            )
2340            .with_time_index(true),
2341            ColumnSchema::new(greptime_value(), ConcreteDataType::float64_datatype(), true),
2342        ])
2343        .unwrap()
2344        .build()
2345        .unwrap();
2346        let meta = TableMetaBuilder::empty()
2347            .schema(Arc::new(schema))
2348            .primary_key_indices(vec![])
2349            .value_indices(vec![1])
2350            .engine("metric")
2351            .next_column_id(0)
2352            .options(Default::default())
2353            .created_on(Default::default())
2354            .build()
2355            .unwrap();
2356        let info = Arc::new(
2357            TableInfoBuilder::default()
2358                .table_id(1)
2359                .table_version(0)
2360                .name("greptime_physical_table")
2361                .schema_name(DEFAULT_SCHEMA_NAME)
2362                .catalog_name(DEFAULT_CATALOG_NAME)
2363                .desc(None)
2364                .table_type(TableType::Base)
2365                .meta(meta)
2366                .build()
2367                .unwrap(),
2368        );
2369        Arc::new(table::Table::new(
2370            info,
2371            table::metadata::FilterPushDownType::Unsupported,
2372            Arc::new(DummyDataSource),
2373        ))
2374    }
2375
2376    fn ms_row_insert_request(timestamp_ms: i64) -> RowInsertRequest {
2377        ms_row_insert_request_named("my_metric", timestamp_ms)
2378    }
2379
2380    fn ms_row_insert_request_named(table: &str, timestamp_ms: i64) -> RowInsertRequest {
2381        RowInsertRequest {
2382            table_name: table.to_string(),
2383            rows: Some(Rows {
2384                schema: vec![
2385                    time_index_column_schema(
2386                        greptime_timestamp(),
2387                        ColumnDataType::TimestampMillisecond,
2388                    ),
2389                    field_column_schema(greptime_value(), ColumnDataType::Float64),
2390                ],
2391                rows: vec![Row {
2392                    values: vec![
2393                        Value {
2394                            value_data: Some(ValueData::TimestampMillisecondValue(timestamp_ms)),
2395                        },
2396                        Value {
2397                            value_data: Some(ValueData::F64Value(1.0)),
2398                        },
2399                    ],
2400                }],
2401            }),
2402        }
2403    }
2404
2405    /// Converts the time index column of each request to `target_unit`.
2406    fn convert_row_insert_requests_time_unit(
2407        requests: &mut RowInsertRequests,
2408        target_unit: TimeUnit,
2409    ) -> Result<()> {
2410        for request in &mut requests.inserts {
2411            let Some(rows) = request.rows.as_mut() else {
2412                continue;
2413            };
2414            convert_rows_time_unit(rows, target_unit)?;
2415        }
2416        Ok(())
2417    }
2418
2419    #[test]
2420    fn test_convert_row_insert_requests_time_unit_noop_when_matching() {
2421        let mut requests = RowInsertRequests {
2422            inserts: vec![ms_row_insert_request(123)],
2423        };
2424        convert_row_insert_requests_time_unit(&mut requests, TimeUnit::Millisecond).unwrap();
2425        let rows = requests.inserts[0].rows.as_ref().unwrap();
2426        assert_eq!(
2427            rows.schema[0].datatype,
2428            ColumnDataType::TimestampMillisecond as i32
2429        );
2430        assert!(matches!(
2431            rows.rows[0].values[0].value_data,
2432            Some(ValueData::TimestampMillisecondValue(123))
2433        ));
2434    }
2435
2436    #[test]
2437    fn test_convert_row_insert_requests_time_unit_widens_losslessly() {
2438        let mut requests = RowInsertRequests {
2439            inserts: vec![ms_row_insert_request(123)],
2440        };
2441        convert_row_insert_requests_time_unit(&mut requests, TimeUnit::Microsecond).unwrap();
2442        let rows = requests.inserts[0].rows.as_ref().unwrap();
2443        assert_eq!(
2444            rows.schema[0].datatype,
2445            ColumnDataType::TimestampMicrosecond as i32
2446        );
2447        assert!(matches!(
2448            rows.rows[0].values[0].value_data,
2449            Some(ValueData::TimestampMicrosecondValue(123_000))
2450        ));
2451    }
2452
2453    #[test]
2454    fn test_convert_row_insert_requests_time_unit_truncates_on_narrowing() {
2455        // 123_456_789 ns floors to 123_456 us and 123 ms; negative values
2456        // floor towards negative infinity, matching `Timestamp::convert_to`.
2457        let requests = |value_ns: i64| RowInsertRequests {
2458            inserts: vec![RowInsertRequest {
2459                table_name: "my_metric".to_string(),
2460                rows: Some(Rows {
2461                    schema: vec![
2462                        time_index_column_schema(
2463                            greptime_timestamp(),
2464                            ColumnDataType::TimestampNanosecond,
2465                        ),
2466                        field_column_schema(greptime_value(), ColumnDataType::Float64),
2467                    ],
2468                    rows: vec![Row {
2469                        values: vec![
2470                            Value {
2471                                value_data: Some(ValueData::TimestampNanosecondValue(value_ns)),
2472                            },
2473                            Value {
2474                                value_data: Some(ValueData::F64Value(1.0)),
2475                            },
2476                        ],
2477                    }],
2478                }),
2479            }],
2480        };
2481
2482        let mut reqs = requests(123_456_789);
2483        convert_row_insert_requests_time_unit(&mut reqs, TimeUnit::Microsecond).unwrap();
2484        let rows = reqs.inserts[0].rows.as_ref().unwrap();
2485        assert!(matches!(
2486            rows.rows[0].values[0].value_data,
2487            Some(ValueData::TimestampMicrosecondValue(123_456))
2488        ));
2489
2490        let mut reqs = requests(123_456_789);
2491        convert_row_insert_requests_time_unit(&mut reqs, TimeUnit::Millisecond).unwrap();
2492        let rows = reqs.inserts[0].rows.as_ref().unwrap();
2493        assert!(matches!(
2494            rows.rows[0].values[0].value_data,
2495            Some(ValueData::TimestampMillisecondValue(123))
2496        ));
2497
2498        let mut reqs = requests(-123_456_789);
2499        convert_row_insert_requests_time_unit(&mut reqs, TimeUnit::Microsecond).unwrap();
2500        let rows = reqs.inserts[0].rows.as_ref().unwrap();
2501        assert!(matches!(
2502            rows.rows[0].values[0].value_data,
2503            Some(ValueData::TimestampMicrosecondValue(-123_457))
2504        ));
2505    }
2506
2507    #[test]
2508    fn test_convert_row_insert_requests_time_unit_overflow() {
2509        let mut requests = RowInsertRequests {
2510            inserts: vec![ms_row_insert_request(i64::MAX)],
2511        };
2512        let err =
2513            convert_row_insert_requests_time_unit(&mut requests, TimeUnit::Nanosecond).unwrap_err();
2514        assert!(err.to_string().contains("overflows"), "{err}");
2515    }
2516
2517    #[test]
2518    fn test_table_time_index_unit() {
2519        assert_eq!(
2520            table_time_index_unit(&make_metric_physical_table_ref_with_time_unit(
2521                TimeUnit::Microsecond
2522            )),
2523            Some(TimeUnit::Microsecond)
2524        );
2525        assert_eq!(
2526            table_time_index_unit(&make_metric_physical_table_ref_with_time_unit(
2527                TimeUnit::Millisecond
2528            )),
2529            Some(TimeUnit::Millisecond)
2530        );
2531    }
2532
2533    #[tokio::test]
2534    async fn test_accommodate_existing_schema_and_reject_kind_changes() {
2535        let ts_name = "my_ts";
2536        let field_name = "my_field";
2537        let table =
2538            make_table_ref_with_schema(ts_name, field_name, ConcreteDataType::float64_datatype());
2539
2540        // The request uses different names for timestamp and field columns
2541        let mut req = RowInsertRequest {
2542            table_name: "test_table".to_string(),
2543            rows: Some(Rows {
2544                schema: vec![
2545                    time_index_column_schema("ts_wrong", ColumnDataType::TimestampMillisecond),
2546                    field_column_schema("field_wrong", ColumnDataType::Float64),
2547                ],
2548                rows: vec![api::v1::Row {
2549                    values: vec![Value::default(), Value::default()],
2550                }],
2551            }),
2552        };
2553        let ctx = Arc::new(QueryContext::with(
2554            DEFAULT_CATALOG_NAME,
2555            DEFAULT_SCHEMA_NAME,
2556        ));
2557
2558        let kv_backend = prepare_mocked_backend().await;
2559        let inserter = Inserter::new(
2560            catalog::memory::MemoryCatalogManager::new(),
2561            create_partition_rule_manager(kv_backend.clone()).await,
2562            Arc::new(MockDatanodeManager::new(NaiveDatanodeHandler)),
2563            Arc::new(new_table_flownode_set_cache(
2564                String::new(),
2565                Cache::new(100),
2566                kv_backend.clone(),
2567            )),
2568            true,
2569        );
2570        // Do not apply an absent-table plan to a table that appeared concurrently.
2571        assert!(
2572            inserter
2573                .get_alter_table_expr_on_demand(&mut req, &table, &ctx, false, false, false)
2574                .unwrap()
2575                .is_none()
2576        );
2577        let alter_expr = inserter
2578            .get_alter_table_expr_on_demand(&mut req, &table, &ctx, true, true, true)
2579            .unwrap();
2580        assert!(alter_expr.is_none());
2581
2582        // The request's schema should have updated names for timestamp and field columns
2583        let req_schema = req.rows.as_ref().unwrap().schema.clone();
2584        assert_eq!(req_schema[0].column_name, ts_name);
2585        assert_eq!(req_schema[1].column_name, field_name);
2586
2587        let (datatype, datatype_extension) =
2588            ColumnDataTypeWrapper::try_from(native_histogram_value_type().clone())
2589                .unwrap()
2590                .into_parts();
2591        let mut histogram_req = RowInsertRequest {
2592            table_name: "test_table".to_string(),
2593            rows: Some(Rows {
2594                schema: vec![
2595                    time_index_column_schema("ts", ColumnDataType::TimestampMillisecond),
2596                    api::v1::ColumnSchema {
2597                        column_name: greptime_native_histogram().to_string(),
2598                        datatype: datatype as i32,
2599                        semantic_type: SemanticType::Field as i32,
2600                        datatype_extension,
2601                        options: None,
2602                    },
2603                ],
2604                rows: vec![],
2605            }),
2606        };
2607        let error = inserter
2608            .get_alter_table_expr_on_demand(&mut histogram_req, &table, &ctx, false, true, true)
2609            .unwrap_err();
2610        assert!(
2611            error
2612                .to_string()
2613                .contains("cannot mix native histogram and float sample fields")
2614        );
2615
2616        let histogram_table = make_table_ref_with_schema(
2617            "ts",
2618            greptime_native_histogram(),
2619            native_histogram_value_type().clone(),
2620        );
2621        let mut sample_req = RowInsertRequest {
2622            table_name: "test_table".to_string(),
2623            rows: Some(Rows {
2624                schema: vec![
2625                    time_index_column_schema("ts", ColumnDataType::TimestampMillisecond),
2626                    field_column_schema(greptime_value(), ColumnDataType::Float64),
2627                ],
2628                rows: vec![],
2629            }),
2630        };
2631        let error = inserter
2632            .get_alter_table_expr_on_demand(
2633                &mut sample_req,
2634                &histogram_table,
2635                &ctx,
2636                false,
2637                true,
2638                true,
2639            )
2640            .unwrap_err();
2641        assert!(
2642            error
2643                .to_string()
2644                .contains("cannot mix native histogram and float sample fields")
2645        );
2646    }
2647
2648    #[test]
2649    fn test_native_histogram_detection_survives_prefix_change() {
2650        set_default_prefix(Some("custom")).unwrap();
2651        let table = make_table_ref_with_schema(
2652            "custom_timestamp",
2653            NATIVE_HISTOGRAM_FIELD,
2654            native_histogram_value_type().clone(),
2655        );
2656        let (datatype, datatype_extension) =
2657            ColumnDataTypeWrapper::try_from(native_histogram_value_type().clone())
2658                .unwrap()
2659                .into_parts();
2660        let request_schema = [api::v1::ColumnSchema {
2661            column_name: greptime_native_histogram().to_string(),
2662            datatype: datatype as i32,
2663            semantic_type: SemanticType::Field as i32,
2664            datatype_extension,
2665            options: None,
2666        }];
2667
2668        assert!(request_is_native_histogram(&request_schema));
2669        assert!(table_is_native_histogram(&table));
2670    }
2671
2672    // Keep global meter registration in one test, isolated by nextest's per-test process.
2673    #[tokio::test]
2674    async fn test_write_meter_admission() {
2675        use std::cell::Cell;
2676        use std::sync::Mutex;
2677        use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
2678
2679        use api::region::RegionResponse;
2680        use api::v1::region::region_request::Body;
2681        use arrow::array::{Int32Array, TimestampMillisecondArray};
2682        use arrow::record_batch::RecordBatch;
2683        use bytes::Bytes;
2684        use common_error::ext::{ErrorExt, RetryHint};
2685        use common_error::status_code::StatusCode;
2686        use common_grpc::flight::{FlightEncoder, FlightMessage};
2687        use common_meta::ddl::test_util::datanode_handler::DatanodeWatcher;
2688        use futures::future::BoxFuture;
2689        use meter_core::ItemCalculator;
2690        use meter_core::collect::{Collect, WriteRejected};
2691        use meter_core::data::MeterRecord;
2692        use meter_core::global::global_registry;
2693        use session::context::Channel;
2694
2695        const CATALOG: &str = "write_meter_test";
2696
2697        #[derive(Default)]
2698        struct Meter {
2699            reject: AtomicBool,
2700            attempts: Mutex<Vec<MeterRecord>>,
2701            accepted_value: AtomicU64,
2702        }
2703
2704        impl Collect for Meter {
2705            fn on_write(
2706                &self,
2707                record: MeterRecord,
2708            ) -> BoxFuture<'_, std::result::Result<(), WriteRejected>> {
2709                Box::pin(async move {
2710                    if record.catalog != CATALOG {
2711                        return Ok(());
2712                    }
2713                    let value = record.value;
2714                    self.attempts.lock().unwrap().push(record);
2715                    if self.reject.load(Ordering::Relaxed) {
2716                        return Err(WriteRejected::new("database row quota exhausted"));
2717                    }
2718                    self.accepted_value.fetch_add(value, Ordering::Relaxed);
2719                    Ok(())
2720                })
2721            }
2722
2723            fn on_read(&self, _: MeterRecord) {}
2724        }
2725
2726        impl ItemCalculator<InstantAndNormalInsertRequests> for Meter {
2727            fn calc(&self, _: &InstantAndNormalInsertRequests) -> u64 {
2728                17
2729            }
2730        }
2731
2732        let kv_backend = prepare_mocked_backend().await;
2733        let partition_manager = create_partition_rule_manager(kv_backend.clone()).await;
2734        let (sender, mut dispatched) = tokio::sync::mpsc::channel(16);
2735        let watcher = DatanodeWatcher::new(sender).with_handler(|_, request| {
2736            let rows = match request.body.unwrap() {
2737                Body::Inserts(requests) => requests
2738                    .requests
2739                    .iter()
2740                    .filter_map(|request| request.rows.as_ref())
2741                    .map(|rows| rows.rows.len())
2742                    .sum(),
2743                // The bulk batches below each contain two rows.
2744                Body::BulkInsert(_) => 2,
2745                body => panic!("unexpected request: {body:?}"),
2746            };
2747            Ok(RegionResponse::new(rows))
2748        });
2749        let flow_cache = Cache::new(100);
2750        let inserter = Inserter::new(
2751            catalog::memory::MemoryCatalogManager::new(),
2752            partition_manager,
2753            Arc::new(MockDatanodeManager::new(watcher)),
2754            Arc::new(new_table_flownode_set_cache(
2755                String::new(),
2756                flow_cache.clone(),
2757                kv_backend,
2758            )),
2759            true,
2760        );
2761        let mut table_info = new_test_table_info(1, "table_1", [1].into_iter());
2762        table_info.catalog_name = CATALOG.to_string();
2763        table_info.schema_name = "target_db".to_string();
2764        let table_info = Arc::new(table_info);
2765        let table_infos = HashMap::from_iter([(1, table_info.clone())]);
2766        let ctx = Arc::new(QueryContext::with_channel(
2767            DEFAULT_CATALOG_NAME,
2768            DEFAULT_SCHEMA_NAME,
2769            Channel::Postgres,
2770        ));
2771        let rows_request = || {
2772            let build = |num_rows| RegionInsertRequests {
2773                requests: vec![RegionInsertRequest {
2774                    region_id: RegionId::new(1, 1).as_u64(),
2775                    rows: Some(Rows {
2776                        schema: vec![],
2777                        rows: vec![api::v1::Row { values: vec![] }; num_rows],
2778                    }),
2779                    ..Default::default()
2780                }],
2781            };
2782            InstantAndNormalInsertRequests {
2783                normal_requests: build(3),
2784                instant_requests: build(2),
2785            }
2786        };
2787
2788        // No collector or calculator: ordinary OSS insertion still succeeds.
2789        let output = inserter
2790            .do_request(rows_request(), &table_infos, &ctx)
2791            .await
2792            .unwrap();
2793        assert_eq!(output.meta.cost, 0);
2794        assert!(matches!(output.data, OutputData::AffectedRows(3)));
2795        dispatched.try_recv().unwrap();
2796
2797        let meter = Arc::new(Meter::default());
2798        global_registry().set_collector(meter.clone());
2799        global_registry().register_calculator(meter.clone());
2800        // The dependency controls noop mode; exercise both builds with this test.
2801        let enabled = Cell::new(false);
2802        write_meter!({
2803            enabled.set(true);
2804            MeterRecord::new("probe".into(), "probe".into(), 0, 0, 0)
2805        })
2806        .await
2807        .unwrap();
2808        let enabled = enabled.get();
2809
2810        let output = inserter
2811            .do_request(rows_request(), &table_infos, &ctx)
2812            .await
2813            .unwrap();
2814        assert_eq!(output.meta.cost, if enabled { 17 } else { 0 });
2815        assert!(matches!(output.data, OutputData::AffectedRows(3)));
2816        dispatched.try_recv().unwrap();
2817        assert!(flow_cache.contains_key(&1));
2818        flow_cache.invalidate_all();
2819        meter.reject.store(true, Ordering::Relaxed);
2820        let result = inserter
2821            .do_request(rows_request(), &table_infos, &ctx)
2822            .await;
2823        if enabled {
2824            let error = result.unwrap_err();
2825            assert_eq!(error.status_code(), StatusCode::RateLimited);
2826            assert_eq!(error.retry_hint(), RetryHint::Retryable);
2827            assert!(error.to_string().contains("database row quota exhausted"));
2828            assert!(dispatched.try_recv().is_err());
2829            assert!(
2830                !flow_cache.contains_key(&1),
2831                "rejected write reached flow mirroring"
2832            );
2833            assert_eq!(meter.accepted_value.load(Ordering::Relaxed), 17);
2834            let attempts = meter.attempts.lock().unwrap();
2835            assert_eq!(attempts.len(), 2);
2836            for record in attempts.iter() {
2837                assert_eq!(record.catalog, CATALOG);
2838                assert_eq!(record.schema, "target_db");
2839                assert_eq!(
2840                    (record.rows, record.value, record.source),
2841                    (5, 17, Channel::Postgres as u8)
2842                );
2843            }
2844        } else {
2845            assert_eq!(result.unwrap().meta.cost, 0);
2846            dispatched.try_recv().unwrap();
2847            assert!(meter.attempts.lock().unwrap().is_empty());
2848        }
2849        meter.attempts.lock().unwrap().clear();
2850
2851        // Rejection must also precede admission to the table batcher's queue.
2852        if enabled {
2853            let batcher: Arc<dyn PendingRowsBatcher> = Arc::new(UnexpectedBatcher);
2854            let rows = Rows {
2855                schema: vec![
2856                    api::v1::helper::tag_column_schema("a", ColumnDataType::Int32),
2857                    time_index_column_schema("ts", ColumnDataType::TimestampMillisecond),
2858                    field_column_schema("b", ColumnDataType::Int32),
2859                ],
2860                rows: vec![api::v1::Row {
2861                    values: vec![
2862                        api::v1::value::ValueData::I32Value(60).into(),
2863                        Value {
2864                            value_data: Some(api::v1::value::ValueData::TimestampMillisecondValue(
2865                                0,
2866                            )),
2867                        },
2868                        api::v1::value::ValueData::I32Value(0).into(),
2869                    ],
2870                }],
2871            };
2872            let error = inserter
2873                .submit_table_rows(rows, table_info.clone(), ctx.clone(), &batcher)
2874                .await
2875                .unwrap_err();
2876            assert_eq!(error.status_code(), StatusCode::RateLimited);
2877            let mut attempts = meter.attempts.lock().unwrap();
2878            assert_eq!(attempts.len(), 1);
2879            assert_eq!(attempts[0].catalog, CATALOG);
2880            assert_eq!(attempts[0].schema, "target_db");
2881            assert_eq!(attempts[0].rows, 1);
2882            attempts.clear();
2883        }
2884
2885        let table = Arc::new(table::Table::new(
2886            table_info.clone(),
2887            table::metadata::FilterPushDownType::Unsupported,
2888            Arc::new(DummyDataSource),
2889        ));
2890        let batch = RecordBatch::try_new(
2891            table_info.meta.schema.arrow_schema().clone(),
2892            vec![
2893                Arc::new(Int32Array::from(vec![60, 70])),
2894                Arc::new(TimestampMillisecondArray::from(vec![0, 1])),
2895                Arc::new(Int32Array::from(vec![0, 0])),
2896            ],
2897        )
2898        .unwrap();
2899        let bulk_insert = |batch: RecordBatch| {
2900            let flight_data = FlightEncoder::default()
2901                .encode(FlightMessage::RecordBatch(batch.clone()))
2902                .into_iter()
2903                .next()
2904                .unwrap();
2905            inserter.handle_bulk_insert(
2906                table.clone(),
2907                flight_data,
2908                batch,
2909                Bytes::new(),
2910                false,
2911                Channel::Grpc,
2912            )
2913        };
2914        // Empty batches bypass admission even while the collector rejects.
2915        assert_eq!(bulk_insert(batch.slice(0, 0)).await.unwrap(), 0);
2916        assert!(meter.attempts.lock().unwrap().is_empty());
2917        assert!(dispatched.try_recv().is_err());
2918
2919        // A collector change between batches takes effect on the very next batch.
2920        for reject in [false, true, false] {
2921            meter.reject.store(reject, Ordering::Relaxed);
2922            let result = bulk_insert(batch.clone()).await;
2923            if enabled && reject {
2924                let error = result.unwrap_err();
2925                assert_eq!(error.status_code(), StatusCode::RateLimited);
2926                assert_eq!(error.retry_hint(), RetryHint::Retryable);
2927                assert!(dispatched.try_recv().is_err());
2928            } else {
2929                assert_eq!(result.unwrap(), 2);
2930                dispatched.try_recv().unwrap();
2931            }
2932        }
2933        {
2934            let attempts = meter.attempts.lock().unwrap();
2935            assert_eq!(attempts.len(), if enabled { 3 } else { 0 });
2936            for record in attempts.iter() {
2937                assert_eq!(record.catalog, CATALOG);
2938                assert_eq!(record.schema, "target_db");
2939                assert_eq!(
2940                    (record.rows, record.value, record.source),
2941                    (2, 0, Channel::Grpc as u8)
2942                );
2943            }
2944        }
2945        assert_eq!(
2946            meter.accepted_value.load(Ordering::Relaxed),
2947            if enabled { 17 } else { 0 }
2948        );
2949
2950        // Aggregate repeated database targets and retain admission across nested
2951        // batching without changing the caller's reusable context.
2952        meter.attempts.lock().unwrap().clear();
2953        let original = Arc::new(QueryContext::with_channel(
2954            CATALOG,
2955            "a",
2956            Channel::Prometheus,
2957        ));
2958        let mut batches = ["a", "b", "a"].map(|schema| {
2959            let ctx = if schema == "a" {
2960                original.clone()
2961            } else {
2962                Arc::new(QueryContext::with_channel(
2963                    CATALOG,
2964                    schema,
2965                    Channel::Prometheus,
2966                ))
2967            };
2968            (
2969                ctx,
2970                RowInsertRequests {
2971                    inserts: vec![RowInsertRequest {
2972                        table_name: "data".into(),
2973                        rows: Some(Rows {
2974                            schema: vec![],
2975                            rows: vec![api::v1::Row::default(); 2],
2976                        }),
2977                    }],
2978                },
2979            )
2980        });
2981        admit_row_insert_batches(&mut batches).await.unwrap();
2982        admit_row_insert_batches(&mut batches).await.unwrap();
2983        assert_eq!(original.write_rows_to_admit(CATALOG, "a", 4), 4);
2984        for (ctx, _) in &batches {
2985            assert_eq!(
2986                ctx.write_rows_to_admit(CATALOG, &ctx.current_schema(), 2),
2987                0
2988            );
2989            assert_eq!(ctx.channel(), Channel::Prometheus);
2990        }
2991        {
2992            let attempts = meter.attempts.lock().unwrap();
2993            let totals = attempts
2994                .iter()
2995                .map(|r| (r.schema.as_str(), r.rows, r.value))
2996                .collect::<Vec<_>>();
2997            assert_eq!(
2998                totals,
2999                if enabled {
3000                    vec![("a", 4, 0), ("b", 2, 0)]
3001                } else {
3002                    vec![]
3003                }
3004            );
3005        }
3006        meter.attempts.lock().unwrap().clear();
3007        meter.reject.store(false, Ordering::Relaxed);
3008        let ctx = Arc::new(QueryContext::with_channel(
3009            CATALOG,
3010            "logical",
3011            Channel::Otlp,
3012        ));
3013        let admitted = admit_write(2, &ctx).await.unwrap();
3014        let mut requests = RowInsertRequests {
3015            inserts: vec![RowInsertRequest {
3016                table_name: "metric".to_string(),
3017                rows: Some(Rows {
3018                    schema: vec![],
3019                    rows: vec![api::v1::Row::default(); 2],
3020                }),
3021            }],
3022        };
3023        let original = requests.clone();
3024        let cost = Inserter::meter_row_inserts(&mut requests, &admitted)
3025            .await
3026            .unwrap();
3027        assert_eq!(cost, if enabled { 17 } else { 0 });
3028        assert_eq!(requests, original);
3029        let records = meter
3030            .attempts
3031            .lock()
3032            .unwrap()
3033            .iter()
3034            .map(|record| (record.rows, record.value))
3035            .collect::<Vec<_>>();
3036        assert_eq!(
3037            records,
3038            if enabled {
3039                vec![(2, 0), (0, 17)]
3040            } else {
3041                vec![]
3042            }
3043        );
3044        meter.reject.store(true, Ordering::Relaxed);
3045        let result = Inserter::meter_row_inserts(&mut requests, &admitted).await;
3046        if enabled {
3047            assert_eq!(result.unwrap_err().status_code(), StatusCode::RateLimited);
3048        } else {
3049            assert_eq!(result.unwrap(), 0);
3050        }
3051        assert_eq!(requests, original);
3052    }
3053
3054    #[test]
3055    fn test_skip_wal_does_not_change_table_options() {
3056        check_skip_wal_does_not_change_table_options(false);
3057        check_skip_wal_does_not_change_table_options(true);
3058    }
3059
3060    fn check_skip_wal_does_not_change_table_options(skip_wal: bool) {
3061        let ctx = Arc::new(QueryContext::with(
3062            DEFAULT_CATALOG_NAME,
3063            DEFAULT_SCHEMA_NAME,
3064        ));
3065        ctx.set_skip_wal(skip_wal);
3066        let mut options = Default::default();
3067        fill_table_options_for_create(&mut options, &AutoCreateTableType::Physical, &ctx);
3068        assert!(!options.contains_key(session::hints::INSERT_SKIP_WAL_HINT));
3069        assert!(!options.contains_key("skip_wal"));
3070    }
3071
3072    #[test]
3073    fn test_last_non_null_create_options_preserve_default_without_append_mode() {
3074        let ctx = Arc::new(QueryContext::with(
3075            DEFAULT_CATALOG_NAME,
3076            DEFAULT_SCHEMA_NAME,
3077        ));
3078        let mut table_options = Default::default();
3079
3080        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::LastNonNull, &ctx);
3081
3082        assert_eq!(
3083            Some("last_non_null"),
3084            table_options.get(MERGE_MODE_KEY).map(String::as_str)
3085        );
3086        assert!(!table_options.contains_key(APPEND_MODE_KEY));
3087    }
3088
3089    #[test]
3090    fn test_fill_table_options_copies_semantic_extensions() {
3091        use table::requests::{
3092            SEMANTIC_METRIC_TYPE, SEMANTIC_PER_TABLE_INDEX_KEY, SEMANTIC_SIGNAL_TYPE,
3093            SEMANTIC_SOURCE, SEMANTIC_SOURCE_VERSION, SIGNAL_TYPE_METRIC, SOURCE_OPENTELEMETRY,
3094        };
3095
3096        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
3097        ctx.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_METRIC);
3098        ctx.set_extension(SEMANTIC_SOURCE, SOURCE_OPENTELEMETRY);
3099        ctx.set_extension(SEMANTIC_SOURCE_VERSION, "2.0");
3100        ctx.set_extension(SEMANTIC_METRIC_TYPE, "bogus");
3101        // The internal transport key must NOT be copied into table options.
3102        ctx.set_extension(SEMANTIC_PER_TABLE_INDEX_KEY, "{}");
3103        let ctx = Arc::new(ctx);
3104        let mut table_options = Default::default();
3105
3106        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::Physical, &ctx);
3107
3108        assert_eq!(
3109            Some(SIGNAL_TYPE_METRIC),
3110            table_options.get(SEMANTIC_SIGNAL_TYPE).map(String::as_str)
3111        );
3112        assert_eq!(
3113            Some(SOURCE_OPENTELEMETRY),
3114            table_options.get(SEMANTIC_SOURCE).map(String::as_str)
3115        );
3116        assert_eq!(
3117            Some("2.0"),
3118            table_options
3119                .get(SEMANTIC_SOURCE_VERSION)
3120                .map(String::as_str)
3121        );
3122        assert!(!table_options.contains_key(SEMANTIC_METRIC_TYPE));
3123        assert!(!table_options.contains_key(SEMANTIC_PER_TABLE_INDEX_KEY));
3124    }
3125
3126    #[test]
3127    fn test_apply_per_table_semantic_options() {
3128        use table::requests::{
3129            SEMANTIC_METRIC_TYPE, SEMANTIC_METRIC_UNIT, SEMANTIC_PER_TABLE_INDEX_KEY,
3130        };
3131
3132        let index = format!(
3133            r#"{{
3134            "{DEFAULT_SCHEMA_NAME}": {{
3135                "http_requests_total": {{
3136                    "greptime.semantic.metric.type": "counter",
3137                    "greptime.semantic.metric.unit": "By",
3138                    "greptime.semantic.metric.type_BOGUS": "x"
3139                }},
3140                "other_table": {{
3141                    "greptime.semantic.metric.type": "gauge"
3142                }}
3143            }},
3144            "other_schema": {{
3145                "http_requests_total": {{
3146                    "greptime.semantic.metric.type": "gauge"
3147                }}
3148            }}
3149        }}"#
3150        );
3151        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
3152        ctx.set_extension(SEMANTIC_PER_TABLE_INDEX_KEY, index);
3153        let ctx = Arc::new(ctx);
3154
3155        let index = parse_per_table_semantic_index(&ctx);
3156        assert!(index.is_some());
3157        let index = index.as_ref();
3158
3159        let mut table_options = std::collections::HashMap::new();
3160        apply_per_table_semantic_options(
3161            &mut table_options,
3162            index,
3163            DEFAULT_SCHEMA_NAME,
3164            "http_requests_total",
3165        );
3166        // The write schema's entry applies — not other_schema's `gauge`.
3167        assert_eq!(
3168            table_options.get(SEMANTIC_METRIC_TYPE).map(String::as_str),
3169            Some("counter")
3170        );
3171        assert_eq!(
3172            table_options.get(SEMANTIC_METRIC_UNIT).map(String::as_str),
3173            Some("By")
3174        );
3175        // The unknown key is rejected by the vocabulary check; other tables' keys
3176        // never appear.
3177        assert!(!table_options.contains_key("greptime.semantic.metric.type_BOGUS"));
3178        assert_eq!(table_options.len(), 2);
3179
3180        let mut empty = std::collections::HashMap::new();
3181        apply_per_table_semantic_options(&mut empty, index, DEFAULT_SCHEMA_NAME, "not_in_index");
3182        assert!(empty.is_empty());
3183
3184        // A schema with no entry is a no-op even when the table name matches
3185        // elsewhere.
3186        let mut opts = std::collections::HashMap::new();
3187        apply_per_table_semantic_options(
3188            &mut opts,
3189            index,
3190            "schema_without_entry",
3191            "http_requests_total",
3192        );
3193        assert!(opts.is_empty());
3194
3195        // No extension at all parses to no index (e.g. logs / Prom RW v1).
3196        let bare = Arc::new(QueryContext::with(
3197            DEFAULT_CATALOG_NAME,
3198            DEFAULT_SCHEMA_NAME,
3199        ));
3200        assert!(parse_per_table_semantic_index(&bare).is_none());
3201        let mut opts = std::collections::HashMap::new();
3202        apply_per_table_semantic_options(
3203            &mut opts,
3204            None,
3205            DEFAULT_SCHEMA_NAME,
3206            "http_requests_total",
3207        );
3208        assert!(opts.is_empty());
3209    }
3210
3211    #[test]
3212    fn test_last_non_null_create_options_preserve_default_with_append_mode_false() {
3213        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
3214        ctx.set_extension(APPEND_MODE_KEY, "false");
3215        let ctx = Arc::new(ctx);
3216        let mut table_options = Default::default();
3217
3218        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::LastNonNull, &ctx);
3219
3220        assert!(!table_options.contains_key(APPEND_MODE_KEY));
3221        assert_eq!(
3222            Some("last_non_null"),
3223            table_options.get(MERGE_MODE_KEY).map(String::as_str)
3224        );
3225    }
3226
3227    #[test]
3228    fn test_last_non_null_create_options_use_configured_merge_mode() {
3229        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
3230        ctx.set_extension(MERGE_MODE_KEY, "last_row");
3231        let ctx = Arc::new(ctx);
3232        let mut table_options = Default::default();
3233
3234        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::LastNonNull, &ctx);
3235
3236        assert_eq!(
3237            Some("last_row"),
3238            table_options.get(MERGE_MODE_KEY).map(String::as_str)
3239        );
3240        assert!(!table_options.contains_key(APPEND_MODE_KEY));
3241    }
3242
3243    #[test]
3244    fn test_last_non_null_create_options_use_last_row_with_append_mode_true() {
3245        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
3246        ctx.set_extension(APPEND_MODE_KEY, "true");
3247        let ctx = Arc::new(ctx);
3248        let mut table_options = Default::default();
3249
3250        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::LastNonNull, &ctx);
3251
3252        assert_eq!(
3253            Some("true"),
3254            table_options.get(APPEND_MODE_KEY).map(String::as_str)
3255        );
3256        assert_eq!(
3257            Some("last_row"),
3258            table_options.get(MERGE_MODE_KEY).map(String::as_str)
3259        );
3260    }
3261
3262    struct UnexpectedBatcher;
3263
3264    #[async_trait::async_trait]
3265    impl PendingRowsBatcher for UnexpectedBatcher {
3266        async fn acquire(&self) -> Result<Arc<tokio::sync::OwnedSemaphorePermit>> {
3267            panic!("empty writes must not acquire batch admission")
3268        }
3269
3270        async fn submit(
3271            &self,
3272            _table_info: TableInfoRef,
3273            _batch: arrow::record_batch::RecordBatch,
3274            _ctx: QueryContextRef,
3275            _permit: Arc<tokio::sync::OwnedSemaphorePermit>,
3276        ) -> Result<Output> {
3277            panic!("empty writes must not submit a batch")
3278        }
3279    }
3280
3281    async fn batcher_test_inserter() -> Inserter {
3282        let kv_backend = prepare_mocked_backend().await;
3283        Inserter::new(
3284            catalog::memory::MemoryCatalogManager::new(),
3285            create_partition_rule_manager(kv_backend.clone()).await,
3286            Arc::new(MockDatanodeManager::new(NaiveDatanodeHandler)),
3287            Arc::new(new_table_flownode_set_cache(
3288                String::new(),
3289                Cache::new(100),
3290                kv_backend,
3291            )),
3292            true,
3293        )
3294    }
3295
3296    #[tokio::test]
3297    async fn test_logical_batcher_eligibility() {
3298        use catalog::RegisterTableRequest;
3299        use catalog::memory::MemoryCatalogManager;
3300        use common_meta::instruction::{CacheIdent, CreateFlow};
3301        use common_meta::kv_backend::KvBackendRef;
3302        use common_meta::kv_backend::memory::MemoryKvBackend;
3303        use datatypes::schema::{ColumnDefaultConstraint, SchemaBuilder};
3304        let requests = RowInsertRequests {
3305            inserts: vec![RowInsertRequest {
3306                table_name: "test_table".to_string(),
3307                rows: None,
3308            }],
3309        };
3310        let original =
3311            make_table_ref_with_schema("ts", "value", ConcreteDataType::float64_datatype())
3312                .table_info();
3313        for case in [
3314            "eligible",
3315            "physical",
3316            "ordinary",
3317            "instant",
3318            "disabled",
3319            "hint",
3320            "flow",
3321            "required_tag",
3322            "default",
3323        ] {
3324            let mut info = (*original).clone();
3325            info.meta.engine = METRIC_ENGINE_NAME.to_string();
3326            info.meta.options.extra_options.insert(
3327                LOGICAL_TABLE_METADATA_KEY.to_string(),
3328                "physical".to_string(),
3329            );
3330            let mut ctx = QueryContext::arc().fork();
3331            match case {
3332                "physical" => {
3333                    info.meta
3334                        .options
3335                        .extra_options
3336                        .insert(LOGICAL_TABLE_METADATA_KEY.to_string(), "other".to_string());
3337                }
3338                "ordinary" => info.meta.engine = "mito".to_string(),
3339                "instant" => info.meta.options.ttl = Some(common_time::ttl::TimeToLive::Instant),
3340                "hint" => ctx.set_extension(AUTO_CREATE_TABLE_KEY, "false"),
3341                "required_tag" | "default" => {
3342                    let mut columns = info.meta.schema.column_schemas().to_vec();
3343                    let mut tag = ColumnSchema::new(
3344                        "tag",
3345                        ConcreteDataType::string_datatype(),
3346                        case != "required_tag",
3347                    );
3348                    if case == "default" {
3349                        tag = tag
3350                            .with_default_constraint(Some(ColumnDefaultConstraint::null_value()))
3351                            .unwrap();
3352                    }
3353                    columns.push(tag);
3354                    info.meta.schema = Arc::new(
3355                        SchemaBuilder::try_from_columns(columns)
3356                            .unwrap()
3357                            .build()
3358                            .unwrap(),
3359                    );
3360                    info.meta.primary_key_indices = vec![2];
3361                }
3362                _ => {}
3363            }
3364            let catalog = MemoryCatalogManager::with_default_setup();
3365            let table = Arc::new(table::Table::new(
3366                Arc::new(info),
3367                table::metadata::FilterPushDownType::Unsupported,
3368                Arc::new(DummyDataSource),
3369            ));
3370            catalog
3371                .register_table_sync(RegisterTableRequest {
3372                    catalog: DEFAULT_CATALOG_NAME.to_string(),
3373                    schema: DEFAULT_SCHEMA_NAME.to_string(),
3374                    table_name: "test_table".to_string(),
3375                    table_id: 1,
3376                    table,
3377                })
3378                .unwrap();
3379            let mut inserter = batcher_test_inserter().await;
3380            inserter.catalog_manager = catalog;
3381            inserter.auto_create_table = case != "disabled";
3382            let kv_backend: KvBackendRef = Arc::new(MemoryKvBackend::default());
3383            inserter.table_flownode_set_cache = Arc::new(new_table_flownode_set_cache(
3384                String::new(),
3385                Cache::new(10),
3386                kv_backend,
3387            ));
3388            if case == "flow" {
3389                inserter
3390                    .table_flownode_set_cache
3391                    .invalidate(&[CacheIdent::CreateFlow(CreateFlow {
3392                        flow_id: 1,
3393                        source_table_ids: vec![1],
3394                        partition_to_peer_mapping: vec![(0, Peer::empty(1))],
3395                    })])
3396                    .await
3397                    .unwrap();
3398            }
3399            assert_eq!(
3400                inserter
3401                    .can_batch_metric_rows(&requests, &Arc::new(ctx), "physical")
3402                    .await
3403                    .unwrap(),
3404                case == "eligible",
3405                "{case}"
3406            );
3407        }
3408    }
3409
3410    #[tokio::test]
3411    async fn test_batcher_meter_preserves_request() {
3412        let mut requests = RowInsertRequests {
3413            inserts: vec![RowInsertRequest {
3414                table_name: "sample".to_string(),
3415                rows: Some(Rows {
3416                    schema: vec![],
3417                    rows: vec![api::v1::Row {
3418                        values: vec![Value {
3419                            value_data: Some(api::v1::value::ValueData::F64Value(1.5)),
3420                        }],
3421                    }],
3422                }),
3423            }],
3424        };
3425        let expected = requests.clone();
3426        Inserter::meter_row_inserts(&mut requests, &QueryContext::arc())
3427            .await
3428            .unwrap();
3429        assert_eq!(requests, expected);
3430    }
3431
3432    #[tokio::test]
3433    async fn test_instant_table_bypasses_batcher() {
3434        let batcher: Arc<dyn PendingRowsBatcher> = Arc::new(UnexpectedBatcher);
3435        let inserter = batcher_test_inserter()
3436            .await
3437            .with_pending_rows_batcher(Some(batcher));
3438        let mut ctx = session::context::QueryContextBuilder::default().build();
3439        ctx.set_batching_enabled(true);
3440        let ctx = Arc::new(ctx);
3441        let table = make_table_ref_with_schema("ts", "value", ConcreteDataType::float64_datatype())
3442            .table_info();
3443        assert!(inserter.table_batcher(&table, &ctx).is_some());
3444        let mut instant = (*table).clone();
3445        instant.meta.options.ttl = Some(common_time::ttl::TimeToLive::Instant);
3446        assert!(inserter.table_batcher(&Arc::new(instant), &ctx).is_none());
3447    }
3448
3449    #[tokio::test]
3450    async fn test_empty_prepared_rows_skip_batcher() {
3451        let inserter = batcher_test_inserter().await;
3452        let table = make_table_ref_with_schema("ts", "value", ConcreteDataType::float64_datatype())
3453            .table_info();
3454        let batcher: Arc<dyn PendingRowsBatcher> = Arc::new(UnexpectedBatcher);
3455        let ctx = QueryContext::arc();
3456        let output = inserter
3457            .submit_table_rows(
3458                Rows {
3459                    schema: vec![],
3460                    rows: vec![],
3461                },
3462                table.clone(),
3463                ctx.clone(),
3464                &batcher,
3465            )
3466            .await
3467            .unwrap();
3468        assert!(matches!(output.data, OutputData::AffectedRows(0)));
3469        let output = inserter
3470            .submit_pending_rows(
3471                RowInsertRequests {
3472                    inserts: vec![RowInsertRequest {
3473                        table_name: table.name.clone(),
3474                        rows: None,
3475                    }],
3476                },
3477                HashMap::from_iter([(table.table_id(), table)]),
3478                ctx,
3479                &batcher,
3480            )
3481            .await
3482            .unwrap();
3483        assert!(matches!(output.data, OutputData::AffectedRows(0)));
3484    }
3485
3486    /// Flownode handler that keeps mirror inserts in flight until the test
3487    /// releases the gate, so the pending mirror budget can be observed
3488    /// deterministically.
3489    #[derive(Clone)]
3490    struct GatedFlownodeHandler {
3491        gate: Arc<tokio::sync::Semaphore>,
3492        affected_rows: u64,
3493    }
3494
3495    #[async_trait::async_trait]
3496    impl MockFlownodeHandler for GatedFlownodeHandler {
3497        async fn handle_inserts(
3498            &self,
3499            _peer: &Peer,
3500            _requests: api::v1::region::InsertRequests,
3501        ) -> common_meta::error::Result<FlowResponse> {
3502            let _permit = self.gate.acquire().await.unwrap();
3503            Ok(FlowResponse {
3504                affected_rows: self.affected_rows,
3505                ..Default::default()
3506            })
3507        }
3508    }
3509
3510    fn flownode_peer() -> Peer {
3511        Peer {
3512            id: 1,
3513            addr: "127.0.0.1:4001".to_string(),
3514        }
3515    }
3516
3517    #[derive(Clone)]
3518    struct RecordingNodeManager {
3519        datanode_dispatch: tokio::sync::mpsc::UnboundedSender<usize>,
3520        flownode_dispatch: tokio::sync::mpsc::UnboundedSender<Peer>,
3521        gates: Arc<std::sync::Mutex<HashMap<u64, Arc<tokio::sync::Semaphore>>>>,
3522        fail_flownode: bool,
3523    }
3524
3525    #[async_trait::async_trait]
3526    impl common_meta::node_manager::DatanodeManager for RecordingNodeManager {
3527        async fn datanode(&self, peer: &Peer) -> common_meta::node_manager::DatanodeRef {
3528            Arc::new(RecordingNode {
3529                peer: peer.clone(),
3530                manager: self.clone(),
3531            })
3532        }
3533    }
3534
3535    #[async_trait::async_trait]
3536    impl common_meta::node_manager::FlownodeManager for RecordingNodeManager {
3537        async fn flownode(&self, peer: &Peer) -> common_meta::node_manager::FlownodeRef {
3538            Arc::new(RecordingNode {
3539                peer: peer.clone(),
3540                manager: self.clone(),
3541            })
3542        }
3543    }
3544
3545    struct RecordingNode {
3546        peer: Peer,
3547        manager: RecordingNodeManager,
3548    }
3549
3550    #[async_trait::async_trait]
3551    impl common_meta::node_manager::Datanode for RecordingNode {
3552        async fn handle(
3553            &self,
3554            _request: api::v1::region::RegionRequest,
3555        ) -> common_meta::error::Result<api::region::RegionResponse> {
3556            let rows = match _request.body.as_ref() {
3557                Some(api::v1::region::region_request::Body::Inserts(requests)) => requests
3558                    .requests
3559                    .iter()
3560                    .filter_map(|request| request.rows.as_ref())
3561                    .map(|rows| rows.rows.len())
3562                    .sum(),
3563                _ => 0,
3564            };
3565            let _ = self.manager.datanode_dispatch.send(rows);
3566            Ok(api::region::RegionResponse::new(rows))
3567        }
3568
3569        async fn handle_query(
3570            &self,
3571            _request: common_query::request::QueryRequest,
3572        ) -> common_meta::error::Result<common_recordbatch::SendableRecordBatchStream> {
3573            unreachable!()
3574        }
3575    }
3576
3577    #[async_trait::async_trait]
3578    impl common_meta::node_manager::Flownode for RecordingNode {
3579        async fn handle(
3580            &self,
3581            _request: api::v1::flow::FlowRequest,
3582        ) -> common_meta::error::Result<FlowResponse> {
3583            unreachable!()
3584        }
3585
3586        async fn handle_inserts(
3587            &self,
3588            _request: api::v1::region::InsertRequests,
3589        ) -> common_meta::error::Result<FlowResponse> {
3590            let _ = self.manager.flownode_dispatch.send(self.peer.clone());
3591            if self.manager.fail_flownode {
3592                return Err(common_meta::error::UnexpectedSnafu {
3593                    err_msg: "test flownode failure".to_string(),
3594                }
3595                .build());
3596            }
3597            let gate = self
3598                .manager
3599                .gates
3600                .lock()
3601                .unwrap()
3602                .get(&self.peer.id)
3603                .cloned();
3604            if let Some(gate) = gate {
3605                let _permit = gate.acquire().await.unwrap();
3606            }
3607            Ok(FlowResponse::default())
3608        }
3609
3610        async fn handle_mark_window_dirty(
3611            &self,
3612            _request: api::v1::flow::DirtyWindowRequests,
3613        ) -> common_meta::error::Result<FlowResponse> {
3614            unreachable!()
3615        }
3616    }
3617
3618    fn mirror_requests(peer: &Peer, num_rows: usize) -> HashMap<Peer, RegionInsertRequests> {
3619        HashMap::from_iter([(
3620            peer.clone(),
3621            RegionInsertRequests {
3622                requests: vec![RegionInsertRequest {
3623                    region_id: RegionId::new(1, 1).as_u64(),
3624                    rows: Some(Rows {
3625                        schema: vec![],
3626                        rows: vec![api::v1::Row { values: vec![] }; num_rows],
3627                    }),
3628                    ..Default::default()
3629                }],
3630            },
3631        )])
3632    }
3633
3634    /// Serializes the mirror metric tests below: the metrics are process-wide
3635    /// singletons, so these tests would otherwise observe each other's updates
3636    /// when the test binary runs tests in parallel (they are isolated per
3637    /// process only under nextest).
3638    static MIRROR_METRIC_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
3639
3640    async fn lock_mirror_metrics() -> tokio::sync::MutexGuard<'static, ()> {
3641        MIRROR_METRIC_TEST_LOCK.lock().await
3642    }
3643
3644    // The tests below share the process-wide mirror metrics, so they take
3645    // `MIRROR_METRIC_TEST_LOCK` and assert on the difference their own budget
3646    // publishes instead of on absolute values.
3647    #[tokio::test]
3648    async fn flow_mirror_saturation_preserves_instant_and_normal_contracts() {
3649        use common_error::ext::{ErrorExt, RetryHint};
3650        use common_error::status_code::StatusCode;
3651
3652        let _guard = lock_mirror_metrics().await;
3653        let kv_backend = prepare_mocked_backend().await;
3654        let partition_manager = create_partition_rule_manager(kv_backend.clone()).await;
3655        let (datanode_tx, mut datanode_rx) = tokio::sync::mpsc::unbounded_channel();
3656        let (flownode_tx, mut flownode_rx) = tokio::sync::mpsc::unbounded_channel();
3657        let node_manager = Arc::new(RecordingNodeManager {
3658            datanode_dispatch: datanode_tx,
3659            flownode_dispatch: flownode_tx,
3660            gates: Arc::new(std::sync::Mutex::new(HashMap::new())),
3661            fail_flownode: false,
3662        });
3663        let flow_cache = Cache::new(10);
3664        let flow_cache_backend = prepare_mocked_backend().await;
3665        let inserter = Inserter::new(
3666            catalog::memory::MemoryCatalogManager::new(),
3667            partition_manager,
3668            node_manager,
3669            Arc::new(new_table_flownode_set_cache(
3670                String::new(),
3671                flow_cache.clone(),
3672                flow_cache_backend,
3673            )),
3674            true,
3675        );
3676        let peer = flownode_peer();
3677        inserter
3678            .table_flownode_set_cache
3679            .invalidate(&[common_meta::instruction::CacheIdent::CreateFlow(
3680                common_meta::instruction::CreateFlow {
3681                    flow_id: 1,
3682                    source_table_ids: vec![1, 2],
3683                    partition_to_peer_mapping: vec![(0, peer)],
3684                },
3685            )])
3686            .await
3687            .unwrap();
3688        let mut normal_info = new_test_table_info(1, "normal_table", [1].into_iter());
3689        normal_info.catalog_name = DEFAULT_CATALOG_NAME.to_string();
3690        normal_info.schema_name = DEFAULT_SCHEMA_NAME.to_string();
3691        let mut instant_info = new_test_table_info(2, "instant_table", [1].into_iter());
3692        instant_info.meta.options.ttl = Some(common_time::ttl::TimeToLive::Instant);
3693        let table_infos =
3694            HashMap::from_iter([(1, Arc::new(normal_info)), (2, Arc::new(instant_info))]);
3695        let ctx = Arc::new(QueryContext::with(
3696            DEFAULT_CATALOG_NAME,
3697            DEFAULT_SCHEMA_NAME,
3698        ));
3699        let insert = |table_id, num_rows| RegionInsertRequests {
3700            requests: vec![RegionInsertRequest {
3701                region_id: RegionId::new(table_id, 1).as_u64(),
3702                rows: Some(Rows {
3703                    schema: vec![],
3704                    rows: vec![api::v1::Row::default(); num_rows],
3705                }),
3706                ..Default::default()
3707            }],
3708        };
3709        let make_request = |normal, instant| InstantAndNormalInsertRequests {
3710            normal_requests: if normal == 0 {
3711                RegionInsertRequests::default()
3712            } else {
3713                insert(1, normal)
3714            },
3715            instant_requests: if instant == 0 {
3716                RegionInsertRequests::default()
3717            } else {
3718                insert(2, instant)
3719            },
3720        };
3721
3722        let pending = inserter.mirror_pending_rows.clone();
3723        let gauge_before = crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get();
3724        crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.add(MAX_MIRROR_PENDING_ROWS as i64);
3725        pending.store(MAX_MIRROR_PENDING_ROWS as u64, Ordering::Relaxed);
3726        let dropped_before = crate::metrics::DIST_MIRROR_DROPPED_ROW_COUNT.get();
3727        for request in [make_request(0, 1), make_request(1, 1)] {
3728            let error = inserter
3729                .do_request(request, &table_infos, &ctx)
3730                .await
3731                .unwrap_err();
3732            assert_eq!(error.status_code(), StatusCode::RateLimited);
3733            assert_eq!(error.retry_hint(), RetryHint::Retryable);
3734            assert!(datanode_rx.try_recv().is_err());
3735            assert!(flownode_rx.try_recv().is_err());
3736            assert_eq!(
3737                pending.load(Ordering::Relaxed),
3738                MAX_MIRROR_PENDING_ROWS as u64
3739            );
3740        }
3741        assert_eq!(
3742            crate::metrics::DIST_MIRROR_DROPPED_ROW_COUNT.get(),
3743            dropped_before
3744        );
3745
3746        for pending_before in [0, MAX_MIRROR_PENDING_ROWS as u64] {
3747            if pending_before == 0 {
3748                pending.store(0, Ordering::Relaxed);
3749                crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.sub(MAX_MIRROR_PENDING_ROWS as i64);
3750            }
3751            let gauge_before_rejection = crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get();
3752            for (normal, instant) in [
3753                (0, MAX_MIRROR_PENDING_ROWS + 1),
3754                (MAX_MIRROR_PENDING_ROWS / 2, MAX_MIRROR_PENDING_ROWS / 2 + 1),
3755            ] {
3756                let error = inserter
3757                    .do_request(make_request(normal, instant), &table_infos, &ctx)
3758                    .await
3759                    .unwrap_err();
3760                assert_eq!(error.status_code(), StatusCode::InvalidArguments);
3761                assert_eq!(error.retry_hint(), RetryHint::NonRetryable);
3762                let message = error.to_string();
3763                assert!(message.contains("1000001"), "{message}");
3764                assert!(message.contains("1000000"), "{message}");
3765                assert!(message.contains("reduce the batch size"), "{message}");
3766                assert!(datanode_rx.try_recv().is_err());
3767                assert!(flownode_rx.try_recv().is_err());
3768                assert_eq!(pending.load(Ordering::Relaxed), pending_before);
3769                assert_eq!(
3770                    crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get(),
3771                    gauge_before_rejection
3772                );
3773                assert_eq!(
3774                    crate::metrics::DIST_MIRROR_DROPPED_ROW_COUNT.get(),
3775                    dropped_before
3776                );
3777            }
3778            if pending_before == 0 {
3779                crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.add(MAX_MIRROR_PENDING_ROWS as i64);
3780                pending.store(MAX_MIRROR_PENDING_ROWS as u64, Ordering::Relaxed);
3781            }
3782        }
3783
3784        let result = inserter
3785            .do_request(make_request(1, 0), &table_infos, &ctx)
3786            .await
3787            .unwrap();
3788        assert!(matches!(result.data, OutputData::AffectedRows(1)));
3789        assert_eq!(1, datanode_rx.try_recv().unwrap());
3790        assert!(flownode_rx.try_recv().is_err());
3791        assert_eq!(
3792            crate::metrics::DIST_MIRROR_DROPPED_ROW_COUNT.get(),
3793            dropped_before + 1
3794        );
3795        pending.store(0, Ordering::Relaxed);
3796        crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.sub(MAX_MIRROR_PENDING_ROWS as i64);
3797        assert_eq!(
3798            crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get(),
3799            gauge_before
3800        );
3801    }
3802
3803    #[tokio::test]
3804    async fn flow_mirror_new_counts_regions_and_peer_clones() {
3805        use common_meta::instruction::{CacheIdent, CreateFlow};
3806
3807        let peer1 = flownode_peer();
3808        let peer2 = Peer {
3809            id: 2,
3810            addr: "127.0.0.1:4002".to_string(),
3811        };
3812        let cache_backend = prepare_mocked_backend().await;
3813        let cache = Arc::new(new_table_flownode_set_cache(
3814            String::new(),
3815            Cache::new(10),
3816            cache_backend,
3817        ));
3818        cache
3819            .invalidate(&[CacheIdent::CreateFlow(CreateFlow {
3820                flow_id: 1,
3821                source_table_ids: vec![1],
3822                partition_to_peer_mapping: vec![(0, peer1), (1, peer2)],
3823            })])
3824            .await
3825            .unwrap();
3826        let request = |region_id| RegionInsertRequest {
3827            region_id,
3828            rows: Some(Rows {
3829                schema: vec![],
3830                rows: vec![api::v1::Row { values: vec![] }; 3],
3831            }),
3832            ..Default::default()
3833        };
3834        let requests = [
3835            request(RegionId::new(1, 0).as_u64()),
3836            request(RegionId::new(1, 1).as_u64()),
3837        ];
3838        let task = FlowMirrorTask::new(&cache, requests.iter()).await.unwrap();
3839        assert_eq!(12, task.pending_rows());
3840        assert_eq!(2, task.requests.len());
3841    }
3842
3843    #[tokio::test]
3844    async fn flow_mirror_dropped_when_pending_exceeds_limit() {
3845        let _guard = lock_mirror_metrics().await;
3846        let pending = Arc::new(AtomicU64::new(MAX_MIRROR_PENDING_ROWS as u64));
3847        let dropped_before = crate::metrics::DIST_MIRROR_DROPPED_ROW_COUNT.get();
3848        let gauge_before = crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get();
3849        let num_rows = 100;
3850
3851        let task = FlowMirrorTask {
3852            requests: mirror_requests(&flownode_peer(), num_rows),
3853        };
3854        // The budget is already full, so nothing may be spawned for this batch.
3855        task.detach(
3856            Arc::new(MockDatanodeManager::new(NaiveDatanodeHandler)),
3857            pending.clone(),
3858            false,
3859        )
3860        .unwrap();
3861
3862        assert_eq!(
3863            num_rows as u64,
3864            crate::metrics::DIST_MIRROR_DROPPED_ROW_COUNT.get() - dropped_before
3865        );
3866        // The dropped batch must not leak its reservation.
3867        assert_eq!(
3868            MAX_MIRROR_PENDING_ROWS as u64,
3869            pending.load(Ordering::Relaxed)
3870        );
3871        // The reservation taken before the limit check is released again, so the
3872        // gauge nets out and keeps matching the pending budget.
3873        assert_eq!(
3874            gauge_before,
3875            crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get(),
3876            "a dropped batch must not change the pending gauge"
3877        );
3878
3879        let task = FlowMirrorTask {
3880            requests: mirror_requests(&flownode_peer(), num_rows),
3881        };
3882        let error = task
3883            .detach(
3884                Arc::new(MockDatanodeManager::new(NaiveDatanodeHandler)),
3885                pending.clone(),
3886                true,
3887            )
3888            .err()
3889            .unwrap();
3890        use common_error::ext::{ErrorExt, RetryHint};
3891        use common_error::status_code::StatusCode;
3892        assert_eq!(error.status_code(), StatusCode::RateLimited);
3893        assert_eq!(error.retry_hint(), RetryHint::Retryable);
3894        assert_eq!(
3895            crate::metrics::DIST_MIRROR_DROPPED_ROW_COUNT.get(),
3896            dropped_before + num_rows as u64,
3897            "rejected instant rows are not successful best-effort drops"
3898        );
3899        assert_eq!(
3900            MAX_MIRROR_PENDING_ROWS as u64,
3901            pending.load(Ordering::Relaxed)
3902        );
3903        assert_eq!(
3904            gauge_before,
3905            crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get()
3906        );
3907    }
3908
3909    /// A source table mapped to several flownodes clones its requests per peer,
3910    /// so the reservation must cover every clone and each spawned task must hand
3911    /// back exactly its own share; otherwise the shared budget drifts.
3912    #[tokio::test]
3913    async fn flow_mirror_reserves_and_releases_per_peer_shares() {
3914        let _guard = lock_mirror_metrics().await;
3915        let per_peer_rows = 10;
3916        let total_rows = (2 * per_peer_rows) as u64;
3917        // Exactly enough room for the whole batch, cloned rows included.
3918        let start = MAX_MIRROR_PENDING_ROWS as u64 - total_rows;
3919        let pending = Arc::new(AtomicU64::new(start));
3920        let gate = Arc::new(tokio::sync::Semaphore::new(0));
3921        let node_manager = Arc::new(MockFlownodeManager::new(GatedFlownodeHandler {
3922            gate: gate.clone(),
3923            affected_rows: per_peer_rows as u64,
3924        }));
3925
3926        let mut requests = mirror_requests(&flownode_peer(), per_peer_rows);
3927        requests.extend(mirror_requests(
3928            &Peer {
3929                id: 2,
3930                addr: "127.0.0.1:4002".to_string(),
3931            },
3932            per_peer_rows,
3933        ));
3934
3935        let gauge_before = crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get();
3936        FlowMirrorTask { requests }
3937            .detach(node_manager, pending.clone(), false)
3938            .unwrap();
3939
3940        // Both per-peer payloads are counted, so the budget is exactly full
3941        // instead of over-reserved by the duplicated rows.
3942        assert_eq!(
3943            MAX_MIRROR_PENDING_ROWS as u64,
3944            pending.load(Ordering::Relaxed)
3945        );
3946
3947        // Let both tasks finish and check that the budget lands back on its
3948        // starting value: over-releasing a shared reservation would saturate it.
3949        // The gauge is checked too because `release_mirror_pending_rows` moves
3950        // the budget before the gauge; exiting on the budget alone could leak a
3951        // pending `gauge.sub` into the next test holding the metrics lock.
3952        gate.add_permits(2);
3953        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
3954        while pending.load(Ordering::Relaxed) != start
3955            || crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get() != gauge_before
3956        {
3957            assert!(
3958                std::time::Instant::now() < deadline,
3959                "the mirror tasks must release exactly their own shares"
3960            );
3961            std::thread::sleep(std::time::Duration::from_millis(1));
3962        }
3963    }
3964
3965    #[tokio::test]
3966    async fn flow_mirror_fast_peer_releases_only_its_share_before_drop() {
3967        let _guard = lock_mirror_metrics().await;
3968        let peer1 = flownode_peer();
3969        let peer2 = Peer {
3970            id: 2,
3971            addr: "127.0.0.1:4002".to_string(),
3972        };
3973        let gate1 = Arc::new(tokio::sync::Semaphore::new(0));
3974        let gate2 = Arc::new(tokio::sync::Semaphore::new(0));
3975        let gates = Arc::new(std::sync::Mutex::new(HashMap::from_iter([
3976            (peer1.id, gate1.clone()),
3977            (peer2.id, gate2.clone()),
3978        ])));
3979        let (datanode_tx, _datanode_rx) = tokio::sync::mpsc::unbounded_channel();
3980        let (flownode_tx, mut flownode_rx) = tokio::sync::mpsc::unbounded_channel();
3981        let node_manager = Arc::new(RecordingNodeManager {
3982            datanode_dispatch: datanode_tx,
3983            flownode_dispatch: flownode_tx,
3984            gates,
3985            fail_flownode: false,
3986        });
3987        let pending = Arc::new(AtomicU64::new(MAX_MIRROR_PENDING_ROWS as u64 - 20));
3988        let gauge_before = crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get();
3989        let mut requests = mirror_requests(&peer1, 10);
3990        requests.extend(mirror_requests(&peer2, 10));
3991        let task = FlowMirrorTask { requests };
3992        task.detach(node_manager.clone(), pending.clone(), false)
3993            .unwrap();
3994        let first = tokio::time::timeout(Duration::from_secs(10), flownode_rx.recv())
3995            .await
3996            .unwrap()
3997            .unwrap();
3998        let second = tokio::time::timeout(Duration::from_secs(10), flownode_rx.recv())
3999            .await
4000            .unwrap()
4001            .unwrap();
4002        assert_ne!(first.id, second.id);
4003        let fast_gate = if first.id == peer1.id {
4004            gate1.clone()
4005        } else {
4006            gate2.clone()
4007        };
4008        let slow_gate = if first.id == peer1.id {
4009            gate2.clone()
4010        } else {
4011            gate1.clone()
4012        };
4013        fast_gate.add_permits(1);
4014        let deadline = std::time::Instant::now() + Duration::from_secs(10);
4015        while pending.load(Ordering::Relaxed) != MAX_MIRROR_PENDING_ROWS as u64 - 10 {
4016            assert!(
4017                std::time::Instant::now() < deadline,
4018                "fast peer did not release"
4019            );
4020            std::thread::sleep(Duration::from_millis(1));
4021        }
4022        let dropped_before = crate::metrics::DIST_MIRROR_DROPPED_ROW_COUNT.get();
4023        FlowMirrorTask {
4024            requests: mirror_requests(&peer1, 11),
4025        }
4026        .detach(node_manager, pending.clone(), false)
4027        .unwrap();
4028        assert_eq!(
4029            crate::metrics::DIST_MIRROR_DROPPED_ROW_COUNT.get(),
4030            dropped_before + 11
4031        );
4032        assert!(flownode_rx.try_recv().is_err());
4033        slow_gate.add_permits(1);
4034        while pending.load(Ordering::Relaxed) != MAX_MIRROR_PENDING_ROWS as u64 - 20
4035            || crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get() != gauge_before
4036        {
4037            assert!(
4038                std::time::Instant::now() < deadline,
4039                "slow peer did not release"
4040            );
4041            std::thread::sleep(Duration::from_millis(1));
4042        }
4043    }
4044
4045    #[tokio::test]
4046    async fn flow_mirror_failure_releases_budget_and_gauge() {
4047        let _guard = lock_mirror_metrics().await;
4048        let (datanode_tx, _datanode_rx) = tokio::sync::mpsc::unbounded_channel();
4049        let (flownode_tx, mut flownode_rx) = tokio::sync::mpsc::unbounded_channel();
4050        let node_manager = Arc::new(RecordingNodeManager {
4051            datanode_dispatch: datanode_tx,
4052            flownode_dispatch: flownode_tx,
4053            gates: Arc::new(std::sync::Mutex::new(HashMap::new())),
4054            fail_flownode: true,
4055        });
4056        let pending = Arc::new(AtomicU64::new(0));
4057        let gauge_before = crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get();
4058        FlowMirrorTask {
4059            requests: mirror_requests(&flownode_peer(), 5),
4060        }
4061        .detach(node_manager, pending.clone(), false)
4062        .unwrap();
4063        assert_eq!(
4064            tokio::time::timeout(Duration::from_secs(10), flownode_rx.recv())
4065                .await
4066                .unwrap()
4067                .unwrap()
4068                .id,
4069            flownode_peer().id
4070        );
4071        let deadline = std::time::Instant::now() + Duration::from_secs(10);
4072        while pending.load(Ordering::Relaxed) != 0
4073            || crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get() != gauge_before
4074        {
4075            assert!(
4076                std::time::Instant::now() < deadline,
4077                "failed task leaked reservation"
4078            );
4079            std::thread::sleep(Duration::from_millis(1));
4080        }
4081    }
4082
4083    #[tokio::test]
4084    async fn flow_mirror_pending_counter_consistent() {
4085        let _guard = lock_mirror_metrics().await;
4086        let pending = Arc::new(AtomicU64::new(0));
4087        let gate = Arc::new(tokio::sync::Semaphore::new(0));
4088        let node_manager = Arc::new(MockFlownodeManager::new(GatedFlownodeHandler {
4089            gate: gate.clone(),
4090            affected_rows: 7,
4091        }));
4092
4093        // A batch without rows short-circuits and leaves the budget untouched.
4094        let gauge_before = crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get();
4095        FlowMirrorTask {
4096            requests: HashMap::new(),
4097        }
4098        .detach(node_manager.clone(), pending.clone(), false)
4099        .unwrap();
4100        assert_eq!(0, pending.load(Ordering::Relaxed));
4101        assert_eq!(
4102            gauge_before,
4103            crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get(),
4104            "an empty batch must not touch the pending gauge"
4105        );
4106
4107        let dropped_before = crate::metrics::DIST_MIRROR_DROPPED_ROW_COUNT.get();
4108        let num_rows = 7;
4109
4110        FlowMirrorTask {
4111            requests: mirror_requests(&flownode_peer(), num_rows),
4112        }
4113        .detach(node_manager, pending.clone(), false)
4114        .unwrap();
4115
4116        // The spawned task is still waiting on the gated flownode, so the
4117        // reservation made by `detach` is the value observable here.
4118        assert_eq!(num_rows as u64, pending.load(Ordering::Relaxed));
4119        assert_eq!(
4120            num_rows as i64,
4121            crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get() - gauge_before,
4122            "the pending gauge must track the pending rows"
4123        );
4124        assert_eq!(
4125            dropped_before,
4126            crate::metrics::DIST_MIRROR_DROPPED_ROW_COUNT.get(),
4127            "a batch within the limit must not be counted as dropped"
4128        );
4129
4130        // Release the in-flight task and wait for it to hand its reservation
4131        // back, so no task is left holding the shared metrics after this test.
4132        gate.add_permits(1);
4133        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
4134        while pending.load(Ordering::Relaxed) != 0
4135            || crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.get() != gauge_before
4136        {
4137            assert!(
4138                std::time::Instant::now() < deadline,
4139                "the mirror task did not release its pending reservation"
4140            );
4141            std::thread::sleep(std::time::Duration::from_millis(1));
4142        }
4143    }
4144
4145    #[test]
4146    fn mirror_logs_are_rate_limited() {
4147        let log = Arc::new(MirrorLog::new());
4148        assert_eq!(Some(1), log.claim_report(1_000));
4149        assert_eq!(None, log.claim_report(1_500));
4150        assert_eq!(None, log.claim_report(10_999));
4151        assert_eq!(Some(3), log.claim_report(11_000));
4152        assert_eq!(None, log.claim_report(11_001));
4153        assert_eq!(Some(2), log.claim_report(21_000));
4154
4155        let concurrent = Arc::new(MirrorLog::new());
4156        assert_eq!(Some(1), concurrent.claim_report(50_000));
4157        let barrier = Arc::new(std::sync::Barrier::new(8));
4158        let threads = (0..8)
4159            .map(|_| {
4160                let log = concurrent.clone();
4161                let barrier = barrier.clone();
4162                std::thread::spawn(move || {
4163                    barrier.wait();
4164                    log.claim_report(60_000)
4165                })
4166            })
4167            .collect::<Vec<_>>();
4168        let winners = threads
4169            .into_iter()
4170            .filter_map(|thread| thread.join().unwrap())
4171            .collect::<Vec<_>>();
4172        assert_eq!(winners.len(), 1);
4173        let residual = concurrent.events.swap(0, Ordering::Relaxed);
4174        assert_eq!(winners[0] + residual, 8);
4175    }
4176}