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::{JSON2_DEFAULT_MAX_AUTO_EXPANDED_PATHS, 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(
373            type_hints,
374            options
375                .max_auto_expanded_paths
376                .or(Some(JSON2_DEFAULT_MAX_AUTO_EXPANDED_PATHS)),
377        )?;
378        Ok(Some(settings))
379    }
380
381    pub fn set_json_settings(&mut self, settings: JsonSettings) -> Result<()> {
382        let (type_hints, max_auto_expanded_paths) = settings.into_parts();
383        let type_hints = type_hints
384            .into_iter()
385            .map(|hint| {
386                let data_type = json_type_hint_sql_data_type(&hint.data_type)?;
387                let default = hint
388                    .default_constraint
389                    .map(|constraint| column_default_constraint_to_expr(&constraint))
390                    .transpose()?;
391                Ok(JsonTypeHint {
392                    path: hint.path,
393                    data_type,
394                    nullable: hint.nullable,
395                    default,
396                    inverted_index: hint.inverted_index,
397                })
398            })
399            .collect::<Result<Vec<_>>>()?;
400        self.json2_options = (max_auto_expanded_paths.is_some() || !type_hints.is_empty())
401            .then_some(Json2Options {
402                max_auto_expanded_paths,
403                type_hints,
404            });
405        Ok(())
406    }
407}
408
409fn build_json_type_hint_default_constraint(
410    hint: &JsonTypeHint,
411) -> Result<Option<ColumnDefaultConstraint>> {
412    let Some(default) = &hint.default else {
413        return Ok(None);
414    };
415
416    let data_type = json_type_hint_concrete_data_type(&hint.data_type)?;
417    let opts = [ColumnOptionDef {
418        name: None,
419        option: ColumnOption::Default(default.clone()),
420    }];
421
422    // Use the JSON path as the column name context for default value parsing errors.
423    let json_path = hint.path.join(".");
424    let default_constraint = parse_column_default_constraint(&json_path, &data_type, &opts, None)
425        .context(crate::error::SqlCommonSnafu)?;
426
427    if let Some(constraint) = &default_constraint {
428        constraint
429            .validate(&data_type, hint.nullable)
430            .map_err(|e| {
431                InvalidSqlSnafu {
432                    msg: format!("invalid DEFAULT for JSON2 type hint '{}': {e}", json_path),
433                }
434                .build()
435            })?;
436    }
437
438    Ok(default_constraint)
439}
440
441fn json_type_hint_concrete_data_type(data_type: &DataType) -> Result<ConcreteDataType> {
442    let data_type = sql_data_type_to_concrete_data_type(data_type)?;
443    normalize_json_type_hint_concrete_data_type(&data_type)
444}
445
446fn normalize_json_type_hint_concrete_data_type(
447    data_type: &ConcreteDataType,
448) -> Result<ConcreteDataType> {
449    let normalized = match data_type {
450        ConcreteDataType::String(_) => ConcreteDataType::string_datatype(),
451        ConcreteDataType::Int8(_)
452        | ConcreteDataType::Int16(_)
453        | ConcreteDataType::Int32(_)
454        | ConcreteDataType::Int64(_) => ConcreteDataType::int64_datatype(),
455        ConcreteDataType::UInt8(_)
456        | ConcreteDataType::UInt16(_)
457        | ConcreteDataType::UInt32(_)
458        | ConcreteDataType::UInt64(_) => ConcreteDataType::uint64_datatype(),
459        ConcreteDataType::Float32(_) | ConcreteDataType::Float64(_) => {
460            ConcreteDataType::float64_datatype()
461        }
462        ConcreteDataType::Boolean(_) => ConcreteDataType::boolean_datatype(),
463        _ => {
464            return InvalidSqlSnafu {
465                msg: format!("unsupported JSON2 type hint data type: {data_type}"),
466            }
467            .fail();
468        }
469    };
470    Ok(normalized)
471}
472
473fn json_type_hint_sql_data_type(data_type: &ConcreteDataType) -> Result<DataType> {
474    let data_type = normalize_json_type_hint_concrete_data_type(data_type)?;
475    let sql_type = match data_type {
476        ConcreteDataType::String(_) => DataType::String(None),
477        ConcreteDataType::Int64(_) => DataType::BigInt(None),
478        ConcreteDataType::UInt64(_) => DataType::BigIntUnsigned(None),
479        ConcreteDataType::Float64(_) => DataType::Double(sqlparser::ast::ExactNumberInfo::None),
480        ConcreteDataType::Boolean(_) => DataType::Boolean,
481        _ => unreachable!("JSON2 type hint data type should have been normalized"),
482    };
483    Ok(sql_type)
484}
485
486fn column_default_constraint_to_expr(constraint: &ColumnDefaultConstraint) -> Result<Expr> {
487    match constraint {
488        ColumnDefaultConstraint::Value(value) => Ok(Expr::Value(value_to_sql_value(value)?.into())),
489        ColumnDefaultConstraint::Function(function) => {
490            ParserContext::parse_function(function, &GreptimeDbDialect {})
491        }
492    }
493}
494
495fn format_json_type_hint(hint: &JsonTypeHint) -> String {
496    let path = hint
497        .path
498        .iter()
499        .map(|segment| format_json_path_segment(segment))
500        .join(".");
501    let nullability = if hint.nullable { " NULL" } else { " NOT NULL" };
502    let default = hint
503        .default
504        .as_ref()
505        .map(|expr| format!(" DEFAULT {expr}"))
506        .unwrap_or_default();
507    let inverted_index = if hint.inverted_index {
508        " INVERTED INDEX"
509    } else {
510        ""
511    };
512    format!(
513        "{} {}{}{}{}",
514        path, hint.data_type, nullability, default, inverted_index
515    )
516}
517
518fn format_json_path_segment(segment: &str) -> String {
519    format!("\"{}\"", segment.replace('"', "\"\""))
520}
521
522/// Partition on columns or values.
523///
524/// - `column_list` is the list of columns in `PARTITION ON COLUMNS` clause.
525/// - `exprs` is the list of expressions in `PARTITION ON VALUES` clause, like
526///   `host <= 'host1'`, `host > 'host1' and host <= 'host2'` or `host > 'host2'`.
527///   Each expression stands for a partition.
528#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
529pub struct Partitions {
530    pub column_list: Vec<Ident>,
531    pub exprs: Vec<Expr>,
532}
533
534impl Partitions {
535    /// set quotes to all [Ident]s from column list
536    pub fn set_quote(&mut self, quote_style: char) {
537        self.column_list
538            .iter_mut()
539            .for_each(|c| c.quote_style = Some(quote_style));
540    }
541}
542
543#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut)]
544pub struct PartitionEntry {
545    pub name: Ident,
546    pub value_list: Vec<SqlValue>,
547}
548
549impl Display for PartitionEntry {
550    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
551        write!(
552            f,
553            "PARTITION {} VALUES LESS THAN ({})",
554            self.name,
555            format_list_comma!(self.value_list),
556        )
557    }
558}
559
560impl Display for Partitions {
561    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
562        if !self.column_list.is_empty() {
563            write!(
564                f,
565                "PARTITION ON COLUMNS ({}) (\n{}\n)",
566                format_list_comma!(self.column_list),
567                format_list_indent!(self.exprs),
568            )?;
569        }
570        Ok(())
571    }
572}
573
574impl Display for CreateTable {
575    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
576        write!(f, "CREATE ")?;
577        if self.engine == FILE_ENGINE {
578            write!(f, "EXTERNAL ")?;
579        }
580        write!(f, "TABLE ")?;
581        if self.if_not_exists {
582            write!(f, "IF NOT EXISTS ")?;
583        }
584        writeln!(f, "{} (", &self.name)?;
585        writeln!(f, "{},", format_list_indent!(self.columns))?;
586        writeln!(f, "{}", format_table_constraint(&self.constraints))?;
587        writeln!(f, ")")?;
588        if let Some(partitions) = &self.partitions {
589            writeln!(f, "{partitions}")?;
590        }
591        writeln!(f, "ENGINE={}", &self.engine)?;
592        if !self.options.is_empty() {
593            let options = self.options.kv_pairs();
594            write!(f, "WITH(\n{}\n)", format_list_indent!(options))?;
595        }
596        Ok(())
597    }
598}
599
600#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
601pub struct CreateDatabase {
602    pub name: ObjectName,
603    /// Create if not exists
604    pub if_not_exists: bool,
605    pub options: OptionMap,
606}
607
608impl CreateDatabase {
609    /// Creates a statement for `CREATE DATABASE`
610    pub fn new(name: ObjectName, if_not_exists: bool, options: OptionMap) -> Self {
611        Self {
612            name,
613            if_not_exists,
614            options,
615        }
616    }
617}
618
619impl Display for CreateDatabase {
620    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
621        write!(f, "CREATE DATABASE ")?;
622        if self.if_not_exists {
623            write!(f, "IF NOT EXISTS ")?;
624        }
625        write!(f, "{}", &self.name)?;
626        if !self.options.is_empty() {
627            let options = self.options.kv_pairs();
628            write!(f, "\nWITH(\n{}\n)", format_list_indent!(options))?;
629        }
630        Ok(())
631    }
632}
633
634#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
635pub struct CreateExternalTable {
636    /// Table name
637    pub name: ObjectName,
638    pub columns: Vec<Column>,
639    pub constraints: Vec<TableConstraint>,
640    /// Table options in `WITH`. All keys are lowercase.
641    pub options: OptionMap,
642    pub if_not_exists: bool,
643    pub engine: String,
644}
645
646impl Display for CreateExternalTable {
647    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
648        write!(f, "CREATE EXTERNAL TABLE ")?;
649        if self.if_not_exists {
650            write!(f, "IF NOT EXISTS ")?;
651        }
652        writeln!(f, "{} (", &self.name)?;
653        writeln!(f, "{},", format_list_indent!(self.columns))?;
654        writeln!(f, "{}", format_table_constraint(&self.constraints))?;
655        writeln!(f, ")")?;
656        writeln!(f, "ENGINE={}", &self.engine)?;
657        if !self.options.is_empty() {
658            let options = self.options.kv_pairs();
659            write!(f, "WITH(\n{}\n)", format_list_indent!(options))?;
660        }
661        Ok(())
662    }
663}
664
665#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
666pub struct CreateTableLike {
667    /// Table name
668    pub table_name: ObjectName,
669    /// The table that is designated to be imitated by `Like`
670    pub source_name: ObjectName,
671}
672
673impl Display for CreateTableLike {
674    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
675        let table_name = &self.table_name;
676        let source_name = &self.source_name;
677        write!(f, r#"CREATE TABLE {table_name} LIKE {source_name}"#)
678    }
679}
680
681#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
682pub struct CreateFlow {
683    /// Flow name
684    pub flow_name: ObjectName,
685    /// Output (sink) table name
686    pub sink_table_name: ObjectName,
687    /// Whether to replace existing task
688    pub or_replace: bool,
689    /// Create if not exist
690    pub if_not_exists: bool,
691    /// `EXPIRE AFTER`
692    /// Duration in second as `i64`
693    pub expire_after: Option<i64>,
694    /// Duration for flow evaluation interval
695    /// Duration in seconds as `i64`
696    /// If not set, flow will be evaluated based on time window size and other args.
697    pub eval_interval: Option<i64>,
698    /// Comment string
699    pub comment: Option<String>,
700    /// Flow creation options from `WITH (...)`
701    pub flow_options: OptionMap,
702    /// SQL statement
703    pub query: Box<SqlOrTql>,
704}
705
706/// Either a sql query or a tql query
707#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
708pub enum SqlOrTql {
709    Sql(GtQuery, String),
710    Tql(Tql, String),
711}
712
713impl std::fmt::Display for SqlOrTql {
714    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
715        match self {
716            Self::Sql(_, s) => write!(f, "{}", s),
717            Self::Tql(_, s) => write!(f, "{}", s),
718        }
719    }
720}
721
722impl SqlOrTql {
723    pub fn try_from_statement(
724        value: Statement,
725        original_query: &str,
726    ) -> std::result::Result<Self, crate::error::Error> {
727        match value {
728            Statement::Query(query) => Ok(Self::Sql(*query, original_query.to_string())),
729            Statement::Tql(tql) => Ok(Self::Tql(tql, original_query.to_string())),
730            _ => InvalidFlowQuerySnafu {
731                reason: format!("Expect either sql query or promql query, found {:?}", value),
732            }
733            .fail(),
734        }
735    }
736}
737
738impl Display for CreateFlow {
739    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
740        write!(f, "CREATE ")?;
741        if self.or_replace {
742            write!(f, "OR REPLACE ")?;
743        }
744        write!(f, "FLOW ")?;
745        if self.if_not_exists {
746            write!(f, "IF NOT EXISTS ")?;
747        }
748        writeln!(f, "{}", &self.flow_name)?;
749        writeln!(f, "SINK TO {}", &self.sink_table_name)?;
750        if let Some(expire_after) = &self.expire_after {
751            writeln!(f, "EXPIRE AFTER '{} s'", expire_after)?;
752        }
753        if let Some(eval_interval) = &self.eval_interval {
754            writeln!(f, "EVAL INTERVAL '{} s'", eval_interval)?;
755        }
756        if let Some(comment) = &self.comment {
757            writeln!(f, "COMMENT '{}'", comment)?;
758        }
759        if !self.flow_options.is_empty() {
760            let options = self.flow_options.kv_pairs();
761            writeln!(f, "WITH ({})", format_list_comma!(options))?;
762        }
763        write!(f, "AS {}", &self.query)
764    }
765}
766
767/// Create SQL view statement.
768#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
769pub struct CreateView {
770    /// View name
771    pub name: ObjectName,
772    /// An optional list of names to be used for columns of the view
773    pub columns: Vec<Ident>,
774    /// The clause after `As` that defines the VIEW.
775    /// Can only be either [Statement::Query] or [Statement::Tql].
776    pub query: Box<Statement>,
777    /// Whether to replace existing VIEW
778    pub or_replace: bool,
779    /// Create VIEW only when it doesn't exists
780    pub if_not_exists: bool,
781}
782
783impl Display for CreateView {
784    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
785        write!(f, "CREATE ")?;
786        if self.or_replace {
787            write!(f, "OR REPLACE ")?;
788        }
789        write!(f, "VIEW ")?;
790        if self.if_not_exists {
791            write!(f, "IF NOT EXISTS ")?;
792        }
793        write!(f, "{} ", &self.name)?;
794        if !self.columns.is_empty() {
795            write!(f, "({}) ", format_list_comma!(self.columns))?;
796        }
797        write!(f, "AS {}", &self.query)
798    }
799}
800
801#[cfg(test)]
802mod tests {
803    use std::assert_matches;
804
805    use datatypes::json::{JsonSettings, JsonTypeHint as DatatypeJsonTypeHint};
806    use datatypes::prelude::ConcreteDataType;
807    use datatypes::schema::ColumnDefaultConstraint;
808    use datatypes::value::Value;
809
810    use super::*;
811    use crate::dialect::GreptimeDbDialect;
812    use crate::error::Error;
813    use crate::parser::{ParseOptions, ParserContext};
814    use crate::statements::statement::Statement;
815
816    #[test]
817    fn test_display_create_table() {
818        let sql = r"create table if not exists demo(
819                             host string,
820                             ts timestamp,
821                             cpu double default 0,
822                             memory double,
823                             TIME INDEX (ts),
824                             PRIMARY KEY(host)
825                       )
826                       PARTITION ON COLUMNS (host) (
827                            host = 'a',
828                            host > 'a',
829                       )
830                       engine=mito
831                       with(ttl='7d', storage='File');
832         ";
833        let result =
834            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
835                .unwrap();
836        assert_eq!(1, result.len());
837
838        match &result[0] {
839            Statement::CreateTable(c) => {
840                let new_sql = format!("\n{}", c);
841                assert_eq!(
842                    r#"
843CREATE TABLE IF NOT EXISTS demo (
844  host STRING,
845  ts TIMESTAMP,
846  cpu DOUBLE DEFAULT 0,
847  memory DOUBLE,
848  TIME INDEX (ts),
849  PRIMARY KEY (host)
850)
851PARTITION ON COLUMNS (host) (
852  host = 'a',
853  host > 'a'
854)
855ENGINE=mito
856WITH(
857  storage = 'File',
858  ttl = '7d'
859)"#,
860                    &new_sql
861                );
862
863                let new_result = ParserContext::create_with_dialect(
864                    &new_sql,
865                    &GreptimeDbDialect {},
866                    ParseOptions::default(),
867                )
868                .unwrap();
869                assert_eq!(result, new_result);
870            }
871            _ => unreachable!(),
872        }
873    }
874
875    #[test]
876    fn test_display_empty_partition_column() {
877        let sql = r"create table if not exists demo(
878            host string,
879            ts timestamp,
880            cpu double default 0,
881            memory double,
882            TIME INDEX (ts),
883            PRIMARY KEY(ts, host)
884            );
885        ";
886        let result =
887            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
888                .unwrap();
889        assert_eq!(1, result.len());
890
891        match &result[0] {
892            Statement::CreateTable(c) => {
893                let new_sql = format!("\n{}", c);
894                assert_eq!(
895                    r#"
896CREATE TABLE IF NOT EXISTS demo (
897  host STRING,
898  ts TIMESTAMP,
899  cpu DOUBLE DEFAULT 0,
900  memory DOUBLE,
901  TIME INDEX (ts),
902  PRIMARY KEY (ts, host)
903)
904ENGINE=mito
905"#,
906                    &new_sql
907                );
908
909                let new_result = ParserContext::create_with_dialect(
910                    &new_sql,
911                    &GreptimeDbDialect {},
912                    ParseOptions::default(),
913                )
914                .unwrap();
915                assert_eq!(result, new_result);
916            }
917            _ => unreachable!(),
918        }
919    }
920
921    #[test]
922    fn test_validate_table_options() {
923        let sql = r"create table if not exists demo(
924            host string,
925            ts timestamp,
926            cpu double default 0,
927            memory double,
928            TIME INDEX (ts),
929            PRIMARY KEY(host)
930      )
931      PARTITION ON COLUMNS (host) ()
932      engine=mito
933      with(ttl='7d', 'compaction.type'='world');
934";
935        let result =
936            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
937                .unwrap();
938        match &result[0] {
939            Statement::CreateTable(c) => {
940                assert_eq!(2, c.options.len());
941            }
942            _ => unreachable!(),
943        }
944
945        let sql = r"create table if not exists demo(
946            host string,
947            ts timestamp,
948            cpu double default 0,
949            memory double,
950            TIME INDEX (ts),
951            PRIMARY KEY(host)
952      )
953      PARTITION ON COLUMNS (host) ()
954      engine=mito
955      with(ttl='7d', hello='world');
956";
957        let result =
958            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
959        assert_matches!(result, Err(Error::InvalidTableOption { .. }));
960
961        // A whitelisted semantic key with an in-domain value is accepted.
962        let semantic = |with: &str| {
963            let sql =
964                format!("create table demo(host string, ts timestamp time index) with({with});");
965            ParserContext::create_with_dialect(&sql, &GreptimeDbDialect {}, ParseOptions::default())
966        };
967        assert!(semantic("'greptime.semantic.signal_type'='metric'").is_ok());
968        // An out-of-domain value is rejected.
969        assert_matches!(
970            semantic("'greptime.semantic.signal_type'='spans'"),
971            Err(Error::InvalidTableOption { .. })
972        );
973        // An unknown key under the semantic prefix is rejected.
974        assert_matches!(
975            semantic("'greptime.semantic.bogus'='x'"),
976            Err(Error::InvalidTableOption { .. })
977        );
978    }
979
980    #[test]
981    fn test_display_json2_type_hints_quotes_path_segments() {
982        let sql = r#"CREATE TABLE traces (
983            log_json_data JSON2 (
984                "service.name" STRING,
985                "a.b"."c" INT64 NOT NULL,
986                a."b.c" STRING
987            ),
988            ts TIMESTAMP TIME INDEX
989        )"#;
990        let result =
991            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
992                .unwrap();
993
994        match &result[0] {
995            Statement::CreateTable(c) => {
996                let new_sql = format!("\n{}", c);
997                assert_eq!(
998                    r#"
999CREATE TABLE traces (
1000  log_json_data JSON2(
1001    "service.name" STRING NULL,
1002    "a.b"."c" BIGINT NOT NULL,
1003    "a"."b.c" STRING NULL
1004  ),
1005  ts TIMESTAMP NOT NULL,
1006  TIME INDEX (ts)
1007)
1008ENGINE=mito
1009"#,
1010                    &new_sql
1011                );
1012
1013                let new_result = ParserContext::create_with_dialect(
1014                    &new_sql,
1015                    &GreptimeDbDialect {},
1016                    ParseOptions::default(),
1017                )
1018                .unwrap();
1019                assert_eq!(result, new_result);
1020            }
1021            _ => unreachable!(),
1022        }
1023    }
1024
1025    #[test]
1026    fn test_parse_json2_max_auto_expanded_paths_option() -> Result<()> {
1027        let sql = r#"CREATE TABLE traces (
1028            log_json_data JSON2 (
1029                status_code INT64 NOT NULL,
1030                max_auto_expanded_paths = 1
1031            ),
1032            ts TIMESTAMP TIME INDEX
1033        )"#;
1034        let result = ParserContext::create_with_dialect(
1035            sql,
1036            &GreptimeDbDialect {},
1037            ParseOptions::default(),
1038        )?;
1039        let Statement::CreateTable(create_table) = &result[0] else {
1040            unreachable!()
1041        };
1042        let settings = create_table.columns[0]
1043            .extensions
1044            .build_json_settings()?
1045            .unwrap();
1046        assert_eq!(settings.max_auto_expanded_paths(), Some(1));
1047        Ok(())
1048    }
1049
1050    #[test]
1051    fn test_display_json2_type_hints_quotes_numeric_segments() {
1052        let sql = r#"CREATE TABLE traces (
1053            log_json_data JSON2 (
1054                "1abc" STRING,
1055                a."2b" INT64 NOT NULL
1056            ),
1057            ts TIMESTAMP TIME INDEX
1058        )"#;
1059        let result =
1060            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1061                .unwrap();
1062
1063        match &result[0] {
1064            Statement::CreateTable(c) => {
1065                let new_sql = format!("\n{}", c);
1066                assert_eq!(
1067                    r#"
1068CREATE TABLE traces (
1069  log_json_data JSON2(
1070    "1abc" STRING NULL,
1071    "a"."2b" BIGINT NOT NULL
1072  ),
1073  ts TIMESTAMP NOT NULL,
1074  TIME INDEX (ts)
1075)
1076ENGINE=mito
1077"#,
1078                    &new_sql
1079                );
1080
1081                let new_result = ParserContext::create_with_dialect(
1082                    &new_sql,
1083                    &GreptimeDbDialect {},
1084                    ParseOptions::default(),
1085                )
1086                .unwrap();
1087                assert_eq!(result, new_result);
1088            }
1089            _ => unreachable!(),
1090        }
1091    }
1092
1093    #[test]
1094    fn test_json2_type_hint_default_builds_default_constraint() {
1095        let sql = r#"CREATE TABLE traces (
1096            log_json_data JSON2 (
1097                status_code INT64 DEFAULT -5,
1098                duration FLOAT64 DEFAULT +1.5,
1099                error BOOLEAN DEFAULT false,
1100                message STRING DEFAULT 'unknown'
1101            ),
1102            ts TIMESTAMP TIME INDEX
1103        )"#;
1104        let result =
1105            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1106                .unwrap();
1107
1108        let Statement::CreateTable(create_table) = &result[0] else {
1109            unreachable!()
1110        };
1111        let settings = create_table.columns[0]
1112            .extensions
1113            .build_json_settings()
1114            .unwrap()
1115            .unwrap();
1116        let hints = settings.type_hints();
1117
1118        assert_eq!(hints[0].data_type, ConcreteDataType::int64_datatype());
1119        assert_eq!(
1120            hints[0].default_constraint,
1121            Some(ColumnDefaultConstraint::Value(Value::Int64(-5)))
1122        );
1123        assert_eq!(hints[1].data_type, ConcreteDataType::float64_datatype());
1124        assert_eq!(
1125            hints[1].default_constraint,
1126            Some(ColumnDefaultConstraint::Value(Value::Float64(1.5.into())))
1127        );
1128        assert_eq!(hints[2].data_type, ConcreteDataType::boolean_datatype());
1129        assert_eq!(
1130            hints[2].default_constraint,
1131            Some(ColumnDefaultConstraint::Value(Value::Boolean(false)))
1132        );
1133        assert_eq!(hints[3].data_type, ConcreteDataType::string_datatype());
1134        assert_eq!(
1135            hints[3].default_constraint,
1136            Some(ColumnDefaultConstraint::Value(Value::String(
1137                "unknown".into()
1138            )))
1139        );
1140    }
1141
1142    #[test]
1143    fn test_json2_type_hint_not_null_default_null_is_rejected() {
1144        let sql = r#"CREATE TABLE traces (
1145            log_json_data JSON2 (
1146                status_code INT64 NOT NULL DEFAULT NULL
1147            ),
1148            ts TIMESTAMP TIME INDEX
1149        )"#;
1150        let result =
1151            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1152                .unwrap();
1153
1154        let Statement::CreateTable(create_table) = &result[0] else {
1155            unreachable!()
1156        };
1157        let err = create_table.columns[0]
1158            .extensions
1159            .build_json_settings()
1160            .unwrap_err();
1161        assert!(
1162            err.to_string()
1163                .contains("Default value should not be null for non null column")
1164        );
1165    }
1166
1167    #[test]
1168    fn test_set_json_settings_normalizes_type_hint_sql_types() -> Result<()> {
1169        let mut extensions = super::ColumnExtensions::default();
1170        let settings = JsonSettings::try_new(
1171            vec![
1172                DatatypeJsonTypeHint {
1173                    path: vec!["i".to_string()],
1174                    data_type: ConcreteDataType::int32_datatype(),
1175                    nullable: true,
1176                    default_constraint: None,
1177                    inverted_index: false,
1178                },
1179                DatatypeJsonTypeHint {
1180                    path: vec!["f".to_string()],
1181                    data_type: ConcreteDataType::float32_datatype(),
1182                    nullable: true,
1183                    default_constraint: None,
1184                    inverted_index: false,
1185                },
1186                DatatypeJsonTypeHint {
1187                    path: vec!["u".to_string()],
1188                    data_type: ConcreteDataType::uint32_datatype(),
1189                    nullable: true,
1190                    default_constraint: None,
1191                    inverted_index: false,
1192                },
1193                DatatypeJsonTypeHint {
1194                    path: vec!["s".to_string()],
1195                    data_type: ConcreteDataType::string_datatype(),
1196                    nullable: true,
1197                    default_constraint: None,
1198                    inverted_index: false,
1199                },
1200                DatatypeJsonTypeHint {
1201                    path: vec!["b".to_string()],
1202                    data_type: ConcreteDataType::boolean_datatype(),
1203                    nullable: true,
1204                    default_constraint: None,
1205                    inverted_index: false,
1206                },
1207            ],
1208            None,
1209        )?;
1210        extensions.set_json_settings(settings)?;
1211
1212        assert_eq!(
1213            extensions
1214                .json2_options
1215                .unwrap()
1216                .type_hints
1217                .iter()
1218                .map(|hint| hint.data_type.to_string())
1219                .collect::<Vec<_>>(),
1220            vec!["BIGINT", "DOUBLE", "BIGINT UNSIGNED", "STRING", "BOOLEAN"]
1221        );
1222        Ok(())
1223    }
1224
1225    #[test]
1226    fn test_set_json_settings_rejects_unsupported_type_hint_type() -> Result<()> {
1227        let err = JsonSettings::try_new(
1228            vec![DatatypeJsonTypeHint {
1229                path: vec!["u".to_string()],
1230                data_type: ConcreteDataType::date_datatype(),
1231                nullable: true,
1232                default_constraint: None,
1233                inverted_index: false,
1234            }],
1235            None,
1236        )
1237        .unwrap_err();
1238
1239        assert!(
1240            err.to_string()
1241                .contains("unsupported JSON2 type hint data type")
1242        );
1243        Ok(())
1244    }
1245
1246    #[test]
1247    fn test_set_empty_json_settings_omits_json2_options() -> Result<()> {
1248        let mut extensions = ColumnExtensions::default();
1249        extensions.set_json_settings(JsonSettings::default())?;
1250        assert!(extensions.json2_options.is_none());
1251        Ok(())
1252    }
1253
1254    #[test]
1255    fn test_display_create_database() {
1256        let sql = r"create database test;";
1257        let stmts =
1258            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1259                .unwrap();
1260        assert_eq!(1, stmts.len());
1261        assert_matches!(&stmts[0], Statement::CreateDatabase { .. });
1262
1263        match &stmts[0] {
1264            Statement::CreateDatabase(set) => {
1265                let new_sql = format!("\n{}", set);
1266                assert_eq!(
1267                    r#"
1268CREATE DATABASE test"#,
1269                    &new_sql
1270                );
1271            }
1272            _ => {
1273                unreachable!();
1274            }
1275        }
1276
1277        let sql = r"create database if not exists test;";
1278        let stmts =
1279            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1280                .unwrap();
1281        assert_eq!(1, stmts.len());
1282        assert_matches!(&stmts[0], Statement::CreateDatabase { .. });
1283
1284        match &stmts[0] {
1285            Statement::CreateDatabase(set) => {
1286                let new_sql = format!("\n{}", set);
1287                assert_eq!(
1288                    r#"
1289CREATE DATABASE IF NOT EXISTS test"#,
1290                    &new_sql
1291                );
1292            }
1293            _ => {
1294                unreachable!();
1295            }
1296        }
1297
1298        let sql = r#"CREATE DATABASE IF NOT EXISTS test WITH (ttl='1h');"#;
1299        let stmts =
1300            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1301                .unwrap();
1302        assert_eq!(1, stmts.len());
1303        assert_matches!(&stmts[0], Statement::CreateDatabase { .. });
1304
1305        match &stmts[0] {
1306            Statement::CreateDatabase(set) => {
1307                let new_sql = format!("\n{}", set);
1308                assert_eq!(
1309                    r#"
1310CREATE DATABASE IF NOT EXISTS test
1311WITH(
1312  ttl = '1h'
1313)"#,
1314                    &new_sql
1315                );
1316            }
1317            _ => {
1318                unreachable!();
1319            }
1320        }
1321    }
1322
1323    #[test]
1324    fn test_display_create_table_like() {
1325        let sql = r"create table t2 like t1;";
1326        let stmts =
1327            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1328                .unwrap();
1329        assert_eq!(1, stmts.len());
1330        assert_matches!(&stmts[0], Statement::CreateTableLike { .. });
1331
1332        match &stmts[0] {
1333            Statement::CreateTableLike(create) => {
1334                let new_sql = format!("\n{}", create);
1335                assert_eq!(
1336                    r#"
1337CREATE TABLE t2 LIKE t1"#,
1338                    &new_sql
1339                );
1340            }
1341            _ => {
1342                unreachable!();
1343            }
1344        }
1345    }
1346
1347    #[test]
1348    fn test_display_create_external_table() {
1349        let sql = r#"CREATE EXTERNAL TABLE city (
1350            host string,
1351            ts timestamp,
1352            cpu float64 default 0,
1353            memory float64,
1354            TIME INDEX (ts),
1355            PRIMARY KEY(host)
1356) WITH (location='/var/data/city.csv', format='csv');"#;
1357        let stmts =
1358            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1359                .unwrap();
1360        assert_eq!(1, stmts.len());
1361        assert_matches!(&stmts[0], Statement::CreateExternalTable { .. });
1362
1363        match &stmts[0] {
1364            Statement::CreateExternalTable(create) => {
1365                let new_sql = format!("\n{}", create);
1366                assert_eq!(
1367                    r#"
1368CREATE EXTERNAL TABLE city (
1369  host STRING,
1370  ts TIMESTAMP,
1371  cpu DOUBLE DEFAULT 0,
1372  memory DOUBLE,
1373  TIME INDEX (ts),
1374  PRIMARY KEY (host)
1375)
1376ENGINE=file
1377WITH(
1378  format = 'csv',
1379  location = '/var/data/city.csv'
1380)"#,
1381                    &new_sql
1382                );
1383            }
1384            _ => {
1385                unreachable!();
1386            }
1387        }
1388    }
1389
1390    #[test]
1391    fn test_display_create_flow() {
1392        let sql = r"CREATE FLOW filter_numbers
1393            SINK TO out_num_cnt
1394            AS SELECT number FROM numbers_input where number > 10;";
1395        let result =
1396            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
1397                .unwrap();
1398        assert_eq!(1, result.len());
1399
1400        match &result[0] {
1401            Statement::CreateFlow(c) => {
1402                let new_sql = format!("\n{}", c);
1403                assert_eq!(
1404                    r#"
1405CREATE FLOW filter_numbers
1406SINK TO out_num_cnt
1407AS SELECT number FROM numbers_input where number > 10"#,
1408                    &new_sql
1409                );
1410
1411                let new_result = ParserContext::create_with_dialect(
1412                    &new_sql,
1413                    &GreptimeDbDialect {},
1414                    ParseOptions::default(),
1415                )
1416                .unwrap();
1417                assert_eq!(result, new_result);
1418            }
1419            _ => unreachable!(),
1420        }
1421    }
1422
1423    #[test]
1424    fn test_vector_index_options_validation() {
1425        use super::{ColumnExtensions, OptionMap};
1426
1427        // Test zero connectivity should fail
1428        let extensions = ColumnExtensions {
1429            vector_index_options: Some(OptionMap::from([(
1430                "connectivity".to_string(),
1431                "0".to_string(),
1432            )])),
1433            ..Default::default()
1434        };
1435        let result = extensions.build_vector_index_options();
1436        assert!(result.is_err());
1437        assert!(
1438            result
1439                .unwrap_err()
1440                .to_string()
1441                .contains("connectivity must be in the range [2, 2048]")
1442        );
1443
1444        // Test zero expansion_add should fail
1445        let extensions = ColumnExtensions {
1446            vector_index_options: Some(OptionMap::from([(
1447                "expansion_add".to_string(),
1448                "0".to_string(),
1449            )])),
1450            ..Default::default()
1451        };
1452        let result = extensions.build_vector_index_options();
1453        assert!(result.is_err());
1454        assert!(
1455            result
1456                .unwrap_err()
1457                .to_string()
1458                .contains("expansion_add must be greater than 0")
1459        );
1460
1461        // Test zero expansion_search should fail
1462        let extensions = ColumnExtensions {
1463            vector_index_options: Some(OptionMap::from([(
1464                "expansion_search".to_string(),
1465                "0".to_string(),
1466            )])),
1467            ..Default::default()
1468        };
1469        let result = extensions.build_vector_index_options();
1470        assert!(result.is_err());
1471        assert!(
1472            result
1473                .unwrap_err()
1474                .to_string()
1475                .contains("expansion_search must be greater than 0")
1476        );
1477
1478        // Test valid values should succeed
1479        let extensions = ColumnExtensions {
1480            vector_index_options: Some(OptionMap::from([
1481                ("connectivity".to_string(), "32".to_string()),
1482                ("expansion_add".to_string(), "200".to_string()),
1483                ("expansion_search".to_string(), "100".to_string()),
1484            ])),
1485            ..Default::default()
1486        };
1487        let result = extensions.build_vector_index_options();
1488        assert!(result.is_ok());
1489        let options = result.unwrap().unwrap();
1490        assert_eq!(options.connectivity, 32);
1491        assert_eq!(options.expansion_add, 200);
1492        assert_eq!(options.expansion_search, 100);
1493    }
1494}