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