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