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::Arc;
17
18use ahash::{HashMap, HashMapExt, HashSet, HashSetExt};
19use api::v1::alter_table_expr::Kind;
20use api::v1::column_def::options_from_skipping;
21use api::v1::region::{
22    InsertRequest as RegionInsertRequest, InsertRequests as RegionInsertRequests,
23    RegionRequestHeader,
24};
25use api::v1::{
26    AlterTableExpr, ColumnDataType, ColumnSchema, CreateTableExpr, InsertRequests,
27    RowInsertRequest, RowInsertRequests, Rows, SemanticType,
28};
29use catalog::CatalogManagerRef;
30use client::{OutputData, OutputMeta};
31use common_catalog::consts::{
32    DEFAULT_PRIVATE_SCHEMA_NAME, PARENT_SPAN_ID_COLUMN, SERVICE_NAME_COLUMN, TRACE_ID_COLUMN,
33    TRACE_TABLE_NAME, TRACE_TABLE_NAME_SESSION_KEY, default_engine, is_ddl_reserved_table,
34    trace_operations_table_name, trace_services_table_name,
35};
36use common_event_recorder::DEFAULT_EVENTS_TABLE_NAME;
37use common_frontend::slow_query_event::SLOW_QUERY_TABLE_NAME;
38use common_grpc_expr::util::ColumnExpr;
39use common_meta::cache::TableFlownodeSetCacheRef;
40use common_meta::datanode::REGION_STATS_HISTORY_TABLE_NAME;
41use common_meta::node_manager::{AffectedRows, NodeManagerRef};
42use common_meta::peer::Peer;
43use common_meta::rpc::ddl::TriggerReason;
44use common_query::Output;
45use common_query::native_histogram::{is_native_histogram_value_type, native_histogram_value_type};
46use common_query::prelude::{greptime_timestamp, greptime_value};
47use common_telemetry::tracing_context::TracingContext;
48use common_telemetry::{debug, error, warn};
49use datatypes::schema::SkippingIndexOptions;
50use futures_util::future;
51use meter_core::data::MeterRecord;
52use meter_macros::write_meter;
53use partition::manager::PartitionRuleManagerRef;
54use session::context::QueryContextRef;
55use snafu::ResultExt;
56use snafu::prelude::*;
57use sql::partition::partition_rule_for_hexstring;
58use sql::statements::create::Partitions;
59use sql::statements::insert::Insert;
60use store_api::metric_engine_consts::{
61    LOGICAL_TABLE_METADATA_KEY, METRIC_ENGINE_NAME, PHYSICAL_TABLE_METADATA_KEY,
62};
63use store_api::mito_engine_options::{
64    APPEND_MODE_KEY, COMPACTION_TYPE, COMPACTION_TYPE_TWCS, MERGE_MODE_KEY, TTL_KEY,
65    TWCS_TIME_WINDOW,
66};
67use store_api::storage::{RegionId, TableId};
68use table::TableRef;
69use table::metadata::{TableInfo, TableInfoRef};
70use table::requests::{
71    AUTO_CREATE_TABLE_KEY, InsertRequest as TableInsertRequest, SEMANTIC_PER_TABLE_INDEX_KEY,
72    SEMANTIC_PIPELINE, TABLE_DATA_MODEL, TABLE_DATA_MODEL_TRACE_V1, TABLE_DATA_MODEL_TRACE_V2,
73    TRACE_TABLE_PARTITIONS_HINT_KEY, VALID_TABLE_OPTION_KEYS, is_semantic_option_key,
74    validate_semantic_option,
75};
76use table::table_reference::TableReference;
77
78use crate::batcher::PendingRowsBatcher;
79use crate::error::{
80    CatalogSnafu, ColumnOptionsSnafu, CreatePartitionRulesSnafu, FindRegionLeaderSnafu,
81    InvalidInsertRequestSnafu, JoinTaskSnafu, RequestInsertsSnafu, Result, TableNotFoundSnafu,
82    WriteRejectedSnafu,
83};
84use crate::expr_helper;
85use crate::region_req_factory::RegionRequestFactory;
86use crate::req_convert::common::preprocess_row_insert_requests;
87use crate::req_convert::insert::{
88    ColumnToRow, ImpureDefaultFiller, RowToRegion, StatementToRegion, TableToRegion,
89    fill_reqs_with_impure_default, rows_to_record_batch,
90};
91use crate::statement::StatementExecutor;
92
93pub struct Inserter {
94    catalog_manager: CatalogManagerRef,
95    pub(crate) partition_manager: PartitionRuleManagerRef,
96    pub(crate) node_manager: NodeManagerRef,
97    pub(crate) table_flownode_set_cache: TableFlownodeSetCacheRef,
98    /// Server-side upper bound for auto table creation on write.
99    /// When `false`, missing tables are never auto-created regardless of the
100    /// per-request `auto_create_table` hint. When `true`, the hint still applies.
101    auto_create_table: bool,
102    pending_rows_batcher: Option<Arc<dyn PendingRowsBatcher>>,
103}
104
105pub type InserterRef = Arc<Inserter>;
106
107/// Hint for the table type to create automatically.
108#[derive(Clone)]
109pub enum AutoCreateTableType {
110    /// A logical table with the physical table name.
111    Logical(String),
112    /// A physical table.
113    Physical,
114    /// A log table which is append-only.
115    Log,
116    /// A table that merges rows by `last_non_null` strategy.
117    LastNonNull,
118    /// Create table that build index and default partition rules on trace_id
119    Trace { alter_existing: bool },
120}
121
122impl AutoCreateTableType {
123    pub fn as_str(&self) -> &'static str {
124        match self {
125            AutoCreateTableType::Logical(_) => "logical",
126            AutoCreateTableType::Physical => "physical",
127            AutoCreateTableType::Log => "log",
128            AutoCreateTableType::LastNonNull => "last_non_null",
129            AutoCreateTableType::Trace { .. } => "trace",
130        }
131    }
132
133    fn alter_existing(&self) -> bool {
134        !matches!(
135            self,
136            Self::Trace {
137                alter_existing: false
138            }
139        )
140    }
141}
142
143/// Split insert requests into normal and instant requests.
144///
145/// Where instant requests are requests with ttl=instant,
146/// and normal requests are requests with ttl set to other values.
147///
148/// This is used to split requests for different processing.
149#[derive(Clone)]
150pub struct InstantAndNormalInsertRequests {
151    /// Requests with normal ttl.
152    pub normal_requests: RegionInsertRequests,
153    /// Requests with ttl=instant.
154    /// Will be discarded immediately at frontend, wouldn't even insert into memtable, and only sent to flow node if needed.
155    pub instant_requests: RegionInsertRequests,
156}
157
158impl Inserter {
159    /// Checks the assumptions of the logical bulk path without changing tables.
160    /// Unsupported requests retain ordinary insertion, including schema policy,
161    /// defaults, instant TTL and row-based Flow delivery.
162    pub async fn can_batch_metric_rows(
163        &self,
164        requests: &RowInsertRequests,
165        ctx: &QueryContextRef,
166        physical_table: &str,
167    ) -> Result<bool> {
168        if self.auto_create_disabled_reason(ctx)?.is_some() || ctx.extension(TTL_KEY).is_some() {
169            return Ok(false);
170        }
171        for request in &requests.inserts {
172            // The logical bulk encoder only supports scalar metric schemas.
173            // Check new tables too, before catalog lookup or schema changes.
174            if request.rows.as_ref().is_some_and(|rows| {
175                rows.schema.iter().any(|column| {
176                    column.datatype_extension.is_some()
177                        || !matches!(
178                            ColumnDataType::try_from(column.datatype),
179                            Ok(ColumnDataType::TimestampMillisecond
180                                | ColumnDataType::Float64
181                                | ColumnDataType::String)
182                        )
183                })
184            }) {
185                return Ok(false);
186            }
187
188            let Some(table) = self
189                .get_table(
190                    ctx.current_catalog(),
191                    &ctx.current_schema(),
192                    &request.table_name,
193                )
194                .await?
195            else {
196                continue;
197            };
198            let info = table.table_info();
199            if info.meta.engine != METRIC_ENGINE_NAME
200                || info.is_ttl_instant_table()
201                || info
202                    .meta
203                    .options
204                    .extra_options
205                    .get(LOGICAL_TABLE_METADATA_KEY)
206                    .map(String::as_str)
207                    != Some(physical_table)
208                || info
209                    .meta
210                    .schema
211                    .column_schemas()
212                    .iter()
213                    .any(|column| column.default_constraint().is_some())
214            {
215                return Ok(false);
216            }
217            // Physical metric tags are nullable even when their logical schema
218            // is not. Arrow alignment is stricter than ordinary metric insertion.
219            if info
220                .meta
221                .primary_key_indices
222                .iter()
223                .any(|&index| !info.meta.schema.column_schemas()[index].is_nullable())
224            {
225                return Ok(false);
226            }
227            // The current Flow cache does not distinguish streaming and batch
228            // flows. Keep all Flow sources on the row-based delivery path.
229            match self.table_flownode_set_cache.get(info.table_id()).await {
230                Ok(None) => {}
231                Ok(Some(flows)) if flows.is_empty() => {}
232                _ => return Ok(false),
233            }
234        }
235        Ok(true)
236    }
237
238    /// Meters an original logical-table request before bulk routing, without
239    /// cloning its rows or changing the request boundary used for accounting.
240    pub async fn meter_row_inserts(
241        requests: &mut RowInsertRequests,
242        ctx: &QueryContextRef,
243    ) -> Result<u64> {
244        let metered = InstantAndNormalInsertRequests {
245            normal_requests: RegionInsertRequests {
246                requests: requests
247                    .inserts
248                    .iter_mut()
249                    .map(|request| RegionInsertRequest {
250                        rows: request.rows.take(),
251                        ..Default::default()
252                    })
253                    .collect(),
254            },
255            instant_requests: RegionInsertRequests::default(),
256        };
257        let cost = write_meter!(
258            ctx.current_catalog(),
259            ctx.current_schema(),
260            metered,
261            ctx.write_rows_to_admit(
262                ctx.current_catalog(),
263                &ctx.current_schema(),
264                count_insert_rows(&metered)?
265            ),
266            ctx.channel() as u8
267        )
268        .await
269        .context(WriteRejectedSnafu);
270        for (request, region) in requests
271            .inserts
272            .iter_mut()
273            .zip(metered.normal_requests.requests)
274        {
275            request.rows = region.rows;
276        }
277        cost
278    }
279
280    pub fn new(
281        catalog_manager: CatalogManagerRef,
282        partition_manager: PartitionRuleManagerRef,
283        node_manager: NodeManagerRef,
284        table_flownode_set_cache: TableFlownodeSetCacheRef,
285        auto_create_table: bool,
286    ) -> Self {
287        Self {
288            catalog_manager,
289            partition_manager,
290            node_manager,
291            table_flownode_set_cache,
292            auto_create_table,
293            pending_rows_batcher: None,
294        }
295    }
296
297    /// Installs the shared batcher; callers explicitly select its ingestion entry point.
298    pub fn with_pending_rows_batcher(
299        mut self,
300        batcher: Option<Arc<dyn PendingRowsBatcher>>,
301    ) -> Self {
302        self.pending_rows_batcher = batcher;
303        self
304    }
305
306    pub async fn handle_column_inserts(
307        &self,
308        requests: InsertRequests,
309        ctx: QueryContextRef,
310        statement_executor: &StatementExecutor,
311    ) -> Result<Output> {
312        let row_inserts = ColumnToRow::convert(requests)?;
313        self.handle_row_inserts(row_inserts, ctx, statement_executor, false, false)
314            .await
315    }
316
317    /// Handles row inserts request and creates a physical table on demand.
318    pub async fn handle_row_inserts(
319        &self,
320        mut requests: RowInsertRequests,
321        ctx: QueryContextRef,
322        statement_executor: &StatementExecutor,
323        accommodate_existing_schema: bool,
324        is_single_value: bool,
325    ) -> Result<Output> {
326        preprocess_row_insert_requests(&mut requests.inserts)?;
327        self.handle_row_inserts_with_create_type(
328            requests,
329            ctx,
330            statement_executor,
331            AutoCreateTableType::Physical,
332            accommodate_existing_schema,
333            is_single_value,
334        )
335        .await
336    }
337
338    /// Handles row inserts request and creates a log table on demand.
339    pub async fn handle_log_inserts(
340        &self,
341        requests: RowInsertRequests,
342        ctx: QueryContextRef,
343        statement_executor: &StatementExecutor,
344    ) -> Result<Output> {
345        self.handle_row_inserts_with_create_type(
346            requests,
347            ctx,
348            statement_executor,
349            AutoCreateTableType::Log,
350            false,
351            false,
352        )
353        .await
354    }
355
356    pub async fn handle_trace_inserts(
357        &self,
358        requests: RowInsertRequests,
359        ctx: QueryContextRef,
360        statement_executor: &StatementExecutor,
361    ) -> Result<Output> {
362        self.handle_row_inserts_with_create_type(
363            requests,
364            ctx,
365            statement_executor,
366            AutoCreateTableType::Trace {
367                alter_existing: true,
368            },
369            false,
370            false,
371        )
372        .await
373    }
374
375    /// Handles row inserts request and creates a table with `last_non_null` merge mode on demand.
376    pub async fn handle_last_non_null_inserts(
377        &self,
378        requests: RowInsertRequests,
379        ctx: QueryContextRef,
380        statement_executor: &StatementExecutor,
381        accommodate_existing_schema: bool,
382        is_single_value: bool,
383    ) -> Result<Output> {
384        self.handle_row_inserts_with_create_type(
385            requests,
386            ctx,
387            statement_executor,
388            AutoCreateTableType::LastNonNull,
389            accommodate_existing_schema,
390            is_single_value,
391        )
392        .await
393    }
394
395    /// Handles row inserts request with specified [AutoCreateTableType].
396    async fn handle_row_inserts_with_create_type(
397        &self,
398        mut requests: RowInsertRequests,
399        ctx: QueryContextRef,
400        statement_executor: &StatementExecutor,
401        create_type: AutoCreateTableType,
402        accommodate_existing_schema: bool,
403        is_single_value: bool,
404    ) -> Result<Output> {
405        let skip_wal = ctx.skip_wal();
406
407        let batcher = self
408            .pending_rows_batcher
409            .as_ref()
410            .filter(|_| ctx.batching_enabled());
411
412        // remove empty requests
413        requests.inserts.retain(|req| {
414            req.rows
415                .as_ref()
416                .map(|r| !r.rows.is_empty())
417                .unwrap_or_default()
418        });
419        validate_column_count_match(&requests)?;
420
421        let CreateAlterTableResult {
422            instant_table_ids,
423            table_infos,
424        } = self
425            .create_or_alter_tables_on_demand(
426                &mut requests,
427                &ctx,
428                create_type,
429                statement_executor,
430                accommodate_existing_schema,
431                is_single_value,
432            )
433            .await?;
434
435        // Instant tables have no persisted data for dirty-window Flow to read.
436        // Metric tables keep their existing dedicated ingestion path.
437        if let Some(batcher) = batcher
438            && instant_table_ids.is_empty()
439            && table_infos
440                .values()
441                .all(|info| info.meta.engine == default_engine())
442        {
443            return self
444                .submit_pending_rows(requests, table_infos, ctx, batcher)
445                .await;
446        }
447
448        let name_to_info = table_infos
449            .values()
450            .map(|info| (info.name.clone(), info.clone()))
451            .collect::<HashMap<_, _>>();
452        let inserts = RowToRegion::new(
453            name_to_info,
454            instant_table_ids,
455            self.partition_manager.as_ref(),
456        )
457        .convert(requests, skip_wal)
458        .await?;
459
460        self.do_request(inserts, &table_infos, &ctx).await
461    }
462
463    async fn submit_pending_rows(
464        &self,
465        mut requests: RowInsertRequests,
466        table_infos: HashMap<TableId, Arc<TableInfo>>,
467        ctx: QueryContextRef,
468        batcher: &Arc<dyn PendingRowsBatcher>,
469    ) -> Result<Output> {
470        // All entry points, including single-table and SQL writes, skip empty input
471        // before evaluating defaults or converting prepared rows.
472        requests.inserts.retain(|request| {
473            request
474                .rows
475                .as_ref()
476                .is_some_and(|rows| !rows.rows.is_empty())
477        });
478        let by_name = table_infos
479            .values()
480            .map(|info| (info.name.as_str(), info))
481            .collect::<HashMap<_, _>>();
482        let mut prepared = Vec::with_capacity(requests.inserts.len());
483        for request in &mut requests.inserts {
484            let table_info =
485                by_name
486                    .get(request.table_name.as_str())
487                    .context(TableNotFoundSnafu {
488                        table_name: &request.table_name,
489                    })?;
490            let Some(rows) = &mut request.rows else {
491                continue;
492            };
493            ImpureDefaultFiller::new((*table_info).clone())?.fill_rows(rows);
494            let batch = rows_to_record_batch(rows, table_info)?;
495            prepared.push(((*table_info).clone(), batch));
496        }
497        // Preserve the existing meter input and original request boundary. These
498        // envelopes are only for accounting; routing happens after batching.
499        let metered = InstantAndNormalInsertRequests {
500            normal_requests: RegionInsertRequests {
501                requests: requests
502                    .inserts
503                    .into_iter()
504                    .map(|request| RegionInsertRequest {
505                        rows: request.rows,
506                        ..Default::default()
507                    })
508                    .collect(),
509            },
510            instant_requests: RegionInsertRequests::default(),
511        };
512        let table_info = table_infos.values().next();
513        let catalog = table_info.map_or(ctx.current_catalog(), |info| info.catalog_name.as_str());
514        let schema =
515            table_info.map_or_else(|| ctx.current_schema(), |info| info.schema_name.clone());
516        let write_cost = write_meter!(
517            catalog,
518            &schema,
519            metered,
520            ctx.write_rows_to_admit(catalog, &schema, count_insert_rows(&metered)?),
521            ctx.channel() as u8
522        )
523        .await
524        .context(WriteRejectedSnafu)?;
525        prepared.retain(|(_, batch)| batch.num_rows() != 0);
526        let results = if prepared.is_empty() {
527            Vec::new()
528        } else {
529            // One original request shares admission across all table submissions.
530            let permit = batcher.acquire().await?;
531            let submissions = prepared.into_iter().map(|(info, batch)| {
532                // Route to the same target database used for admission above.
533                let mut target_ctx = ctx.fork();
534                target_ctx.set_current_catalog(&info.catalog_name);
535                target_ctx.set_current_schema(&info.schema_name);
536                batcher.submit(info, batch, Arc::new(target_ctx), permit.clone())
537            });
538            // Observe every table completion even when another table fails.
539            future::join_all(submissions).await
540        };
541        let affected_rows = results.into_iter().sum::<Result<usize>>()?;
542        Ok(Output::new(
543            OutputData::AffectedRows(affected_rows),
544            OutputMeta::new_with_cost(write_cost as _),
545        ))
546    }
547
548    /// Handles row inserts request with metric engine.
549    pub async fn handle_metric_row_inserts(
550        &self,
551        mut requests: RowInsertRequests,
552        ctx: QueryContextRef,
553        statement_executor: &StatementExecutor,
554        physical_table: String,
555    ) -> Result<Output> {
556        let skip_wal = ctx.skip_wal();
557
558        // remove empty requests
559        requests.inserts.retain(|req| {
560            req.rows
561                .as_ref()
562                .map(|r| !r.rows.is_empty())
563                .unwrap_or_default()
564        });
565        validate_column_count_match(&requests)?;
566
567        // check and create physical table
568        self.create_physical_table_on_demand(&ctx, physical_table.clone(), statement_executor)
569            .await?;
570
571        // check and create logical tables
572        let CreateAlterTableResult {
573            instant_table_ids,
574            table_infos,
575        } = self
576            .create_or_alter_tables_on_demand(
577                &mut requests,
578                &ctx,
579                AutoCreateTableType::Logical(physical_table.clone()),
580                statement_executor,
581                true,
582                true,
583            )
584            .await?;
585        let name_to_info = table_infos
586            .values()
587            .map(|info| (info.name.clone(), info.clone()))
588            .collect::<HashMap<_, _>>();
589        let inserts = RowToRegion::new(name_to_info, instant_table_ids, &self.partition_manager)
590            .convert(requests, skip_wal)
591            .await?;
592
593        self.do_request(inserts, &table_infos, &ctx).await
594    }
595
596    fn table_batcher(
597        &self,
598        table_info: &TableInfoRef,
599        ctx: &QueryContextRef,
600    ) -> Option<&Arc<dyn PendingRowsBatcher>> {
601        self.pending_rows_batcher.as_ref().filter(|_| {
602            ctx.batching_enabled()
603                && !table_info.is_ttl_instant_table()
604                && table_info.meta.engine == default_engine()
605        })
606    }
607
608    async fn submit_table_rows(
609        &self,
610        rows: Rows,
611        table_info: TableInfoRef,
612        ctx: QueryContextRef,
613        batcher: &Arc<dyn PendingRowsBatcher>,
614    ) -> Result<Output> {
615        let requests = RowInsertRequests {
616            inserts: vec![RowInsertRequest {
617                table_name: table_info.name.clone(),
618                rows: Some(rows),
619            }],
620        };
621        let table_infos = HashMap::from_iter([(table_info.table_id(), table_info)]);
622        self.submit_pending_rows(requests, table_infos, ctx, batcher)
623            .await
624    }
625
626    pub async fn handle_table_insert(
627        &self,
628        request: TableInsertRequest,
629        ctx: QueryContextRef,
630    ) -> Result<Output> {
631        let catalog = request.catalog_name.as_str();
632        let schema = request.schema_name.as_str();
633        let table_name = request.table_name.as_str();
634        let table = self.get_table(catalog, schema, table_name).await?;
635        let table = table.with_context(|| TableNotFoundSnafu {
636            table_name: common_catalog::format_full_table_name(catalog, schema, table_name),
637        })?;
638        let table_info = table.table_info();
639
640        let converter = TableToRegion::new(&table_info, &self.partition_manager);
641        let skip_wal = request.skip_wal;
642        let rows = converter.prepare(request)?;
643        if let Some(batcher) = self.table_batcher(&table_info, &ctx) {
644            return self.submit_table_rows(rows, table_info, ctx, batcher).await;
645        }
646        let inserts = converter.partition(rows, skip_wal).await?;
647
648        let table_infos = HashMap::from_iter([(table_info.table_id(), table_info.clone())]);
649
650        self.do_request(inserts, &table_infos, &ctx).await
651    }
652
653    pub async fn handle_statement_insert(
654        &self,
655        insert: &Insert,
656        ctx: &QueryContextRef,
657    ) -> Result<Output> {
658        let converter =
659            StatementToRegion::new(self.catalog_manager.as_ref(), &self.partition_manager, ctx);
660        let (rows, table_info) = converter.prepare(insert, ctx).await?;
661        if let Some(batcher) = self.table_batcher(&table_info, ctx) {
662            return self
663                .submit_table_rows(rows, table_info, ctx.clone(), batcher)
664                .await;
665        }
666        let inserts = converter.partition(rows, table_info.clone(), ctx).await?;
667
668        let table_infos = HashMap::from_iter([(table_info.table_id(), table_info.clone())]);
669
670        self.do_request(inserts, &table_infos, ctx).await
671    }
672}
673
674/// Admits a finite request before it is split into internal writes.
675/// The returned context preserves accounting while preventing a second row debit.
676pub async fn admit_write(rows: u64, ctx: &QueryContextRef) -> Result<QueryContextRef> {
677    // The zero value is WCU: this record only admits rows. Actual inserts retain
678    // their existing WCU accounting, so charging here would count it twice.
679    write_meter!(MeterRecord::new(
680        ctx.current_catalog().to_string(),
681        ctx.current_schema(),
682        0,
683        ctx.write_rows_to_admit(ctx.current_catalog(), &ctx.current_schema(), rows),
684        ctx.channel() as u8,
685    ))
686    .await
687    .context(WriteRejectedSnafu)?;
688    Ok(Arc::new(ctx.with_write_admission()))
689}
690
691/// Admits all database totals before dispatching any batch of a finite request.
692/// Each batch keeps its own protocol options and target database.
693pub async fn admit_row_insert_batches(
694    batches: &mut [(QueryContextRef, RowInsertRequests)],
695) -> Result<()> {
696    let mut totals = BTreeMap::<_, (QueryContextRef, u64)>::new();
697    for (ctx, requests) in batches.iter() {
698        let catalog = ctx.current_catalog();
699        let schema = ctx.current_schema();
700        if ctx.write_rows_to_admit(catalog, &schema, 1) == 0 {
701            continue;
702        }
703        let (_, total) = totals
704            .entry((catalog.to_string(), schema.clone()))
705            .or_insert_with(|| (ctx.clone(), 0));
706        for rows in requests.inserts.iter().filter_map(|r| r.rows.as_ref()) {
707            *total =
708                total
709                    .checked_add(rows.rows.len() as u64)
710                    .context(InvalidInsertRequestSnafu {
711                        reason: "Insert row count exceeds u64::MAX",
712                    })?;
713        }
714    }
715    for (ctx, rows) in totals.values() {
716        admit_write(*rows, ctx).await?;
717    }
718    for (ctx, _) in batches {
719        *ctx = Arc::new(ctx.with_write_admission());
720    }
721    Ok(())
722}
723
724fn count_insert_rows(requests: &InstantAndNormalInsertRequests) -> Result<u64> {
725    requests
726        .normal_requests
727        .requests
728        .iter()
729        .chain(&requests.instant_requests.requests)
730        .filter_map(|request| request.rows.as_ref())
731        .try_fold(0u64, |total, rows| {
732            total
733                .checked_add(rows.rows.len() as u64)
734                .context(InvalidInsertRequestSnafu {
735                    reason: "Insert row count exceeds u64::MAX",
736                })
737        })
738}
739
740impl Inserter {
741    async fn do_request(
742        &self,
743        requests: InstantAndNormalInsertRequests,
744        table_infos: &HashMap<TableId, Arc<TableInfo>>,
745        ctx: &QueryContextRef,
746    ) -> Result<Output> {
747        // Fill impure default values in the request
748        let requests = fill_reqs_with_impure_default(table_infos, requests)?;
749
750        // All tables in a batch resolve to the same database. Qualified SQL
751        // inserts may target a different database than the session's current one.
752        let table_info = table_infos.values().next();
753        let catalog = table_info.map_or(ctx.current_catalog(), |info| info.catalog_name.as_str());
754        let schema =
755            table_info.map_or_else(|| ctx.current_schema(), |info| info.schema_name.clone());
756        let write_cost = write_meter!(
757            catalog,
758            schema.clone(),
759            requests,
760            ctx.write_rows_to_admit(catalog, &schema, count_insert_rows(&requests)?),
761            ctx.channel() as u8
762        )
763        .await
764        .context(WriteRejectedSnafu)?;
765        let request_factory = RegionRequestFactory::new(RegionRequestHeader {
766            tracing_context: TracingContext::from_current_span().to_w3c(),
767            dbname: ctx.get_db_string(),
768            ..Default::default()
769        });
770
771        let InstantAndNormalInsertRequests {
772            normal_requests,
773            instant_requests,
774        } = requests;
775
776        // Mirror requests for source table to flownode asynchronously
777        let flow_mirror_task = FlowMirrorTask::new(
778            &self.table_flownode_set_cache,
779            normal_requests
780                .requests
781                .iter()
782                .chain(instant_requests.requests.iter()),
783        )
784        .await?;
785        flow_mirror_task.detach(self.node_manager.clone())?;
786
787        // Write requests to datanode and wait for response
788        let write_tasks = self
789            .group_requests_by_peer(normal_requests)
790            .await?
791            .into_iter()
792            .map(|(peer, inserts)| {
793                let node_manager = self.node_manager.clone();
794                let request = request_factory.build_insert(inserts);
795                common_runtime::spawn_global(async move {
796                    node_manager
797                        .datanode(&peer)
798                        .await
799                        .handle(request)
800                        .await
801                        .context(RequestInsertsSnafu)
802                })
803            });
804        let results = future::try_join_all(write_tasks)
805            .await
806            .context(JoinTaskSnafu)?;
807        let affected_rows = results
808            .into_iter()
809            .map(|resp| resp.map(|r| r.affected_rows))
810            .sum::<Result<AffectedRows>>()?;
811        crate::metrics::DIST_INGEST_ROW_COUNT
812            .with_label_values(&[ctx.get_db_string().as_str()])
813            .inc_by(affected_rows as u64);
814        Ok(Output::new(
815            OutputData::AffectedRows(affected_rows),
816            OutputMeta::new_with_cost(write_cost as _),
817        ))
818    }
819
820    async fn group_requests_by_peer(
821        &self,
822        requests: RegionInsertRequests,
823    ) -> Result<HashMap<Peer, RegionInsertRequests>> {
824        // group by region ids first to reduce repeatedly call `find_region_leader`
825        // TODO(discord9): determine if a addition clone is worth it
826        let mut requests_per_region: HashMap<RegionId, RegionInsertRequests> = HashMap::new();
827        for req in requests.requests {
828            let region_id = RegionId::from_u64(req.region_id);
829            requests_per_region
830                .entry(region_id)
831                .or_default()
832                .requests
833                .push(req);
834        }
835
836        let mut inserts: HashMap<Peer, RegionInsertRequests> = HashMap::new();
837
838        for (region_id, reqs) in requests_per_region {
839            let peer = self
840                .partition_manager
841                .find_region_leader(region_id)
842                .await
843                .context(FindRegionLeaderSnafu)?;
844            inserts
845                .entry(peer)
846                .or_default()
847                .requests
848                .extend(reqs.requests);
849        }
850
851        Ok(inserts)
852    }
853
854    /// Returns `Some(reason)` if the config or request hint disables automatic
855    /// table creation. Exempt private system tables are handled by
856    /// [`Self::is_auto_create_exempt_private_table`].
857    fn auto_create_disabled_reason(&self, ctx: &QueryContextRef) -> Result<Option<&'static str>> {
858        let auto_create_table_hint = ctx
859            .extension(AUTO_CREATE_TABLE_KEY)
860            .map(|v| v.parse::<bool>())
861            .transpose()
862            .map_err(|_| {
863                InvalidInsertRequestSnafu {
864                    reason: "`auto_create_table` hint must be a boolean",
865                }
866                .build()
867            })?
868            .unwrap_or(true);
869        Ok(if !self.auto_create_table {
870            Some("auto-create table is disabled by frontend config")
871        } else if !auto_create_table_hint {
872            Some("`auto_create_table` hint is disabled")
873        } else {
874            None
875        })
876    }
877
878    /// Returns whether a private system table may infer and reconcile its schema
879    /// even when automatic table creation is disabled.
880    fn is_auto_create_exempt_private_table(schema: &str, table: &str) -> bool {
881        schema == DEFAULT_PRIVATE_SCHEMA_NAME
882            && matches!(
883                table,
884                DEFAULT_EVENTS_TABLE_NAME | SLOW_QUERY_TABLE_NAME | REGION_STATS_HISTORY_TABLE_NAME
885            )
886    }
887
888    /// Ensures a trace table has the request-global schema without requiring a
889    /// padded data row to drive on-demand creation or alteration. When
890    /// `alter_existing` is false, a table created after planning is left for the
891    /// caller to re-plan.
892    pub async fn ensure_trace_table_on_demand(
893        &self,
894        table_name: &str,
895        request_schema: Vec<ColumnSchema>,
896        alter_existing: bool,
897        ctx: &QueryContextRef,
898        statement_executor: &StatementExecutor,
899    ) -> Result<()> {
900        let mut requests = RowInsertRequests {
901            inserts: vec![RowInsertRequest {
902                table_name: table_name.to_string(),
903                rows: Some(api::v1::Rows {
904                    schema: request_schema,
905                    rows: Vec::new(),
906                }),
907            }],
908        };
909        self.create_or_alter_tables_on_demand(
910            &mut requests,
911            ctx,
912            AutoCreateTableType::Trace { alter_existing },
913            statement_executor,
914            false,
915            false,
916        )
917        .await?;
918        Ok(())
919    }
920
921    /// Creates or alter tables on demand:
922    /// - if table does not exist, create table by inferred CreateExpr
923    /// - if table exist, check if schema matches. If any new column found, alter table by inferred `AlterExpr`
924    ///
925    /// Returns a mapping from table name to table id, where table name is the table name involved in the requests.
926    /// This mapping is used in the conversion of RowToRegion.
927    ///
928    /// `accommodate_existing_schema` is used to determine if the existing schema should override the new schema.
929    /// It only works for TIME_INDEX and single VALUE columns. This is for the case where the user creates a table with
930    /// custom schema, and then inserts data with endpoints that have default schema setting, like prometheus
931    /// remote write. This will modify the `RowInsertRequests` in place.
932    /// `is_single_value` indicates whether the default schema only contains single value column so we can accommodate it.
933    async fn create_or_alter_tables_on_demand(
934        &self,
935        requests: &mut RowInsertRequests,
936        ctx: &QueryContextRef,
937        auto_create_table_type: AutoCreateTableType,
938        statement_executor: &StatementExecutor,
939        accommodate_existing_schema: bool,
940        is_single_value: bool,
941    ) -> Result<CreateAlterTableResult> {
942        let _timer = crate::metrics::CREATE_ALTER_ON_DEMAND
943            .with_label_values(&[auto_create_table_type.as_str()])
944            .start_timer();
945        let catalog = ctx.current_catalog();
946        let schema = ctx.current_schema();
947
948        let auto_create_disabled_reason = self.auto_create_disabled_reason(ctx)?;
949        // Enabled batches permit every table, so only disabled batches need a whitelist scan.
950        let has_auto_create_exempt_table = auto_create_disabled_reason.is_some()
951            && requests
952                .inserts
953                .iter()
954                .any(|req| Self::is_auto_create_exempt_private_table(&schema, &req.table_name));
955        let mut table_infos = HashMap::new();
956        // Without exempt tables, verify existing tables and reject missing ones without inferring schemas.
957        if let Some(disabled_reason) = auto_create_disabled_reason
958            && !has_auto_create_exempt_table
959        {
960            let mut instant_table_ids = HashSet::new();
961            for req in &requests.inserts {
962                let table = match self.get_table(catalog, &schema, &req.table_name).await? {
963                    Some(table) => table,
964                    // System-defined table: created canonically by the system,
965                    // so the auto-create config/hint does not apply.
966                    None if is_ddl_reserved_table(&schema, &req.table_name) => {
967                        statement_executor
968                            .create_declared_relationships_table(catalog, ctx.clone())
969                            .await?
970                    }
971                    None => {
972                        return InvalidInsertRequestSnafu {
973                            reason: format!(
974                                "Table `{}` does not exist, and {}",
975                                req.table_name, disabled_reason
976                            ),
977                        }
978                        .fail();
979                    }
980                };
981                let table_info = table.table_info();
982                if matches!(auto_create_table_type, AutoCreateTableType::Trace { .. }) {
983                    validate_trace_table_model(&table_info, ctx)?;
984                }
985                if table_info.is_ttl_instant_table() {
986                    instant_table_ids.insert(table_info.table_id());
987                }
988                table_infos.insert(table_info.table_id(), table.table_info());
989            }
990            let ret = CreateAlterTableResult {
991                instant_table_ids,
992                table_infos,
993            };
994            return Ok(ret);
995        }
996
997        let mut create_tables = vec![];
998        let mut alter_tables = vec![];
999        let mut need_refresh_table_infos = HashSet::new();
1000        let mut instant_table_ids = HashSet::new();
1001        let mut per_table_semantics: Option<Option<PerTableSemanticIndex>> = None;
1002
1003        for req in &mut requests.inserts {
1004            // Mixed batches need a per-table decision so an exempt table cannot authorize others.
1005            let auto_create_allowed = auto_create_disabled_reason.is_none()
1006                || Self::is_auto_create_exempt_private_table(&schema, &req.table_name);
1007            match self.get_table(catalog, &schema, &req.table_name).await? {
1008                Some(table) => {
1009                    let table_info = table.table_info();
1010                    if matches!(auto_create_table_type, AutoCreateTableType::Trace { .. }) {
1011                        validate_trace_table_model(&table_info, ctx)?;
1012                    }
1013                    if table_info.is_ttl_instant_table() {
1014                        instant_table_ids.insert(table_info.table_id());
1015                    }
1016                    if auto_create_allowed
1017                        && let Some(alter_expr) = self.get_alter_table_expr_on_demand(
1018                            req,
1019                            &table,
1020                            ctx,
1021                            accommodate_existing_schema,
1022                            is_single_value,
1023                            auto_create_table_type.alter_existing(),
1024                        )?
1025                    {
1026                        alter_tables.push(alter_expr);
1027                        need_refresh_table_infos.insert((
1028                            catalog.to_string(),
1029                            schema.clone(),
1030                            req.table_name.clone(),
1031                        ));
1032                    } else {
1033                        table_infos.insert(table_info.table_id(), table.table_info());
1034                    }
1035                }
1036                // A DDL-reserved table's definition never derives from the
1037                // write request; the system creates it canonically, below the
1038                // user-DDL guard that rejects the generic create path.
1039                None if is_ddl_reserved_table(&schema, &req.table_name) => {
1040                    let table = statement_executor
1041                        .create_declared_relationships_table(catalog, ctx.clone())
1042                        .await?;
1043                    let table_info = table.table_info();
1044                    if table_info.is_ttl_instant_table() {
1045                        instant_table_ids.insert(table_info.table_id());
1046                    }
1047                    table_infos.insert(table_info.table_id(), table_info);
1048                }
1049                None if !auto_create_allowed
1050                    && let Some(disabled_reason) = auto_create_disabled_reason =>
1051                {
1052                    return InvalidInsertRequestSnafu {
1053                        reason: format!(
1054                            "Table `{}` does not exist, and {}",
1055                            req.table_name, disabled_reason,
1056                        ),
1057                    }
1058                    .fail();
1059                }
1060                None => {
1061                    let semantic_index = per_table_semantics
1062                        .get_or_insert_with(|| parse_per_table_semantic_index(ctx))
1063                        .as_ref();
1064                    let create_expr = self.get_create_table_expr_on_demand(
1065                        req,
1066                        &auto_create_table_type,
1067                        ctx,
1068                        semantic_index,
1069                    )?;
1070                    create_tables.push(create_expr);
1071                }
1072            }
1073        }
1074
1075        match auto_create_table_type {
1076            AutoCreateTableType::Logical(_) => {
1077                if !create_tables.is_empty() {
1078                    // Creates logical tables in batch.
1079                    let tables = self
1080                        .create_logical_tables(create_tables, ctx, statement_executor)
1081                        .await?;
1082
1083                    for table in tables {
1084                        let table_info = table.table_info();
1085                        if table_info.is_ttl_instant_table() {
1086                            instant_table_ids.insert(table_info.table_id());
1087                        }
1088                        table_infos.insert(table_info.table_id(), table.table_info());
1089                    }
1090                }
1091                if !alter_tables.is_empty() {
1092                    // Alter logical tables in batch.
1093                    statement_executor
1094                        .alter_logical_tables(alter_tables, ctx.clone(), TriggerReason::AutoAlter)
1095                        .await?;
1096                }
1097            }
1098            AutoCreateTableType::Physical
1099            | AutoCreateTableType::Log
1100            | AutoCreateTableType::LastNonNull => {
1101                // note that auto create table shouldn't be ttl instant table
1102                // for it's a very unexpected behavior and should be set by user explicitly
1103                for create_table in create_tables {
1104                    let table = self
1105                        .create_physical_table(create_table, None, ctx, statement_executor)
1106                        .await?;
1107                    let table_info = table.table_info();
1108                    if table_info.is_ttl_instant_table() {
1109                        instant_table_ids.insert(table_info.table_id());
1110                    }
1111                    table_infos.insert(table_info.table_id(), table.table_info());
1112                }
1113                for alter_expr in alter_tables.into_iter() {
1114                    statement_executor
1115                        .alter_table_inner(alter_expr, ctx.clone(), TriggerReason::AutoAlter)
1116                        .await?;
1117                }
1118            }
1119
1120            AutoCreateTableType::Trace { .. } => {
1121                let trace_table_name = ctx
1122                    .extension(TRACE_TABLE_NAME_SESSION_KEY)
1123                    .unwrap_or(TRACE_TABLE_NAME);
1124
1125                let trace_table_partitions = if let Some(trace_table_partitions) =
1126                    ctx.extension(TRACE_TABLE_PARTITIONS_HINT_KEY)
1127                {
1128                    let p = trace_table_partitions.parse::<u32>().map_err(|_| {
1129                        InvalidInsertRequestSnafu {
1130                            reason: format!(
1131                                "Failed to parse trace_table_partitions: {}",
1132                                trace_table_partitions
1133                            ),
1134                        }
1135                        .build()
1136                    })?;
1137                    Some(p)
1138                } else {
1139                    None
1140                };
1141
1142                // note that auto create table shouldn't be ttl instant table
1143                // for it's a very unexpected behavior and should be set by user explicitly
1144                for mut create_table in create_tables {
1145                    if create_table.table_name == trace_services_table_name(trace_table_name)
1146                        || create_table.table_name == trace_operations_table_name(trace_table_name)
1147                    {
1148                        // Disable append mode for auxiliary tables (services/operations) since they require upsert behavior.
1149                        create_table
1150                            .table_options
1151                            .insert(APPEND_MODE_KEY.to_string(), "false".to_string());
1152                        // Remove `ttl` key from table options if it exists
1153                        create_table.table_options.remove(TTL_KEY);
1154
1155                        let table = self
1156                            .create_physical_table(create_table, None, ctx, statement_executor)
1157                            .await?;
1158                        let table_info = table.table_info();
1159                        if table_info.is_ttl_instant_table() {
1160                            instant_table_ids.insert(table_info.table_id());
1161                        }
1162                        table_infos.insert(table_info.table_id(), table.table_info());
1163                    } else {
1164                        // prebuilt partition rules for uuid data: see the function
1165                        // for more information
1166                        let partitions = if matches!(trace_table_partitions, Some(0) | Some(1)) {
1167                            // disable partitions
1168                            None
1169                        } else {
1170                            let p = partition_rule_for_hexstring(
1171                                TRACE_ID_COLUMN,
1172                                trace_table_partitions,
1173                            )
1174                            .context(CreatePartitionRulesSnafu)?;
1175                            Some(p)
1176                        };
1177
1178                        // add skip index to
1179                        // - trace_id: when searching by trace id
1180                        // - parent_span_id: when searching root span
1181                        // - span_name: when searching certain types of span
1182                        let index_columns =
1183                            [TRACE_ID_COLUMN, PARENT_SPAN_ID_COLUMN, SERVICE_NAME_COLUMN];
1184                        for index_column in index_columns {
1185                            if let Some(col) = create_table
1186                                .column_defs
1187                                .iter_mut()
1188                                .find(|c| c.name == index_column)
1189                            {
1190                                col.options =
1191                                    options_from_skipping(&SkippingIndexOptions::default())
1192                                        .context(ColumnOptionsSnafu)?;
1193                            } else {
1194                                warn!(
1195                                    "Column {} not found when creating index for trace table: {}.",
1196                                    index_column, create_table.table_name
1197                                );
1198                            }
1199                        }
1200
1201                        // use table_options to mark table model version
1202                        create_table.table_options.insert(
1203                            TABLE_DATA_MODEL.to_string(),
1204                            ctx.extension(SEMANTIC_PIPELINE)
1205                                .unwrap_or(TABLE_DATA_MODEL_TRACE_V1)
1206                                .to_string(),
1207                        );
1208
1209                        let table = self
1210                            .create_physical_table(
1211                                create_table,
1212                                partitions,
1213                                ctx,
1214                                statement_executor,
1215                            )
1216                            .await?;
1217                        let table_info = table.table_info();
1218                        if table_info.is_ttl_instant_table() {
1219                            instant_table_ids.insert(table_info.table_id());
1220                        }
1221                        table_infos.insert(table_info.table_id(), table.table_info());
1222                    }
1223                }
1224                for alter_expr in alter_tables.into_iter() {
1225                    statement_executor
1226                        .alter_table_inner(alter_expr, ctx.clone(), TriggerReason::AutoAlter)
1227                        .await?;
1228                }
1229            }
1230        }
1231
1232        // refresh table infos for altered tables
1233        for (catalog, schema, table_name) in need_refresh_table_infos {
1234            let table = self
1235                .get_table(&catalog, &schema, &table_name)
1236                .await?
1237                .context(TableNotFoundSnafu {
1238                    table_name: common_catalog::format_full_table_name(
1239                        &catalog,
1240                        &schema,
1241                        &table_name,
1242                    ),
1243                })?;
1244            let table_info = table.table_info();
1245            table_infos.insert(table_info.table_id(), table.table_info());
1246        }
1247
1248        Ok(CreateAlterTableResult {
1249            instant_table_ids,
1250            table_infos,
1251        })
1252    }
1253
1254    async fn create_physical_table_on_demand(
1255        &self,
1256        ctx: &QueryContextRef,
1257        physical_table: String,
1258        statement_executor: &StatementExecutor,
1259    ) -> Result<()> {
1260        let catalog_name = ctx.current_catalog();
1261        let schema_name = ctx.current_schema();
1262
1263        // check if exist
1264        if self
1265            .get_table(catalog_name, &schema_name, &physical_table)
1266            .await?
1267            .is_some()
1268        {
1269            return Ok(());
1270        }
1271
1272        // Gate here too, otherwise a disabled switch would still leak the physical table.
1273        if let Some(disabled_reason) = self.auto_create_disabled_reason(ctx)? {
1274            return InvalidInsertRequestSnafu {
1275                reason: format!(
1276                    "Physical table `{physical_table}` does not exist, and {disabled_reason}"
1277                ),
1278            }
1279            .fail();
1280        }
1281
1282        let table_reference = TableReference::full(catalog_name, &schema_name, &physical_table);
1283        debug!("Ensuring physical metric table `{table_reference}` exists for insert");
1284
1285        // schema with timestamp and field column
1286        let default_schema = vec![
1287            ColumnSchema {
1288                column_name: greptime_timestamp().to_string(),
1289                datatype: ColumnDataType::TimestampMillisecond as _,
1290                semantic_type: SemanticType::Timestamp as _,
1291                datatype_extension: None,
1292                options: None,
1293            },
1294            ColumnSchema {
1295                column_name: greptime_value().to_string(),
1296                datatype: ColumnDataType::Float64 as _,
1297                semantic_type: SemanticType::Field as _,
1298                datatype_extension: None,
1299                options: None,
1300            },
1301        ];
1302        let create_table_expr =
1303            &mut build_create_table_expr(&table_reference, &default_schema, default_engine())?;
1304
1305        create_table_expr.engine = METRIC_ENGINE_NAME.to_string();
1306        create_table_expr
1307            .table_options
1308            .insert(PHYSICAL_TABLE_METADATA_KEY.to_string(), "true".to_string());
1309
1310        // create physical table
1311        let res = statement_executor
1312            .create_table_inner(
1313                create_table_expr,
1314                None,
1315                ctx.clone(),
1316                TriggerReason::AutoCreate,
1317            )
1318            .await;
1319
1320        match res {
1321            Ok(_) => Ok(()),
1322            Err(err) => {
1323                error!(err; "Failed to create table {table_reference}");
1324                Err(err)
1325            }
1326        }
1327    }
1328
1329    async fn get_table(
1330        &self,
1331        catalog: &str,
1332        schema: &str,
1333        table: &str,
1334    ) -> Result<Option<TableRef>> {
1335        self.catalog_manager
1336            .table(catalog, schema, table, None)
1337            .await
1338            .context(CatalogSnafu)
1339    }
1340
1341    fn get_create_table_expr_on_demand(
1342        &self,
1343        req: &RowInsertRequest,
1344        create_type: &AutoCreateTableType,
1345        ctx: &QueryContextRef,
1346        semantic_index: Option<&PerTableSemanticIndex>,
1347    ) -> Result<CreateTableExpr> {
1348        let schema = ctx.current_schema();
1349        let mut table_options = std::collections::HashMap::with_capacity(4);
1350        fill_table_options_for_create(&mut table_options, create_type, ctx);
1351        apply_per_table_semantic_options(
1352            &mut table_options,
1353            semantic_index,
1354            ctx.current_schema().as_str(),
1355            &req.table_name,
1356        );
1357
1358        let engine_name = if let AutoCreateTableType::Logical(_) = create_type {
1359            // engine should be metric engine when creating logical tables.
1360            METRIC_ENGINE_NAME
1361        } else {
1362            default_engine()
1363        };
1364
1365        let table_ref = TableReference::full(ctx.current_catalog(), &schema, &req.table_name);
1366        // SAFETY: `req.rows` is guaranteed to be `Some` by `handle_row_inserts_with_create_type()`.
1367        let request_schema = req.rows.as_ref().unwrap().schema.as_slice();
1368        let mut create_table_expr =
1369            build_create_table_expr(&table_ref, request_schema, engine_name)?;
1370
1371        // extension set by the Splunk HEC handler for identity path
1372        if ctx.extension(SPLUNK_PK_METADATA_ORDER_KEY).is_some() {
1373            reorder_splunk_primary_keys(&mut create_table_expr.primary_keys);
1374        }
1375
1376        debug!("Ensuring table `{table_ref}` exists for insert");
1377        create_table_expr.table_options.extend(table_options);
1378        Ok(create_table_expr)
1379    }
1380
1381    /// Returns an alter table expression if it finds new columns in the request.
1382    /// When `accommodate_existing_schema` is false, it always adds columns if not exist.
1383    /// When `accommodate_existing_schema` is true, it may modify the input `req` to
1384    /// accommodate it with existing schema. See [`create_or_alter_tables_on_demand`](Self::create_or_alter_tables_on_demand)
1385    /// for more details.
1386    /// When `is_single_value` is true, it also rejects native-histogram/float kind changes.
1387    /// When both options are true, it considers fields when modifying the input `req`.
1388    fn get_alter_table_expr_on_demand(
1389        &self,
1390        req: &mut RowInsertRequest,
1391        table: &TableRef,
1392        ctx: &QueryContextRef,
1393        accommodate_existing_schema: bool,
1394        is_single_value: bool,
1395        alter_existing: bool,
1396    ) -> Result<Option<AlterTableExpr>> {
1397        if !alter_existing {
1398            return Ok(None);
1399        }
1400
1401        let catalog_name = ctx.current_catalog();
1402        let schema_name = ctx.current_schema();
1403        let table_name = table.table_info().name.clone();
1404
1405        // Never auto-alter a system-defined table to fit a write; a request
1406        // with unknown columns fails instead.
1407        if is_ddl_reserved_table(&schema_name, &table_name) {
1408            return Ok(None);
1409        }
1410
1411        let request_schema = req.rows.as_ref().unwrap().schema.as_slice();
1412        let request_field_count = request_schema
1413            .iter()
1414            .filter(|col| col.semantic_type == SemanticType::Field as i32)
1415            .count();
1416        let column_exprs = ColumnExpr::from_column_schemas(request_schema);
1417        let add_columns = expr_helper::extract_add_columns_expr(&table.schema(), column_exprs)?;
1418        let Some(mut add_columns) = add_columns else {
1419            return Ok(None);
1420        };
1421
1422        if is_single_value {
1423            let request_is_native_histogram = request_is_native_histogram(request_schema);
1424            let table_is_native_histogram = table_is_native_histogram(table);
1425            ensure!(
1426                request_is_native_histogram == table_is_native_histogram,
1427                InvalidInsertRequestSnafu {
1428                    reason: format!(
1429                        "Table `{table_name}` cannot mix native histogram and float sample fields"
1430                    ),
1431                }
1432            );
1433        }
1434
1435        // If accommodate_existing_schema is true, update request schema for Timestamp/Field columns
1436        if accommodate_existing_schema {
1437            let table_schema = table.schema();
1438            // Find timestamp column name
1439            let ts_col_name = table_schema.timestamp_column().map(|c| c.name.clone());
1440            // Find field column name if there is only one and `is_single_value` is true.
1441            let mut field_col_name = None;
1442            if is_single_value && request_field_count <= 1 {
1443                let mut multiple_field_cols = false;
1444                table.field_columns().for_each(|col| {
1445                    if field_col_name.is_none() {
1446                        field_col_name = Some(col.name.clone());
1447                    } else {
1448                        multiple_field_cols = true;
1449                    }
1450                });
1451                if multiple_field_cols {
1452                    field_col_name = None;
1453                }
1454            }
1455
1456            // Update column name in request schema for Timestamp/Field columns
1457            if let Some(rows) = req.rows.as_mut() {
1458                for col in &mut rows.schema {
1459                    match col.semantic_type {
1460                        x if x == SemanticType::Timestamp as i32 => {
1461                            if let Some(ref ts_name) = ts_col_name
1462                                && col.column_name != *ts_name
1463                            {
1464                                col.column_name = ts_name.clone();
1465                            }
1466                        }
1467                        x if x == SemanticType::Field as i32 => {
1468                            if let Some(ref field_name) = field_col_name
1469                                && col.column_name != *field_name
1470                            {
1471                                col.column_name = field_name.clone();
1472                            }
1473                        }
1474                        _ => {}
1475                    }
1476                }
1477            }
1478
1479            // Only keep columns that are tags or non-single field.
1480            add_columns.add_columns.retain(|col| {
1481                let def = col.column_def.as_ref().unwrap();
1482                def.semantic_type == SemanticType::Tag as i32
1483                    || (def.semantic_type == SemanticType::Field as i32 && field_col_name.is_none())
1484            });
1485
1486            if add_columns.add_columns.is_empty() {
1487                return Ok(None);
1488            }
1489        }
1490
1491        Ok(Some(AlterTableExpr {
1492            catalog_name: catalog_name.to_string(),
1493            schema_name: schema_name.clone(),
1494            table_name: table_name.clone(),
1495            kind: Some(Kind::AddColumns(add_columns)),
1496        }))
1497    }
1498
1499    /// Creates a table with options.
1500    async fn create_physical_table(
1501        &self,
1502        mut create_table_expr: CreateTableExpr,
1503        partitions: Option<Partitions>,
1504        ctx: &QueryContextRef,
1505        statement_executor: &StatementExecutor,
1506    ) -> Result<TableRef> {
1507        let res = statement_executor
1508            .create_table_inner(
1509                &mut create_table_expr,
1510                partitions,
1511                ctx.clone(),
1512                TriggerReason::AutoCreate,
1513            )
1514            .await;
1515
1516        let table_ref = TableReference::full(
1517            &create_table_expr.catalog_name,
1518            &create_table_expr.schema_name,
1519            &create_table_expr.table_name,
1520        );
1521
1522        match res {
1523            Ok(table) => {
1524                validate_trace_table_model(&table.table_info(), ctx)?;
1525                Ok(table)
1526            }
1527            Err(err) => {
1528                error!(err; "Failed to create table {}", table_ref);
1529                Err(err)
1530            }
1531        }
1532    }
1533
1534    async fn create_logical_tables(
1535        &self,
1536        create_table_exprs: Vec<CreateTableExpr>,
1537        ctx: &QueryContextRef,
1538        statement_executor: &StatementExecutor,
1539    ) -> Result<Vec<TableRef>> {
1540        let res = statement_executor
1541            .create_logical_tables(&create_table_exprs, ctx.clone(), TriggerReason::AutoCreate)
1542            .await;
1543
1544        match res {
1545            Ok(res) => Ok(res),
1546            Err(err) => {
1547                let failed_tables = create_table_exprs
1548                    .into_iter()
1549                    .map(|expr| {
1550                        format!(
1551                            "{}.{}.{}",
1552                            expr.catalog_name, expr.schema_name, expr.table_name
1553                        )
1554                    })
1555                    .collect::<Vec<_>>();
1556                error!(
1557                    err;
1558                    "Failed to create logical tables {:?}",
1559                    failed_tables
1560                );
1561                Err(err)
1562            }
1563        }
1564    }
1565
1566    pub fn node_manager(&self) -> &NodeManagerRef {
1567        &self.node_manager
1568    }
1569
1570    pub fn partition_manager(&self) -> &PartitionRuleManagerRef {
1571        &self.partition_manager
1572    }
1573
1574    pub fn table_flownode_set_cache(&self) -> &TableFlownodeSetCacheRef {
1575        &self.table_flownode_set_cache
1576    }
1577}
1578
1579fn request_is_native_histogram(request_schema: &[ColumnSchema]) -> bool {
1580    let mut fields = request_schema
1581        .iter()
1582        .filter(|col| col.semantic_type == SemanticType::Field as i32);
1583    let Some(col) = fields.next() else {
1584        return false;
1585    };
1586
1587    fields.next().is_none()
1588        && api::helper::is_column_type_value_eq(
1589            col.datatype,
1590            col.datatype_extension.clone(),
1591            native_histogram_value_type(),
1592        )
1593}
1594
1595fn table_is_native_histogram(table: &TableRef) -> bool {
1596    let mut fields = table.field_columns();
1597    let Some(col) = fields.next() else {
1598        return false;
1599    };
1600
1601    fields.next().is_none() && is_native_histogram_value_type(&col.data_type)
1602}
1603
1604fn validate_column_count_match(requests: &RowInsertRequests) -> Result<()> {
1605    for request in &requests.inserts {
1606        let rows = request.rows.as_ref().unwrap();
1607        let column_count = rows.schema.len();
1608        rows.rows.iter().try_for_each(|r| {
1609            ensure!(
1610                r.values.len() == column_count,
1611                InvalidInsertRequestSnafu {
1612                    reason: format!(
1613                        "column count mismatch, columns: {}, values: {}",
1614                        column_count,
1615                        r.values.len()
1616                    )
1617                }
1618            );
1619            Ok(())
1620        })?;
1621    }
1622    Ok(())
1623}
1624
1625/// Rejects writes from a different built-in trace model before schema mutation.
1626/// Unstamped, explicitly created tables remain subject to normal schema validation.
1627pub fn validate_trace_table_model(table_info: &TableInfo, ctx: &QueryContextRef) -> Result<()> {
1628    let Some(expected @ (TABLE_DATA_MODEL_TRACE_V1 | TABLE_DATA_MODEL_TRACE_V2)) =
1629        ctx.extension(SEMANTIC_PIPELINE)
1630    else {
1631        return Ok(());
1632    };
1633    if let Some(actual) = table_info.meta.options.data_model() {
1634        ensure!(
1635            actual == expected,
1636            InvalidInsertRequestSnafu {
1637                reason: format!(
1638                    "Trace table `{}` uses {actual}, but the request uses {expected}",
1639                    table_info.name,
1640                ),
1641            }
1642        );
1643    }
1644    Ok(())
1645}
1646
1647/// Fill table options for a new table by create type.
1648pub fn fill_table_options_for_create(
1649    table_options: &mut std::collections::HashMap<String, String>,
1650    create_type: &AutoCreateTableType,
1651    ctx: &QueryContextRef,
1652) {
1653    for key in VALID_TABLE_OPTION_KEYS {
1654        if let Some(value) = ctx.extension(key) {
1655            table_options.insert(key.to_string(), value.to_string());
1656        }
1657    }
1658
1659    // Semantic keys use their own vocabulary instead of the fixed option list.
1660    for (key, value) in ctx.extensions() {
1661        if is_semantic_option_key(&key) && validate_semantic_option(&key, &value) {
1662            table_options.insert(key, value);
1663        }
1664    }
1665
1666    match create_type {
1667        AutoCreateTableType::Logical(physical_table) => {
1668            table_options.insert(
1669                LOGICAL_TABLE_METADATA_KEY.to_string(),
1670                physical_table.clone(),
1671            );
1672        }
1673        AutoCreateTableType::Physical => {
1674            if let Some(append_mode) = ctx.extension(APPEND_MODE_KEY) {
1675                table_options.insert(APPEND_MODE_KEY.to_string(), append_mode.to_string());
1676            }
1677            if let Some(merge_mode) = ctx.extension(MERGE_MODE_KEY) {
1678                table_options.insert(MERGE_MODE_KEY.to_string(), merge_mode.to_string());
1679            }
1680            if let Some(time_window) = ctx.extension(TWCS_TIME_WINDOW) {
1681                table_options.insert(TWCS_TIME_WINDOW.to_string(), time_window.to_string());
1682                // We need to set the compaction type explicitly.
1683                table_options.insert(
1684                    COMPACTION_TYPE.to_string(),
1685                    COMPACTION_TYPE_TWCS.to_string(),
1686                );
1687            }
1688        }
1689        // Set append_mode to true for log table.
1690        // because log tables should keep rows with the same ts and tags.
1691        AutoCreateTableType::Log => {
1692            table_options.insert(APPEND_MODE_KEY.to_string(), "true".to_string());
1693        }
1694        AutoCreateTableType::LastNonNull => {
1695            if ctx
1696                .extension(APPEND_MODE_KEY)
1697                .is_some_and(|value| value.eq_ignore_ascii_case("true"))
1698            {
1699                table_options.insert(APPEND_MODE_KEY.to_string(), "true".to_string());
1700                table_options.insert(MERGE_MODE_KEY.to_string(), "last_row".to_string());
1701            } else if let Some(merge_mode) = ctx.extension(MERGE_MODE_KEY) {
1702                table_options.insert(MERGE_MODE_KEY.to_string(), merge_mode.to_string());
1703            } else {
1704                table_options.insert(MERGE_MODE_KEY.to_string(), "last_non_null".to_string());
1705            }
1706        }
1707        AutoCreateTableType::Trace { .. } => {
1708            table_options.insert(APPEND_MODE_KEY.to_string(), "true".to_string());
1709        }
1710    }
1711}
1712
1713/// The parsed per-table semantic index: `{schema -> {table -> {key -> value}}}`,
1714/// produced by the OTLP metrics encode path (where one metric can fan out into
1715/// several tables with distinct keys) and the Prometheus remote write v2 path
1716/// (where per-series metadata declares type/unit, and a series may override its
1717/// target schema).
1718pub type PerTableSemanticIndex = BTreeMap<String, BTreeMap<String, BTreeMap<String, String>>>;
1719
1720/// Parses the per-table semantic index off the context extension. Call once per
1721/// create-planning round: a first write creating N tables would otherwise
1722/// re-parse the whole index N times. `None` when the request carries no index
1723/// (logs, traces, Prom RW v1) or it fails to parse.
1724pub fn parse_per_table_semantic_index(ctx: &QueryContextRef) -> Option<PerTableSemanticIndex> {
1725    let raw = ctx.extension(SEMANTIC_PER_TABLE_INDEX_KEY)?;
1726    match serde_json::from_str(raw) {
1727        Ok(index) => Some(index),
1728        Err(_) => {
1729            warn!("failed to parse semantic per-table index, skipping per-table options");
1730            None
1731        }
1732    }
1733}
1734
1735/// Folds the semantic keys of the table being created into `table_options`.
1736///
1737/// Common keys shared by every table in a request travel as plain semantic
1738/// extensions and are handled by [`fill_table_options_for_create`]; this
1739/// carries only the per-table tail and is applied after it, so a per-table
1740/// value (e.g. `declared` quality) wins. Keys are re-checked against the
1741/// vocabulary defensively.
1742pub fn apply_per_table_semantic_options(
1743    table_options: &mut std::collections::HashMap<String, String>,
1744    index: Option<&PerTableSemanticIndex>,
1745    schema: &str,
1746    table_name: &str,
1747) {
1748    let Some(entry) = index
1749        .and_then(|index| index.get(schema))
1750        .and_then(|tables| tables.get(table_name))
1751    else {
1752        return;
1753    };
1754    for (key, value) in entry {
1755        if is_semantic_option_key(key) && validate_semantic_option(key, value) {
1756            table_options.insert(key.clone(), value.clone());
1757        }
1758    }
1759}
1760
1761pub fn build_create_table_expr(
1762    table: &TableReference,
1763    request_schema: &[ColumnSchema],
1764    engine: &str,
1765) -> Result<CreateTableExpr> {
1766    expr_helper::create_table_expr_by_column_schemas(table, request_schema, engine, None)
1767}
1768
1769/// `QueryContext` extension key the Splunk HEC handler sets (to `"true"`) on its identity
1770/// path to request metadata-first primary-key ordering at table creation. It is absent for
1771/// user-supplied pipelines, so their primary-key order is left untouched.
1772pub const SPLUNK_PK_METADATA_ORDER_KEY: &str = "splunk_pk_metadata_order";
1773
1774/// Moves Splunk's metadata tags (`host`, `source`, `sourcetype`) to the front of the
1775/// primary key, keeping the relative order of the remaining tags.
1776fn reorder_splunk_primary_keys(primary_keys: &mut [String]) {
1777    const LEAD: [&str; 3] = ["host", "source", "sourcetype"];
1778    // Stable sort: `LEAD` columns move to the front in `host`/`source`/`sourcetype` order;
1779    // every other column keeps its existing relative position.
1780    primary_keys.sort_by_key(|name| {
1781        LEAD.iter()
1782            .position(|&lead| lead == name.as_str())
1783            .unwrap_or(LEAD.len())
1784    });
1785}
1786
1787/// Result of `create_or_alter_tables_on_demand`.
1788struct CreateAlterTableResult {
1789    /// table ids of ttl=instant tables.
1790    instant_table_ids: HashSet<TableId>,
1791    /// Table Info of the created tables.
1792    table_infos: HashMap<TableId, Arc<TableInfo>>,
1793}
1794
1795struct FlowMirrorTask {
1796    requests: HashMap<Peer, RegionInsertRequests>,
1797    num_rows: usize,
1798}
1799
1800impl FlowMirrorTask {
1801    async fn new(
1802        cache: &TableFlownodeSetCacheRef,
1803        requests: impl Iterator<Item = &RegionInsertRequest>,
1804    ) -> Result<Self> {
1805        let mut src_table_reqs: HashMap<TableId, Option<(Vec<Peer>, RegionInsertRequests)>> =
1806            HashMap::new();
1807        let mut num_rows = 0;
1808
1809        for req in requests {
1810            let table_id = RegionId::from_u64(req.region_id).table_id();
1811            match src_table_reqs.get_mut(&table_id) {
1812                Some(Some((_peers, reqs))) => reqs.requests.push(req.clone()),
1813                // already know this is not source table
1814                Some(None) => continue,
1815                _ => {
1816                    // dedup peers
1817                    let peers = cache
1818                        .get(table_id)
1819                        .await
1820                        .context(RequestInsertsSnafu)?
1821                        .unwrap_or_default()
1822                        .values()
1823                        .cloned()
1824                        .collect::<HashSet<_>>()
1825                        .into_iter()
1826                        .collect::<Vec<_>>();
1827
1828                    if !peers.is_empty() {
1829                        let mut reqs = RegionInsertRequests::default();
1830                        reqs.requests.push(req.clone());
1831                        num_rows += reqs
1832                            .requests
1833                            .iter()
1834                            .map(|r| r.rows.as_ref().unwrap().rows.len())
1835                            .sum::<usize>();
1836                        src_table_reqs.insert(table_id, Some((peers, reqs)));
1837                    } else {
1838                        // insert a empty entry to avoid repeat query
1839                        src_table_reqs.insert(table_id, None);
1840                    }
1841                }
1842            }
1843        }
1844
1845        let mut inserts: HashMap<Peer, RegionInsertRequests> = HashMap::new();
1846
1847        for (_table_id, (peers, reqs)) in src_table_reqs
1848            .into_iter()
1849            .filter_map(|(k, v)| v.map(|v| (k, v)))
1850        {
1851            if peers.len() == 1 {
1852                // fast path, zero copy
1853                inserts
1854                    .entry(peers[0].clone())
1855                    .or_default()
1856                    .requests
1857                    .extend(reqs.requests);
1858                continue;
1859            } else {
1860                // TODO(discord9): need to split requests to multiple flownodes
1861                for flownode in peers {
1862                    inserts
1863                        .entry(flownode.clone())
1864                        .or_default()
1865                        .requests
1866                        .extend(reqs.requests.clone());
1867                }
1868            }
1869        }
1870
1871        Ok(Self {
1872            requests: inserts,
1873            num_rows,
1874        })
1875    }
1876
1877    fn detach(self, node_manager: NodeManagerRef) -> Result<()> {
1878        crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.add(self.num_rows as i64);
1879        for (peer, inserts) in self.requests {
1880            let node_manager = node_manager.clone();
1881            common_runtime::spawn_global(async move {
1882                let result = node_manager
1883                    .flownode(&peer)
1884                    .await
1885                    .handle_inserts(inserts)
1886                    .await
1887                    .context(RequestInsertsSnafu);
1888
1889                match result {
1890                    Ok(resp) => {
1891                        let affected_rows = resp.affected_rows;
1892                        crate::metrics::DIST_MIRROR_ROW_COUNT.inc_by(affected_rows);
1893                        crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.sub(affected_rows as _);
1894                    }
1895                    Err(err) => {
1896                        error!(err; "Failed to insert data into flownode {}", peer);
1897                    }
1898                }
1899            });
1900        }
1901
1902        Ok(())
1903    }
1904}
1905
1906#[cfg(test)]
1907mod tests {
1908    use std::sync::Arc;
1909
1910    use api::helper::ColumnDataTypeWrapper;
1911    use api::v1::helper::{field_column_schema, time_index_column_schema};
1912    use api::v1::{RowInsertRequest, Rows, Value};
1913    use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
1914    use common_meta::cache::new_table_flownode_set_cache;
1915    use common_meta::ddl::test_util::datanode_handler::NaiveDatanodeHandler;
1916    use common_meta::test_util::MockDatanodeManager;
1917    use common_query::native_histogram::NATIVE_HISTOGRAM_FIELD;
1918    use common_query::prelude::{greptime_native_histogram, set_default_prefix};
1919    use datatypes::data_type::ConcreteDataType;
1920    use datatypes::schema::ColumnSchema;
1921    use moka::future::Cache;
1922    use session::context::QueryContext;
1923    use table::TableRef;
1924    use table::dist_table::DummyDataSource;
1925    use table::metadata::{TableInfoBuilder, TableMetaBuilder, TableType};
1926
1927    use crate::insert::*;
1928    use crate::test_util::{
1929        create_partition_rule_manager, new_test_table_info, prepare_mocked_backend,
1930    };
1931
1932    fn make_table_ref_with_schema(
1933        ts_name: &str,
1934        field_name: &str,
1935        field_type: ConcreteDataType,
1936    ) -> TableRef {
1937        let schema = datatypes::schema::SchemaBuilder::try_from_columns(vec![
1938            ColumnSchema::new(
1939                ts_name,
1940                ConcreteDataType::timestamp_millisecond_datatype(),
1941                false,
1942            )
1943            .with_time_index(true),
1944            ColumnSchema::new(field_name, field_type, true),
1945        ])
1946        .unwrap()
1947        .build()
1948        .unwrap();
1949        let meta = TableMetaBuilder::empty()
1950            .schema(Arc::new(schema))
1951            .primary_key_indices(vec![])
1952            .value_indices(vec![1])
1953            .engine("mito")
1954            .next_column_id(0)
1955            .options(Default::default())
1956            .created_on(Default::default())
1957            .build()
1958            .unwrap();
1959        let info = Arc::new(
1960            TableInfoBuilder::default()
1961                .table_id(1)
1962                .table_version(0)
1963                .name("test_table")
1964                .schema_name(DEFAULT_SCHEMA_NAME)
1965                .catalog_name(DEFAULT_CATALOG_NAME)
1966                .desc(None)
1967                .table_type(TableType::Base)
1968                .meta(meta)
1969                .build()
1970                .unwrap(),
1971        );
1972        Arc::new(table::Table::new(
1973            info,
1974            table::metadata::FilterPushDownType::Unsupported,
1975            Arc::new(DummyDataSource),
1976        ))
1977    }
1978
1979    #[tokio::test]
1980    async fn test_accommodate_existing_schema_and_reject_kind_changes() {
1981        let ts_name = "my_ts";
1982        let field_name = "my_field";
1983        let table =
1984            make_table_ref_with_schema(ts_name, field_name, ConcreteDataType::float64_datatype());
1985
1986        // The request uses different names for timestamp and field columns
1987        let mut req = RowInsertRequest {
1988            table_name: "test_table".to_string(),
1989            rows: Some(Rows {
1990                schema: vec![
1991                    time_index_column_schema("ts_wrong", ColumnDataType::TimestampMillisecond),
1992                    field_column_schema("field_wrong", ColumnDataType::Float64),
1993                ],
1994                rows: vec![api::v1::Row {
1995                    values: vec![Value::default(), Value::default()],
1996                }],
1997            }),
1998        };
1999        let ctx = Arc::new(QueryContext::with(
2000            DEFAULT_CATALOG_NAME,
2001            DEFAULT_SCHEMA_NAME,
2002        ));
2003
2004        let kv_backend = prepare_mocked_backend().await;
2005        let inserter = Inserter::new(
2006            catalog::memory::MemoryCatalogManager::new(),
2007            create_partition_rule_manager(kv_backend.clone()).await,
2008            Arc::new(MockDatanodeManager::new(NaiveDatanodeHandler)),
2009            Arc::new(new_table_flownode_set_cache(
2010                String::new(),
2011                Cache::new(100),
2012                kv_backend.clone(),
2013            )),
2014            true,
2015        );
2016        // Do not apply an absent-table plan to a table that appeared concurrently.
2017        assert!(
2018            inserter
2019                .get_alter_table_expr_on_demand(&mut req, &table, &ctx, false, false, false)
2020                .unwrap()
2021                .is_none()
2022        );
2023        let alter_expr = inserter
2024            .get_alter_table_expr_on_demand(&mut req, &table, &ctx, true, true, true)
2025            .unwrap();
2026        assert!(alter_expr.is_none());
2027
2028        // The request's schema should have updated names for timestamp and field columns
2029        let req_schema = req.rows.as_ref().unwrap().schema.clone();
2030        assert_eq!(req_schema[0].column_name, ts_name);
2031        assert_eq!(req_schema[1].column_name, field_name);
2032
2033        let (datatype, datatype_extension) =
2034            ColumnDataTypeWrapper::try_from(native_histogram_value_type().clone())
2035                .unwrap()
2036                .into_parts();
2037        let mut histogram_req = RowInsertRequest {
2038            table_name: "test_table".to_string(),
2039            rows: Some(Rows {
2040                schema: vec![
2041                    time_index_column_schema("ts", ColumnDataType::TimestampMillisecond),
2042                    api::v1::ColumnSchema {
2043                        column_name: greptime_native_histogram().to_string(),
2044                        datatype: datatype as i32,
2045                        semantic_type: SemanticType::Field as i32,
2046                        datatype_extension,
2047                        options: None,
2048                    },
2049                ],
2050                rows: vec![],
2051            }),
2052        };
2053        let error = inserter
2054            .get_alter_table_expr_on_demand(&mut histogram_req, &table, &ctx, false, true, true)
2055            .unwrap_err();
2056        assert!(
2057            error
2058                .to_string()
2059                .contains("cannot mix native histogram and float sample fields")
2060        );
2061
2062        let histogram_table = make_table_ref_with_schema(
2063            "ts",
2064            greptime_native_histogram(),
2065            native_histogram_value_type().clone(),
2066        );
2067        let mut sample_req = RowInsertRequest {
2068            table_name: "test_table".to_string(),
2069            rows: Some(Rows {
2070                schema: vec![
2071                    time_index_column_schema("ts", ColumnDataType::TimestampMillisecond),
2072                    field_column_schema(greptime_value(), ColumnDataType::Float64),
2073                ],
2074                rows: vec![],
2075            }),
2076        };
2077        let error = inserter
2078            .get_alter_table_expr_on_demand(
2079                &mut sample_req,
2080                &histogram_table,
2081                &ctx,
2082                false,
2083                true,
2084                true,
2085            )
2086            .unwrap_err();
2087        assert!(
2088            error
2089                .to_string()
2090                .contains("cannot mix native histogram and float sample fields")
2091        );
2092    }
2093
2094    #[test]
2095    fn test_native_histogram_detection_survives_prefix_change() {
2096        set_default_prefix(Some("custom")).unwrap();
2097        let table = make_table_ref_with_schema(
2098            "custom_timestamp",
2099            NATIVE_HISTOGRAM_FIELD,
2100            native_histogram_value_type().clone(),
2101        );
2102        let (datatype, datatype_extension) =
2103            ColumnDataTypeWrapper::try_from(native_histogram_value_type().clone())
2104                .unwrap()
2105                .into_parts();
2106        let request_schema = [api::v1::ColumnSchema {
2107            column_name: greptime_native_histogram().to_string(),
2108            datatype: datatype as i32,
2109            semantic_type: SemanticType::Field as i32,
2110            datatype_extension,
2111            options: None,
2112        }];
2113
2114        assert!(request_is_native_histogram(&request_schema));
2115        assert!(table_is_native_histogram(&table));
2116    }
2117
2118    // Keep global meter registration in one test, isolated by nextest's per-test process.
2119    #[tokio::test]
2120    async fn test_write_meter_admission() {
2121        use std::cell::Cell;
2122        use std::sync::Mutex;
2123        use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
2124
2125        use api::region::RegionResponse;
2126        use api::v1::region::region_request::Body;
2127        use arrow::array::{Int32Array, TimestampMillisecondArray};
2128        use arrow::record_batch::RecordBatch;
2129        use bytes::Bytes;
2130        use common_error::ext::{ErrorExt, RetryHint};
2131        use common_error::status_code::StatusCode;
2132        use common_grpc::flight::{FlightEncoder, FlightMessage};
2133        use common_meta::ddl::test_util::datanode_handler::DatanodeWatcher;
2134        use futures::future::BoxFuture;
2135        use meter_core::ItemCalculator;
2136        use meter_core::collect::{Collect, WriteRejected};
2137        use meter_core::data::MeterRecord;
2138        use meter_core::global::global_registry;
2139        use session::context::Channel;
2140
2141        const CATALOG: &str = "write_meter_test";
2142
2143        #[derive(Default)]
2144        struct Meter {
2145            reject: AtomicBool,
2146            attempts: Mutex<Vec<MeterRecord>>,
2147            accepted_value: AtomicU64,
2148        }
2149
2150        impl Collect for Meter {
2151            fn on_write(
2152                &self,
2153                record: MeterRecord,
2154            ) -> BoxFuture<'_, std::result::Result<(), WriteRejected>> {
2155                Box::pin(async move {
2156                    if record.catalog != CATALOG {
2157                        return Ok(());
2158                    }
2159                    let value = record.value;
2160                    self.attempts.lock().unwrap().push(record);
2161                    if self.reject.load(Ordering::Relaxed) {
2162                        return Err(WriteRejected::new("database row quota exhausted"));
2163                    }
2164                    self.accepted_value.fetch_add(value, Ordering::Relaxed);
2165                    Ok(())
2166                })
2167            }
2168
2169            fn on_read(&self, _: MeterRecord) {}
2170        }
2171
2172        impl ItemCalculator<InstantAndNormalInsertRequests> for Meter {
2173            fn calc(&self, _: &InstantAndNormalInsertRequests) -> u64 {
2174                17
2175            }
2176        }
2177
2178        let kv_backend = prepare_mocked_backend().await;
2179        let partition_manager = create_partition_rule_manager(kv_backend.clone()).await;
2180        let (sender, mut dispatched) = tokio::sync::mpsc::channel(16);
2181        let watcher = DatanodeWatcher::new(sender).with_handler(|_, request| {
2182            let rows = match request.body.unwrap() {
2183                Body::Inserts(requests) => requests
2184                    .requests
2185                    .iter()
2186                    .filter_map(|request| request.rows.as_ref())
2187                    .map(|rows| rows.rows.len())
2188                    .sum(),
2189                // The bulk batches below each contain two rows.
2190                Body::BulkInsert(_) => 2,
2191                body => panic!("unexpected request: {body:?}"),
2192            };
2193            Ok(RegionResponse::new(rows))
2194        });
2195        let flow_cache = Cache::new(100);
2196        let inserter = Inserter::new(
2197            catalog::memory::MemoryCatalogManager::new(),
2198            partition_manager,
2199            Arc::new(MockDatanodeManager::new(watcher)),
2200            Arc::new(new_table_flownode_set_cache(
2201                String::new(),
2202                flow_cache.clone(),
2203                kv_backend,
2204            )),
2205            true,
2206        );
2207        let mut table_info = new_test_table_info(1, "table_1", [1].into_iter());
2208        table_info.catalog_name = CATALOG.to_string();
2209        table_info.schema_name = "target_db".to_string();
2210        let table_info = Arc::new(table_info);
2211        let table_infos = HashMap::from_iter([(1, table_info.clone())]);
2212        let ctx = Arc::new(QueryContext::with_channel(
2213            DEFAULT_CATALOG_NAME,
2214            DEFAULT_SCHEMA_NAME,
2215            Channel::Postgres,
2216        ));
2217        let rows_request = || {
2218            let build = |num_rows| RegionInsertRequests {
2219                requests: vec![RegionInsertRequest {
2220                    region_id: RegionId::new(1, 1).as_u64(),
2221                    rows: Some(Rows {
2222                        schema: vec![],
2223                        rows: vec![api::v1::Row { values: vec![] }; num_rows],
2224                    }),
2225                    ..Default::default()
2226                }],
2227            };
2228            InstantAndNormalInsertRequests {
2229                normal_requests: build(3),
2230                instant_requests: build(2),
2231            }
2232        };
2233
2234        // No collector or calculator: ordinary OSS insertion still succeeds.
2235        let output = inserter
2236            .do_request(rows_request(), &table_infos, &ctx)
2237            .await
2238            .unwrap();
2239        assert_eq!(output.meta.cost, 0);
2240        assert!(matches!(output.data, OutputData::AffectedRows(3)));
2241        dispatched.try_recv().unwrap();
2242
2243        let meter = Arc::new(Meter::default());
2244        global_registry().set_collector(meter.clone());
2245        global_registry().register_calculator(meter.clone());
2246        // The dependency controls noop mode; exercise both builds with this test.
2247        let enabled = Cell::new(false);
2248        write_meter!({
2249            enabled.set(true);
2250            MeterRecord::new("probe".into(), "probe".into(), 0, 0, 0)
2251        })
2252        .await
2253        .unwrap();
2254        let enabled = enabled.get();
2255
2256        let output = inserter
2257            .do_request(rows_request(), &table_infos, &ctx)
2258            .await
2259            .unwrap();
2260        assert_eq!(output.meta.cost, if enabled { 17 } else { 0 });
2261        assert!(matches!(output.data, OutputData::AffectedRows(3)));
2262        dispatched.try_recv().unwrap();
2263        assert!(flow_cache.contains_key(&1));
2264        flow_cache.invalidate_all();
2265        meter.reject.store(true, Ordering::Relaxed);
2266        let result = inserter
2267            .do_request(rows_request(), &table_infos, &ctx)
2268            .await;
2269        if enabled {
2270            let error = result.unwrap_err();
2271            assert_eq!(error.status_code(), StatusCode::RateLimited);
2272            assert_eq!(error.retry_hint(), RetryHint::Retryable);
2273            assert!(error.to_string().contains("database row quota exhausted"));
2274            assert!(dispatched.try_recv().is_err());
2275            assert!(
2276                !flow_cache.contains_key(&1),
2277                "rejected write reached flow mirroring"
2278            );
2279            assert_eq!(meter.accepted_value.load(Ordering::Relaxed), 17);
2280            let attempts = meter.attempts.lock().unwrap();
2281            assert_eq!(attempts.len(), 2);
2282            for record in attempts.iter() {
2283                assert_eq!(record.catalog, CATALOG);
2284                assert_eq!(record.schema, "target_db");
2285                assert_eq!(
2286                    (record.rows, record.value, record.source),
2287                    (5, 17, Channel::Postgres as u8)
2288                );
2289            }
2290        } else {
2291            assert_eq!(result.unwrap().meta.cost, 0);
2292            dispatched.try_recv().unwrap();
2293            assert!(meter.attempts.lock().unwrap().is_empty());
2294        }
2295        meter.attempts.lock().unwrap().clear();
2296
2297        // Rejection must also precede admission to the table batcher's queue.
2298        if enabled {
2299            let batcher: Arc<dyn PendingRowsBatcher> = Arc::new(UnexpectedBatcher);
2300            let rows = Rows {
2301                schema: vec![
2302                    api::v1::helper::tag_column_schema("a", ColumnDataType::Int32),
2303                    time_index_column_schema("ts", ColumnDataType::TimestampMillisecond),
2304                    field_column_schema("b", ColumnDataType::Int32),
2305                ],
2306                rows: vec![api::v1::Row {
2307                    values: vec![
2308                        api::v1::value::ValueData::I32Value(60).into(),
2309                        Value {
2310                            value_data: Some(api::v1::value::ValueData::TimestampMillisecondValue(
2311                                0,
2312                            )),
2313                        },
2314                        api::v1::value::ValueData::I32Value(0).into(),
2315                    ],
2316                }],
2317            };
2318            let error = inserter
2319                .submit_table_rows(rows, table_info.clone(), ctx.clone(), &batcher)
2320                .await
2321                .unwrap_err();
2322            assert_eq!(error.status_code(), StatusCode::RateLimited);
2323            let mut attempts = meter.attempts.lock().unwrap();
2324            assert_eq!(attempts.len(), 1);
2325            assert_eq!(attempts[0].catalog, CATALOG);
2326            assert_eq!(attempts[0].schema, "target_db");
2327            assert_eq!(attempts[0].rows, 1);
2328            attempts.clear();
2329        }
2330
2331        let table = Arc::new(table::Table::new(
2332            table_info.clone(),
2333            table::metadata::FilterPushDownType::Unsupported,
2334            Arc::new(DummyDataSource),
2335        ));
2336        let batch = RecordBatch::try_new(
2337            table_info.meta.schema.arrow_schema().clone(),
2338            vec![
2339                Arc::new(Int32Array::from(vec![60, 70])),
2340                Arc::new(TimestampMillisecondArray::from(vec![0, 1])),
2341                Arc::new(Int32Array::from(vec![0, 0])),
2342            ],
2343        )
2344        .unwrap();
2345        let bulk_insert = |batch: RecordBatch| {
2346            let flight_data = FlightEncoder::default()
2347                .encode(FlightMessage::RecordBatch(batch.clone()))
2348                .into_iter()
2349                .next()
2350                .unwrap();
2351            inserter.handle_bulk_insert(
2352                table.clone(),
2353                flight_data,
2354                batch,
2355                Bytes::new(),
2356                false,
2357                Channel::Grpc,
2358            )
2359        };
2360        // Empty batches bypass admission even while the collector rejects.
2361        assert_eq!(bulk_insert(batch.slice(0, 0)).await.unwrap(), 0);
2362        assert!(meter.attempts.lock().unwrap().is_empty());
2363        assert!(dispatched.try_recv().is_err());
2364
2365        // A collector change between batches takes effect on the very next batch.
2366        for reject in [false, true, false] {
2367            meter.reject.store(reject, Ordering::Relaxed);
2368            let result = bulk_insert(batch.clone()).await;
2369            if enabled && reject {
2370                let error = result.unwrap_err();
2371                assert_eq!(error.status_code(), StatusCode::RateLimited);
2372                assert_eq!(error.retry_hint(), RetryHint::Retryable);
2373                assert!(dispatched.try_recv().is_err());
2374            } else {
2375                assert_eq!(result.unwrap(), 2);
2376                dispatched.try_recv().unwrap();
2377            }
2378        }
2379        {
2380            let attempts = meter.attempts.lock().unwrap();
2381            assert_eq!(attempts.len(), if enabled { 3 } else { 0 });
2382            for record in attempts.iter() {
2383                assert_eq!(record.catalog, CATALOG);
2384                assert_eq!(record.schema, "target_db");
2385                assert_eq!(
2386                    (record.rows, record.value, record.source),
2387                    (2, 0, Channel::Grpc as u8)
2388                );
2389            }
2390        }
2391        assert_eq!(
2392            meter.accepted_value.load(Ordering::Relaxed),
2393            if enabled { 17 } else { 0 }
2394        );
2395
2396        // Aggregate repeated database targets and retain admission across nested
2397        // batching without changing the caller's reusable context.
2398        meter.attempts.lock().unwrap().clear();
2399        let original = Arc::new(QueryContext::with_channel(
2400            CATALOG,
2401            "a",
2402            Channel::Prometheus,
2403        ));
2404        let mut batches = ["a", "b", "a"].map(|schema| {
2405            let ctx = if schema == "a" {
2406                original.clone()
2407            } else {
2408                Arc::new(QueryContext::with_channel(
2409                    CATALOG,
2410                    schema,
2411                    Channel::Prometheus,
2412                ))
2413            };
2414            (
2415                ctx,
2416                RowInsertRequests {
2417                    inserts: vec![RowInsertRequest {
2418                        table_name: "data".into(),
2419                        rows: Some(Rows {
2420                            schema: vec![],
2421                            rows: vec![api::v1::Row::default(); 2],
2422                        }),
2423                    }],
2424                },
2425            )
2426        });
2427        admit_row_insert_batches(&mut batches).await.unwrap();
2428        admit_row_insert_batches(&mut batches).await.unwrap();
2429        assert_eq!(original.write_rows_to_admit(CATALOG, "a", 4), 4);
2430        for (ctx, _) in &batches {
2431            assert_eq!(
2432                ctx.write_rows_to_admit(CATALOG, &ctx.current_schema(), 2),
2433                0
2434            );
2435            assert_eq!(ctx.channel(), Channel::Prometheus);
2436        }
2437        {
2438            let attempts = meter.attempts.lock().unwrap();
2439            let totals = attempts
2440                .iter()
2441                .map(|r| (r.schema.as_str(), r.rows, r.value))
2442                .collect::<Vec<_>>();
2443            assert_eq!(
2444                totals,
2445                if enabled {
2446                    vec![("a", 4, 0), ("b", 2, 0)]
2447                } else {
2448                    vec![]
2449                }
2450            );
2451        }
2452        meter.attempts.lock().unwrap().clear();
2453        meter.reject.store(false, Ordering::Relaxed);
2454        let ctx = Arc::new(QueryContext::with_channel(
2455            CATALOG,
2456            "logical",
2457            Channel::Otlp,
2458        ));
2459        let admitted = admit_write(2, &ctx).await.unwrap();
2460        let mut requests = RowInsertRequests {
2461            inserts: vec![RowInsertRequest {
2462                table_name: "metric".to_string(),
2463                rows: Some(Rows {
2464                    schema: vec![],
2465                    rows: vec![api::v1::Row::default(); 2],
2466                }),
2467            }],
2468        };
2469        let original = requests.clone();
2470        let cost = Inserter::meter_row_inserts(&mut requests, &admitted)
2471            .await
2472            .unwrap();
2473        assert_eq!(cost, if enabled { 17 } else { 0 });
2474        assert_eq!(requests, original);
2475        let records = meter
2476            .attempts
2477            .lock()
2478            .unwrap()
2479            .iter()
2480            .map(|record| (record.rows, record.value))
2481            .collect::<Vec<_>>();
2482        assert_eq!(
2483            records,
2484            if enabled {
2485                vec![(2, 0), (0, 17)]
2486            } else {
2487                vec![]
2488            }
2489        );
2490        meter.reject.store(true, Ordering::Relaxed);
2491        let result = Inserter::meter_row_inserts(&mut requests, &admitted).await;
2492        if enabled {
2493            assert_eq!(result.unwrap_err().status_code(), StatusCode::RateLimited);
2494        } else {
2495            assert_eq!(result.unwrap(), 0);
2496        }
2497        assert_eq!(requests, original);
2498    }
2499
2500    #[test]
2501    fn test_skip_wal_does_not_change_table_options() {
2502        check_skip_wal_does_not_change_table_options(false);
2503        check_skip_wal_does_not_change_table_options(true);
2504    }
2505
2506    fn check_skip_wal_does_not_change_table_options(skip_wal: bool) {
2507        let ctx = Arc::new(QueryContext::with(
2508            DEFAULT_CATALOG_NAME,
2509            DEFAULT_SCHEMA_NAME,
2510        ));
2511        ctx.set_skip_wal(skip_wal);
2512        let mut options = Default::default();
2513        fill_table_options_for_create(&mut options, &AutoCreateTableType::Physical, &ctx);
2514        assert!(!options.contains_key(session::hints::INSERT_SKIP_WAL_HINT));
2515        assert!(!options.contains_key("skip_wal"));
2516    }
2517
2518    #[test]
2519    fn test_last_non_null_create_options_preserve_default_without_append_mode() {
2520        let ctx = Arc::new(QueryContext::with(
2521            DEFAULT_CATALOG_NAME,
2522            DEFAULT_SCHEMA_NAME,
2523        ));
2524        let mut table_options = Default::default();
2525
2526        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::LastNonNull, &ctx);
2527
2528        assert_eq!(
2529            Some("last_non_null"),
2530            table_options.get(MERGE_MODE_KEY).map(String::as_str)
2531        );
2532        assert!(!table_options.contains_key(APPEND_MODE_KEY));
2533    }
2534
2535    #[test]
2536    fn test_fill_table_options_copies_semantic_extensions() {
2537        use table::requests::{
2538            SEMANTIC_METRIC_TYPE, SEMANTIC_PER_TABLE_INDEX_KEY, SEMANTIC_SIGNAL_TYPE,
2539            SEMANTIC_SOURCE, SEMANTIC_SOURCE_VERSION, SIGNAL_TYPE_METRIC, SOURCE_OPENTELEMETRY,
2540        };
2541
2542        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
2543        ctx.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_METRIC);
2544        ctx.set_extension(SEMANTIC_SOURCE, SOURCE_OPENTELEMETRY);
2545        ctx.set_extension(SEMANTIC_SOURCE_VERSION, "2.0");
2546        ctx.set_extension(SEMANTIC_METRIC_TYPE, "bogus");
2547        // The internal transport key must NOT be copied into table options.
2548        ctx.set_extension(SEMANTIC_PER_TABLE_INDEX_KEY, "{}");
2549        let ctx = Arc::new(ctx);
2550        let mut table_options = Default::default();
2551
2552        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::Physical, &ctx);
2553
2554        assert_eq!(
2555            Some(SIGNAL_TYPE_METRIC),
2556            table_options.get(SEMANTIC_SIGNAL_TYPE).map(String::as_str)
2557        );
2558        assert_eq!(
2559            Some(SOURCE_OPENTELEMETRY),
2560            table_options.get(SEMANTIC_SOURCE).map(String::as_str)
2561        );
2562        assert_eq!(
2563            Some("2.0"),
2564            table_options
2565                .get(SEMANTIC_SOURCE_VERSION)
2566                .map(String::as_str)
2567        );
2568        assert!(!table_options.contains_key(SEMANTIC_METRIC_TYPE));
2569        assert!(!table_options.contains_key(SEMANTIC_PER_TABLE_INDEX_KEY));
2570    }
2571
2572    #[test]
2573    fn test_apply_per_table_semantic_options() {
2574        use table::requests::{
2575            SEMANTIC_METRIC_TYPE, SEMANTIC_METRIC_UNIT, SEMANTIC_PER_TABLE_INDEX_KEY,
2576        };
2577
2578        let index = format!(
2579            r#"{{
2580            "{DEFAULT_SCHEMA_NAME}": {{
2581                "http_requests_total": {{
2582                    "greptime.semantic.metric.type": "counter",
2583                    "greptime.semantic.metric.unit": "By",
2584                    "greptime.semantic.metric.type_BOGUS": "x"
2585                }},
2586                "other_table": {{
2587                    "greptime.semantic.metric.type": "gauge"
2588                }}
2589            }},
2590            "other_schema": {{
2591                "http_requests_total": {{
2592                    "greptime.semantic.metric.type": "gauge"
2593                }}
2594            }}
2595        }}"#
2596        );
2597        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
2598        ctx.set_extension(SEMANTIC_PER_TABLE_INDEX_KEY, index);
2599        let ctx = Arc::new(ctx);
2600
2601        let index = parse_per_table_semantic_index(&ctx);
2602        assert!(index.is_some());
2603        let index = index.as_ref();
2604
2605        let mut table_options = std::collections::HashMap::new();
2606        apply_per_table_semantic_options(
2607            &mut table_options,
2608            index,
2609            DEFAULT_SCHEMA_NAME,
2610            "http_requests_total",
2611        );
2612        // The write schema's entry applies — not other_schema's `gauge`.
2613        assert_eq!(
2614            table_options.get(SEMANTIC_METRIC_TYPE).map(String::as_str),
2615            Some("counter")
2616        );
2617        assert_eq!(
2618            table_options.get(SEMANTIC_METRIC_UNIT).map(String::as_str),
2619            Some("By")
2620        );
2621        // The unknown key is rejected by the vocabulary check; other tables' keys
2622        // never appear.
2623        assert!(!table_options.contains_key("greptime.semantic.metric.type_BOGUS"));
2624        assert_eq!(table_options.len(), 2);
2625
2626        let mut empty = std::collections::HashMap::new();
2627        apply_per_table_semantic_options(&mut empty, index, DEFAULT_SCHEMA_NAME, "not_in_index");
2628        assert!(empty.is_empty());
2629
2630        // A schema with no entry is a no-op even when the table name matches
2631        // elsewhere.
2632        let mut opts = std::collections::HashMap::new();
2633        apply_per_table_semantic_options(
2634            &mut opts,
2635            index,
2636            "schema_without_entry",
2637            "http_requests_total",
2638        );
2639        assert!(opts.is_empty());
2640
2641        // No extension at all parses to no index (e.g. logs / Prom RW v1).
2642        let bare = Arc::new(QueryContext::with(
2643            DEFAULT_CATALOG_NAME,
2644            DEFAULT_SCHEMA_NAME,
2645        ));
2646        assert!(parse_per_table_semantic_index(&bare).is_none());
2647        let mut opts = std::collections::HashMap::new();
2648        apply_per_table_semantic_options(
2649            &mut opts,
2650            None,
2651            DEFAULT_SCHEMA_NAME,
2652            "http_requests_total",
2653        );
2654        assert!(opts.is_empty());
2655    }
2656
2657    #[test]
2658    fn test_last_non_null_create_options_preserve_default_with_append_mode_false() {
2659        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
2660        ctx.set_extension(APPEND_MODE_KEY, "false");
2661        let ctx = Arc::new(ctx);
2662        let mut table_options = Default::default();
2663
2664        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::LastNonNull, &ctx);
2665
2666        assert!(!table_options.contains_key(APPEND_MODE_KEY));
2667        assert_eq!(
2668            Some("last_non_null"),
2669            table_options.get(MERGE_MODE_KEY).map(String::as_str)
2670        );
2671    }
2672
2673    #[test]
2674    fn test_last_non_null_create_options_use_configured_merge_mode() {
2675        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
2676        ctx.set_extension(MERGE_MODE_KEY, "last_row");
2677        let ctx = Arc::new(ctx);
2678        let mut table_options = Default::default();
2679
2680        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::LastNonNull, &ctx);
2681
2682        assert_eq!(
2683            Some("last_row"),
2684            table_options.get(MERGE_MODE_KEY).map(String::as_str)
2685        );
2686        assert!(!table_options.contains_key(APPEND_MODE_KEY));
2687    }
2688
2689    #[test]
2690    fn test_last_non_null_create_options_use_last_row_with_append_mode_true() {
2691        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
2692        ctx.set_extension(APPEND_MODE_KEY, "true");
2693        let ctx = Arc::new(ctx);
2694        let mut table_options = Default::default();
2695
2696        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::LastNonNull, &ctx);
2697
2698        assert_eq!(
2699            Some("true"),
2700            table_options.get(APPEND_MODE_KEY).map(String::as_str)
2701        );
2702        assert_eq!(
2703            Some("last_row"),
2704            table_options.get(MERGE_MODE_KEY).map(String::as_str)
2705        );
2706    }
2707
2708    struct UnexpectedBatcher;
2709
2710    #[async_trait::async_trait]
2711    impl PendingRowsBatcher for UnexpectedBatcher {
2712        async fn acquire(&self) -> Result<Arc<tokio::sync::OwnedSemaphorePermit>> {
2713            panic!("empty writes must not acquire batch admission")
2714        }
2715
2716        async fn submit(
2717            &self,
2718            _table_info: TableInfoRef,
2719            _batch: arrow::record_batch::RecordBatch,
2720            _ctx: QueryContextRef,
2721            _permit: Arc<tokio::sync::OwnedSemaphorePermit>,
2722        ) -> Result<usize> {
2723            panic!("empty writes must not submit a batch")
2724        }
2725    }
2726
2727    async fn batcher_test_inserter() -> Inserter {
2728        let kv_backend = prepare_mocked_backend().await;
2729        Inserter::new(
2730            catalog::memory::MemoryCatalogManager::new(),
2731            create_partition_rule_manager(kv_backend.clone()).await,
2732            Arc::new(MockDatanodeManager::new(NaiveDatanodeHandler)),
2733            Arc::new(new_table_flownode_set_cache(
2734                String::new(),
2735                Cache::new(100),
2736                kv_backend,
2737            )),
2738            true,
2739        )
2740    }
2741
2742    #[tokio::test]
2743    async fn test_logical_batcher_eligibility() {
2744        use catalog::RegisterTableRequest;
2745        use catalog::memory::MemoryCatalogManager;
2746        use common_meta::instruction::{CacheIdent, CreateFlow};
2747        use common_meta::kv_backend::KvBackendRef;
2748        use common_meta::kv_backend::memory::MemoryKvBackend;
2749        use datatypes::schema::{ColumnDefaultConstraint, SchemaBuilder};
2750        let requests = RowInsertRequests {
2751            inserts: vec![RowInsertRequest {
2752                table_name: "test_table".to_string(),
2753                rows: None,
2754            }],
2755        };
2756        let original =
2757            make_table_ref_with_schema("ts", "value", ConcreteDataType::float64_datatype())
2758                .table_info();
2759        for case in [
2760            "eligible",
2761            "physical",
2762            "ordinary",
2763            "instant",
2764            "disabled",
2765            "hint",
2766            "flow",
2767            "required_tag",
2768            "default",
2769        ] {
2770            let mut info = (*original).clone();
2771            info.meta.engine = METRIC_ENGINE_NAME.to_string();
2772            info.meta.options.extra_options.insert(
2773                LOGICAL_TABLE_METADATA_KEY.to_string(),
2774                "physical".to_string(),
2775            );
2776            let mut ctx = QueryContext::arc().fork();
2777            match case {
2778                "physical" => {
2779                    info.meta
2780                        .options
2781                        .extra_options
2782                        .insert(LOGICAL_TABLE_METADATA_KEY.to_string(), "other".to_string());
2783                }
2784                "ordinary" => info.meta.engine = "mito".to_string(),
2785                "instant" => info.meta.options.ttl = Some(common_time::ttl::TimeToLive::Instant),
2786                "hint" => ctx.set_extension(AUTO_CREATE_TABLE_KEY, "false"),
2787                "required_tag" | "default" => {
2788                    let mut columns = info.meta.schema.column_schemas().to_vec();
2789                    let mut tag = ColumnSchema::new(
2790                        "tag",
2791                        ConcreteDataType::string_datatype(),
2792                        case != "required_tag",
2793                    );
2794                    if case == "default" {
2795                        tag = tag
2796                            .with_default_constraint(Some(ColumnDefaultConstraint::null_value()))
2797                            .unwrap();
2798                    }
2799                    columns.push(tag);
2800                    info.meta.schema = Arc::new(
2801                        SchemaBuilder::try_from_columns(columns)
2802                            .unwrap()
2803                            .build()
2804                            .unwrap(),
2805                    );
2806                    info.meta.primary_key_indices = vec![2];
2807                }
2808                _ => {}
2809            }
2810            let catalog = MemoryCatalogManager::with_default_setup();
2811            let table = Arc::new(table::Table::new(
2812                Arc::new(info),
2813                table::metadata::FilterPushDownType::Unsupported,
2814                Arc::new(DummyDataSource),
2815            ));
2816            catalog
2817                .register_table_sync(RegisterTableRequest {
2818                    catalog: DEFAULT_CATALOG_NAME.to_string(),
2819                    schema: DEFAULT_SCHEMA_NAME.to_string(),
2820                    table_name: "test_table".to_string(),
2821                    table_id: 1,
2822                    table,
2823                })
2824                .unwrap();
2825            let mut inserter = batcher_test_inserter().await;
2826            inserter.catalog_manager = catalog;
2827            inserter.auto_create_table = case != "disabled";
2828            let kv_backend: KvBackendRef = Arc::new(MemoryKvBackend::default());
2829            inserter.table_flownode_set_cache = Arc::new(new_table_flownode_set_cache(
2830                String::new(),
2831                Cache::new(10),
2832                kv_backend,
2833            ));
2834            if case == "flow" {
2835                inserter
2836                    .table_flownode_set_cache
2837                    .invalidate(&[CacheIdent::CreateFlow(CreateFlow {
2838                        flow_id: 1,
2839                        source_table_ids: vec![1],
2840                        partition_to_peer_mapping: vec![(0, Peer::empty(1))],
2841                    })])
2842                    .await
2843                    .unwrap();
2844            }
2845            assert_eq!(
2846                inserter
2847                    .can_batch_metric_rows(&requests, &Arc::new(ctx), "physical")
2848                    .await
2849                    .unwrap(),
2850                case == "eligible",
2851                "{case}"
2852            );
2853        }
2854    }
2855
2856    #[tokio::test]
2857    async fn test_batcher_meter_preserves_request() {
2858        let mut requests = RowInsertRequests {
2859            inserts: vec![RowInsertRequest {
2860                table_name: "sample".to_string(),
2861                rows: Some(Rows {
2862                    schema: vec![],
2863                    rows: vec![api::v1::Row {
2864                        values: vec![Value {
2865                            value_data: Some(api::v1::value::ValueData::F64Value(1.5)),
2866                        }],
2867                    }],
2868                }),
2869            }],
2870        };
2871        let expected = requests.clone();
2872        Inserter::meter_row_inserts(&mut requests, &QueryContext::arc())
2873            .await
2874            .unwrap();
2875        assert_eq!(requests, expected);
2876    }
2877
2878    #[tokio::test]
2879    async fn test_instant_table_bypasses_batcher() {
2880        let batcher: Arc<dyn PendingRowsBatcher> = Arc::new(UnexpectedBatcher);
2881        let inserter = batcher_test_inserter()
2882            .await
2883            .with_pending_rows_batcher(Some(batcher));
2884        let mut ctx = session::context::QueryContextBuilder::default().build();
2885        ctx.set_batching_enabled(true);
2886        let ctx = Arc::new(ctx);
2887        let table = make_table_ref_with_schema("ts", "value", ConcreteDataType::float64_datatype())
2888            .table_info();
2889        assert!(inserter.table_batcher(&table, &ctx).is_some());
2890        let mut instant = (*table).clone();
2891        instant.meta.options.ttl = Some(common_time::ttl::TimeToLive::Instant);
2892        assert!(inserter.table_batcher(&Arc::new(instant), &ctx).is_none());
2893    }
2894
2895    #[tokio::test]
2896    async fn test_empty_prepared_rows_skip_batcher() {
2897        let inserter = batcher_test_inserter().await;
2898        let table = make_table_ref_with_schema("ts", "value", ConcreteDataType::float64_datatype())
2899            .table_info();
2900        let batcher: Arc<dyn PendingRowsBatcher> = Arc::new(UnexpectedBatcher);
2901        let ctx = QueryContext::arc();
2902        let output = inserter
2903            .submit_table_rows(
2904                Rows {
2905                    schema: vec![],
2906                    rows: vec![],
2907                },
2908                table.clone(),
2909                ctx.clone(),
2910                &batcher,
2911            )
2912            .await
2913            .unwrap();
2914        assert!(matches!(output.data, OutputData::AffectedRows(0)));
2915        let output = inserter
2916            .submit_pending_rows(
2917                RowInsertRequests {
2918                    inserts: vec![RowInsertRequest {
2919                        table_name: table.name.clone(),
2920                        rows: None,
2921                    }],
2922                },
2923                HashMap::from_iter([(table.table_id(), table)]),
2924                ctx,
2925                &batcher,
2926            )
2927            .await
2928            .unwrap();
2929        assert!(matches!(output.data, OutputData::AffectedRows(0)));
2930    }
2931}