Skip to main content

query/sql/
show_create_table.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
15//! Implementation of `SHOW CREATE TABLE` statement.
16
17use std::collections::HashMap;
18
19use arrow_schema::extension::ExtensionType;
20use common_meta::SchemaOptions;
21use datatypes::extension::json::JsonExtensionType;
22use datatypes::schema::{
23    COLUMN_FULLTEXT_OPT_KEY_ANALYZER, COLUMN_FULLTEXT_OPT_KEY_BACKEND,
24    COLUMN_FULLTEXT_OPT_KEY_CASE_SENSITIVE, COLUMN_FULLTEXT_OPT_KEY_FALSE_POSITIVE_RATE,
25    COLUMN_FULLTEXT_OPT_KEY_GRANULARITY, COLUMN_SKIPPING_INDEX_OPT_KEY_FALSE_POSITIVE_RATE,
26    COLUMN_SKIPPING_INDEX_OPT_KEY_GRANULARITY, COLUMN_SKIPPING_INDEX_OPT_KEY_TYPE,
27    COLUMN_VECTOR_INDEX_OPT_KEY_CONNECTIVITY, COLUMN_VECTOR_INDEX_OPT_KEY_ENGINE,
28    COLUMN_VECTOR_INDEX_OPT_KEY_EXPANSION_ADD, COLUMN_VECTOR_INDEX_OPT_KEY_EXPANSION_SEARCH,
29    COLUMN_VECTOR_INDEX_OPT_KEY_METRIC, COMMENT_KEY, ColumnDefaultConstraint, ColumnSchema,
30    FulltextBackend, SchemaRef,
31};
32use datatypes::types::JsonFormat;
33use snafu::ResultExt;
34use sql::ast::{ColumnDef, ColumnOption, ColumnOptionDef, DataType, Expr, Ident, ObjectName};
35use sql::dialect::GreptimeDbDialect;
36use sql::parser::ParserContext;
37use sql::statements::create::{Column, ColumnExtensions, CreateTable, TableConstraint};
38use sql::statements::{self, OptionMap, concrete_data_type_to_sql_data_type};
39use store_api::metric_engine_consts::{is_metric_engine, is_metric_engine_internal_column};
40use table::metadata::{TableInfoRef, TableMeta};
41use table::requests::{
42    COMMENT_KEY as TABLE_COMMENT_KEY, FILE_TABLE_META_KEY, TTL_KEY, WRITE_BUFFER_SIZE_KEY,
43};
44
45use crate::error::{
46    ConvertSqlTypeSnafu, ConvertSqlValueSnafu, GetFulltextOptionsSnafu,
47    GetSkippingIndexOptionsSnafu, GetVectorIndexOptionsSnafu, Result, SqlSnafu,
48};
49
50/// Generates CREATE TABLE options from given table metadata and schema-level options.
51fn create_sql_options(table_meta: &TableMeta, schema_options: Option<SchemaOptions>) -> OptionMap {
52    let table_opts = &table_meta.options;
53    let mut options = OptionMap::default();
54    if let Some(write_buffer_size) = table_opts.write_buffer_size {
55        options.insert(
56            WRITE_BUFFER_SIZE_KEY.to_string(),
57            write_buffer_size.to_string(),
58        );
59    }
60    if let Some(ttl) = table_opts.ttl.map(|t| t.to_string()) {
61        options.insert(TTL_KEY.to_string(), ttl);
62    } else if let Some(database_ttl) = schema_options
63        .as_ref()
64        .and_then(|o| o.ttl)
65        .map(|ttl| ttl.to_string())
66    {
67        options.insert(TTL_KEY.to_string(), database_ttl);
68    };
69
70    for (k, v) in table_opts
71        .extra_options
72        .iter()
73        .filter(|(k, _)| k != &FILE_TABLE_META_KEY)
74    {
75        options.insert(k.clone(), v.clone());
76    }
77    options
78}
79
80#[inline]
81fn column_option_def(option: ColumnOption) -> ColumnOptionDef {
82    ColumnOptionDef { name: None, option }
83}
84
85fn create_column(column_schema: &ColumnSchema, quote_style: char) -> Result<Column> {
86    let name = &column_schema.name;
87    let mut options = Vec::with_capacity(2);
88    let mut extensions = ColumnExtensions::default();
89
90    if column_schema.is_nullable() {
91        options.push(column_option_def(ColumnOption::Null));
92    } else {
93        options.push(column_option_def(ColumnOption::NotNull));
94    }
95
96    if let Some(c) = column_schema.default_constraint() {
97        let expr = match c {
98            ColumnDefaultConstraint::Value(v) => Expr::Value(
99                statements::value_to_sql_value(v)
100                    .with_context(|_| ConvertSqlValueSnafu { value: v.clone() })?
101                    .into(),
102            ),
103            ColumnDefaultConstraint::Function(expr) => {
104                ParserContext::parse_function(expr, &GreptimeDbDialect {}).context(SqlSnafu)?
105            }
106        };
107
108        options.push(column_option_def(ColumnOption::Default(expr)));
109    }
110
111    if let Some(c) = column_schema.metadata().get(COMMENT_KEY) {
112        options.push(column_option_def(ColumnOption::Comment(c.clone())));
113    }
114
115    if let Some(opt) = column_schema
116        .fulltext_options()
117        .context(GetFulltextOptionsSnafu)?
118        && opt.enable
119    {
120        let mut map = HashMap::from([
121            (
122                COLUMN_FULLTEXT_OPT_KEY_ANALYZER.to_string(),
123                opt.analyzer.to_string(),
124            ),
125            (
126                COLUMN_FULLTEXT_OPT_KEY_CASE_SENSITIVE.to_string(),
127                opt.case_sensitive.to_string(),
128            ),
129            (
130                COLUMN_FULLTEXT_OPT_KEY_BACKEND.to_string(),
131                opt.backend.to_string(),
132            ),
133        ]);
134        if opt.backend == FulltextBackend::Bloom {
135            map.insert(
136                COLUMN_FULLTEXT_OPT_KEY_GRANULARITY.to_string(),
137                opt.granularity.to_string(),
138            );
139            map.insert(
140                COLUMN_FULLTEXT_OPT_KEY_FALSE_POSITIVE_RATE.to_string(),
141                opt.false_positive_rate().to_string(),
142            );
143        }
144        extensions.fulltext_index_options = Some(map.into());
145    }
146
147    if let Some(opt) = column_schema
148        .skipping_index_options()
149        .context(GetSkippingIndexOptionsSnafu)?
150    {
151        let map = HashMap::from([
152            (
153                COLUMN_SKIPPING_INDEX_OPT_KEY_GRANULARITY.to_string(),
154                opt.granularity.to_string(),
155            ),
156            (
157                COLUMN_SKIPPING_INDEX_OPT_KEY_FALSE_POSITIVE_RATE.to_string(),
158                opt.false_positive_rate().to_string(),
159            ),
160            (
161                COLUMN_SKIPPING_INDEX_OPT_KEY_TYPE.to_string(),
162                opt.index_type.to_string(),
163            ),
164        ]);
165        extensions.skipping_index_options = Some(map.into());
166    }
167
168    if let Some(opt) = column_schema
169        .vector_index_options()
170        .context(GetVectorIndexOptionsSnafu)?
171    {
172        let map = HashMap::from([
173            (
174                COLUMN_VECTOR_INDEX_OPT_KEY_ENGINE.to_string(),
175                opt.engine.to_string(),
176            ),
177            (
178                COLUMN_VECTOR_INDEX_OPT_KEY_METRIC.to_string(),
179                opt.metric.to_string(),
180            ),
181            (
182                COLUMN_VECTOR_INDEX_OPT_KEY_CONNECTIVITY.to_string(),
183                opt.connectivity.to_string(),
184            ),
185            (
186                COLUMN_VECTOR_INDEX_OPT_KEY_EXPANSION_ADD.to_string(),
187                opt.expansion_add.to_string(),
188            ),
189            (
190                COLUMN_VECTOR_INDEX_OPT_KEY_EXPANSION_SEARCH.to_string(),
191                opt.expansion_search.to_string(),
192            ),
193        ]);
194        extensions.vector_index_options = Some(map.into());
195    }
196
197    if column_schema.is_inverted_indexed() {
198        extensions.inverted_index_options = Some(HashMap::new().into());
199    }
200
201    let mut data_type = concrete_data_type_to_sql_data_type(&column_schema.data_type)
202        .with_context(|_| ConvertSqlTypeSnafu {
203            datatype: column_schema.data_type.clone(),
204        })?;
205
206    if matches!(
207        &column_schema.data_type,
208        datatypes::data_type::ConcreteDataType::Json(json_type)
209            if matches!(json_type.format, JsonFormat::Json2(_))
210    ) {
211        data_type = DataType::Custom(ObjectName::from(vec![Ident::new("JSON2")]), vec![]);
212    }
213
214    if let Some(json_extension) = column_schema.extension_type::<JsonExtensionType>()? {
215        let settings = json_extension
216            .metadata()
217            .json_settings
218            .clone()
219            .unwrap_or_default();
220        extensions.set_json_settings(settings).context(SqlSnafu)?;
221    }
222
223    Ok(Column {
224        column_def: ColumnDef {
225            name: Ident::with_quote(quote_style, name),
226            data_type,
227            options,
228        },
229        extensions,
230    })
231}
232
233/// Returns the primary key columns for `SHOW CREATE TABLE` statement.
234///
235/// For metric engine, it will only return the primary key columns that are not internal columns.
236fn primary_key_columns_for_show_create<'a>(
237    table_meta: &'a TableMeta,
238    engine: &str,
239) -> Vec<&'a String> {
240    let is_metric_engine = is_metric_engine(engine);
241    if is_metric_engine {
242        table_meta
243            .row_key_column_names()
244            .filter(|name| !is_metric_engine_internal_column(name))
245            .collect()
246    } else {
247        table_meta.row_key_column_names().collect()
248    }
249}
250
251fn create_table_constraints(
252    engine: &str,
253    schema: &SchemaRef,
254    table_meta: &TableMeta,
255    quote_style: char,
256) -> Vec<TableConstraint> {
257    let mut constraints = Vec::with_capacity(2);
258    if let Some(timestamp_column) = schema.timestamp_column() {
259        let column_name = &timestamp_column.name;
260        constraints.push(TableConstraint::TimeIndex {
261            column: Ident::with_quote(quote_style, column_name),
262        });
263    }
264    if !table_meta.primary_key_indices.is_empty() {
265        let columns = primary_key_columns_for_show_create(table_meta, engine)
266            .into_iter()
267            .map(|name| Ident::with_quote(quote_style, name))
268            .collect();
269        constraints.push(TableConstraint::PrimaryKey { columns });
270    }
271
272    constraints
273}
274
275/// Create a CreateTable statement from table info.
276pub fn create_table_stmt(
277    table_info: &TableInfoRef,
278    schema_options: Option<SchemaOptions>,
279    quote_style: char,
280) -> Result<CreateTable> {
281    let table_meta = &table_info.meta;
282    let table_name = &table_info.name;
283    let schema = &table_info.meta.schema;
284    let is_metric_engine = is_metric_engine(&table_meta.engine);
285    let columns = schema
286        .column_schemas()
287        .iter()
288        .filter_map(|c| {
289            if is_metric_engine && is_metric_engine_internal_column(&c.name) {
290                None
291            } else {
292                Some(create_column(c, quote_style))
293            }
294        })
295        .collect::<Result<Vec<_>>>()?;
296
297    let constraints = create_table_constraints(&table_meta.engine, schema, table_meta, quote_style);
298
299    let mut options = create_sql_options(table_meta, schema_options);
300    if let Some(comment) = &table_info.desc
301        && options.get(TABLE_COMMENT_KEY).is_none()
302    {
303        options.insert(format!("'{TABLE_COMMENT_KEY}'"), comment.clone());
304    }
305
306    Ok(CreateTable {
307        if_not_exists: true,
308        table_id: table_info.ident.table_id,
309        name: ObjectName::from(vec![Ident::with_quote(quote_style, table_name)]),
310        columns,
311        engine: table_meta.engine.clone(),
312        constraints,
313        options,
314        partitions: None,
315    })
316}
317
318#[cfg(test)]
319mod tests {
320    use std::sync::Arc;
321    use std::time::Duration;
322
323    use common_time::timestamp::TimeUnit;
324    use datatypes::prelude::ConcreteDataType;
325    use datatypes::schema::{
326        FulltextOptions, Schema, SchemaRef, SkippingIndexOptions, VectorIndexOptions,
327    };
328    use table::metadata::*;
329    use table::requests::{
330        FILE_TABLE_FORMAT_KEY, FILE_TABLE_LOCATION_KEY, FILE_TABLE_META_KEY, TableOptions,
331    };
332
333    use super::*;
334
335    #[test]
336    fn test_show_create_table_sql() {
337        let schema = vec![
338            ColumnSchema::new("id", ConcreteDataType::uint32_datatype(), true)
339                .with_skipping_options(SkippingIndexOptions {
340                    granularity: 4096,
341                    ..Default::default()
342                })
343                .unwrap(),
344            ColumnSchema::new("host", ConcreteDataType::string_datatype(), true)
345                .with_inverted_index(true),
346            ColumnSchema::new("cpu", ConcreteDataType::float64_datatype(), true),
347            ColumnSchema::new("disk", ConcreteDataType::float32_datatype(), true),
348            ColumnSchema::new("msg", ConcreteDataType::string_datatype(), true)
349                .with_fulltext_options(FulltextOptions {
350                    enable: true,
351                    ..Default::default()
352                })
353                .unwrap(),
354            ColumnSchema::new("embedding", ConcreteDataType::vector_datatype(4), true)
355                .with_vector_index_options(&VectorIndexOptions::default())
356                .unwrap(),
357            ColumnSchema::new(
358                "ts",
359                ConcreteDataType::timestamp_datatype(TimeUnit::Millisecond),
360                false,
361            )
362            .with_default_constraint(Some(ColumnDefaultConstraint::Function(String::from(
363                "current_timestamp()",
364            ))))
365            .unwrap()
366            .with_time_index(true),
367        ];
368
369        let table_schema = SchemaRef::new(Schema::new(schema));
370        let table_name = "system_metrics";
371        let schema_name = "public".to_string();
372        let catalog_name = "greptime".to_string();
373
374        let mut options = table::requests::TableOptions {
375            ttl: Some(Duration::from_secs(30).into()),
376            ..Default::default()
377        };
378
379        let _ = options
380            .extra_options
381            .insert("compaction.type".to_string(), "twcs".to_string());
382
383        let meta = TableMetaBuilder::empty()
384            .schema(table_schema)
385            .primary_key_indices(vec![0, 1])
386            .value_indices(vec![2, 3])
387            .engine("mito".to_string())
388            .next_column_id(0)
389            .options(options)
390            .created_on(Default::default())
391            .build()
392            .unwrap();
393
394        let info = Arc::new(
395            TableInfoBuilder::default()
396                .table_id(1024)
397                .table_version(0 as TableVersion)
398                .name(table_name)
399                .schema_name(schema_name)
400                .catalog_name(catalog_name)
401                .desc(None)
402                .table_type(TableType::Base)
403                .meta(meta)
404                .build()
405                .unwrap(),
406        );
407
408        let stmt = create_table_stmt(&info, None, '"').unwrap();
409
410        let sql = format!("\n{}", stmt);
411        assert_eq!(
412            r#"
413CREATE TABLE IF NOT EXISTS "system_metrics" (
414  "id" INT UNSIGNED NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '4096', type = 'BLOOM'),
415  "host" STRING NULL INVERTED INDEX,
416  "cpu" DOUBLE NULL,
417  "disk" FLOAT NULL,
418  "msg" STRING NULL FULLTEXT INDEX WITH(analyzer = 'English', backend = 'bloom', case_sensitive = 'false', false_positive_rate = '0.01', granularity = '10240'),
419  "embedding" VECTOR(4) NULL VECTOR INDEX WITH(connectivity = '16', engine = 'usearch', expansion_add = '128', expansion_search = '64', metric = 'l2sq'),
420  "ts" TIMESTAMP(3) NOT NULL DEFAULT current_timestamp(),
421  TIME INDEX ("ts"),
422  PRIMARY KEY ("id", "host")
423)
424ENGINE=mito
425WITH(
426  'compaction.type' = 'twcs',
427  ttl = '30s'
428)"#,
429            sql
430        );
431    }
432
433    #[test]
434    fn test_show_create_legacy_json_with_json_extension() {
435        let mut json_column = ColumnSchema::new("j", ConcreteDataType::json_datatype(), true);
436        json_column
437            .with_extension_type(&JsonExtensionType::new(Arc::new(
438                datatypes::extension::json::JsonMetadata::default(),
439            )))
440            .unwrap();
441
442        let table_schema = SchemaRef::new(Schema::new(vec![
443            json_column,
444            ColumnSchema::new(
445                "ts",
446                ConcreteDataType::timestamp_datatype(TimeUnit::Millisecond),
447                false,
448            )
449            .with_time_index(true),
450        ]));
451        let table_name = "legacy_json";
452        let meta = TableMetaBuilder::empty()
453            .schema(table_schema)
454            .primary_key_indices(vec![])
455            .value_indices(vec![0])
456            .engine("mito".to_string())
457            .next_column_id(0)
458            .options(Default::default())
459            .created_on(Default::default())
460            .build()
461            .unwrap();
462
463        let info = Arc::new(
464            TableInfoBuilder::default()
465                .table_id(1024)
466                .table_version(0 as TableVersion)
467                .name(table_name)
468                .schema_name("public")
469                .catalog_name("greptime")
470                .desc(None)
471                .table_type(TableType::Base)
472                .meta(meta)
473                .build()
474                .unwrap(),
475        );
476
477        let stmt = create_table_stmt(&info, None, '"').unwrap();
478        let sql = format!("\n{}", stmt);
479        assert_eq!(
480            r#"
481CREATE TABLE IF NOT EXISTS "legacy_json" (
482  "j" JSON NULL,
483  "ts" TIMESTAMP(3) NOT NULL,
484  TIME INDEX ("ts")
485)
486ENGINE=mito
487"#,
488            sql
489        );
490    }
491
492    #[test]
493    fn test_show_create_external_table_sql() {
494        let schema = vec![
495            ColumnSchema::new("host", ConcreteDataType::string_datatype(), true),
496            ColumnSchema::new("cpu", ConcreteDataType::float64_datatype(), true),
497        ];
498        let table_schema = SchemaRef::new(Schema::new(schema));
499        let table_name = "system_metrics";
500        let schema_name = "public".to_string();
501        let catalog_name = "greptime".to_string();
502        let mut options: TableOptions = Default::default();
503        let _ = options
504            .extra_options
505            .insert(FILE_TABLE_LOCATION_KEY.to_string(), "foo.csv".to_string());
506        let _ = options.extra_options.insert(
507            FILE_TABLE_META_KEY.to_string(),
508            "{{\"files\":[\"foo.csv\"]}}".to_string(),
509        );
510        let _ = options
511            .extra_options
512            .insert(FILE_TABLE_FORMAT_KEY.to_string(), "csv".to_string());
513        let meta = TableMetaBuilder::empty()
514            .schema(table_schema)
515            .primary_key_indices(vec![])
516            .engine("file".to_string())
517            .next_column_id(0)
518            .options(options)
519            .created_on(Default::default())
520            .build()
521            .unwrap();
522
523        let info = Arc::new(
524            TableInfoBuilder::default()
525                .table_id(1024)
526                .table_version(0 as TableVersion)
527                .name(table_name)
528                .schema_name(schema_name)
529                .catalog_name(catalog_name)
530                .desc(None)
531                .table_type(TableType::Base)
532                .meta(meta)
533                .build()
534                .unwrap(),
535        );
536
537        let stmt = create_table_stmt(&info, None, '"').unwrap();
538
539        let sql = format!("\n{}", stmt);
540        assert_eq!(
541            r#"
542CREATE EXTERNAL TABLE IF NOT EXISTS "system_metrics" (
543  "host" STRING NULL,
544  "cpu" DOUBLE NULL,
545
546)
547ENGINE=file
548WITH(
549  format = 'csv',
550  location = 'foo.csv'
551)"#,
552            sql
553        );
554    }
555}