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, SemanticType,
28};
29use catalog::CatalogManagerRef;
30use client::{OutputData, OutputMeta};
31use common_catalog::consts::{
32    PARENT_SPAN_ID_COLUMN, SERVICE_NAME_COLUMN, TRACE_ID_COLUMN, TRACE_TABLE_NAME,
33    TRACE_TABLE_NAME_SESSION_KEY, default_engine, is_ddl_reserved_table,
34    trace_operations_table_name, trace_services_table_name,
35};
36use common_grpc_expr::util::ColumnExpr;
37use common_meta::cache::TableFlownodeSetCacheRef;
38use common_meta::node_manager::{AffectedRows, NodeManagerRef};
39use common_meta::peer::Peer;
40use common_meta::rpc::ddl::TriggerReason;
41use common_query::Output;
42use common_query::native_histogram::{is_native_histogram_value_type, native_histogram_value_type};
43use common_query::prelude::{greptime_timestamp, greptime_value};
44use common_telemetry::tracing_context::TracingContext;
45use common_telemetry::{error, info, warn};
46use datatypes::schema::SkippingIndexOptions;
47use futures_util::future;
48use meter_macros::write_meter;
49use partition::manager::PartitionRuleManagerRef;
50use session::context::QueryContextRef;
51use snafu::ResultExt;
52use snafu::prelude::*;
53use sql::partition::partition_rule_for_hexstring;
54use sql::statements::create::Partitions;
55use sql::statements::insert::Insert;
56use store_api::metric_engine_consts::{
57    LOGICAL_TABLE_METADATA_KEY, METRIC_ENGINE_NAME, PHYSICAL_TABLE_METADATA_KEY,
58};
59use store_api::mito_engine_options::{
60    APPEND_MODE_KEY, COMPACTION_TYPE, COMPACTION_TYPE_TWCS, MERGE_MODE_KEY, TTL_KEY,
61    TWCS_TIME_WINDOW,
62};
63use store_api::storage::{RegionId, TableId};
64use table::TableRef;
65use table::metadata::TableInfo;
66use table::requests::{
67    AUTO_CREATE_TABLE_KEY, InsertRequest as TableInsertRequest, SEMANTIC_PER_TABLE_INDEX_KEY,
68    TABLE_DATA_MODEL, TABLE_DATA_MODEL_TRACE_V1, TRACE_TABLE_PARTITIONS_HINT_KEY,
69    VALID_TABLE_OPTION_KEYS, is_semantic_option_key, validate_semantic_option,
70};
71use table::table_reference::TableReference;
72
73use crate::error::{
74    CatalogSnafu, ColumnOptionsSnafu, CreatePartitionRulesSnafu, FindRegionLeaderSnafu,
75    InvalidInsertRequestSnafu, JoinTaskSnafu, RequestInsertsSnafu, Result, TableNotFoundSnafu,
76};
77use crate::expr_helper;
78use crate::region_req_factory::RegionRequestFactory;
79use crate::req_convert::common::preprocess_row_insert_requests;
80use crate::req_convert::insert::{
81    ColumnToRow, RowToRegion, StatementToRegion, TableToRegion, fill_reqs_with_impure_default,
82};
83use crate::statement::StatementExecutor;
84
85pub struct Inserter {
86    catalog_manager: CatalogManagerRef,
87    pub(crate) partition_manager: PartitionRuleManagerRef,
88    pub(crate) node_manager: NodeManagerRef,
89    pub(crate) table_flownode_set_cache: TableFlownodeSetCacheRef,
90    /// Server-side upper bound for auto table creation on write.
91    /// When `false`, missing tables are never auto-created regardless of the
92    /// per-request `auto_create_table` hint. When `true`, the hint still applies.
93    auto_create_table: bool,
94}
95
96pub type InserterRef = Arc<Inserter>;
97
98/// Hint for the table type to create automatically.
99#[derive(Clone)]
100pub enum AutoCreateTableType {
101    /// A logical table with the physical table name.
102    Logical(String),
103    /// A physical table.
104    Physical,
105    /// A log table which is append-only.
106    Log,
107    /// A table that merges rows by `last_non_null` strategy.
108    LastNonNull,
109    /// Create table that build index and default partition rules on trace_id
110    Trace { alter_existing: bool },
111}
112
113impl AutoCreateTableType {
114    pub fn as_str(&self) -> &'static str {
115        match self {
116            AutoCreateTableType::Logical(_) => "logical",
117            AutoCreateTableType::Physical => "physical",
118            AutoCreateTableType::Log => "log",
119            AutoCreateTableType::LastNonNull => "last_non_null",
120            AutoCreateTableType::Trace { .. } => "trace",
121        }
122    }
123
124    fn alter_existing(&self) -> bool {
125        !matches!(
126            self,
127            Self::Trace {
128                alter_existing: false
129            }
130        )
131    }
132}
133
134/// Split insert requests into normal and instant requests.
135///
136/// Where instant requests are requests with ttl=instant,
137/// and normal requests are requests with ttl set to other values.
138///
139/// This is used to split requests for different processing.
140#[derive(Clone)]
141pub struct InstantAndNormalInsertRequests {
142    /// Requests with normal ttl.
143    pub normal_requests: RegionInsertRequests,
144    /// Requests with ttl=instant.
145    /// Will be discarded immediately at frontend, wouldn't even insert into memtable, and only sent to flow node if needed.
146    pub instant_requests: RegionInsertRequests,
147}
148
149impl Inserter {
150    pub fn new(
151        catalog_manager: CatalogManagerRef,
152        partition_manager: PartitionRuleManagerRef,
153        node_manager: NodeManagerRef,
154        table_flownode_set_cache: TableFlownodeSetCacheRef,
155        auto_create_table: bool,
156    ) -> Self {
157        Self {
158            catalog_manager,
159            partition_manager,
160            node_manager,
161            table_flownode_set_cache,
162            auto_create_table,
163        }
164    }
165
166    pub async fn handle_column_inserts(
167        &self,
168        requests: InsertRequests,
169        ctx: QueryContextRef,
170        statement_executor: &StatementExecutor,
171    ) -> Result<Output> {
172        let row_inserts = ColumnToRow::convert(requests)?;
173        self.handle_row_inserts(row_inserts, ctx, statement_executor, false, false)
174            .await
175    }
176
177    /// Handles row inserts request and creates a physical table on demand.
178    pub async fn handle_row_inserts(
179        &self,
180        mut requests: RowInsertRequests,
181        ctx: QueryContextRef,
182        statement_executor: &StatementExecutor,
183        accommodate_existing_schema: bool,
184        is_single_value: bool,
185    ) -> Result<Output> {
186        preprocess_row_insert_requests(&mut requests.inserts)?;
187        self.handle_row_inserts_with_create_type(
188            requests,
189            ctx,
190            statement_executor,
191            AutoCreateTableType::Physical,
192            accommodate_existing_schema,
193            is_single_value,
194        )
195        .await
196    }
197
198    /// Handles row inserts request and creates a log table on demand.
199    pub async fn handle_log_inserts(
200        &self,
201        requests: RowInsertRequests,
202        ctx: QueryContextRef,
203        statement_executor: &StatementExecutor,
204    ) -> Result<Output> {
205        self.handle_row_inserts_with_create_type(
206            requests,
207            ctx,
208            statement_executor,
209            AutoCreateTableType::Log,
210            false,
211            false,
212        )
213        .await
214    }
215
216    pub async fn handle_trace_inserts(
217        &self,
218        requests: RowInsertRequests,
219        ctx: QueryContextRef,
220        statement_executor: &StatementExecutor,
221    ) -> Result<Output> {
222        self.handle_row_inserts_with_create_type(
223            requests,
224            ctx,
225            statement_executor,
226            AutoCreateTableType::Trace {
227                alter_existing: true,
228            },
229            false,
230            false,
231        )
232        .await
233    }
234
235    /// Handles row inserts request and creates a table with `last_non_null` merge mode on demand.
236    pub async fn handle_last_non_null_inserts(
237        &self,
238        requests: RowInsertRequests,
239        ctx: QueryContextRef,
240        statement_executor: &StatementExecutor,
241        accommodate_existing_schema: bool,
242        is_single_value: bool,
243    ) -> Result<Output> {
244        self.handle_row_inserts_with_create_type(
245            requests,
246            ctx,
247            statement_executor,
248            AutoCreateTableType::LastNonNull,
249            accommodate_existing_schema,
250            is_single_value,
251        )
252        .await
253    }
254
255    /// Handles row inserts request with specified [AutoCreateTableType].
256    async fn handle_row_inserts_with_create_type(
257        &self,
258        mut requests: RowInsertRequests,
259        ctx: QueryContextRef,
260        statement_executor: &StatementExecutor,
261        create_type: AutoCreateTableType,
262        accommodate_existing_schema: bool,
263        is_single_value: bool,
264    ) -> Result<Output> {
265        // remove empty requests
266        requests.inserts.retain(|req| {
267            req.rows
268                .as_ref()
269                .map(|r| !r.rows.is_empty())
270                .unwrap_or_default()
271        });
272        validate_column_count_match(&requests)?;
273
274        let CreateAlterTableResult {
275            instant_table_ids,
276            table_infos,
277        } = self
278            .create_or_alter_tables_on_demand(
279                &mut requests,
280                &ctx,
281                create_type,
282                statement_executor,
283                accommodate_existing_schema,
284                is_single_value,
285            )
286            .await?;
287
288        let name_to_info = table_infos
289            .values()
290            .map(|info| (info.name.clone(), info.clone()))
291            .collect::<HashMap<_, _>>();
292        let inserts = RowToRegion::new(
293            name_to_info,
294            instant_table_ids,
295            self.partition_manager.as_ref(),
296        )
297        .convert(requests)
298        .await?;
299
300        self.do_request(inserts, &table_infos, &ctx).await
301    }
302
303    /// Handles row inserts request with metric engine.
304    pub async fn handle_metric_row_inserts(
305        &self,
306        mut requests: RowInsertRequests,
307        ctx: QueryContextRef,
308        statement_executor: &StatementExecutor,
309        physical_table: String,
310    ) -> Result<Output> {
311        // remove empty requests
312        requests.inserts.retain(|req| {
313            req.rows
314                .as_ref()
315                .map(|r| !r.rows.is_empty())
316                .unwrap_or_default()
317        });
318        validate_column_count_match(&requests)?;
319
320        // check and create physical table
321        self.create_physical_table_on_demand(&ctx, physical_table.clone(), statement_executor)
322            .await?;
323
324        // check and create logical tables
325        let CreateAlterTableResult {
326            instant_table_ids,
327            table_infos,
328        } = self
329            .create_or_alter_tables_on_demand(
330                &mut requests,
331                &ctx,
332                AutoCreateTableType::Logical(physical_table.clone()),
333                statement_executor,
334                true,
335                true,
336            )
337            .await?;
338        let name_to_info = table_infos
339            .values()
340            .map(|info| (info.name.clone(), info.clone()))
341            .collect::<HashMap<_, _>>();
342        let inserts = RowToRegion::new(name_to_info, instant_table_ids, &self.partition_manager)
343            .convert(requests)
344            .await?;
345
346        self.do_request(inserts, &table_infos, &ctx).await
347    }
348
349    pub async fn handle_table_insert(
350        &self,
351        request: TableInsertRequest,
352        ctx: QueryContextRef,
353    ) -> Result<Output> {
354        let catalog = request.catalog_name.as_str();
355        let schema = request.schema_name.as_str();
356        let table_name = request.table_name.as_str();
357        let table = self.get_table(catalog, schema, table_name).await?;
358        let table = table.with_context(|| TableNotFoundSnafu {
359            table_name: common_catalog::format_full_table_name(catalog, schema, table_name),
360        })?;
361        let table_info = table.table_info();
362
363        let inserts = TableToRegion::new(&table_info, &self.partition_manager)
364            .convert(request)
365            .await?;
366
367        let table_infos = HashMap::from_iter([(table_info.table_id(), table_info.clone())]);
368
369        self.do_request(inserts, &table_infos, &ctx).await
370    }
371
372    pub async fn handle_statement_insert(
373        &self,
374        insert: &Insert,
375        ctx: &QueryContextRef,
376    ) -> Result<Output> {
377        let (inserts, table_info) =
378            StatementToRegion::new(self.catalog_manager.as_ref(), &self.partition_manager, ctx)
379                .convert(insert, ctx)
380                .await?;
381
382        let table_infos = HashMap::from_iter([(table_info.table_id(), table_info.clone())]);
383
384        self.do_request(inserts, &table_infos, ctx).await
385    }
386}
387
388impl Inserter {
389    async fn do_request(
390        &self,
391        requests: InstantAndNormalInsertRequests,
392        table_infos: &HashMap<TableId, Arc<TableInfo>>,
393        ctx: &QueryContextRef,
394    ) -> Result<Output> {
395        // Fill impure default values in the request
396        let requests = fill_reqs_with_impure_default(table_infos, requests)?;
397
398        let write_cost = write_meter!(
399            ctx.current_catalog(),
400            ctx.current_schema(),
401            requests,
402            ctx.channel() as u8
403        );
404        let request_factory = RegionRequestFactory::new(RegionRequestHeader {
405            tracing_context: TracingContext::from_current_span().to_w3c(),
406            dbname: ctx.get_db_string(),
407            ..Default::default()
408        });
409
410        let InstantAndNormalInsertRequests {
411            normal_requests,
412            instant_requests,
413        } = requests;
414
415        // Mirror requests for source table to flownode asynchronously
416        let flow_mirror_task = FlowMirrorTask::new(
417            &self.table_flownode_set_cache,
418            normal_requests
419                .requests
420                .iter()
421                .chain(instant_requests.requests.iter()),
422        )
423        .await?;
424        flow_mirror_task.detach(self.node_manager.clone())?;
425
426        // Write requests to datanode and wait for response
427        let write_tasks = self
428            .group_requests_by_peer(normal_requests)
429            .await?
430            .into_iter()
431            .map(|(peer, inserts)| {
432                let node_manager = self.node_manager.clone();
433                let request = request_factory.build_insert(inserts);
434                common_runtime::spawn_global(async move {
435                    node_manager
436                        .datanode(&peer)
437                        .await
438                        .handle(request)
439                        .await
440                        .context(RequestInsertsSnafu)
441                })
442            });
443        let results = future::try_join_all(write_tasks)
444            .await
445            .context(JoinTaskSnafu)?;
446        let affected_rows = results
447            .into_iter()
448            .map(|resp| resp.map(|r| r.affected_rows))
449            .sum::<Result<AffectedRows>>()?;
450        crate::metrics::DIST_INGEST_ROW_COUNT
451            .with_label_values(&[ctx.get_db_string().as_str()])
452            .inc_by(affected_rows as u64);
453        Ok(Output::new(
454            OutputData::AffectedRows(affected_rows),
455            OutputMeta::new_with_cost(write_cost as _),
456        ))
457    }
458
459    async fn group_requests_by_peer(
460        &self,
461        requests: RegionInsertRequests,
462    ) -> Result<HashMap<Peer, RegionInsertRequests>> {
463        // group by region ids first to reduce repeatedly call `find_region_leader`
464        // TODO(discord9): determine if a addition clone is worth it
465        let mut requests_per_region: HashMap<RegionId, RegionInsertRequests> = HashMap::new();
466        for req in requests.requests {
467            let region_id = RegionId::from_u64(req.region_id);
468            requests_per_region
469                .entry(region_id)
470                .or_default()
471                .requests
472                .push(req);
473        }
474
475        let mut inserts: HashMap<Peer, RegionInsertRequests> = HashMap::new();
476
477        for (region_id, reqs) in requests_per_region {
478            let peer = self
479                .partition_manager
480                .find_region_leader(region_id)
481                .await
482                .context(FindRegionLeaderSnafu)?;
483            inserts
484                .entry(peer)
485                .or_default()
486                .requests
487                .extend(reqs.requests);
488        }
489
490        Ok(inserts)
491    }
492
493    /// Returns `None` if auto table creation is allowed, or `Some(reason)` if
494    /// disabled by either the global config or the request hint. The reason tells
495    /// which one, for a clearer error.
496    fn auto_create_disabled_reason(&self, ctx: &QueryContextRef) -> Result<Option<&'static str>> {
497        let auto_create_table_hint = ctx
498            .extension(AUTO_CREATE_TABLE_KEY)
499            .map(|v| v.parse::<bool>())
500            .transpose()
501            .map_err(|_| {
502                InvalidInsertRequestSnafu {
503                    reason: "`auto_create_table` hint must be a boolean",
504                }
505                .build()
506            })?
507            .unwrap_or(true);
508        Ok(if !self.auto_create_table {
509            Some("auto-create table is disabled by frontend config")
510        } else if !auto_create_table_hint {
511            Some("`auto_create_table` hint is disabled")
512        } else {
513            None
514        })
515    }
516
517    /// Ensures a trace table has the request-global schema without requiring a
518    /// padded data row to drive on-demand creation or alteration. When
519    /// `alter_existing` is false, a table created after planning is left for the
520    /// caller to re-plan.
521    pub async fn ensure_trace_table_on_demand(
522        &self,
523        table_name: &str,
524        request_schema: Vec<ColumnSchema>,
525        alter_existing: bool,
526        ctx: &QueryContextRef,
527        statement_executor: &StatementExecutor,
528    ) -> Result<()> {
529        let mut requests = RowInsertRequests {
530            inserts: vec![RowInsertRequest {
531                table_name: table_name.to_string(),
532                rows: Some(api::v1::Rows {
533                    schema: request_schema,
534                    rows: Vec::new(),
535                }),
536            }],
537        };
538        self.create_or_alter_tables_on_demand(
539            &mut requests,
540            ctx,
541            AutoCreateTableType::Trace { alter_existing },
542            statement_executor,
543            false,
544            false,
545        )
546        .await?;
547        Ok(())
548    }
549
550    /// Creates or alter tables on demand:
551    /// - if table does not exist, create table by inferred CreateExpr
552    /// - if table exist, check if schema matches. If any new column found, alter table by inferred `AlterExpr`
553    ///
554    /// Returns a mapping from table name to table id, where table name is the table name involved in the requests.
555    /// This mapping is used in the conversion of RowToRegion.
556    ///
557    /// `accommodate_existing_schema` is used to determine if the existing schema should override the new schema.
558    /// It only works for TIME_INDEX and single VALUE columns. This is for the case where the user creates a table with
559    /// custom schema, and then inserts data with endpoints that have default schema setting, like prometheus
560    /// remote write. This will modify the `RowInsertRequests` in place.
561    /// `is_single_value` indicates whether the default schema only contains single value column so we can accommodate it.
562    async fn create_or_alter_tables_on_demand(
563        &self,
564        requests: &mut RowInsertRequests,
565        ctx: &QueryContextRef,
566        auto_create_table_type: AutoCreateTableType,
567        statement_executor: &StatementExecutor,
568        accommodate_existing_schema: bool,
569        is_single_value: bool,
570    ) -> Result<CreateAlterTableResult> {
571        let _timer = crate::metrics::CREATE_ALTER_ON_DEMAND
572            .with_label_values(&[auto_create_table_type.as_str()])
573            .start_timer();
574
575        let catalog = ctx.current_catalog();
576        let schema = ctx.current_schema();
577
578        let mut table_infos = HashMap::new();
579        if let Some(disabled_reason) = self.auto_create_disabled_reason(ctx)? {
580            let mut instant_table_ids = HashSet::new();
581            for req in &requests.inserts {
582                let table = match self.get_table(catalog, &schema, &req.table_name).await? {
583                    Some(table) => table,
584                    // System-defined table: created canonically by the system,
585                    // so the auto-create config/hint does not apply.
586                    None if is_ddl_reserved_table(&schema, &req.table_name) => {
587                        statement_executor
588                            .create_declared_relationships_table(catalog, ctx.clone())
589                            .await?
590                    }
591                    None => {
592                        return InvalidInsertRequestSnafu {
593                            reason: format!(
594                                "Table `{}` does not exist, and {}",
595                                req.table_name, disabled_reason
596                            ),
597                        }
598                        .fail();
599                    }
600                };
601                let table_info = table.table_info();
602                if table_info.is_ttl_instant_table() {
603                    instant_table_ids.insert(table_info.table_id());
604                }
605                table_infos.insert(table_info.table_id(), table.table_info());
606            }
607            let ret = CreateAlterTableResult {
608                instant_table_ids,
609                table_infos,
610            };
611            return Ok(ret);
612        }
613
614        let mut create_tables = vec![];
615        let mut alter_tables = vec![];
616        let mut need_refresh_table_infos = HashSet::new();
617        let mut instant_table_ids = HashSet::new();
618        let mut per_table_semantics: Option<Option<PerTableSemanticIndex>> = None;
619
620        for req in &mut requests.inserts {
621            match self.get_table(catalog, &schema, &req.table_name).await? {
622                Some(table) => {
623                    let table_info = table.table_info();
624                    if table_info.is_ttl_instant_table() {
625                        instant_table_ids.insert(table_info.table_id());
626                    }
627                    if let Some(alter_expr) = self.get_alter_table_expr_on_demand(
628                        req,
629                        &table,
630                        ctx,
631                        accommodate_existing_schema,
632                        is_single_value,
633                        auto_create_table_type.alter_existing(),
634                    )? {
635                        alter_tables.push(alter_expr);
636                        need_refresh_table_infos.insert((
637                            catalog.to_string(),
638                            schema.clone(),
639                            req.table_name.clone(),
640                        ));
641                    } else {
642                        table_infos.insert(table_info.table_id(), table.table_info());
643                    }
644                }
645                // A DDL-reserved table's definition never derives from the
646                // write request; the system creates it canonically, below the
647                // user-DDL guard that rejects the generic create path.
648                None if is_ddl_reserved_table(&schema, &req.table_name) => {
649                    let table = statement_executor
650                        .create_declared_relationships_table(catalog, ctx.clone())
651                        .await?;
652                    let table_info = table.table_info();
653                    if table_info.is_ttl_instant_table() {
654                        instant_table_ids.insert(table_info.table_id());
655                    }
656                    table_infos.insert(table_info.table_id(), table_info);
657                }
658                None => {
659                    let semantic_index = per_table_semantics
660                        .get_or_insert_with(|| parse_per_table_semantic_index(ctx))
661                        .as_ref();
662                    let create_expr = self.get_create_table_expr_on_demand(
663                        req,
664                        &auto_create_table_type,
665                        ctx,
666                        semantic_index,
667                    )?;
668                    create_tables.push(create_expr);
669                }
670            }
671        }
672
673        match auto_create_table_type {
674            AutoCreateTableType::Logical(_) => {
675                if !create_tables.is_empty() {
676                    // Creates logical tables in batch.
677                    let tables = self
678                        .create_logical_tables(create_tables, ctx, statement_executor)
679                        .await?;
680
681                    for table in tables {
682                        let table_info = table.table_info();
683                        if table_info.is_ttl_instant_table() {
684                            instant_table_ids.insert(table_info.table_id());
685                        }
686                        table_infos.insert(table_info.table_id(), table.table_info());
687                    }
688                }
689                if !alter_tables.is_empty() {
690                    // Alter logical tables in batch.
691                    statement_executor
692                        .alter_logical_tables(alter_tables, ctx.clone(), TriggerReason::AutoAlter)
693                        .await?;
694                }
695            }
696            AutoCreateTableType::Physical
697            | AutoCreateTableType::Log
698            | AutoCreateTableType::LastNonNull => {
699                // note that auto create table shouldn't be ttl instant table
700                // for it's a very unexpected behavior and should be set by user explicitly
701                for create_table in create_tables {
702                    let table = self
703                        .create_physical_table(create_table, None, ctx, statement_executor)
704                        .await?;
705                    let table_info = table.table_info();
706                    if table_info.is_ttl_instant_table() {
707                        instant_table_ids.insert(table_info.table_id());
708                    }
709                    table_infos.insert(table_info.table_id(), table.table_info());
710                }
711                for alter_expr in alter_tables.into_iter() {
712                    statement_executor
713                        .alter_table_inner(alter_expr, ctx.clone(), TriggerReason::AutoAlter)
714                        .await?;
715                }
716            }
717
718            AutoCreateTableType::Trace { .. } => {
719                let trace_table_name = ctx
720                    .extension(TRACE_TABLE_NAME_SESSION_KEY)
721                    .unwrap_or(TRACE_TABLE_NAME);
722
723                let trace_table_partitions = if let Some(trace_table_partitions) =
724                    ctx.extension(TRACE_TABLE_PARTITIONS_HINT_KEY)
725                {
726                    let p = trace_table_partitions.parse::<u32>().map_err(|_| {
727                        InvalidInsertRequestSnafu {
728                            reason: format!(
729                                "Failed to parse trace_table_partitions: {}",
730                                trace_table_partitions
731                            ),
732                        }
733                        .build()
734                    })?;
735                    Some(p)
736                } else {
737                    None
738                };
739
740                // note that auto create table shouldn't be ttl instant table
741                // for it's a very unexpected behavior and should be set by user explicitly
742                for mut create_table in create_tables {
743                    if create_table.table_name == trace_services_table_name(trace_table_name)
744                        || create_table.table_name == trace_operations_table_name(trace_table_name)
745                    {
746                        // Disable append mode for auxiliary tables (services/operations) since they require upsert behavior.
747                        create_table
748                            .table_options
749                            .insert(APPEND_MODE_KEY.to_string(), "false".to_string());
750                        // Remove `ttl` key from table options if it exists
751                        create_table.table_options.remove(TTL_KEY);
752
753                        let table = self
754                            .create_physical_table(create_table, None, ctx, statement_executor)
755                            .await?;
756                        let table_info = table.table_info();
757                        if table_info.is_ttl_instant_table() {
758                            instant_table_ids.insert(table_info.table_id());
759                        }
760                        table_infos.insert(table_info.table_id(), table.table_info());
761                    } else {
762                        // prebuilt partition rules for uuid data: see the function
763                        // for more information
764                        let partitions = if matches!(trace_table_partitions, Some(0) | Some(1)) {
765                            // disable partitions
766                            None
767                        } else {
768                            let p = partition_rule_for_hexstring(
769                                TRACE_ID_COLUMN,
770                                trace_table_partitions,
771                            )
772                            .context(CreatePartitionRulesSnafu)?;
773                            Some(p)
774                        };
775
776                        // add skip index to
777                        // - trace_id: when searching by trace id
778                        // - parent_span_id: when searching root span
779                        // - span_name: when searching certain types of span
780                        let index_columns =
781                            [TRACE_ID_COLUMN, PARENT_SPAN_ID_COLUMN, SERVICE_NAME_COLUMN];
782                        for index_column in index_columns {
783                            if let Some(col) = create_table
784                                .column_defs
785                                .iter_mut()
786                                .find(|c| c.name == index_column)
787                            {
788                                col.options =
789                                    options_from_skipping(&SkippingIndexOptions::default())
790                                        .context(ColumnOptionsSnafu)?;
791                            } else {
792                                warn!(
793                                    "Column {} not found when creating index for trace table: {}.",
794                                    index_column, create_table.table_name
795                                );
796                            }
797                        }
798
799                        // use table_options to mark table model version
800                        create_table.table_options.insert(
801                            TABLE_DATA_MODEL.to_string(),
802                            TABLE_DATA_MODEL_TRACE_V1.to_string(),
803                        );
804
805                        let table = self
806                            .create_physical_table(
807                                create_table,
808                                partitions,
809                                ctx,
810                                statement_executor,
811                            )
812                            .await?;
813                        let table_info = table.table_info();
814                        if table_info.is_ttl_instant_table() {
815                            instant_table_ids.insert(table_info.table_id());
816                        }
817                        table_infos.insert(table_info.table_id(), table.table_info());
818                    }
819                }
820                for alter_expr in alter_tables.into_iter() {
821                    statement_executor
822                        .alter_table_inner(alter_expr, ctx.clone(), TriggerReason::AutoAlter)
823                        .await?;
824                }
825            }
826        }
827
828        // refresh table infos for altered tables
829        for (catalog, schema, table_name) in need_refresh_table_infos {
830            let table = self
831                .get_table(&catalog, &schema, &table_name)
832                .await?
833                .context(TableNotFoundSnafu {
834                    table_name: common_catalog::format_full_table_name(
835                        &catalog,
836                        &schema,
837                        &table_name,
838                    ),
839                })?;
840            let table_info = table.table_info();
841            table_infos.insert(table_info.table_id(), table.table_info());
842        }
843
844        Ok(CreateAlterTableResult {
845            instant_table_ids,
846            table_infos,
847        })
848    }
849
850    async fn create_physical_table_on_demand(
851        &self,
852        ctx: &QueryContextRef,
853        physical_table: String,
854        statement_executor: &StatementExecutor,
855    ) -> Result<()> {
856        let catalog_name = ctx.current_catalog();
857        let schema_name = ctx.current_schema();
858
859        // check if exist
860        if self
861            .get_table(catalog_name, &schema_name, &physical_table)
862            .await?
863            .is_some()
864        {
865            return Ok(());
866        }
867
868        // Gate here too, otherwise a disabled switch would still leak the physical table.
869        if let Some(disabled_reason) = self.auto_create_disabled_reason(ctx)? {
870            return InvalidInsertRequestSnafu {
871                reason: format!(
872                    "Physical table `{physical_table}` does not exist, and {disabled_reason}"
873                ),
874            }
875            .fail();
876        }
877
878        let table_reference = TableReference::full(catalog_name, &schema_name, &physical_table);
879        info!("Physical metric table `{table_reference}` does not exist, try creating table");
880
881        // schema with timestamp and field column
882        let default_schema = vec![
883            ColumnSchema {
884                column_name: greptime_timestamp().to_string(),
885                datatype: ColumnDataType::TimestampMillisecond as _,
886                semantic_type: SemanticType::Timestamp as _,
887                datatype_extension: None,
888                options: None,
889            },
890            ColumnSchema {
891                column_name: greptime_value().to_string(),
892                datatype: ColumnDataType::Float64 as _,
893                semantic_type: SemanticType::Field as _,
894                datatype_extension: None,
895                options: None,
896            },
897        ];
898        let create_table_expr =
899            &mut build_create_table_expr(&table_reference, &default_schema, default_engine())?;
900
901        create_table_expr.engine = METRIC_ENGINE_NAME.to_string();
902        create_table_expr
903            .table_options
904            .insert(PHYSICAL_TABLE_METADATA_KEY.to_string(), "true".to_string());
905
906        // create physical table
907        let res = statement_executor
908            .create_table_inner(
909                create_table_expr,
910                None,
911                ctx.clone(),
912                TriggerReason::AutoCreate,
913            )
914            .await;
915
916        match res {
917            Ok(_) => {
918                info!("Successfully created table {table_reference}",);
919                Ok(())
920            }
921            Err(err) => {
922                error!(err; "Failed to create table {table_reference}");
923                Err(err)
924            }
925        }
926    }
927
928    async fn get_table(
929        &self,
930        catalog: &str,
931        schema: &str,
932        table: &str,
933    ) -> Result<Option<TableRef>> {
934        self.catalog_manager
935            .table(catalog, schema, table, None)
936            .await
937            .context(CatalogSnafu)
938    }
939
940    fn get_create_table_expr_on_demand(
941        &self,
942        req: &RowInsertRequest,
943        create_type: &AutoCreateTableType,
944        ctx: &QueryContextRef,
945        semantic_index: Option<&PerTableSemanticIndex>,
946    ) -> Result<CreateTableExpr> {
947        let schema = ctx.current_schema();
948        let mut table_options = std::collections::HashMap::with_capacity(4);
949        fill_table_options_for_create(&mut table_options, create_type, ctx);
950        apply_per_table_semantic_options(
951            &mut table_options,
952            semantic_index,
953            ctx.current_schema().as_str(),
954            &req.table_name,
955        );
956
957        let engine_name = if let AutoCreateTableType::Logical(_) = create_type {
958            // engine should be metric engine when creating logical tables.
959            METRIC_ENGINE_NAME
960        } else {
961            default_engine()
962        };
963
964        let table_ref = TableReference::full(ctx.current_catalog(), &schema, &req.table_name);
965        // SAFETY: `req.rows` is guaranteed to be `Some` by `handle_row_inserts_with_create_type()`.
966        let request_schema = req.rows.as_ref().unwrap().schema.as_slice();
967        let mut create_table_expr =
968            build_create_table_expr(&table_ref, request_schema, engine_name)?;
969
970        // extension set by the Splunk HEC handler for identity path
971        if ctx.extension(SPLUNK_PK_METADATA_ORDER_KEY).is_some() {
972            reorder_splunk_primary_keys(&mut create_table_expr.primary_keys);
973        }
974
975        info!("Table `{table_ref}` does not exist, try creating table");
976        create_table_expr.table_options.extend(table_options);
977        Ok(create_table_expr)
978    }
979
980    /// Returns an alter table expression if it finds new columns in the request.
981    /// When `accommodate_existing_schema` is false, it always adds columns if not exist.
982    /// When `accommodate_existing_schema` is true, it may modify the input `req` to
983    /// accommodate it with existing schema. See [`create_or_alter_tables_on_demand`](Self::create_or_alter_tables_on_demand)
984    /// for more details.
985    /// When `accommodate_existing_schema` is true and `is_single_value` is true, it also consider fields when modifying the
986    /// input `req`.
987    fn get_alter_table_expr_on_demand(
988        &self,
989        req: &mut RowInsertRequest,
990        table: &TableRef,
991        ctx: &QueryContextRef,
992        accommodate_existing_schema: bool,
993        is_single_value: bool,
994        alter_existing: bool,
995    ) -> Result<Option<AlterTableExpr>> {
996        if !alter_existing {
997            return Ok(None);
998        }
999
1000        let catalog_name = ctx.current_catalog();
1001        let schema_name = ctx.current_schema();
1002        let table_name = table.table_info().name.clone();
1003
1004        // Never auto-alter a system-defined table to fit a write; a request
1005        // with unknown columns fails instead.
1006        if is_ddl_reserved_table(&schema_name, &table_name) {
1007            return Ok(None);
1008        }
1009
1010        let request_schema = req.rows.as_ref().unwrap().schema.as_slice();
1011        let request_field_count = request_schema
1012            .iter()
1013            .filter(|col| col.semantic_type == SemanticType::Field as i32)
1014            .count();
1015        let column_exprs = ColumnExpr::from_column_schemas(request_schema);
1016        let add_columns = expr_helper::extract_add_columns_expr(&table.schema(), column_exprs)?;
1017        let Some(mut add_columns) = add_columns else {
1018            return Ok(None);
1019        };
1020
1021        // If accommodate_existing_schema is true, update request schema for Timestamp/Field columns
1022        if accommodate_existing_schema {
1023            let request_is_native_histogram = request_is_native_histogram(request_schema);
1024            let table_is_native_histogram = table_is_native_histogram(table);
1025            ensure!(
1026                request_is_native_histogram == table_is_native_histogram,
1027                InvalidInsertRequestSnafu {
1028                    reason: format!(
1029                        "Table `{table_name}` cannot mix native histogram and float sample fields"
1030                    ),
1031                }
1032            );
1033            let table_schema = table.schema();
1034            // Find timestamp column name
1035            let ts_col_name = table_schema.timestamp_column().map(|c| c.name.clone());
1036            // Find field column name if there is only one and `is_single_value` is true.
1037            let mut field_col_name = None;
1038            if is_single_value && request_field_count <= 1 {
1039                let mut multiple_field_cols = false;
1040                table.field_columns().for_each(|col| {
1041                    if field_col_name.is_none() {
1042                        field_col_name = Some(col.name.clone());
1043                    } else {
1044                        multiple_field_cols = true;
1045                    }
1046                });
1047                if multiple_field_cols {
1048                    field_col_name = None;
1049                }
1050            }
1051
1052            // Update column name in request schema for Timestamp/Field columns
1053            if let Some(rows) = req.rows.as_mut() {
1054                for col in &mut rows.schema {
1055                    match col.semantic_type {
1056                        x if x == SemanticType::Timestamp as i32 => {
1057                            if let Some(ref ts_name) = ts_col_name
1058                                && col.column_name != *ts_name
1059                            {
1060                                col.column_name = ts_name.clone();
1061                            }
1062                        }
1063                        x if x == SemanticType::Field as i32 => {
1064                            if let Some(ref field_name) = field_col_name
1065                                && col.column_name != *field_name
1066                            {
1067                                col.column_name = field_name.clone();
1068                            }
1069                        }
1070                        _ => {}
1071                    }
1072                }
1073            }
1074
1075            // Only keep columns that are tags or non-single field.
1076            add_columns.add_columns.retain(|col| {
1077                let def = col.column_def.as_ref().unwrap();
1078                def.semantic_type == SemanticType::Tag as i32
1079                    || (def.semantic_type == SemanticType::Field as i32 && field_col_name.is_none())
1080            });
1081
1082            if add_columns.add_columns.is_empty() {
1083                return Ok(None);
1084            }
1085        }
1086
1087        Ok(Some(AlterTableExpr {
1088            catalog_name: catalog_name.to_string(),
1089            schema_name: schema_name.clone(),
1090            table_name: table_name.clone(),
1091            kind: Some(Kind::AddColumns(add_columns)),
1092        }))
1093    }
1094
1095    /// Creates a table with options.
1096    async fn create_physical_table(
1097        &self,
1098        mut create_table_expr: CreateTableExpr,
1099        partitions: Option<Partitions>,
1100        ctx: &QueryContextRef,
1101        statement_executor: &StatementExecutor,
1102    ) -> Result<TableRef> {
1103        {
1104            let table_ref = TableReference::full(
1105                &create_table_expr.catalog_name,
1106                &create_table_expr.schema_name,
1107                &create_table_expr.table_name,
1108            );
1109
1110            info!("Table `{table_ref}` does not exist, try creating table");
1111        }
1112        let res = statement_executor
1113            .create_table_inner(
1114                &mut create_table_expr,
1115                partitions,
1116                ctx.clone(),
1117                TriggerReason::AutoCreate,
1118            )
1119            .await;
1120
1121        let table_ref = TableReference::full(
1122            &create_table_expr.catalog_name,
1123            &create_table_expr.schema_name,
1124            &create_table_expr.table_name,
1125        );
1126
1127        match res {
1128            Ok(table) => {
1129                info!(
1130                    "Successfully created table {} with options: {:?}",
1131                    table_ref, create_table_expr.table_options,
1132                );
1133                Ok(table)
1134            }
1135            Err(err) => {
1136                error!(err; "Failed to create table {}", table_ref);
1137                Err(err)
1138            }
1139        }
1140    }
1141
1142    async fn create_logical_tables(
1143        &self,
1144        create_table_exprs: Vec<CreateTableExpr>,
1145        ctx: &QueryContextRef,
1146        statement_executor: &StatementExecutor,
1147    ) -> Result<Vec<TableRef>> {
1148        let res = statement_executor
1149            .create_logical_tables(&create_table_exprs, ctx.clone(), TriggerReason::AutoCreate)
1150            .await;
1151
1152        match res {
1153            Ok(res) => {
1154                info!("Successfully created logical tables");
1155                Ok(res)
1156            }
1157            Err(err) => {
1158                let failed_tables = create_table_exprs
1159                    .into_iter()
1160                    .map(|expr| {
1161                        format!(
1162                            "{}.{}.{}",
1163                            expr.catalog_name, expr.schema_name, expr.table_name
1164                        )
1165                    })
1166                    .collect::<Vec<_>>();
1167                error!(
1168                    err;
1169                    "Failed to create logical tables {:?}",
1170                    failed_tables
1171                );
1172                Err(err)
1173            }
1174        }
1175    }
1176
1177    pub fn node_manager(&self) -> &NodeManagerRef {
1178        &self.node_manager
1179    }
1180
1181    pub fn partition_manager(&self) -> &PartitionRuleManagerRef {
1182        &self.partition_manager
1183    }
1184
1185    pub fn table_flownode_set_cache(&self) -> &TableFlownodeSetCacheRef {
1186        &self.table_flownode_set_cache
1187    }
1188}
1189
1190fn request_is_native_histogram(request_schema: &[ColumnSchema]) -> bool {
1191    let mut fields = request_schema
1192        .iter()
1193        .filter(|col| col.semantic_type == SemanticType::Field as i32);
1194    let Some(col) = fields.next() else {
1195        return false;
1196    };
1197
1198    fields.next().is_none()
1199        && api::helper::is_column_type_value_eq(
1200            col.datatype,
1201            col.datatype_extension.clone(),
1202            native_histogram_value_type(),
1203        )
1204}
1205
1206fn table_is_native_histogram(table: &TableRef) -> bool {
1207    let mut fields = table.field_columns();
1208    let Some(col) = fields.next() else {
1209        return false;
1210    };
1211
1212    fields.next().is_none() && is_native_histogram_value_type(&col.data_type)
1213}
1214
1215fn validate_column_count_match(requests: &RowInsertRequests) -> Result<()> {
1216    for request in &requests.inserts {
1217        let rows = request.rows.as_ref().unwrap();
1218        let column_count = rows.schema.len();
1219        rows.rows.iter().try_for_each(|r| {
1220            ensure!(
1221                r.values.len() == column_count,
1222                InvalidInsertRequestSnafu {
1223                    reason: format!(
1224                        "column count mismatch, columns: {}, values: {}",
1225                        column_count,
1226                        r.values.len()
1227                    )
1228                }
1229            );
1230            Ok(())
1231        })?;
1232    }
1233    Ok(())
1234}
1235
1236/// Fill table options for a new table by create type.
1237pub fn fill_table_options_for_create(
1238    table_options: &mut std::collections::HashMap<String, String>,
1239    create_type: &AutoCreateTableType,
1240    ctx: &QueryContextRef,
1241) {
1242    for key in VALID_TABLE_OPTION_KEYS {
1243        if let Some(value) = ctx.extension(key) {
1244            table_options.insert(key.to_string(), value.to_string());
1245        }
1246    }
1247
1248    // Semantic keys use their own vocabulary instead of the fixed option list.
1249    for (key, value) in ctx.extensions() {
1250        if is_semantic_option_key(&key) && validate_semantic_option(&key, &value) {
1251            table_options.insert(key, value);
1252        }
1253    }
1254
1255    match create_type {
1256        AutoCreateTableType::Logical(physical_table) => {
1257            table_options.insert(
1258                LOGICAL_TABLE_METADATA_KEY.to_string(),
1259                physical_table.clone(),
1260            );
1261        }
1262        AutoCreateTableType::Physical => {
1263            if let Some(append_mode) = ctx.extension(APPEND_MODE_KEY) {
1264                table_options.insert(APPEND_MODE_KEY.to_string(), append_mode.to_string());
1265            }
1266            if let Some(merge_mode) = ctx.extension(MERGE_MODE_KEY) {
1267                table_options.insert(MERGE_MODE_KEY.to_string(), merge_mode.to_string());
1268            }
1269            if let Some(time_window) = ctx.extension(TWCS_TIME_WINDOW) {
1270                table_options.insert(TWCS_TIME_WINDOW.to_string(), time_window.to_string());
1271                // We need to set the compaction type explicitly.
1272                table_options.insert(
1273                    COMPACTION_TYPE.to_string(),
1274                    COMPACTION_TYPE_TWCS.to_string(),
1275                );
1276            }
1277        }
1278        // Set append_mode to true for log table.
1279        // because log tables should keep rows with the same ts and tags.
1280        AutoCreateTableType::Log => {
1281            table_options.insert(APPEND_MODE_KEY.to_string(), "true".to_string());
1282        }
1283        AutoCreateTableType::LastNonNull => {
1284            if ctx
1285                .extension(APPEND_MODE_KEY)
1286                .is_some_and(|value| value.eq_ignore_ascii_case("true"))
1287            {
1288                table_options.insert(APPEND_MODE_KEY.to_string(), "true".to_string());
1289                table_options.insert(MERGE_MODE_KEY.to_string(), "last_row".to_string());
1290            } else if let Some(merge_mode) = ctx.extension(MERGE_MODE_KEY) {
1291                table_options.insert(MERGE_MODE_KEY.to_string(), merge_mode.to_string());
1292            } else {
1293                table_options.insert(MERGE_MODE_KEY.to_string(), "last_non_null".to_string());
1294            }
1295        }
1296        AutoCreateTableType::Trace { .. } => {
1297            table_options.insert(APPEND_MODE_KEY.to_string(), "true".to_string());
1298        }
1299    }
1300}
1301
1302/// The parsed per-table semantic index: `{schema -> {table -> {key -> value}}}`,
1303/// produced by the OTLP metrics encode path (where one metric can fan out into
1304/// several tables with distinct keys) and the Prometheus remote write v2 path
1305/// (where per-series metadata declares type/unit, and a series may override its
1306/// target schema).
1307pub type PerTableSemanticIndex = BTreeMap<String, BTreeMap<String, BTreeMap<String, String>>>;
1308
1309/// Parses the per-table semantic index off the context extension. Call once per
1310/// create-planning round: a first write creating N tables would otherwise
1311/// re-parse the whole index N times. `None` when the request carries no index
1312/// (logs, traces, Prom RW v1) or it fails to parse.
1313pub fn parse_per_table_semantic_index(ctx: &QueryContextRef) -> Option<PerTableSemanticIndex> {
1314    let raw = ctx.extension(SEMANTIC_PER_TABLE_INDEX_KEY)?;
1315    match serde_json::from_str(raw) {
1316        Ok(index) => Some(index),
1317        Err(_) => {
1318            warn!("failed to parse semantic per-table index, skipping per-table options");
1319            None
1320        }
1321    }
1322}
1323
1324/// Folds the semantic keys of the table being created into `table_options`.
1325///
1326/// Common keys shared by every table in a request travel as plain semantic
1327/// extensions and are handled by [`fill_table_options_for_create`]; this
1328/// carries only the per-table tail and is applied after it, so a per-table
1329/// value (e.g. `declared` quality) wins. Keys are re-checked against the
1330/// vocabulary defensively.
1331pub fn apply_per_table_semantic_options(
1332    table_options: &mut std::collections::HashMap<String, String>,
1333    index: Option<&PerTableSemanticIndex>,
1334    schema: &str,
1335    table_name: &str,
1336) {
1337    let Some(entry) = index
1338        .and_then(|index| index.get(schema))
1339        .and_then(|tables| tables.get(table_name))
1340    else {
1341        return;
1342    };
1343    for (key, value) in entry {
1344        if is_semantic_option_key(key) && validate_semantic_option(key, value) {
1345            table_options.insert(key.clone(), value.clone());
1346        }
1347    }
1348}
1349
1350pub fn build_create_table_expr(
1351    table: &TableReference,
1352    request_schema: &[ColumnSchema],
1353    engine: &str,
1354) -> Result<CreateTableExpr> {
1355    expr_helper::create_table_expr_by_column_schemas(table, request_schema, engine, None)
1356}
1357
1358/// `QueryContext` extension key the Splunk HEC handler sets (to `"true"`) on its identity
1359/// path to request metadata-first primary-key ordering at table creation. It is absent for
1360/// user-supplied pipelines, so their primary-key order is left untouched.
1361pub const SPLUNK_PK_METADATA_ORDER_KEY: &str = "splunk_pk_metadata_order";
1362
1363/// Moves Splunk's metadata tags (`host`, `source`, `sourcetype`) to the front of the
1364/// primary key, keeping the relative order of the remaining tags.
1365fn reorder_splunk_primary_keys(primary_keys: &mut [String]) {
1366    const LEAD: [&str; 3] = ["host", "source", "sourcetype"];
1367    // Stable sort: `LEAD` columns move to the front in `host`/`source`/`sourcetype` order;
1368    // every other column keeps its existing relative position.
1369    primary_keys.sort_by_key(|name| {
1370        LEAD.iter()
1371            .position(|&lead| lead == name.as_str())
1372            .unwrap_or(LEAD.len())
1373    });
1374}
1375
1376/// Result of `create_or_alter_tables_on_demand`.
1377struct CreateAlterTableResult {
1378    /// table ids of ttl=instant tables.
1379    instant_table_ids: HashSet<TableId>,
1380    /// Table Info of the created tables.
1381    table_infos: HashMap<TableId, Arc<TableInfo>>,
1382}
1383
1384struct FlowMirrorTask {
1385    requests: HashMap<Peer, RegionInsertRequests>,
1386    num_rows: usize,
1387}
1388
1389impl FlowMirrorTask {
1390    async fn new(
1391        cache: &TableFlownodeSetCacheRef,
1392        requests: impl Iterator<Item = &RegionInsertRequest>,
1393    ) -> Result<Self> {
1394        let mut src_table_reqs: HashMap<TableId, Option<(Vec<Peer>, RegionInsertRequests)>> =
1395            HashMap::new();
1396        let mut num_rows = 0;
1397
1398        for req in requests {
1399            let table_id = RegionId::from_u64(req.region_id).table_id();
1400            match src_table_reqs.get_mut(&table_id) {
1401                Some(Some((_peers, reqs))) => reqs.requests.push(req.clone()),
1402                // already know this is not source table
1403                Some(None) => continue,
1404                _ => {
1405                    // dedup peers
1406                    let peers = cache
1407                        .get(table_id)
1408                        .await
1409                        .context(RequestInsertsSnafu)?
1410                        .unwrap_or_default()
1411                        .values()
1412                        .cloned()
1413                        .collect::<HashSet<_>>()
1414                        .into_iter()
1415                        .collect::<Vec<_>>();
1416
1417                    if !peers.is_empty() {
1418                        let mut reqs = RegionInsertRequests::default();
1419                        reqs.requests.push(req.clone());
1420                        num_rows += reqs
1421                            .requests
1422                            .iter()
1423                            .map(|r| r.rows.as_ref().unwrap().rows.len())
1424                            .sum::<usize>();
1425                        src_table_reqs.insert(table_id, Some((peers, reqs)));
1426                    } else {
1427                        // insert a empty entry to avoid repeat query
1428                        src_table_reqs.insert(table_id, None);
1429                    }
1430                }
1431            }
1432        }
1433
1434        let mut inserts: HashMap<Peer, RegionInsertRequests> = HashMap::new();
1435
1436        for (_table_id, (peers, reqs)) in src_table_reqs
1437            .into_iter()
1438            .filter_map(|(k, v)| v.map(|v| (k, v)))
1439        {
1440            if peers.len() == 1 {
1441                // fast path, zero copy
1442                inserts
1443                    .entry(peers[0].clone())
1444                    .or_default()
1445                    .requests
1446                    .extend(reqs.requests);
1447                continue;
1448            } else {
1449                // TODO(discord9): need to split requests to multiple flownodes
1450                for flownode in peers {
1451                    inserts
1452                        .entry(flownode.clone())
1453                        .or_default()
1454                        .requests
1455                        .extend(reqs.requests.clone());
1456                }
1457            }
1458        }
1459
1460        Ok(Self {
1461            requests: inserts,
1462            num_rows,
1463        })
1464    }
1465
1466    fn detach(self, node_manager: NodeManagerRef) -> Result<()> {
1467        crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.add(self.num_rows as i64);
1468        for (peer, inserts) in self.requests {
1469            let node_manager = node_manager.clone();
1470            common_runtime::spawn_global(async move {
1471                let result = node_manager
1472                    .flownode(&peer)
1473                    .await
1474                    .handle_inserts(inserts)
1475                    .await
1476                    .context(RequestInsertsSnafu);
1477
1478                match result {
1479                    Ok(resp) => {
1480                        let affected_rows = resp.affected_rows;
1481                        crate::metrics::DIST_MIRROR_ROW_COUNT.inc_by(affected_rows);
1482                        crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.sub(affected_rows as _);
1483                    }
1484                    Err(err) => {
1485                        error!(err; "Failed to insert data into flownode {}", peer);
1486                    }
1487                }
1488            });
1489        }
1490
1491        Ok(())
1492    }
1493}
1494
1495#[cfg(test)]
1496mod tests {
1497    use std::sync::Arc;
1498
1499    use api::helper::ColumnDataTypeWrapper;
1500    use api::v1::helper::{field_column_schema, time_index_column_schema};
1501    use api::v1::{RowInsertRequest, Rows, Value};
1502    use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
1503    use common_meta::cache::new_table_flownode_set_cache;
1504    use common_meta::ddl::test_util::datanode_handler::NaiveDatanodeHandler;
1505    use common_meta::test_util::MockDatanodeManager;
1506    use common_query::native_histogram::NATIVE_HISTOGRAM_FIELD;
1507    use common_query::prelude::{greptime_native_histogram, set_default_prefix};
1508    use datatypes::data_type::ConcreteDataType;
1509    use datatypes::schema::ColumnSchema;
1510    use moka::future::Cache;
1511    use session::context::QueryContext;
1512    use table::TableRef;
1513    use table::dist_table::DummyDataSource;
1514    use table::metadata::{TableInfoBuilder, TableMetaBuilder, TableType};
1515
1516    use super::*;
1517    use crate::tests::{create_partition_rule_manager, prepare_mocked_backend};
1518
1519    fn make_table_ref_with_schema(
1520        ts_name: &str,
1521        field_name: &str,
1522        field_type: ConcreteDataType,
1523    ) -> TableRef {
1524        let schema = datatypes::schema::SchemaBuilder::try_from_columns(vec![
1525            ColumnSchema::new(
1526                ts_name,
1527                ConcreteDataType::timestamp_millisecond_datatype(),
1528                false,
1529            )
1530            .with_time_index(true),
1531            ColumnSchema::new(field_name, field_type, true),
1532        ])
1533        .unwrap()
1534        .build()
1535        .unwrap();
1536        let meta = TableMetaBuilder::empty()
1537            .schema(Arc::new(schema))
1538            .primary_key_indices(vec![])
1539            .value_indices(vec![1])
1540            .engine("mito")
1541            .next_column_id(0)
1542            .options(Default::default())
1543            .created_on(Default::default())
1544            .build()
1545            .unwrap();
1546        let info = Arc::new(
1547            TableInfoBuilder::default()
1548                .table_id(1)
1549                .table_version(0)
1550                .name("test_table")
1551                .schema_name(DEFAULT_SCHEMA_NAME)
1552                .catalog_name(DEFAULT_CATALOG_NAME)
1553                .desc(None)
1554                .table_type(TableType::Base)
1555                .meta(meta)
1556                .build()
1557                .unwrap(),
1558        );
1559        Arc::new(table::Table::new(
1560            info,
1561            table::metadata::FilterPushDownType::Unsupported,
1562            Arc::new(DummyDataSource),
1563        ))
1564    }
1565
1566    #[tokio::test]
1567    async fn test_accommodate_existing_schema_and_reject_kind_changes() {
1568        let ts_name = "my_ts";
1569        let field_name = "my_field";
1570        let table =
1571            make_table_ref_with_schema(ts_name, field_name, ConcreteDataType::float64_datatype());
1572
1573        // The request uses different names for timestamp and field columns
1574        let mut req = RowInsertRequest {
1575            table_name: "test_table".to_string(),
1576            rows: Some(Rows {
1577                schema: vec![
1578                    time_index_column_schema("ts_wrong", ColumnDataType::TimestampMillisecond),
1579                    field_column_schema("field_wrong", ColumnDataType::Float64),
1580                ],
1581                rows: vec![api::v1::Row {
1582                    values: vec![Value::default(), Value::default()],
1583                }],
1584            }),
1585        };
1586        let ctx = Arc::new(QueryContext::with(
1587            DEFAULT_CATALOG_NAME,
1588            DEFAULT_SCHEMA_NAME,
1589        ));
1590
1591        let kv_backend = prepare_mocked_backend().await;
1592        let inserter = Inserter::new(
1593            catalog::memory::MemoryCatalogManager::new(),
1594            create_partition_rule_manager(kv_backend.clone()).await,
1595            Arc::new(MockDatanodeManager::new(NaiveDatanodeHandler)),
1596            Arc::new(new_table_flownode_set_cache(
1597                String::new(),
1598                Cache::new(100),
1599                kv_backend.clone(),
1600            )),
1601            true,
1602        );
1603        // Do not apply an absent-table plan to a table that appeared concurrently.
1604        assert!(
1605            inserter
1606                .get_alter_table_expr_on_demand(&mut req, &table, &ctx, false, false, false)
1607                .unwrap()
1608                .is_none()
1609        );
1610        let alter_expr = inserter
1611            .get_alter_table_expr_on_demand(&mut req, &table, &ctx, true, true, true)
1612            .unwrap();
1613        assert!(alter_expr.is_none());
1614
1615        // The request's schema should have updated names for timestamp and field columns
1616        let req_schema = req.rows.as_ref().unwrap().schema.clone();
1617        assert_eq!(req_schema[0].column_name, ts_name);
1618        assert_eq!(req_schema[1].column_name, field_name);
1619
1620        let (datatype, datatype_extension) =
1621            ColumnDataTypeWrapper::try_from(native_histogram_value_type().clone())
1622                .unwrap()
1623                .into_parts();
1624        let mut histogram_req = RowInsertRequest {
1625            table_name: "test_table".to_string(),
1626            rows: Some(Rows {
1627                schema: vec![
1628                    time_index_column_schema("ts", ColumnDataType::TimestampMillisecond),
1629                    api::v1::ColumnSchema {
1630                        column_name: greptime_native_histogram().to_string(),
1631                        datatype: datatype as i32,
1632                        semantic_type: SemanticType::Field as i32,
1633                        datatype_extension,
1634                        options: None,
1635                    },
1636                ],
1637                rows: vec![],
1638            }),
1639        };
1640        let error = inserter
1641            .get_alter_table_expr_on_demand(&mut histogram_req, &table, &ctx, true, true, true)
1642            .unwrap_err();
1643        assert!(
1644            error
1645                .to_string()
1646                .contains("cannot mix native histogram and float sample fields")
1647        );
1648
1649        let histogram_table = make_table_ref_with_schema(
1650            "ts",
1651            greptime_native_histogram(),
1652            native_histogram_value_type().clone(),
1653        );
1654        let mut sample_req = RowInsertRequest {
1655            table_name: "test_table".to_string(),
1656            rows: Some(Rows {
1657                schema: vec![
1658                    time_index_column_schema("ts", ColumnDataType::TimestampMillisecond),
1659                    field_column_schema(greptime_value(), ColumnDataType::Float64),
1660                ],
1661                rows: vec![],
1662            }),
1663        };
1664        let error = inserter
1665            .get_alter_table_expr_on_demand(
1666                &mut sample_req,
1667                &histogram_table,
1668                &ctx,
1669                true,
1670                true,
1671                true,
1672            )
1673            .unwrap_err();
1674        assert!(
1675            error
1676                .to_string()
1677                .contains("cannot mix native histogram and float sample fields")
1678        );
1679    }
1680
1681    #[test]
1682    fn test_native_histogram_detection_survives_prefix_change() {
1683        set_default_prefix(Some("custom")).unwrap();
1684        let table = make_table_ref_with_schema(
1685            "custom_timestamp",
1686            NATIVE_HISTOGRAM_FIELD,
1687            native_histogram_value_type().clone(),
1688        );
1689        let (datatype, datatype_extension) =
1690            ColumnDataTypeWrapper::try_from(native_histogram_value_type().clone())
1691                .unwrap()
1692                .into_parts();
1693        let request_schema = [api::v1::ColumnSchema {
1694            column_name: greptime_native_histogram().to_string(),
1695            datatype: datatype as i32,
1696            semantic_type: SemanticType::Field as i32,
1697            datatype_extension,
1698            options: None,
1699        }];
1700
1701        assert!(request_is_native_histogram(&request_schema));
1702        assert!(table_is_native_histogram(&table));
1703    }
1704
1705    #[test]
1706    fn test_last_non_null_create_options_preserve_default_without_append_mode() {
1707        let ctx = Arc::new(QueryContext::with(
1708            DEFAULT_CATALOG_NAME,
1709            DEFAULT_SCHEMA_NAME,
1710        ));
1711        let mut table_options = Default::default();
1712
1713        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::LastNonNull, &ctx);
1714
1715        assert_eq!(
1716            Some("last_non_null"),
1717            table_options.get(MERGE_MODE_KEY).map(String::as_str)
1718        );
1719        assert!(!table_options.contains_key(APPEND_MODE_KEY));
1720    }
1721
1722    #[test]
1723    fn test_fill_table_options_copies_semantic_extensions() {
1724        use table::requests::{
1725            SEMANTIC_METRIC_TYPE, SEMANTIC_PER_TABLE_INDEX_KEY, SEMANTIC_SIGNAL_TYPE,
1726            SEMANTIC_SOURCE, SEMANTIC_SOURCE_VERSION, SIGNAL_TYPE_METRIC, SOURCE_OPENTELEMETRY,
1727        };
1728
1729        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
1730        ctx.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_METRIC);
1731        ctx.set_extension(SEMANTIC_SOURCE, SOURCE_OPENTELEMETRY);
1732        ctx.set_extension(SEMANTIC_SOURCE_VERSION, "2.0");
1733        ctx.set_extension(SEMANTIC_METRIC_TYPE, "bogus");
1734        // The internal transport key must NOT be copied into table options.
1735        ctx.set_extension(SEMANTIC_PER_TABLE_INDEX_KEY, "{}");
1736        let ctx = Arc::new(ctx);
1737        let mut table_options = Default::default();
1738
1739        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::Physical, &ctx);
1740
1741        assert_eq!(
1742            Some(SIGNAL_TYPE_METRIC),
1743            table_options.get(SEMANTIC_SIGNAL_TYPE).map(String::as_str)
1744        );
1745        assert_eq!(
1746            Some(SOURCE_OPENTELEMETRY),
1747            table_options.get(SEMANTIC_SOURCE).map(String::as_str)
1748        );
1749        assert_eq!(
1750            Some("2.0"),
1751            table_options
1752                .get(SEMANTIC_SOURCE_VERSION)
1753                .map(String::as_str)
1754        );
1755        assert!(!table_options.contains_key(SEMANTIC_METRIC_TYPE));
1756        assert!(!table_options.contains_key(SEMANTIC_PER_TABLE_INDEX_KEY));
1757    }
1758
1759    #[test]
1760    fn test_apply_per_table_semantic_options() {
1761        use table::requests::{
1762            SEMANTIC_METRIC_TYPE, SEMANTIC_METRIC_UNIT, SEMANTIC_PER_TABLE_INDEX_KEY,
1763        };
1764
1765        let index = format!(
1766            r#"{{
1767            "{DEFAULT_SCHEMA_NAME}": {{
1768                "http_requests_total": {{
1769                    "greptime.semantic.metric.type": "counter",
1770                    "greptime.semantic.metric.unit": "By",
1771                    "greptime.semantic.metric.type_BOGUS": "x"
1772                }},
1773                "other_table": {{
1774                    "greptime.semantic.metric.type": "gauge"
1775                }}
1776            }},
1777            "other_schema": {{
1778                "http_requests_total": {{
1779                    "greptime.semantic.metric.type": "gauge"
1780                }}
1781            }}
1782        }}"#
1783        );
1784        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
1785        ctx.set_extension(SEMANTIC_PER_TABLE_INDEX_KEY, index);
1786        let ctx = Arc::new(ctx);
1787
1788        let index = parse_per_table_semantic_index(&ctx);
1789        assert!(index.is_some());
1790        let index = index.as_ref();
1791
1792        let mut table_options = std::collections::HashMap::new();
1793        apply_per_table_semantic_options(
1794            &mut table_options,
1795            index,
1796            DEFAULT_SCHEMA_NAME,
1797            "http_requests_total",
1798        );
1799        // The write schema's entry applies — not other_schema's `gauge`.
1800        assert_eq!(
1801            table_options.get(SEMANTIC_METRIC_TYPE).map(String::as_str),
1802            Some("counter")
1803        );
1804        assert_eq!(
1805            table_options.get(SEMANTIC_METRIC_UNIT).map(String::as_str),
1806            Some("By")
1807        );
1808        // The unknown key is rejected by the vocabulary check; other tables' keys
1809        // never appear.
1810        assert!(!table_options.contains_key("greptime.semantic.metric.type_BOGUS"));
1811        assert_eq!(table_options.len(), 2);
1812
1813        let mut empty = std::collections::HashMap::new();
1814        apply_per_table_semantic_options(&mut empty, index, DEFAULT_SCHEMA_NAME, "not_in_index");
1815        assert!(empty.is_empty());
1816
1817        // A schema with no entry is a no-op even when the table name matches
1818        // elsewhere.
1819        let mut opts = std::collections::HashMap::new();
1820        apply_per_table_semantic_options(
1821            &mut opts,
1822            index,
1823            "schema_without_entry",
1824            "http_requests_total",
1825        );
1826        assert!(opts.is_empty());
1827
1828        // No extension at all parses to no index (e.g. logs / Prom RW v1).
1829        let bare = Arc::new(QueryContext::with(
1830            DEFAULT_CATALOG_NAME,
1831            DEFAULT_SCHEMA_NAME,
1832        ));
1833        assert!(parse_per_table_semantic_index(&bare).is_none());
1834        let mut opts = std::collections::HashMap::new();
1835        apply_per_table_semantic_options(
1836            &mut opts,
1837            None,
1838            DEFAULT_SCHEMA_NAME,
1839            "http_requests_total",
1840        );
1841        assert!(opts.is_empty());
1842    }
1843
1844    #[test]
1845    fn test_last_non_null_create_options_preserve_default_with_append_mode_false() {
1846        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
1847        ctx.set_extension(APPEND_MODE_KEY, "false");
1848        let ctx = Arc::new(ctx);
1849        let mut table_options = Default::default();
1850
1851        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::LastNonNull, &ctx);
1852
1853        assert!(!table_options.contains_key(APPEND_MODE_KEY));
1854        assert_eq!(
1855            Some("last_non_null"),
1856            table_options.get(MERGE_MODE_KEY).map(String::as_str)
1857        );
1858    }
1859
1860    #[test]
1861    fn test_last_non_null_create_options_use_configured_merge_mode() {
1862        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
1863        ctx.set_extension(MERGE_MODE_KEY, "last_row");
1864        let ctx = Arc::new(ctx);
1865        let mut table_options = Default::default();
1866
1867        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::LastNonNull, &ctx);
1868
1869        assert_eq!(
1870            Some("last_row"),
1871            table_options.get(MERGE_MODE_KEY).map(String::as_str)
1872        );
1873        assert!(!table_options.contains_key(APPEND_MODE_KEY));
1874    }
1875
1876    #[test]
1877    fn test_last_non_null_create_options_use_last_row_with_append_mode_true() {
1878        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
1879        ctx.set_extension(APPEND_MODE_KEY, "true");
1880        let ctx = Arc::new(ctx);
1881        let mut table_options = Default::default();
1882
1883        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::LastNonNull, &ctx);
1884
1885        assert_eq!(
1886            Some("true"),
1887            table_options.get(APPEND_MODE_KEY).map(String::as_str)
1888        );
1889        assert_eq!(
1890            Some("last_row"),
1891            table_options.get(MERGE_MODE_KEY).map(String::as_str)
1892        );
1893    }
1894}