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::sync::Arc;
16
17use ahash::{HashMap, HashMapExt, HashSet, HashSetExt};
18use api::v1::alter_table_expr::Kind;
19use api::v1::column_def::options_from_skipping;
20use api::v1::region::{
21    InsertRequest as RegionInsertRequest, InsertRequests as RegionInsertRequests,
22    RegionRequestHeader,
23};
24use api::v1::{
25    AlterTableExpr, ColumnDataType, ColumnSchema, CreateTableExpr, InsertRequests,
26    RowInsertRequest, RowInsertRequests, SemanticType,
27};
28use catalog::CatalogManagerRef;
29use client::{OutputData, OutputMeta};
30use common_catalog::consts::{
31    PARENT_SPAN_ID_COLUMN, SERVICE_NAME_COLUMN, TRACE_ID_COLUMN, TRACE_TABLE_NAME,
32    TRACE_TABLE_NAME_SESSION_KEY, default_engine, trace_operations_table_name,
33    trace_services_table_name,
34};
35use common_grpc_expr::util::ColumnExpr;
36use common_meta::cache::TableFlownodeSetCacheRef;
37use common_meta::node_manager::{AffectedRows, NodeManagerRef};
38use common_meta::peer::Peer;
39use common_query::Output;
40use common_query::prelude::{greptime_timestamp, greptime_value};
41use common_telemetry::tracing_context::TracingContext;
42use common_telemetry::{error, info, warn};
43use datatypes::schema::SkippingIndexOptions;
44use futures_util::future;
45use meter_macros::write_meter;
46use partition::manager::PartitionRuleManagerRef;
47use session::context::QueryContextRef;
48use snafu::ResultExt;
49use snafu::prelude::*;
50use sql::partition::partition_rule_for_hexstring;
51use sql::statements::create::Partitions;
52use sql::statements::insert::Insert;
53use store_api::metric_engine_consts::{
54    LOGICAL_TABLE_METADATA_KEY, METRIC_ENGINE_NAME, PHYSICAL_TABLE_METADATA_KEY,
55};
56use store_api::mito_engine_options::{
57    APPEND_MODE_KEY, COMPACTION_TYPE, COMPACTION_TYPE_TWCS, MERGE_MODE_KEY, TTL_KEY,
58    TWCS_TIME_WINDOW,
59};
60use store_api::storage::{RegionId, TableId};
61use table::TableRef;
62use table::metadata::TableInfo;
63use table::requests::{
64    AUTO_CREATE_TABLE_KEY, InsertRequest as TableInsertRequest, TABLE_DATA_MODEL,
65    TABLE_DATA_MODEL_TRACE_V1, TRACE_TABLE_PARTITIONS_HINT_KEY, VALID_TABLE_OPTION_KEYS,
66    is_semantic_option_key,
67};
68use table::table_reference::TableReference;
69
70use crate::error::{
71    CatalogSnafu, ColumnOptionsSnafu, CreatePartitionRulesSnafu, FindRegionLeaderSnafu,
72    InvalidInsertRequestSnafu, JoinTaskSnafu, RequestInsertsSnafu, Result, TableNotFoundSnafu,
73};
74use crate::expr_helper;
75use crate::region_req_factory::RegionRequestFactory;
76use crate::req_convert::common::preprocess_row_insert_requests;
77use crate::req_convert::insert::{
78    ColumnToRow, RowToRegion, StatementToRegion, TableToRegion, fill_reqs_with_impure_default,
79};
80use crate::statement::StatementExecutor;
81
82pub struct Inserter {
83    catalog_manager: CatalogManagerRef,
84    pub(crate) partition_manager: PartitionRuleManagerRef,
85    pub(crate) node_manager: NodeManagerRef,
86    pub(crate) table_flownode_set_cache: TableFlownodeSetCacheRef,
87    /// Server-side upper bound for auto table creation on write.
88    /// When `false`, missing tables are never auto-created regardless of the
89    /// per-request `auto_create_table` hint. When `true`, the hint still applies.
90    auto_create_table: bool,
91}
92
93pub type InserterRef = Arc<Inserter>;
94
95/// Hint for the table type to create automatically.
96#[derive(Clone)]
97pub enum AutoCreateTableType {
98    /// A logical table with the physical table name.
99    Logical(String),
100    /// A physical table.
101    Physical,
102    /// A log table which is append-only.
103    Log,
104    /// A table that merges rows by `last_non_null` strategy.
105    LastNonNull,
106    /// Create table that build index and default partition rules on trace_id
107    Trace,
108}
109
110impl AutoCreateTableType {
111    pub fn as_str(&self) -> &'static str {
112        match self {
113            AutoCreateTableType::Logical(_) => "logical",
114            AutoCreateTableType::Physical => "physical",
115            AutoCreateTableType::Log => "log",
116            AutoCreateTableType::LastNonNull => "last_non_null",
117            AutoCreateTableType::Trace => "trace",
118        }
119    }
120}
121
122/// Split insert requests into normal and instant requests.
123///
124/// Where instant requests are requests with ttl=instant,
125/// and normal requests are requests with ttl set to other values.
126///
127/// This is used to split requests for different processing.
128#[derive(Clone)]
129pub struct InstantAndNormalInsertRequests {
130    /// Requests with normal ttl.
131    pub normal_requests: RegionInsertRequests,
132    /// Requests with ttl=instant.
133    /// Will be discarded immediately at frontend, wouldn't even insert into memtable, and only sent to flow node if needed.
134    pub instant_requests: RegionInsertRequests,
135}
136
137impl Inserter {
138    pub fn new(
139        catalog_manager: CatalogManagerRef,
140        partition_manager: PartitionRuleManagerRef,
141        node_manager: NodeManagerRef,
142        table_flownode_set_cache: TableFlownodeSetCacheRef,
143        auto_create_table: bool,
144    ) -> Self {
145        Self {
146            catalog_manager,
147            partition_manager,
148            node_manager,
149            table_flownode_set_cache,
150            auto_create_table,
151        }
152    }
153
154    pub async fn handle_column_inserts(
155        &self,
156        requests: InsertRequests,
157        ctx: QueryContextRef,
158        statement_executor: &StatementExecutor,
159    ) -> Result<Output> {
160        let row_inserts = ColumnToRow::convert(requests)?;
161        self.handle_row_inserts(row_inserts, ctx, statement_executor, false, false)
162            .await
163    }
164
165    /// Handles row inserts request and creates a physical table on demand.
166    pub async fn handle_row_inserts(
167        &self,
168        mut requests: RowInsertRequests,
169        ctx: QueryContextRef,
170        statement_executor: &StatementExecutor,
171        accommodate_existing_schema: bool,
172        is_single_value: bool,
173    ) -> Result<Output> {
174        preprocess_row_insert_requests(&mut requests.inserts)?;
175        self.handle_row_inserts_with_create_type(
176            requests,
177            ctx,
178            statement_executor,
179            AutoCreateTableType::Physical,
180            accommodate_existing_schema,
181            is_single_value,
182        )
183        .await
184    }
185
186    /// Handles row inserts request and creates a log table on demand.
187    pub async fn handle_log_inserts(
188        &self,
189        requests: RowInsertRequests,
190        ctx: QueryContextRef,
191        statement_executor: &StatementExecutor,
192    ) -> Result<Output> {
193        self.handle_row_inserts_with_create_type(
194            requests,
195            ctx,
196            statement_executor,
197            AutoCreateTableType::Log,
198            false,
199            false,
200        )
201        .await
202    }
203
204    pub async fn handle_trace_inserts(
205        &self,
206        requests: RowInsertRequests,
207        ctx: QueryContextRef,
208        statement_executor: &StatementExecutor,
209    ) -> Result<Output> {
210        self.handle_row_inserts_with_create_type(
211            requests,
212            ctx,
213            statement_executor,
214            AutoCreateTableType::Trace,
215            false,
216            false,
217        )
218        .await
219    }
220
221    /// Handles row inserts request and creates a table with `last_non_null` merge mode on demand.
222    pub async fn handle_last_non_null_inserts(
223        &self,
224        requests: RowInsertRequests,
225        ctx: QueryContextRef,
226        statement_executor: &StatementExecutor,
227        accommodate_existing_schema: bool,
228        is_single_value: bool,
229    ) -> Result<Output> {
230        self.handle_row_inserts_with_create_type(
231            requests,
232            ctx,
233            statement_executor,
234            AutoCreateTableType::LastNonNull,
235            accommodate_existing_schema,
236            is_single_value,
237        )
238        .await
239    }
240
241    /// Handles row inserts request with specified [AutoCreateTableType].
242    async fn handle_row_inserts_with_create_type(
243        &self,
244        mut requests: RowInsertRequests,
245        ctx: QueryContextRef,
246        statement_executor: &StatementExecutor,
247        create_type: AutoCreateTableType,
248        accommodate_existing_schema: bool,
249        is_single_value: bool,
250    ) -> Result<Output> {
251        // remove empty requests
252        requests.inserts.retain(|req| {
253            req.rows
254                .as_ref()
255                .map(|r| !r.rows.is_empty())
256                .unwrap_or_default()
257        });
258        validate_column_count_match(&requests)?;
259
260        let CreateAlterTableResult {
261            instant_table_ids,
262            table_infos,
263        } = self
264            .create_or_alter_tables_on_demand(
265                &mut requests,
266                &ctx,
267                create_type,
268                statement_executor,
269                accommodate_existing_schema,
270                is_single_value,
271            )
272            .await?;
273
274        let name_to_info = table_infos
275            .values()
276            .map(|info| (info.name.clone(), info.clone()))
277            .collect::<HashMap<_, _>>();
278        let inserts = RowToRegion::new(
279            name_to_info,
280            instant_table_ids,
281            self.partition_manager.as_ref(),
282        )
283        .convert(requests)
284        .await?;
285
286        self.do_request(inserts, &table_infos, &ctx).await
287    }
288
289    /// Handles row inserts request with metric engine.
290    pub async fn handle_metric_row_inserts(
291        &self,
292        mut requests: RowInsertRequests,
293        ctx: QueryContextRef,
294        statement_executor: &StatementExecutor,
295        physical_table: String,
296    ) -> Result<Output> {
297        // remove empty requests
298        requests.inserts.retain(|req| {
299            req.rows
300                .as_ref()
301                .map(|r| !r.rows.is_empty())
302                .unwrap_or_default()
303        });
304        validate_column_count_match(&requests)?;
305
306        // check and create physical table
307        self.create_physical_table_on_demand(&ctx, physical_table.clone(), statement_executor)
308            .await?;
309
310        // check and create logical tables
311        let CreateAlterTableResult {
312            instant_table_ids,
313            table_infos,
314        } = self
315            .create_or_alter_tables_on_demand(
316                &mut requests,
317                &ctx,
318                AutoCreateTableType::Logical(physical_table.clone()),
319                statement_executor,
320                true,
321                true,
322            )
323            .await?;
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(name_to_info, instant_table_ids, &self.partition_manager)
329            .convert(requests)
330            .await?;
331
332        self.do_request(inserts, &table_infos, &ctx).await
333    }
334
335    pub async fn handle_table_insert(
336        &self,
337        request: TableInsertRequest,
338        ctx: QueryContextRef,
339    ) -> Result<Output> {
340        let catalog = request.catalog_name.as_str();
341        let schema = request.schema_name.as_str();
342        let table_name = request.table_name.as_str();
343        let table = self.get_table(catalog, schema, table_name).await?;
344        let table = table.with_context(|| TableNotFoundSnafu {
345            table_name: common_catalog::format_full_table_name(catalog, schema, table_name),
346        })?;
347        let table_info = table.table_info();
348
349        let inserts = TableToRegion::new(&table_info, &self.partition_manager)
350            .convert(request)
351            .await?;
352
353        let table_infos = HashMap::from_iter([(table_info.table_id(), table_info.clone())]);
354
355        self.do_request(inserts, &table_infos, &ctx).await
356    }
357
358    pub async fn handle_statement_insert(
359        &self,
360        insert: &Insert,
361        ctx: &QueryContextRef,
362    ) -> Result<Output> {
363        let (inserts, table_info) =
364            StatementToRegion::new(self.catalog_manager.as_ref(), &self.partition_manager, ctx)
365                .convert(insert, ctx)
366                .await?;
367
368        let table_infos = HashMap::from_iter([(table_info.table_id(), table_info.clone())]);
369
370        self.do_request(inserts, &table_infos, ctx).await
371    }
372}
373
374impl Inserter {
375    async fn do_request(
376        &self,
377        requests: InstantAndNormalInsertRequests,
378        table_infos: &HashMap<TableId, Arc<TableInfo>>,
379        ctx: &QueryContextRef,
380    ) -> Result<Output> {
381        // Fill impure default values in the request
382        let requests = fill_reqs_with_impure_default(table_infos, requests)?;
383
384        let write_cost = write_meter!(
385            ctx.current_catalog(),
386            ctx.current_schema(),
387            requests,
388            ctx.channel() as u8
389        );
390        let request_factory = RegionRequestFactory::new(RegionRequestHeader {
391            tracing_context: TracingContext::from_current_span().to_w3c(),
392            dbname: ctx.get_db_string(),
393            ..Default::default()
394        });
395
396        let InstantAndNormalInsertRequests {
397            normal_requests,
398            instant_requests,
399        } = requests;
400
401        // Mirror requests for source table to flownode asynchronously
402        let flow_mirror_task = FlowMirrorTask::new(
403            &self.table_flownode_set_cache,
404            normal_requests
405                .requests
406                .iter()
407                .chain(instant_requests.requests.iter()),
408        )
409        .await?;
410        flow_mirror_task.detach(self.node_manager.clone())?;
411
412        // Write requests to datanode and wait for response
413        let write_tasks = self
414            .group_requests_by_peer(normal_requests)
415            .await?
416            .into_iter()
417            .map(|(peer, inserts)| {
418                let node_manager = self.node_manager.clone();
419                let request = request_factory.build_insert(inserts);
420                common_runtime::spawn_global(async move {
421                    node_manager
422                        .datanode(&peer)
423                        .await
424                        .handle(request)
425                        .await
426                        .context(RequestInsertsSnafu)
427                })
428            });
429        let results = future::try_join_all(write_tasks)
430            .await
431            .context(JoinTaskSnafu)?;
432        let affected_rows = results
433            .into_iter()
434            .map(|resp| resp.map(|r| r.affected_rows))
435            .sum::<Result<AffectedRows>>()?;
436        crate::metrics::DIST_INGEST_ROW_COUNT
437            .with_label_values(&[ctx.get_db_string().as_str()])
438            .inc_by(affected_rows as u64);
439        Ok(Output::new(
440            OutputData::AffectedRows(affected_rows),
441            OutputMeta::new_with_cost(write_cost as _),
442        ))
443    }
444
445    async fn group_requests_by_peer(
446        &self,
447        requests: RegionInsertRequests,
448    ) -> Result<HashMap<Peer, RegionInsertRequests>> {
449        // group by region ids first to reduce repeatedly call `find_region_leader`
450        // TODO(discord9): determine if a addition clone is worth it
451        let mut requests_per_region: HashMap<RegionId, RegionInsertRequests> = HashMap::new();
452        for req in requests.requests {
453            let region_id = RegionId::from_u64(req.region_id);
454            requests_per_region
455                .entry(region_id)
456                .or_default()
457                .requests
458                .push(req);
459        }
460
461        let mut inserts: HashMap<Peer, RegionInsertRequests> = HashMap::new();
462
463        for (region_id, reqs) in requests_per_region {
464            let peer = self
465                .partition_manager
466                .find_region_leader(region_id)
467                .await
468                .context(FindRegionLeaderSnafu)?;
469            inserts
470                .entry(peer)
471                .or_default()
472                .requests
473                .extend(reqs.requests);
474        }
475
476        Ok(inserts)
477    }
478
479    /// Returns `None` if auto table creation is allowed, or `Some(reason)` if
480    /// disabled by either the global config or the request hint. The reason tells
481    /// which one, for a clearer error.
482    fn auto_create_disabled_reason(&self, ctx: &QueryContextRef) -> Result<Option<&'static str>> {
483        let auto_create_table_hint = ctx
484            .extension(AUTO_CREATE_TABLE_KEY)
485            .map(|v| v.parse::<bool>())
486            .transpose()
487            .map_err(|_| {
488                InvalidInsertRequestSnafu {
489                    reason: "`auto_create_table` hint must be a boolean",
490                }
491                .build()
492            })?
493            .unwrap_or(true);
494        Ok(if !self.auto_create_table {
495            Some("auto-create table is disabled by frontend config")
496        } else if !auto_create_table_hint {
497            Some("`auto_create_table` hint is disabled")
498        } else {
499            None
500        })
501    }
502
503    /// Creates or alter tables on demand:
504    /// - if table does not exist, create table by inferred CreateExpr
505    /// - if table exist, check if schema matches. If any new column found, alter table by inferred `AlterExpr`
506    ///
507    /// Returns a mapping from table name to table id, where table name is the table name involved in the requests.
508    /// This mapping is used in the conversion of RowToRegion.
509    ///
510    /// `accommodate_existing_schema` is used to determine if the existing schema should override the new schema.
511    /// It only works for TIME_INDEX and single VALUE columns. This is for the case where the user creates a table with
512    /// custom schema, and then inserts data with endpoints that have default schema setting, like prometheus
513    /// remote write. This will modify the `RowInsertRequests` in place.
514    /// `is_single_value` indicates whether the default schema only contains single value column so we can accommodate it.
515    async fn create_or_alter_tables_on_demand(
516        &self,
517        requests: &mut RowInsertRequests,
518        ctx: &QueryContextRef,
519        auto_create_table_type: AutoCreateTableType,
520        statement_executor: &StatementExecutor,
521        accommodate_existing_schema: bool,
522        is_single_value: bool,
523    ) -> Result<CreateAlterTableResult> {
524        let _timer = crate::metrics::CREATE_ALTER_ON_DEMAND
525            .with_label_values(&[auto_create_table_type.as_str()])
526            .start_timer();
527
528        let catalog = ctx.current_catalog();
529        let schema = ctx.current_schema();
530
531        let mut table_infos = HashMap::new();
532        if let Some(disabled_reason) = self.auto_create_disabled_reason(ctx)? {
533            let mut instant_table_ids = HashSet::new();
534            for req in &requests.inserts {
535                let table = self
536                    .get_table(catalog, &schema, &req.table_name)
537                    .await?
538                    .context(InvalidInsertRequestSnafu {
539                        reason: format!(
540                            "Table `{}` does not exist, and {}",
541                            req.table_name, disabled_reason
542                        ),
543                    })?;
544                let table_info = table.table_info();
545                if table_info.is_ttl_instant_table() {
546                    instant_table_ids.insert(table_info.table_id());
547                }
548                table_infos.insert(table_info.table_id(), table.table_info());
549            }
550            let ret = CreateAlterTableResult {
551                instant_table_ids,
552                table_infos,
553            };
554            return Ok(ret);
555        }
556
557        let mut create_tables = vec![];
558        let mut alter_tables = vec![];
559        let mut need_refresh_table_infos = HashSet::new();
560        let mut instant_table_ids = HashSet::new();
561
562        for req in &mut requests.inserts {
563            match self.get_table(catalog, &schema, &req.table_name).await? {
564                Some(table) => {
565                    let table_info = table.table_info();
566                    if table_info.is_ttl_instant_table() {
567                        instant_table_ids.insert(table_info.table_id());
568                    }
569                    if let Some(alter_expr) = self.get_alter_table_expr_on_demand(
570                        req,
571                        &table,
572                        ctx,
573                        accommodate_existing_schema,
574                        is_single_value,
575                    )? {
576                        alter_tables.push(alter_expr);
577                        need_refresh_table_infos.insert((
578                            catalog.to_string(),
579                            schema.clone(),
580                            req.table_name.clone(),
581                        ));
582                    } else {
583                        table_infos.insert(table_info.table_id(), table.table_info());
584                    }
585                }
586                None => {
587                    let create_expr =
588                        self.get_create_table_expr_on_demand(req, &auto_create_table_type, ctx)?;
589                    create_tables.push(create_expr);
590                }
591            }
592        }
593
594        match auto_create_table_type {
595            AutoCreateTableType::Logical(_) => {
596                if !create_tables.is_empty() {
597                    // Creates logical tables in batch.
598                    let tables = self
599                        .create_logical_tables(create_tables, ctx, statement_executor)
600                        .await?;
601
602                    for table in tables {
603                        let table_info = table.table_info();
604                        if table_info.is_ttl_instant_table() {
605                            instant_table_ids.insert(table_info.table_id());
606                        }
607                        table_infos.insert(table_info.table_id(), table.table_info());
608                    }
609                }
610                if !alter_tables.is_empty() {
611                    // Alter logical tables in batch.
612                    statement_executor
613                        .alter_logical_tables(alter_tables, ctx.clone())
614                        .await?;
615                }
616            }
617            AutoCreateTableType::Physical
618            | AutoCreateTableType::Log
619            | AutoCreateTableType::LastNonNull => {
620                // note that auto create table shouldn't be ttl instant table
621                // for it's a very unexpected behavior and should be set by user explicitly
622                for create_table in create_tables {
623                    let table = self
624                        .create_physical_table(create_table, None, ctx, statement_executor)
625                        .await?;
626                    let table_info = table.table_info();
627                    if table_info.is_ttl_instant_table() {
628                        instant_table_ids.insert(table_info.table_id());
629                    }
630                    table_infos.insert(table_info.table_id(), table.table_info());
631                }
632                for alter_expr in alter_tables.into_iter() {
633                    statement_executor
634                        .alter_table_inner(alter_expr, ctx.clone())
635                        .await?;
636                }
637            }
638
639            AutoCreateTableType::Trace => {
640                let trace_table_name = ctx
641                    .extension(TRACE_TABLE_NAME_SESSION_KEY)
642                    .unwrap_or(TRACE_TABLE_NAME);
643
644                let trace_table_partitions = if let Some(trace_table_partitions) =
645                    ctx.extension(TRACE_TABLE_PARTITIONS_HINT_KEY)
646                {
647                    let p = trace_table_partitions.parse::<u32>().map_err(|_| {
648                        InvalidInsertRequestSnafu {
649                            reason: format!(
650                                "Failed to parse trace_table_partitions: {}",
651                                trace_table_partitions
652                            ),
653                        }
654                        .build()
655                    })?;
656                    Some(p)
657                } else {
658                    None
659                };
660
661                // note that auto create table shouldn't be ttl instant table
662                // for it's a very unexpected behavior and should be set by user explicitly
663                for mut create_table in create_tables {
664                    if create_table.table_name == trace_services_table_name(trace_table_name)
665                        || create_table.table_name == trace_operations_table_name(trace_table_name)
666                    {
667                        // Disable append mode for auxiliary tables (services/operations) since they require upsert behavior.
668                        create_table
669                            .table_options
670                            .insert(APPEND_MODE_KEY.to_string(), "false".to_string());
671                        // Remove `ttl` key from table options if it exists
672                        create_table.table_options.remove(TTL_KEY);
673
674                        let table = self
675                            .create_physical_table(create_table, None, ctx, statement_executor)
676                            .await?;
677                        let table_info = table.table_info();
678                        if table_info.is_ttl_instant_table() {
679                            instant_table_ids.insert(table_info.table_id());
680                        }
681                        table_infos.insert(table_info.table_id(), table.table_info());
682                    } else {
683                        // prebuilt partition rules for uuid data: see the function
684                        // for more information
685                        let partitions = if matches!(trace_table_partitions, Some(0) | Some(1)) {
686                            // disable partitions
687                            None
688                        } else {
689                            let p = partition_rule_for_hexstring(
690                                TRACE_ID_COLUMN,
691                                trace_table_partitions,
692                            )
693                            .context(CreatePartitionRulesSnafu)?;
694                            Some(p)
695                        };
696
697                        // add skip index to
698                        // - trace_id: when searching by trace id
699                        // - parent_span_id: when searching root span
700                        // - span_name: when searching certain types of span
701                        let index_columns =
702                            [TRACE_ID_COLUMN, PARENT_SPAN_ID_COLUMN, SERVICE_NAME_COLUMN];
703                        for index_column in index_columns {
704                            if let Some(col) = create_table
705                                .column_defs
706                                .iter_mut()
707                                .find(|c| c.name == index_column)
708                            {
709                                col.options =
710                                    options_from_skipping(&SkippingIndexOptions::default())
711                                        .context(ColumnOptionsSnafu)?;
712                            } else {
713                                warn!(
714                                    "Column {} not found when creating index for trace table: {}.",
715                                    index_column, create_table.table_name
716                                );
717                            }
718                        }
719
720                        // use table_options to mark table model version
721                        create_table.table_options.insert(
722                            TABLE_DATA_MODEL.to_string(),
723                            TABLE_DATA_MODEL_TRACE_V1.to_string(),
724                        );
725
726                        let table = self
727                            .create_physical_table(
728                                create_table,
729                                partitions,
730                                ctx,
731                                statement_executor,
732                            )
733                            .await?;
734                        let table_info = table.table_info();
735                        if table_info.is_ttl_instant_table() {
736                            instant_table_ids.insert(table_info.table_id());
737                        }
738                        table_infos.insert(table_info.table_id(), table.table_info());
739                    }
740                }
741                for alter_expr in alter_tables.into_iter() {
742                    statement_executor
743                        .alter_table_inner(alter_expr, ctx.clone())
744                        .await?;
745                }
746            }
747        }
748
749        // refresh table infos for altered tables
750        for (catalog, schema, table_name) in need_refresh_table_infos {
751            let table = self
752                .get_table(&catalog, &schema, &table_name)
753                .await?
754                .context(TableNotFoundSnafu {
755                    table_name: common_catalog::format_full_table_name(
756                        &catalog,
757                        &schema,
758                        &table_name,
759                    ),
760                })?;
761            let table_info = table.table_info();
762            table_infos.insert(table_info.table_id(), table.table_info());
763        }
764
765        Ok(CreateAlterTableResult {
766            instant_table_ids,
767            table_infos,
768        })
769    }
770
771    async fn create_physical_table_on_demand(
772        &self,
773        ctx: &QueryContextRef,
774        physical_table: String,
775        statement_executor: &StatementExecutor,
776    ) -> Result<()> {
777        let catalog_name = ctx.current_catalog();
778        let schema_name = ctx.current_schema();
779
780        // check if exist
781        if self
782            .get_table(catalog_name, &schema_name, &physical_table)
783            .await?
784            .is_some()
785        {
786            return Ok(());
787        }
788
789        // Gate here too, otherwise a disabled switch would still leak the physical table.
790        if let Some(disabled_reason) = self.auto_create_disabled_reason(ctx)? {
791            return InvalidInsertRequestSnafu {
792                reason: format!(
793                    "Physical table `{physical_table}` does not exist, and {disabled_reason}"
794                ),
795            }
796            .fail();
797        }
798
799        let table_reference = TableReference::full(catalog_name, &schema_name, &physical_table);
800        info!("Physical metric table `{table_reference}` does not exist, try creating table");
801
802        // schema with timestamp and field column
803        let default_schema = vec![
804            ColumnSchema {
805                column_name: greptime_timestamp().to_string(),
806                datatype: ColumnDataType::TimestampMillisecond as _,
807                semantic_type: SemanticType::Timestamp as _,
808                datatype_extension: None,
809                options: None,
810            },
811            ColumnSchema {
812                column_name: greptime_value().to_string(),
813                datatype: ColumnDataType::Float64 as _,
814                semantic_type: SemanticType::Field as _,
815                datatype_extension: None,
816                options: None,
817            },
818        ];
819        let create_table_expr =
820            &mut build_create_table_expr(&table_reference, &default_schema, default_engine())?;
821
822        create_table_expr.engine = METRIC_ENGINE_NAME.to_string();
823        create_table_expr
824            .table_options
825            .insert(PHYSICAL_TABLE_METADATA_KEY.to_string(), "true".to_string());
826
827        // create physical table
828        let res = statement_executor
829            .create_table_inner(create_table_expr, None, ctx.clone())
830            .await;
831
832        match res {
833            Ok(_) => {
834                info!("Successfully created table {table_reference}",);
835                Ok(())
836            }
837            Err(err) => {
838                error!(err; "Failed to create table {table_reference}");
839                Err(err)
840            }
841        }
842    }
843
844    async fn get_table(
845        &self,
846        catalog: &str,
847        schema: &str,
848        table: &str,
849    ) -> Result<Option<TableRef>> {
850        self.catalog_manager
851            .table(catalog, schema, table, None)
852            .await
853            .context(CatalogSnafu)
854    }
855
856    fn get_create_table_expr_on_demand(
857        &self,
858        req: &RowInsertRequest,
859        create_type: &AutoCreateTableType,
860        ctx: &QueryContextRef,
861    ) -> Result<CreateTableExpr> {
862        let mut table_options = std::collections::HashMap::with_capacity(4);
863        fill_table_options_for_create(&mut table_options, create_type, ctx);
864
865        let engine_name = if let AutoCreateTableType::Logical(_) = create_type {
866            // engine should be metric engine when creating logical tables.
867            METRIC_ENGINE_NAME
868        } else {
869            default_engine()
870        };
871
872        let schema = ctx.current_schema();
873        let table_ref = TableReference::full(ctx.current_catalog(), &schema, &req.table_name);
874        // SAFETY: `req.rows` is guaranteed to be `Some` by `handle_row_inserts_with_create_type()`.
875        let request_schema = req.rows.as_ref().unwrap().schema.as_slice();
876        let mut create_table_expr =
877            build_create_table_expr(&table_ref, request_schema, engine_name)?;
878
879        info!("Table `{table_ref}` does not exist, try creating table");
880        create_table_expr.table_options.extend(table_options);
881        Ok(create_table_expr)
882    }
883
884    /// Returns an alter table expression if it finds new columns in the request.
885    /// When `accommodate_existing_schema` is false, it always adds columns if not exist.
886    /// When `accommodate_existing_schema` is true, it may modify the input `req` to
887    /// accommodate it with existing schema. See [`create_or_alter_tables_on_demand`](Self::create_or_alter_tables_on_demand)
888    /// for more details.
889    /// When `accommodate_existing_schema` is true and `is_single_value` is true, it also consider fields when modifying the
890    /// input `req`.
891    fn get_alter_table_expr_on_demand(
892        &self,
893        req: &mut RowInsertRequest,
894        table: &TableRef,
895        ctx: &QueryContextRef,
896        accommodate_existing_schema: bool,
897        is_single_value: bool,
898    ) -> Result<Option<AlterTableExpr>> {
899        let catalog_name = ctx.current_catalog();
900        let schema_name = ctx.current_schema();
901        let table_name = table.table_info().name.clone();
902
903        let request_schema = req.rows.as_ref().unwrap().schema.as_slice();
904        let column_exprs = ColumnExpr::from_column_schemas(request_schema);
905        let add_columns = expr_helper::extract_add_columns_expr(&table.schema(), column_exprs)?;
906        let Some(mut add_columns) = add_columns else {
907            return Ok(None);
908        };
909
910        // If accommodate_existing_schema is true, update request schema for Timestamp/Field columns
911        if accommodate_existing_schema {
912            let table_schema = table.schema();
913            // Find timestamp column name
914            let ts_col_name = table_schema.timestamp_column().map(|c| c.name.clone());
915            // Find field column name if there is only one and `is_single_value` is true.
916            let mut field_col_name = None;
917            if is_single_value {
918                let mut multiple_field_cols = false;
919                table.field_columns().for_each(|col| {
920                    if field_col_name.is_none() {
921                        field_col_name = Some(col.name.clone());
922                    } else {
923                        multiple_field_cols = true;
924                    }
925                });
926                if multiple_field_cols {
927                    field_col_name = None;
928                }
929            }
930
931            // Update column name in request schema for Timestamp/Field columns
932            if let Some(rows) = req.rows.as_mut() {
933                for col in &mut rows.schema {
934                    match col.semantic_type {
935                        x if x == SemanticType::Timestamp as i32 => {
936                            if let Some(ref ts_name) = ts_col_name
937                                && col.column_name != *ts_name
938                            {
939                                col.column_name = ts_name.clone();
940                            }
941                        }
942                        x if x == SemanticType::Field as i32 => {
943                            if let Some(ref field_name) = field_col_name
944                                && col.column_name != *field_name
945                            {
946                                col.column_name = field_name.clone();
947                            }
948                        }
949                        _ => {}
950                    }
951                }
952            }
953
954            // Only keep columns that are tags or non-single field.
955            add_columns.add_columns.retain(|col| {
956                let def = col.column_def.as_ref().unwrap();
957                def.semantic_type == SemanticType::Tag as i32
958                    || (def.semantic_type == SemanticType::Field as i32 && field_col_name.is_none())
959            });
960
961            if add_columns.add_columns.is_empty() {
962                return Ok(None);
963            }
964        }
965
966        Ok(Some(AlterTableExpr {
967            catalog_name: catalog_name.to_string(),
968            schema_name: schema_name.clone(),
969            table_name: table_name.clone(),
970            kind: Some(Kind::AddColumns(add_columns)),
971        }))
972    }
973
974    /// Creates a table with options.
975    async fn create_physical_table(
976        &self,
977        mut create_table_expr: CreateTableExpr,
978        partitions: Option<Partitions>,
979        ctx: &QueryContextRef,
980        statement_executor: &StatementExecutor,
981    ) -> Result<TableRef> {
982        {
983            let table_ref = TableReference::full(
984                &create_table_expr.catalog_name,
985                &create_table_expr.schema_name,
986                &create_table_expr.table_name,
987            );
988
989            info!("Table `{table_ref}` does not exist, try creating table");
990        }
991        let res = statement_executor
992            .create_table_inner(&mut create_table_expr, partitions, ctx.clone())
993            .await;
994
995        let table_ref = TableReference::full(
996            &create_table_expr.catalog_name,
997            &create_table_expr.schema_name,
998            &create_table_expr.table_name,
999        );
1000
1001        match res {
1002            Ok(table) => {
1003                info!(
1004                    "Successfully created table {} with options: {:?}",
1005                    table_ref, create_table_expr.table_options,
1006                );
1007                Ok(table)
1008            }
1009            Err(err) => {
1010                error!(err; "Failed to create table {}", table_ref);
1011                Err(err)
1012            }
1013        }
1014    }
1015
1016    async fn create_logical_tables(
1017        &self,
1018        create_table_exprs: Vec<CreateTableExpr>,
1019        ctx: &QueryContextRef,
1020        statement_executor: &StatementExecutor,
1021    ) -> Result<Vec<TableRef>> {
1022        let res = statement_executor
1023            .create_logical_tables(&create_table_exprs, ctx.clone())
1024            .await;
1025
1026        match res {
1027            Ok(res) => {
1028                info!("Successfully created logical tables");
1029                Ok(res)
1030            }
1031            Err(err) => {
1032                let failed_tables = create_table_exprs
1033                    .into_iter()
1034                    .map(|expr| {
1035                        format!(
1036                            "{}.{}.{}",
1037                            expr.catalog_name, expr.schema_name, expr.table_name
1038                        )
1039                    })
1040                    .collect::<Vec<_>>();
1041                error!(
1042                    err;
1043                    "Failed to create logical tables {:?}",
1044                    failed_tables
1045                );
1046                Err(err)
1047            }
1048        }
1049    }
1050
1051    pub fn node_manager(&self) -> &NodeManagerRef {
1052        &self.node_manager
1053    }
1054
1055    pub fn partition_manager(&self) -> &PartitionRuleManagerRef {
1056        &self.partition_manager
1057    }
1058}
1059
1060fn validate_column_count_match(requests: &RowInsertRequests) -> Result<()> {
1061    for request in &requests.inserts {
1062        let rows = request.rows.as_ref().unwrap();
1063        let column_count = rows.schema.len();
1064        rows.rows.iter().try_for_each(|r| {
1065            ensure!(
1066                r.values.len() == column_count,
1067                InvalidInsertRequestSnafu {
1068                    reason: format!(
1069                        "column count mismatch, columns: {}, values: {}",
1070                        column_count,
1071                        r.values.len()
1072                    )
1073                }
1074            );
1075            Ok(())
1076        })?;
1077    }
1078    Ok(())
1079}
1080
1081/// Fill table options for a new table by create type.
1082pub fn fill_table_options_for_create(
1083    table_options: &mut std::collections::HashMap<String, String>,
1084    create_type: &AutoCreateTableType,
1085    ctx: &QueryContextRef,
1086) {
1087    for key in VALID_TABLE_OPTION_KEYS {
1088        if let Some(value) = ctx.extension(key) {
1089            table_options.insert(key.to_string(), value.to_string());
1090        }
1091    }
1092
1093    // Semantic keys are prefix-matched, not in the fixed allowlist above.
1094    for (key, value) in ctx.extensions() {
1095        if is_semantic_option_key(&key) {
1096            table_options.insert(key, value);
1097        }
1098    }
1099
1100    match create_type {
1101        AutoCreateTableType::Logical(physical_table) => {
1102            table_options.insert(
1103                LOGICAL_TABLE_METADATA_KEY.to_string(),
1104                physical_table.clone(),
1105            );
1106        }
1107        AutoCreateTableType::Physical => {
1108            if let Some(append_mode) = ctx.extension(APPEND_MODE_KEY) {
1109                table_options.insert(APPEND_MODE_KEY.to_string(), append_mode.to_string());
1110            }
1111            if let Some(merge_mode) = ctx.extension(MERGE_MODE_KEY) {
1112                table_options.insert(MERGE_MODE_KEY.to_string(), merge_mode.to_string());
1113            }
1114            if let Some(time_window) = ctx.extension(TWCS_TIME_WINDOW) {
1115                table_options.insert(TWCS_TIME_WINDOW.to_string(), time_window.to_string());
1116                // We need to set the compaction type explicitly.
1117                table_options.insert(
1118                    COMPACTION_TYPE.to_string(),
1119                    COMPACTION_TYPE_TWCS.to_string(),
1120                );
1121            }
1122        }
1123        // Set append_mode to true for log table.
1124        // because log tables should keep rows with the same ts and tags.
1125        AutoCreateTableType::Log => {
1126            table_options.insert(APPEND_MODE_KEY.to_string(), "true".to_string());
1127        }
1128        AutoCreateTableType::LastNonNull => {
1129            if ctx
1130                .extension(APPEND_MODE_KEY)
1131                .is_some_and(|value| value.eq_ignore_ascii_case("true"))
1132            {
1133                table_options.insert(APPEND_MODE_KEY.to_string(), "true".to_string());
1134                table_options.insert(MERGE_MODE_KEY.to_string(), "last_row".to_string());
1135            } else if let Some(merge_mode) = ctx.extension(MERGE_MODE_KEY) {
1136                table_options.insert(MERGE_MODE_KEY.to_string(), merge_mode.to_string());
1137            } else {
1138                table_options.insert(MERGE_MODE_KEY.to_string(), "last_non_null".to_string());
1139            }
1140        }
1141        AutoCreateTableType::Trace => {
1142            table_options.insert(APPEND_MODE_KEY.to_string(), "true".to_string());
1143        }
1144    }
1145}
1146
1147pub fn build_create_table_expr(
1148    table: &TableReference,
1149    request_schema: &[ColumnSchema],
1150    engine: &str,
1151) -> Result<CreateTableExpr> {
1152    expr_helper::create_table_expr_by_column_schemas(table, request_schema, engine, None)
1153}
1154
1155/// Result of `create_or_alter_tables_on_demand`.
1156struct CreateAlterTableResult {
1157    /// table ids of ttl=instant tables.
1158    instant_table_ids: HashSet<TableId>,
1159    /// Table Info of the created tables.
1160    table_infos: HashMap<TableId, Arc<TableInfo>>,
1161}
1162
1163struct FlowMirrorTask {
1164    requests: HashMap<Peer, RegionInsertRequests>,
1165    num_rows: usize,
1166}
1167
1168impl FlowMirrorTask {
1169    async fn new(
1170        cache: &TableFlownodeSetCacheRef,
1171        requests: impl Iterator<Item = &RegionInsertRequest>,
1172    ) -> Result<Self> {
1173        let mut src_table_reqs: HashMap<TableId, Option<(Vec<Peer>, RegionInsertRequests)>> =
1174            HashMap::new();
1175        let mut num_rows = 0;
1176
1177        for req in requests {
1178            let table_id = RegionId::from_u64(req.region_id).table_id();
1179            match src_table_reqs.get_mut(&table_id) {
1180                Some(Some((_peers, reqs))) => reqs.requests.push(req.clone()),
1181                // already know this is not source table
1182                Some(None) => continue,
1183                _ => {
1184                    // dedup peers
1185                    let peers = cache
1186                        .get(table_id)
1187                        .await
1188                        .context(RequestInsertsSnafu)?
1189                        .unwrap_or_default()
1190                        .values()
1191                        .cloned()
1192                        .collect::<HashSet<_>>()
1193                        .into_iter()
1194                        .collect::<Vec<_>>();
1195
1196                    if !peers.is_empty() {
1197                        let mut reqs = RegionInsertRequests::default();
1198                        reqs.requests.push(req.clone());
1199                        num_rows += reqs
1200                            .requests
1201                            .iter()
1202                            .map(|r| r.rows.as_ref().unwrap().rows.len())
1203                            .sum::<usize>();
1204                        src_table_reqs.insert(table_id, Some((peers, reqs)));
1205                    } else {
1206                        // insert a empty entry to avoid repeat query
1207                        src_table_reqs.insert(table_id, None);
1208                    }
1209                }
1210            }
1211        }
1212
1213        let mut inserts: HashMap<Peer, RegionInsertRequests> = HashMap::new();
1214
1215        for (_table_id, (peers, reqs)) in src_table_reqs
1216            .into_iter()
1217            .filter_map(|(k, v)| v.map(|v| (k, v)))
1218        {
1219            if peers.len() == 1 {
1220                // fast path, zero copy
1221                inserts
1222                    .entry(peers[0].clone())
1223                    .or_default()
1224                    .requests
1225                    .extend(reqs.requests);
1226                continue;
1227            } else {
1228                // TODO(discord9): need to split requests to multiple flownodes
1229                for flownode in peers {
1230                    inserts
1231                        .entry(flownode.clone())
1232                        .or_default()
1233                        .requests
1234                        .extend(reqs.requests.clone());
1235                }
1236            }
1237        }
1238
1239        Ok(Self {
1240            requests: inserts,
1241            num_rows,
1242        })
1243    }
1244
1245    fn detach(self, node_manager: NodeManagerRef) -> Result<()> {
1246        crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.add(self.num_rows as i64);
1247        for (peer, inserts) in self.requests {
1248            let node_manager = node_manager.clone();
1249            common_runtime::spawn_global(async move {
1250                let result = node_manager
1251                    .flownode(&peer)
1252                    .await
1253                    .handle_inserts(inserts)
1254                    .await
1255                    .context(RequestInsertsSnafu);
1256
1257                match result {
1258                    Ok(resp) => {
1259                        let affected_rows = resp.affected_rows;
1260                        crate::metrics::DIST_MIRROR_ROW_COUNT.inc_by(affected_rows);
1261                        crate::metrics::DIST_MIRROR_PENDING_ROW_COUNT.sub(affected_rows as _);
1262                    }
1263                    Err(err) => {
1264                        error!(err; "Failed to insert data into flownode {}", peer);
1265                    }
1266                }
1267            });
1268        }
1269
1270        Ok(())
1271    }
1272}
1273
1274#[cfg(test)]
1275mod tests {
1276    use std::sync::Arc;
1277
1278    use api::v1::helper::{field_column_schema, time_index_column_schema};
1279    use api::v1::{RowInsertRequest, Rows, Value};
1280    use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
1281    use common_meta::cache::new_table_flownode_set_cache;
1282    use common_meta::ddl::test_util::datanode_handler::NaiveDatanodeHandler;
1283    use common_meta::test_util::MockDatanodeManager;
1284    use datatypes::data_type::ConcreteDataType;
1285    use datatypes::schema::ColumnSchema;
1286    use moka::future::Cache;
1287    use session::context::QueryContext;
1288    use table::TableRef;
1289    use table::dist_table::DummyDataSource;
1290    use table::metadata::{TableInfoBuilder, TableMetaBuilder, TableType};
1291
1292    use super::*;
1293    use crate::tests::{create_partition_rule_manager, prepare_mocked_backend};
1294
1295    fn make_table_ref_with_schema(ts_name: &str, field_name: &str) -> TableRef {
1296        let schema = datatypes::schema::SchemaBuilder::try_from_columns(vec![
1297            ColumnSchema::new(
1298                ts_name,
1299                ConcreteDataType::timestamp_millisecond_datatype(),
1300                false,
1301            )
1302            .with_time_index(true),
1303            ColumnSchema::new(field_name, ConcreteDataType::float64_datatype(), true),
1304        ])
1305        .unwrap()
1306        .build()
1307        .unwrap();
1308        let meta = TableMetaBuilder::empty()
1309            .schema(Arc::new(schema))
1310            .primary_key_indices(vec![])
1311            .value_indices(vec![1])
1312            .engine("mito")
1313            .next_column_id(0)
1314            .options(Default::default())
1315            .created_on(Default::default())
1316            .build()
1317            .unwrap();
1318        let info = Arc::new(
1319            TableInfoBuilder::default()
1320                .table_id(1)
1321                .table_version(0)
1322                .name("test_table")
1323                .schema_name(DEFAULT_SCHEMA_NAME)
1324                .catalog_name(DEFAULT_CATALOG_NAME)
1325                .desc(None)
1326                .table_type(TableType::Base)
1327                .meta(meta)
1328                .build()
1329                .unwrap(),
1330        );
1331        Arc::new(table::Table::new(
1332            info,
1333            table::metadata::FilterPushDownType::Unsupported,
1334            Arc::new(DummyDataSource),
1335        ))
1336    }
1337
1338    #[tokio::test]
1339    async fn test_accommodate_existing_schema_logic() {
1340        let ts_name = "my_ts";
1341        let field_name = "my_field";
1342        let table = make_table_ref_with_schema(ts_name, field_name);
1343
1344        // The request uses different names for timestamp and field columns
1345        let mut req = RowInsertRequest {
1346            table_name: "test_table".to_string(),
1347            rows: Some(Rows {
1348                schema: vec![
1349                    time_index_column_schema("ts_wrong", ColumnDataType::TimestampMillisecond),
1350                    field_column_schema("field_wrong", ColumnDataType::Float64),
1351                ],
1352                rows: vec![api::v1::Row {
1353                    values: vec![Value::default(), Value::default()],
1354                }],
1355            }),
1356        };
1357        let ctx = Arc::new(QueryContext::with(
1358            DEFAULT_CATALOG_NAME,
1359            DEFAULT_SCHEMA_NAME,
1360        ));
1361
1362        let kv_backend = prepare_mocked_backend().await;
1363        let inserter = Inserter::new(
1364            catalog::memory::MemoryCatalogManager::new(),
1365            create_partition_rule_manager(kv_backend.clone()).await,
1366            Arc::new(MockDatanodeManager::new(NaiveDatanodeHandler)),
1367            Arc::new(new_table_flownode_set_cache(
1368                String::new(),
1369                Cache::new(100),
1370                kv_backend.clone(),
1371            )),
1372            true,
1373        );
1374        let alter_expr = inserter
1375            .get_alter_table_expr_on_demand(&mut req, &table, &ctx, true, true)
1376            .unwrap();
1377        assert!(alter_expr.is_none());
1378
1379        // The request's schema should have updated names for timestamp and field columns
1380        let req_schema = req.rows.as_ref().unwrap().schema.clone();
1381        assert_eq!(req_schema[0].column_name, ts_name);
1382        assert_eq!(req_schema[1].column_name, field_name);
1383    }
1384
1385    #[test]
1386    fn test_last_non_null_create_options_preserve_default_without_append_mode() {
1387        let ctx = Arc::new(QueryContext::with(
1388            DEFAULT_CATALOG_NAME,
1389            DEFAULT_SCHEMA_NAME,
1390        ));
1391        let mut table_options = Default::default();
1392
1393        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::LastNonNull, &ctx);
1394
1395        assert_eq!(
1396            Some("last_non_null"),
1397            table_options.get(MERGE_MODE_KEY).map(String::as_str)
1398        );
1399        assert!(!table_options.contains_key(APPEND_MODE_KEY));
1400    }
1401
1402    #[test]
1403    fn test_fill_table_options_copies_semantic_extensions() {
1404        use table::requests::{
1405            SEMANTIC_PER_TABLE_INDEX_KEY, SEMANTIC_SIGNAL_TYPE, SEMANTIC_SOURCE,
1406            SIGNAL_TYPE_METRIC, SOURCE_OPENTELEMETRY,
1407        };
1408
1409        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
1410        ctx.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_METRIC);
1411        ctx.set_extension(SEMANTIC_SOURCE, SOURCE_OPENTELEMETRY);
1412        // The internal transport key must NOT be copied into table options.
1413        ctx.set_extension(SEMANTIC_PER_TABLE_INDEX_KEY, "{}");
1414        let ctx = Arc::new(ctx);
1415        let mut table_options = Default::default();
1416
1417        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::Physical, &ctx);
1418
1419        assert_eq!(
1420            Some(SIGNAL_TYPE_METRIC),
1421            table_options.get(SEMANTIC_SIGNAL_TYPE).map(String::as_str)
1422        );
1423        assert_eq!(
1424            Some(SOURCE_OPENTELEMETRY),
1425            table_options.get(SEMANTIC_SOURCE).map(String::as_str)
1426        );
1427        assert!(!table_options.contains_key(SEMANTIC_PER_TABLE_INDEX_KEY));
1428    }
1429
1430    #[test]
1431    fn test_last_non_null_create_options_preserve_default_with_append_mode_false() {
1432        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
1433        ctx.set_extension(APPEND_MODE_KEY, "false");
1434        let ctx = Arc::new(ctx);
1435        let mut table_options = Default::default();
1436
1437        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::LastNonNull, &ctx);
1438
1439        assert!(!table_options.contains_key(APPEND_MODE_KEY));
1440        assert_eq!(
1441            Some("last_non_null"),
1442            table_options.get(MERGE_MODE_KEY).map(String::as_str)
1443        );
1444    }
1445
1446    #[test]
1447    fn test_last_non_null_create_options_use_configured_merge_mode() {
1448        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
1449        ctx.set_extension(MERGE_MODE_KEY, "last_row");
1450        let ctx = Arc::new(ctx);
1451        let mut table_options = Default::default();
1452
1453        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::LastNonNull, &ctx);
1454
1455        assert_eq!(
1456            Some("last_row"),
1457            table_options.get(MERGE_MODE_KEY).map(String::as_str)
1458        );
1459        assert!(!table_options.contains_key(APPEND_MODE_KEY));
1460    }
1461
1462    #[test]
1463    fn test_last_non_null_create_options_use_last_row_with_append_mode_true() {
1464        let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
1465        ctx.set_extension(APPEND_MODE_KEY, "true");
1466        let ctx = Arc::new(ctx);
1467        let mut table_options = Default::default();
1468
1469        fill_table_options_for_create(&mut table_options, &AutoCreateTableType::LastNonNull, &ctx);
1470
1471        assert_eq!(
1472            Some("true"),
1473            table_options.get(APPEND_MODE_KEY).map(String::as_str)
1474        );
1475        assert_eq!(
1476            Some("last_row"),
1477            table_options.get(MERGE_MODE_KEY).map(String::as_str)
1478        );
1479    }
1480}