Skip to main content

sql/statements/
create.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::HashMap;
16use std::fmt::{Display, Formatter};
17
18use common_catalog::consts::FILE_ENGINE;
19use common_sql::default_constraint::parse_column_default_constraint;
20use datatypes::json::JsonSettings;
21use datatypes::prelude::ConcreteDataType;
22use datatypes::schema::{
23    ColumnDefaultConstraint, FulltextOptions, SkippingIndexOptions, VectorDistanceMetric,
24    VectorIndexEngineType, VectorIndexOptions,
25};
26use itertools::Itertools;
27use serde::Serialize;
28use snafu::ResultExt;
29use sqlparser::ast::{ColumnOption, ColumnOptionDef, DataType, Expr};
30use sqlparser_derive::{Visit, VisitMut};
31
32use crate::ast::{ColumnDef, Ident, ObjectName, Value as SqlValue};
33use crate::dialect::GreptimeDbDialect;
34use crate::error::{
35    InvalidFlowQuerySnafu, InvalidSqlSnafu, Result, SetFulltextOptionSnafu,
36    SetSkippingIndexOptionSnafu,
37};
38use crate::parser::ParserContext;
39use crate::statements::query::Query as GtQuery;
40use crate::statements::statement::Statement;
41use crate::statements::tql::Tql;
42use crate::statements::{OptionMap, sql_data_type_to_concrete_data_type, value_to_sql_value};
43
44const LINE_SEP: &str = ",\n";
45const COMMA_SEP: &str = ", ";
46const INDENT: usize = 2;
47pub const VECTOR_OPT_DIM: &str = "dim";
48
49macro_rules! format_indent {
50    ($fmt: expr, $arg: expr) => {
51        format!($fmt, format_args!("{: >1$}", "", INDENT), $arg)
52    };
53    ($arg: expr) => {
54        format_indent!("{}{}", $arg)
55    };
56}
57
58macro_rules! format_list_indent {
59    ($list: expr) => {
60        $list.iter().map(|e| format_indent!(e)).join(LINE_SEP)
61    };
62}
63
64macro_rules! format_list_comma {
65    ($list: expr) => {
66        $list.iter().map(|e| format!("{}", e)).join(COMMA_SEP)
67    };
68}
69
70#[cfg(feature = "enterprise")]
71pub mod trigger;
72
73fn format_table_constraint(constraints: &[TableConstraint]) -> String {
74    constraints.iter().map(|c| format_indent!(c)).join(LINE_SEP)
75}
76
77/// Table constraint for create table statement.
78#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
79pub enum TableConstraint {
80    /// Primary key constraint.
81    PrimaryKey { columns: Vec<Ident> },
82    /// Time index constraint.
83    TimeIndex { column: Ident },
84}
85
86impl Display for TableConstraint {
87    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
88        match self {
89            TableConstraint::PrimaryKey { columns } => {
90                write!(f, "PRIMARY KEY ({})", format_list_comma!(columns))
91            }
92            TableConstraint::TimeIndex { column } => {
93                write!(f, "TIME INDEX ({})", column)
94            }
95        }
96    }
97}
98
99#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
100pub struct CreateTable {
101    /// Create if not exists
102    pub if_not_exists: bool,
103    pub table_id: u32,
104    /// Table name
105    pub name: ObjectName,
106    pub columns: Vec<Column>,
107    pub engine: String,
108    pub constraints: Vec<TableConstraint>,
109    /// Table options in `WITH`. All keys are lowercase.
110    pub options: OptionMap,
111    pub partitions: Option<Partitions>,
112}
113
114/// Column definition in `CREATE TABLE` statement.
115#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
116pub struct Column {
117    /// `ColumnDef` from `sqlparser::ast`
118    pub column_def: ColumnDef,
119    /// Column extensions for greptimedb dialect.
120    pub extensions: ColumnExtensions,
121}
122
123/// Column extensions for greptimedb dialect.
124#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Default, Serialize)]
125pub struct ColumnExtensions {
126    /// Vector type options.
127    pub vector_options: Option<OptionMap>,
128
129    /// Fulltext index options.
130    pub fulltext_index_options: Option<OptionMap>,
131    /// Skipping index options.
132    pub skipping_index_options: Option<OptionMap>,
133    /// Inverted index options.
134    ///
135    /// Inverted index doesn't have options at present. There won't be any options in that map.
136    pub inverted_index_options: Option<OptionMap>,
137    /// Vector index options for HNSW-based vector similarity search.
138    pub vector_index_options: Option<OptionMap>,
139    /// JSON2-specific column options.
140    pub json2_options: Option<Json2Options>,
141}
142
143/// JSON2-specific options represented in the SQL AST.
144#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
145pub struct Json2Options {
146    /// Maximum number of unhinted JSON2 paths expanded into Arrow fields.
147    pub(crate) max_auto_expanded_paths: Option<u32>,
148    /// Paths stored as explicitly typed JSON2 fields.
149    pub(crate) type_hints: Vec<JsonTypeHint>,
150}
151
152impl Display for Json2Options {
153    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
154        let mut options = Vec::with_capacity(self.type_hints.len() + 1);
155        if let Some(max) = self.max_auto_expanded_paths {
156            options.push(format!("max_auto_expanded_paths = {max}"));
157        }
158        options.extend(self.type_hints.iter().map(format_json_type_hint));
159        write!(f, "(\n    {}\n  )", options.iter().join(",\n    "))
160    }
161}
162
163#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
164pub struct JsonTypeHint {
165    pub path: Vec<String>,
166    pub data_type: DataType,
167    pub nullable: bool,
168    pub default: Option<Expr>,
169    pub inverted_index: bool,
170}
171
172impl Column {
173    pub fn name(&self) -> &Ident {
174        &self.column_def.name
175    }
176
177    pub fn data_type(&self) -> &DataType {
178        &self.column_def.data_type
179    }
180
181    pub fn mut_data_type(&mut self) -> &mut DataType {
182        &mut self.column_def.data_type
183    }
184
185    pub fn options(&self) -> &[ColumnOptionDef] {
186        &self.column_def.options
187    }
188
189    pub fn mut_options(&mut self) -> &mut Vec<ColumnOptionDef> {
190        &mut self.column_def.options
191    }
192}
193
194impl Display for Column {
195    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
196        if let Some(vector_options) = &self.extensions.vector_options
197            && let Some(dim) = vector_options.get(VECTOR_OPT_DIM)
198        {
199            write!(f, "{} VECTOR({})", self.column_def.name, dim)?;
200            return Ok(());
201        }
202
203        write!(f, "{} {}", self.column_def.name, self.column_def.data_type)?;
204        if let Some(options) = &self.extensions.json2_options {
205            write!(f, "{options}")?;
206        }
207        for option in &self.column_def.options {
208            write!(f, " {option}")?;
209        }
210
211        if let Some(fulltext_options) = &self.extensions.fulltext_index_options {
212            if !fulltext_options.is_empty() {
213                let options = fulltext_options.kv_pairs();
214                write!(f, " FULLTEXT INDEX WITH({})", format_list_comma!(options))?;
215            } else {
216                write!(f, " FULLTEXT INDEX")?;
217            }
218        }
219
220        if let Some(skipping_index_options) = &self.extensions.skipping_index_options {
221            if !skipping_index_options.is_empty() {
222                let options = skipping_index_options.kv_pairs();
223                write!(f, " SKIPPING INDEX WITH({})", format_list_comma!(options))?;
224            } else {
225                write!(f, " SKIPPING INDEX")?;
226            }
227        }
228
229        if let Some(inverted_index_options) = &self.extensions.inverted_index_options {
230            if !inverted_index_options.is_empty() {
231                let options = inverted_index_options.kv_pairs();
232                write!(f, " INVERTED INDEX WITH({})", format_list_comma!(options))?;
233            } else {
234                write!(f, " INVERTED INDEX")?;
235            }
236        }
237
238        if let Some(vector_index_options) = &self.extensions.vector_index_options {
239            if !vector_index_options.is_empty() {
240                let options = vector_index_options.kv_pairs();
241                write!(f, " VECTOR INDEX WITH({})", format_list_comma!(options))?;
242            } else {
243                write!(f, " VECTOR INDEX")?;
244            }
245        }
246        Ok(())
247    }
248}
249
250impl ColumnExtensions {
251    pub fn build_fulltext_options(&self) -> Result<Option<FulltextOptions>> {
252        let Some(options) = self.fulltext_index_options.as_ref() else {
253            return Ok(None);
254        };
255
256        let options: HashMap<String, String> = options.clone().into_map();
257        Ok(Some(options.try_into().context(SetFulltextOptionSnafu)?))
258    }
259
260    pub fn build_skipping_index_options(&self) -> Result<Option<SkippingIndexOptions>> {
261        let Some(options) = self.skipping_index_options.as_ref() else {
262            return Ok(None);
263        };
264
265        let options: HashMap<String, String> = options.clone().into_map();
266        Ok(Some(
267            options.try_into().context(SetSkippingIndexOptionSnafu)?,
268        ))
269    }
270
271    pub fn build_vector_index_options(&self) -> Result<Option<VectorIndexOptions>> {
272        let Some(options) = self.vector_index_options.as_ref() else {
273            return Ok(None);
274        };
275
276        let options_map: HashMap<String, String> = options.clone().into_map();
277        let mut result = VectorIndexOptions::default();
278
279        if let Some(s) = options_map.get("engine") {
280            result.engine = s.parse::<VectorIndexEngineType>().map_err(|e| {
281                InvalidSqlSnafu {
282                    msg: format!("invalid VECTOR INDEX engine: {e}"),
283                }
284                .build()
285            })?;
286        }
287
288        if let Some(s) = options_map.get("metric") {
289            result.metric = s.parse::<VectorDistanceMetric>().map_err(|e| {
290                InvalidSqlSnafu {
291                    msg: format!("invalid VECTOR INDEX metric: {e}"),
292                }
293                .build()
294            })?;
295        }
296
297        if let Some(s) = options_map.get("connectivity") {
298            let value = s.parse::<u32>().map_err(|_| {
299                InvalidSqlSnafu {
300                    msg: format!(
301                        "invalid VECTOR INDEX connectivity: {s}, expected positive integer"
302                    ),
303                }
304                .build()
305            })?;
306            if !(2..=2048).contains(&value) {
307                return InvalidSqlSnafu {
308                    msg: "VECTOR INDEX connectivity must be in the range [2, 2048].".to_string(),
309                }
310                .fail();
311            }
312            result.connectivity = value;
313        }
314
315        if let Some(s) = options_map.get("expansion_add") {
316            let value = s.parse::<u32>().map_err(|_| {
317                InvalidSqlSnafu {
318                    msg: format!(
319                        "invalid VECTOR INDEX expansion_add: {s}, expected positive integer"
320                    ),
321                }
322                .build()
323            })?;
324            if value == 0 {
325                return InvalidSqlSnafu {
326                    msg: "VECTOR INDEX expansion_add must be greater than 0".to_string(),
327                }
328                .fail();
329            }
330            result.expansion_add = value;
331        }
332
333        if let Some(s) = options_map.get("expansion_search") {
334            let value = s.parse::<u32>().map_err(|_| {
335                InvalidSqlSnafu {
336                    msg: format!(
337                        "invalid VECTOR INDEX expansion_search: {s}, expected positive integer"
338                    ),
339                }
340                .build()
341            })?;
342            if value == 0 {
343                return InvalidSqlSnafu {
344                    msg: "VECTOR INDEX expansion_search must be greater than 0".to_string(),
345                }
346                .fail();
347            }
348            result.expansion_search = value;
349        }
350
351        Ok(Some(result))
352    }
353
354    pub fn build_json_settings(&self) -> Result<Option<JsonSettings>> {
355        let Some(options) = &self.json2_options else {
356            return Ok(None);
357        };
358
359        let type_hints = options
360            .type_hints
361            .iter()
362            .map(|hint| {
363                Ok(datatypes::json::JsonTypeHint {
364                    path: hint.path.clone(),
365                    data_type: json_type_hint_concrete_data_type(&hint.data_type)?,
366                    nullable: hint.nullable,
367                    default_constraint: build_json_type_hint_default_constraint(hint)?,
368                    inverted_index: hint.inverted_index,
369                })
370            })
371            .collect::<Result<Vec<_>>>()?;
372        let settings = JsonSettings::try_new(type_hints, options.max_auto_expanded_paths)?;
373        Ok(Some(settings))
374    }
375
376    pub fn set_json_settings(&mut self, settings: JsonSettings) -> Result<()> {
377        let (type_hints, max_auto_expanded_paths) = settings.into_parts();
378        let type_hints = type_hints
379            .into_iter()
380            .map(|hint| {
381                let data_type = json_type_hint_sql_data_type(&hint.data_type)?;
382                let default = hint
383                    .default_constraint
384                    .map(|constraint| column_default_constraint_to_expr(&constraint))
385                    .transpose()?;
386                Ok(JsonTypeHint {
387                    path: hint.path,
388                    data_type,
389                    nullable: hint.nullable,
390                    default,
391                    inverted_index: hint.inverted_index,
392                })
393            })
394            .collect::<Result<Vec<_>>>()?;
395        self.json2_options = (max_auto_expanded_paths.is_some() || !type_hints.is_empty())
396            .then_some(Json2Options {
397                max_auto_expanded_paths,
398                type_hints,
399            });
400        Ok(())
401    }
402}
403
404fn build_json_type_hint_default_constraint(
405    hint: &JsonTypeHint,
406) -> Result<Option<ColumnDefaultConstraint>> {
407    let Some(default) = &hint.default else {
408        return Ok(None);
409    };
410
411    let data_type = json_type_hint_concrete_data_type(&hint.data_type)?;
412    let opts = [ColumnOptionDef {
413        name: None,
414        option: ColumnOption::Default(default.clone()),
415    }];
416
417    // Use the JSON path as the column name context for default value parsing errors.
418    let json_path = hint.path.join(".");
419    let default_constraint = parse_column_default_constraint(&json_path, &data_type, &opts, None)
420        .context(crate::error::SqlCommonSnafu)?;
421
422    if let Some(constraint) = &default_constraint {
423        constraint
424            .validate(&data_type, hint.nullable)
425            .map_err(|e| {
426                InvalidSqlSnafu {
427                    msg: format!("invalid DEFAULT for JSON2 type hint '{}': {e}", json_path),
428                }
429                .build()
430            })?;
431    }
432
433    Ok(default_constraint)
434}
435
436fn json_type_hint_concrete_data_type(data_type: &DataType) -> Result<ConcreteDataType> {
437    let data_type = sql_data_type_to_concrete_data_type(data_type)?;
438    normalize_json_type_hint_concrete_data_type(&data_type)
439}
440
441fn normalize_json_type_hint_concrete_data_type(
442    data_type: &ConcreteDataType,
443) -> Result<ConcreteDataType> {
444    let normalized = match data_type {
445        ConcreteDataType::String(_) => ConcreteDataType::string_datatype(),
446        ConcreteDataType::Int8(_)
447        | ConcreteDataType::Int16(_)
448        | ConcreteDataType::Int32(_)
449        | ConcreteDataType::Int64(_) => ConcreteDataType::int64_datatype(),
450        ConcreteDataType::UInt8(_)
451        | ConcreteDataType::UInt16(_)
452        | ConcreteDataType::UInt32(_)
453        | ConcreteDataType::UInt64(_) => ConcreteDataType::uint64_datatype(),
454        ConcreteDataType::Float32(_) | ConcreteDataType::Float64(_) => {
455            ConcreteDataType::float64_datatype()
456        }
457        ConcreteDataType::Boolean(_) => ConcreteDataType::boolean_datatype(),
458        _ => {
459            return InvalidSqlSnafu {
460                msg: format!("unsupported JSON2 type hint data type: {data_type}"),
461            }
462            .fail();
463        }
464    };
465    Ok(normalized)
466}
467
468fn json_type_hint_sql_data_type(data_type: &ConcreteDataType) -> Result<DataType> {
469    let data_type = normalize_json_type_hint_concrete_data_type(data_type)?;
470    let sql_type = match data_type {
471        ConcreteDataType::String(_) => DataType::String(None),
472        ConcreteDataType::Int64(_) => DataType::BigInt(None),
473        ConcreteDataType::UInt64(_) => DataType::BigIntUnsigned(None),
474        ConcreteDataType::Float64(_) => DataType::Double(sqlparser::ast::ExactNumberInfo::None),
475        ConcreteDataType::Boolean(_) => DataType::Boolean,
476        _ => unreachable!("JSON2 type hint data type should have been normalized"),
477    };
478    Ok(sql_type)
479}
480
481fn column_default_constraint_to_expr(constraint: &ColumnDefaultConstraint) -> Result<Expr> {
482    match constraint {
483        ColumnDefaultConstraint::Value(value) => Ok(Expr::Value(value_to_sql_value(value)?.into())),
484        ColumnDefaultConstraint::Function(function) => {
485            ParserContext::parse_function(function, &GreptimeDbDialect {})
486        }
487    }
488}
489
490fn format_json_type_hint(hint: &JsonTypeHint) -> String {
491    let path = hint
492        .path
493        .iter()
494        .map(|segment| format_json_path_segment(segment))
495        .join(".");
496    let nullability = if hint.nullable { " NULL" } else { " NOT NULL" };
497    let default = hint
498        .default
499        .as_ref()
500        .map(|expr| format!(" DEFAULT {expr}"))
501        .unwrap_or_default();
502    let inverted_index = if hint.inverted_index {
503        " INVERTED INDEX"
504    } else {
505        ""
506    };
507    format!(
508        "{} {}{}{}{}",
509        path, hint.data_type, nullability, default, inverted_index
510    )
511}
512
513fn format_json_path_segment(segment: &str) -> String {
514    format!("\"{}\"", segment.replace('"', "\"\""))
515}
516
517/// Partition on columns or values.
518///
519/// - `column_list` is the list of columns in `PARTITION ON COLUMNS` clause.
520/// - `exprs` is the list of expressions in `PARTITION ON VALUES` clause, like
521///   `host <= 'host1'`, `host > 'host1' and host <= 'host2'` or `host > 'host2'`.
522///   Each expression stands for a partition.
523#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
524pub struct Partitions {
525    pub column_list: Vec<Ident>,
526    pub exprs: Vec<Expr>,
527}
528
529impl Partitions {
530    /// set quotes to all [Ident]s from column list
531    pub fn set_quote(&mut self, quote_style: char) {
532        self.column_list
533            .iter_mut()
534            .for_each(|c| c.quote_style = Some(quote_style));
535    }
536}
537
538#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut)]
539pub struct PartitionEntry {
540    pub name: Ident,
541    pub value_list: Vec<SqlValue>,
542}
543
544impl Display for PartitionEntry {
545    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
546        write!(
547            f,
548            "PARTITION {} VALUES LESS THAN ({})",
549            self.name,
550            format_list_comma!(self.value_list),
551        )
552    }
553}
554
555impl Display for Partitions {
556    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
557        if !self.column_list.is_empty() {
558            write!(
559                f,
560                "PARTITION ON COLUMNS ({}) (\n{}\n)",
561                format_list_comma!(self.column_list),
562                format_list_indent!(self.exprs),
563            )?;
564        }
565        Ok(())
566    }
567}
568
569impl Display for CreateTable {
570    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
571        write!(f, "CREATE ")?;
572        if self.engine == FILE_ENGINE {
573            write!(f, "EXTERNAL ")?;
574        }
575        write!(f, "TABLE ")?;
576        if self.if_not_exists {
577            write!(f, "IF NOT EXISTS ")?;
578        }
579        writeln!(f, "{} (", &self.name)?;
580        writeln!(f, "{},", format_list_indent!(self.columns))?;
581        writeln!(f, "{}", format_table_constraint(&self.constraints))?;
582        writeln!(f, ")")?;
583        if let Some(partitions) = &self.partitions {
584            writeln!(f, "{partitions}")?;
585        }
586        writeln!(f, "ENGINE={}", &self.engine)?;
587        if !self.options.is_empty() {
588            let options = self.options.kv_pairs();
589            write!(f, "WITH(\n{}\n)", format_list_indent!(options))?;
590        }
591        Ok(())
592    }
593}
594
595#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
596pub struct CreateDatabase {
597    pub name: ObjectName,
598    /// Create if not exists
599    pub if_not_exists: bool,
600    pub options: OptionMap,
601}
602
603impl CreateDatabase {
604    /// Creates a statement for `CREATE DATABASE`
605    pub fn new(name: ObjectName, if_not_exists: bool, options: OptionMap) -> Self {
606        Self {
607            name,
608            if_not_exists,
609            options,
610        }
611    }
612}
613
614impl Display for CreateDatabase {
615    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
616        write!(f, "CREATE DATABASE ")?;
617        if self.if_not_exists {
618            write!(f, "IF NOT EXISTS ")?;
619        }
620        write!(f, "{}", &self.name)?;
621        if !self.options.is_empty() {
622            let options = self.options.kv_pairs();
623            write!(f, "\nWITH(\n{}\n)", format_list_indent!(options))?;
624        }
625        Ok(())
626    }
627}
628
629#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
630pub struct CreateExternalTable {
631    /// Table name
632    pub name: ObjectName,
633    pub columns: Vec<Column>,
634    pub constraints: Vec<TableConstraint>,
635    /// Table options in `WITH`. All keys are lowercase.
636    pub options: OptionMap,
637    pub if_not_exists: bool,
638    pub engine: String,
639}
640
641impl Display for CreateExternalTable {
642    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
643        write!(f, "CREATE EXTERNAL TABLE ")?;
644        if self.if_not_exists {
645            write!(f, "IF NOT EXISTS ")?;
646        }
647        writeln!(f, "{} (", &self.name)?;
648        writeln!(f, "{},", format_list_indent!(self.columns))?;
649        writeln!(f, "{}", format_table_constraint(&self.constraints))?;
650        writeln!(f, ")")?;
651        writeln!(f, "ENGINE={}", &self.engine)?;
652        if !self.options.is_empty() {
653            let options = self.options.kv_pairs();
654            write!(f, "WITH(\n{}\n)", format_list_indent!(options))?;
655        }
656        Ok(())
657    }
658}
659
660#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
661pub struct CreateTableLike {
662    /// Table name
663    pub table_name: ObjectName,
664    /// The table that is designated to be imitated by `Like`
665    pub source_name: ObjectName,
666}
667
668impl Display for CreateTableLike {
669    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
670        let table_name = &self.table_name;
671        let source_name = &self.source_name;
672        write!(f, r#"CREATE TABLE {table_name} LIKE {source_name}"#)
673    }
674}
675
676#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
677pub struct CreateFlow {
678    /// Flow name
679    pub flow_name: ObjectName,
680    /// Output (sink) table name
681    pub sink_table_name: ObjectName,
682    /// Whether to replace existing task
683    pub or_replace: bool,
684    /// Create if not exist
685    pub if_not_exists: bool,
686    /// `EXPIRE AFTER`
687    /// Duration in second as `i64`
688    pub expire_after: Option<i64>,
689    /// Duration for flow evaluation interval
690    /// Duration in seconds as `i64`
691    /// If not set, flow will be evaluated based on time window size and other args.
692    pub eval_interval: Option<i64>,
693    /// Comment string
694    pub comment: Option<String>,
695    /// Flow creation options from `WITH (...)`
696    pub flow_options: OptionMap,
697    /// SQL statement
698    pub query: Box<SqlOrTql>,
699}
700
701/// Either a sql query or a tql query
702#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
703pub enum SqlOrTql {
704    Sql(GtQuery, String),
705    Tql(Tql, String),
706}
707
708impl std::fmt::Display for SqlOrTql {
709    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
710        match self {
711            Self::Sql(_, s) => write!(f, "{}", s),
712            Self::Tql(_, s) => write!(f, "{}", s),
713        }
714    }
715}
716
717impl SqlOrTql {
718    pub fn try_from_statement(
719        value: Statement,
720        original_query: &str,
721    ) -> std::result::Result<Self, crate::error::Error> {
722        match value {
723            Statement::Query(query) => Ok(Self::Sql(*query, original_query.to_string())),
724            Statement::Tql(tql) => Ok(Self::Tql(tql, original_query.to_string())),
725            _ => InvalidFlowQuerySnafu {
726                reason: format!("Expect either sql query or promql query, found {:?}", value),
727            }
728            .fail(),
729        }
730    }
731}
732
733impl Display for CreateFlow {
734    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
735        write!(f, "CREATE ")?;
736        if self.or_replace {
737            write!(f, "OR REPLACE ")?;
738        }
739        write!(f, "FLOW ")?;
740        if self.if_not_exists {
741            write!(f, "IF NOT EXISTS ")?;
742        }
743        writeln!(f, "{}", &self.flow_name)?;
744        writeln!(f, "SINK TO {}", &self.sink_table_name)?;
745        if let Some(expire_after) = &self.expire_after {
746            writeln!(f, "EXPIRE AFTER '{} s'", expire_after)?;
747        }
748        if let Some(eval_interval) = &self.eval_interval {
749            writeln!(f, "EVAL INTERVAL '{} s'", eval_interval)?;
750        }
751        if let Some(comment) = &self.comment {
752            writeln!(f, "COMMENT '{}'", comment)?;
753        }
754        if !self.flow_options.is_empty() {
755            let options = self.flow_options.kv_pairs();
756            writeln!(f, "WITH ({})", format_list_comma!(options))?;
757        }
758        write!(f, "AS {}", &self.query)
759    }
760}
761
762/// Create SQL view statement.
763#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
764pub struct CreateView {
765    /// View name
766    pub name: ObjectName,
767    /// An optional list of names to be used for columns of the view
768    pub columns: Vec<Ident>,
769    /// The clause after `As` that defines the VIEW.
770    /// Can only be either [Statement::Query] or [Statement::Tql].
771    pub query: Box<Statement>,
772    /// Whether to replace existing VIEW
773    pub or_replace: bool,
774    /// Create VIEW only when it doesn't exists
775    pub if_not_exists: bool,
776}
777
778impl Display for CreateView {
779    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
780        write!(f, "CREATE ")?;
781        if self.or_replace {
782            write!(f, "OR REPLACE ")?;
783        }
784        write!(f, "VIEW ")?;
785        if self.if_not_exists {
786            write!(f, "IF NOT EXISTS ")?;
787        }
788        write!(f, "{} ", &self.name)?;
789        if !self.columns.is_empty() {
790            write!(f, "({}) ", format_list_comma!(self.columns))?;
791        }
792        write!(f, "AS {}", &self.query)
793    }
794}
795
796#[cfg(test)]
797mod tests {
798    use std::assert_matches;
799
800    use datatypes::json::{JsonSettings, JsonTypeHint as DatatypeJsonTypeHint};
801    use datatypes::prelude::ConcreteDataType;
802    use datatypes::schema::ColumnDefaultConstraint;
803    use datatypes::value::Value;
804
805    use super::*;
806    use crate::dialect::GreptimeDbDialect;
807    use crate::error::Error;
808    use crate::parser::{ParseOptions, ParserContext};
809    use crate::statements::statement::Statement;
810
811    #[test]
812    fn test_display_create_table() {
813        let sql = r"create table if not exists demo(
814                             host string,
815                             ts timestamp,
816                             cpu double default 0,
817                             memory double,
818                             TIME INDEX (ts),
819                             PRIMARY KEY(host)
820                       )
821                       PARTITION ON COLUMNS (host) (
822                            host = 'a',
823                            host > 'a',
824                       )
825                       engine=mito
826                       with(ttl='7d', storage='File');
827         ";
828        let result =
829            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
830                .unwrap();
831        assert_eq!(1, result.len());
832
833        match &result[0] {
834            Statement::CreateTable(c) => {
835                let new_sql = format!("\n{}", c);
836                assert_eq!(
837                    r#"
838CREATE TABLE IF NOT EXISTS demo (
839  host STRING,
840  ts TIMESTAMP,
841  cpu DOUBLE DEFAULT 0,
842  memory DOUBLE,
843  TIME INDEX (ts),
844  PRIMARY KEY (host)
845)
846PARTITION ON COLUMNS (host) (
847  host = 'a',
848  host > 'a'
849)
850ENGINE=mito
851WITH(
852  storage = 'File',
853  ttl = '7d'
854)"#,
855                    &new_sql
856                );
857
858                let new_result = ParserContext::create_with_dialect(
859                    &new_sql,
860                    &GreptimeDbDialect {},
861                    ParseOptions::default(),
862                )
863                .unwrap();
864                assert_eq!(result, new_result);
865            }
866            _ => unreachable!(),
867        }
868    }
869
870    #[test]
871    fn test_display_empty_partition_column() {
872        let sql = r"create table if not exists demo(
873            host string,
874            ts timestamp,
875            cpu double default 0,
876            memory double,
877            TIME INDEX (ts),
878            PRIMARY KEY(ts, host)
879            );
880        ";
881        let result =
882            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
883                .unwrap();
884        assert_eq!(1, result.len());
885
886        match &result[0] {
887            Statement::CreateTable(c) => {
888                let new_sql = format!("\n{}", c);
889                assert_eq!(
890                    r#"
891CREATE TABLE IF NOT EXISTS demo (
892  host STRING,
893  ts TIMESTAMP,
894  cpu DOUBLE DEFAULT 0,
895  memory DOUBLE,
896  TIME INDEX (ts),
897  PRIMARY KEY (ts, host)
898)
899ENGINE=mito
900"#,
901                    &new_sql
902                );
903
904                let new_result = ParserContext::create_with_dialect(
905                    &new_sql,
906                    &GreptimeDbDialect {},
907                    ParseOptions::default(),
908                )
909                .unwrap();
910                assert_eq!(result, new_result);
911            }
912            _ => unreachable!(),
913        }
914    }
915
916    #[test]
917    fn test_validate_table_options() {
918        let sql = r"create table if not exists demo(
919            host string,
920            ts timestamp,
921            cpu double default 0,
922            memory double,
923            TIME INDEX (ts),
924            PRIMARY KEY(host)
925      )
926      PARTITION ON COLUMNS (host) ()
927      engine=mito
928      with(ttl='7d', 'compaction.type'='world');
929";
930        let result =
931            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
932                .unwrap();
933        match &result[0] {
934            Statement::CreateTable(c) => {
935                assert_eq!(2, c.options.len());
936            }
937            _ => unreachable!(),
938        }
939
940        let sql = r"create table if not exists demo(
941            host string,
942            ts timestamp,
943            cpu double default 0,
944            memory double,
945            TIME INDEX (ts),
946            PRIMARY KEY(host)
947      )
948      PARTITION ON COLUMNS (host) ()
949      engine=mito
950      with(ttl='7d', hello='world');
951";
952        let result =
953            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
954        assert_matches!(result, Err(Error::InvalidTableOption { .. }));
955
956        // A whitelisted semantic key with an in-domain value is accepted.
957        let semantic = |with: &str| {
958            let sql =
959                format!("create table demo(host string, ts timestamp time index) with({with});");
960            ParserContext::create_with_dialect(&sql, &GreptimeDbDialect {}, ParseOptions::default())
961        };
962        assert!(semantic("'greptime.semantic.signal_type'='metric'").is_ok());
963        // An out-of-domain value is rejected.
964        assert_matches!(
965            semantic("'greptime.semantic.signal_type'='spans'"),
966            Err(Error::InvalidTableOption { .. })
967        );
968        // An unknown key under the semantic prefix is rejected.
969        assert_matches!(
970            semantic("'greptime.semantic.bogus'='x'"),
971            Err(Error::InvalidTableOption { .. })
972        );
973    }
974
975    #[test]
976    fn test_display_json2_type_hints_quotes_path_segments() {
977        let sql = r#"CREATE TABLE traces (
978            log_json_data JSON2 (
979                "service.name" STRING,
980                "a.b"."c" INT64 NOT NULL,
981                a."b.c" STRING
982            ),
983            ts TIMESTAMP TIME INDEX
984        )"#;
985        let result =
986            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
987                .unwrap();
988
989        match &result[0] {
990            Statement::CreateTable(c) => {
991                let new_sql = format!("\n{}", c);
992                assert_eq!(
993                    r#"
994CREATE TABLE traces (
995  log_json_data JSON2(
996    "service.name" STRING NULL,
997    "a.b"."c" BIGINT NOT NULL,
998    "a"."b.c" STRING NULL
999  ),
1000  ts TIMESTAMP NOT NULL,
1001  TIME INDEX (ts)
1002)
1003ENGINE=mito
1004"#,
1005                    &new_sql
1006                );
1007
1008                let new_result = ParserContext::create_with_dialect(
1009                    &new_sql,
1010                    &GreptimeDbDialect {},
1011                    ParseOptions::default(),
1012                )
1013                .unwrap();
1014                assert_eq!(result, new_result);
1015            }
1016            _ => unreachable!(),
1017        }
1018    }
1019
1020    #[test]
1021    fn test_display_json2_type_hints_quotes_numeric_segments() {
1022        let sql = r#"CREATE TABLE traces (
1023            log_json_data JSON2 (
1024                "1abc" STRING,
1025                a."2b" INT64 NOT NULL
1026            ),
1027            ts TIMESTAMP TIME INDEX
1028        )"#;
1029        let result =
1030            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1031                .unwrap();
1032
1033        match &result[0] {
1034            Statement::CreateTable(c) => {
1035                let new_sql = format!("\n{}", c);
1036                assert_eq!(
1037                    r#"
1038CREATE TABLE traces (
1039  log_json_data JSON2(
1040    "1abc" STRING NULL,
1041    "a"."2b" BIGINT NOT NULL
1042  ),
1043  ts TIMESTAMP NOT NULL,
1044  TIME INDEX (ts)
1045)
1046ENGINE=mito
1047"#,
1048                    &new_sql
1049                );
1050
1051                let new_result = ParserContext::create_with_dialect(
1052                    &new_sql,
1053                    &GreptimeDbDialect {},
1054                    ParseOptions::default(),
1055                )
1056                .unwrap();
1057                assert_eq!(result, new_result);
1058            }
1059            _ => unreachable!(),
1060        }
1061    }
1062
1063    #[test]
1064    fn test_json2_type_hint_default_builds_default_constraint() {
1065        let sql = r#"CREATE TABLE traces (
1066            log_json_data JSON2 (
1067                status_code INT64 DEFAULT -5,
1068                duration FLOAT64 DEFAULT +1.5,
1069                error BOOLEAN DEFAULT false,
1070                message STRING DEFAULT 'unknown'
1071            ),
1072            ts TIMESTAMP TIME INDEX
1073        )"#;
1074        let result =
1075            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1076                .unwrap();
1077
1078        let Statement::CreateTable(create_table) = &result[0] else {
1079            unreachable!()
1080        };
1081        let settings = create_table.columns[0]
1082            .extensions
1083            .build_json_settings()
1084            .unwrap()
1085            .unwrap();
1086        let hints = settings.type_hints();
1087
1088        assert_eq!(hints[0].data_type, ConcreteDataType::int64_datatype());
1089        assert_eq!(
1090            hints[0].default_constraint,
1091            Some(ColumnDefaultConstraint::Value(Value::Int64(-5)))
1092        );
1093        assert_eq!(hints[1].data_type, ConcreteDataType::float64_datatype());
1094        assert_eq!(
1095            hints[1].default_constraint,
1096            Some(ColumnDefaultConstraint::Value(Value::Float64(1.5.into())))
1097        );
1098        assert_eq!(hints[2].data_type, ConcreteDataType::boolean_datatype());
1099        assert_eq!(
1100            hints[2].default_constraint,
1101            Some(ColumnDefaultConstraint::Value(Value::Boolean(false)))
1102        );
1103        assert_eq!(hints[3].data_type, ConcreteDataType::string_datatype());
1104        assert_eq!(
1105            hints[3].default_constraint,
1106            Some(ColumnDefaultConstraint::Value(Value::String(
1107                "unknown".into()
1108            )))
1109        );
1110    }
1111
1112    #[test]
1113    fn test_json2_type_hint_not_null_default_null_is_rejected() {
1114        let sql = r#"CREATE TABLE traces (
1115            log_json_data JSON2 (
1116                status_code INT64 NOT NULL DEFAULT NULL
1117            ),
1118            ts TIMESTAMP TIME INDEX
1119        )"#;
1120        let result =
1121            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1122                .unwrap();
1123
1124        let Statement::CreateTable(create_table) = &result[0] else {
1125            unreachable!()
1126        };
1127        let err = create_table.columns[0]
1128            .extensions
1129            .build_json_settings()
1130            .unwrap_err();
1131        assert!(
1132            err.to_string()
1133                .contains("Default value should not be null for non null column")
1134        );
1135    }
1136
1137    #[test]
1138    fn test_set_json_settings_normalizes_type_hint_sql_types() -> Result<()> {
1139        let mut extensions = super::ColumnExtensions::default();
1140        let settings = JsonSettings::try_new(
1141            vec![
1142                DatatypeJsonTypeHint {
1143                    path: vec!["i".to_string()],
1144                    data_type: ConcreteDataType::int32_datatype(),
1145                    nullable: true,
1146                    default_constraint: None,
1147                    inverted_index: false,
1148                },
1149                DatatypeJsonTypeHint {
1150                    path: vec!["f".to_string()],
1151                    data_type: ConcreteDataType::float32_datatype(),
1152                    nullable: true,
1153                    default_constraint: None,
1154                    inverted_index: false,
1155                },
1156                DatatypeJsonTypeHint {
1157                    path: vec!["u".to_string()],
1158                    data_type: ConcreteDataType::uint32_datatype(),
1159                    nullable: true,
1160                    default_constraint: None,
1161                    inverted_index: false,
1162                },
1163                DatatypeJsonTypeHint {
1164                    path: vec!["s".to_string()],
1165                    data_type: ConcreteDataType::string_datatype(),
1166                    nullable: true,
1167                    default_constraint: None,
1168                    inverted_index: false,
1169                },
1170                DatatypeJsonTypeHint {
1171                    path: vec!["b".to_string()],
1172                    data_type: ConcreteDataType::boolean_datatype(),
1173                    nullable: true,
1174                    default_constraint: None,
1175                    inverted_index: false,
1176                },
1177            ],
1178            None,
1179        )?;
1180        extensions.set_json_settings(settings)?;
1181
1182        assert_eq!(
1183            extensions
1184                .json2_options
1185                .unwrap()
1186                .type_hints
1187                .iter()
1188                .map(|hint| hint.data_type.to_string())
1189                .collect::<Vec<_>>(),
1190            vec!["BIGINT", "DOUBLE", "BIGINT UNSIGNED", "STRING", "BOOLEAN"]
1191        );
1192        Ok(())
1193    }
1194
1195    #[test]
1196    fn test_set_json_settings_rejects_unsupported_type_hint_type() -> Result<()> {
1197        let err = JsonSettings::try_new(
1198            vec![DatatypeJsonTypeHint {
1199                path: vec!["u".to_string()],
1200                data_type: ConcreteDataType::date_datatype(),
1201                nullable: true,
1202                default_constraint: None,
1203                inverted_index: false,
1204            }],
1205            None,
1206        )
1207        .unwrap_err();
1208
1209        assert!(
1210            err.to_string()
1211                .contains("unsupported JSON2 type hint data type")
1212        );
1213        Ok(())
1214    }
1215
1216    #[test]
1217    fn test_set_empty_json_settings_omits_json2_options() -> Result<()> {
1218        let mut extensions = ColumnExtensions::default();
1219        extensions.set_json_settings(JsonSettings::default())?;
1220        assert!(extensions.json2_options.is_none());
1221        Ok(())
1222    }
1223
1224    #[test]
1225    fn test_display_create_database() {
1226        let sql = r"create database test;";
1227        let stmts =
1228            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1229                .unwrap();
1230        assert_eq!(1, stmts.len());
1231        assert_matches!(&stmts[0], Statement::CreateDatabase { .. });
1232
1233        match &stmts[0] {
1234            Statement::CreateDatabase(set) => {
1235                let new_sql = format!("\n{}", set);
1236                assert_eq!(
1237                    r#"
1238CREATE DATABASE test"#,
1239                    &new_sql
1240                );
1241            }
1242            _ => {
1243                unreachable!();
1244            }
1245        }
1246
1247        let sql = r"create database if not exists test;";
1248        let stmts =
1249            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1250                .unwrap();
1251        assert_eq!(1, stmts.len());
1252        assert_matches!(&stmts[0], Statement::CreateDatabase { .. });
1253
1254        match &stmts[0] {
1255            Statement::CreateDatabase(set) => {
1256                let new_sql = format!("\n{}", set);
1257                assert_eq!(
1258                    r#"
1259CREATE DATABASE IF NOT EXISTS test"#,
1260                    &new_sql
1261                );
1262            }
1263            _ => {
1264                unreachable!();
1265            }
1266        }
1267
1268        let sql = r#"CREATE DATABASE IF NOT EXISTS test WITH (ttl='1h');"#;
1269        let stmts =
1270            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1271                .unwrap();
1272        assert_eq!(1, stmts.len());
1273        assert_matches!(&stmts[0], Statement::CreateDatabase { .. });
1274
1275        match &stmts[0] {
1276            Statement::CreateDatabase(set) => {
1277                let new_sql = format!("\n{}", set);
1278                assert_eq!(
1279                    r#"
1280CREATE DATABASE IF NOT EXISTS test
1281WITH(
1282  ttl = '1h'
1283)"#,
1284                    &new_sql
1285                );
1286            }
1287            _ => {
1288                unreachable!();
1289            }
1290        }
1291    }
1292
1293    #[test]
1294    fn test_display_create_table_like() {
1295        let sql = r"create table t2 like t1;";
1296        let stmts =
1297            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1298                .unwrap();
1299        assert_eq!(1, stmts.len());
1300        assert_matches!(&stmts[0], Statement::CreateTableLike { .. });
1301
1302        match &stmts[0] {
1303            Statement::CreateTableLike(create) => {
1304                let new_sql = format!("\n{}", create);
1305                assert_eq!(
1306                    r#"
1307CREATE TABLE t2 LIKE t1"#,
1308                    &new_sql
1309                );
1310            }
1311            _ => {
1312                unreachable!();
1313            }
1314        }
1315    }
1316
1317    #[test]
1318    fn test_display_create_external_table() {
1319        let sql = r#"CREATE EXTERNAL TABLE city (
1320            host string,
1321            ts timestamp,
1322            cpu float64 default 0,
1323            memory float64,
1324            TIME INDEX (ts),
1325            PRIMARY KEY(host)
1326) WITH (location='/var/data/city.csv', format='csv');"#;
1327        let stmts =
1328            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1329                .unwrap();
1330        assert_eq!(1, stmts.len());
1331        assert_matches!(&stmts[0], Statement::CreateExternalTable { .. });
1332
1333        match &stmts[0] {
1334            Statement::CreateExternalTable(create) => {
1335                let new_sql = format!("\n{}", create);
1336                assert_eq!(
1337                    r#"
1338CREATE EXTERNAL TABLE city (
1339  host STRING,
1340  ts TIMESTAMP,
1341  cpu DOUBLE DEFAULT 0,
1342  memory DOUBLE,
1343  TIME INDEX (ts),
1344  PRIMARY KEY (host)
1345)
1346ENGINE=file
1347WITH(
1348  format = 'csv',
1349  location = '/var/data/city.csv'
1350)"#,
1351                    &new_sql
1352                );
1353            }
1354            _ => {
1355                unreachable!();
1356            }
1357        }
1358    }
1359
1360    #[test]
1361    fn test_display_create_flow() {
1362        let sql = r"CREATE FLOW filter_numbers
1363            SINK TO out_num_cnt
1364            AS SELECT number FROM numbers_input where number > 10;";
1365        let result =
1366            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1367                .unwrap();
1368        assert_eq!(1, result.len());
1369
1370        match &result[0] {
1371            Statement::CreateFlow(c) => {
1372                let new_sql = format!("\n{}", c);
1373                assert_eq!(
1374                    r#"
1375CREATE FLOW filter_numbers
1376SINK TO out_num_cnt
1377AS SELECT number FROM numbers_input where number > 10"#,
1378                    &new_sql
1379                );
1380
1381                let new_result = ParserContext::create_with_dialect(
1382                    &new_sql,
1383                    &GreptimeDbDialect {},
1384                    ParseOptions::default(),
1385                )
1386                .unwrap();
1387                assert_eq!(result, new_result);
1388            }
1389            _ => unreachable!(),
1390        }
1391    }
1392
1393    #[test]
1394    fn test_vector_index_options_validation() {
1395        use super::{ColumnExtensions, OptionMap};
1396
1397        // Test zero connectivity should fail
1398        let extensions = ColumnExtensions {
1399            vector_index_options: Some(OptionMap::from([(
1400                "connectivity".to_string(),
1401                "0".to_string(),
1402            )])),
1403            ..Default::default()
1404        };
1405        let result = extensions.build_vector_index_options();
1406        assert!(result.is_err());
1407        assert!(
1408            result
1409                .unwrap_err()
1410                .to_string()
1411                .contains("connectivity must be in the range [2, 2048]")
1412        );
1413
1414        // Test zero expansion_add should fail
1415        let extensions = ColumnExtensions {
1416            vector_index_options: Some(OptionMap::from([(
1417                "expansion_add".to_string(),
1418                "0".to_string(),
1419            )])),
1420            ..Default::default()
1421        };
1422        let result = extensions.build_vector_index_options();
1423        assert!(result.is_err());
1424        assert!(
1425            result
1426                .unwrap_err()
1427                .to_string()
1428                .contains("expansion_add must be greater than 0")
1429        );
1430
1431        // Test zero expansion_search should fail
1432        let extensions = ColumnExtensions {
1433            vector_index_options: Some(OptionMap::from([(
1434                "expansion_search".to_string(),
1435                "0".to_string(),
1436            )])),
1437            ..Default::default()
1438        };
1439        let result = extensions.build_vector_index_options();
1440        assert!(result.is_err());
1441        assert!(
1442            result
1443                .unwrap_err()
1444                .to_string()
1445                .contains("expansion_search must be greater than 0")
1446        );
1447
1448        // Test valid values should succeed
1449        let extensions = ColumnExtensions {
1450            vector_index_options: Some(OptionMap::from([
1451                ("connectivity".to_string(), "32".to_string()),
1452                ("expansion_add".to_string(), "200".to_string()),
1453                ("expansion_search".to_string(), "100".to_string()),
1454            ])),
1455            ..Default::default()
1456        };
1457        let result = extensions.build_vector_index_options();
1458        assert!(result.is_ok());
1459        let options = result.unwrap().unwrap();
1460        assert_eq!(options.connectivity, 32);
1461        assert_eq!(options.expansion_add, 200);
1462        assert_eq!(options.expansion_search, 100);
1463    }
1464}