Skip to main content

sql/parsers/create_parser/
json.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 datatypes::extension::json::JSON2_REMAINDER_FIELD_NAME;
16use datatypes::json::JSON2_MAX_STRUCTURED_DEPTH;
17use datatypes::schema::FulltextOptions;
18use snafu::{ResultExt, ensure};
19use sqlparser::ast::{DataType, ExactNumberInfo, ObjectName};
20use sqlparser::dialect::keywords::Keyword;
21use sqlparser::parser::Parser;
22use sqlparser::tokenizer::Token;
23
24use crate::ast::Ident;
25use crate::dialect::GreptimeDbDialect;
26use crate::error::{InvalidSqlSnafu, Result, SyntaxSnafu};
27use crate::parsers::create_parser::{INVERTED, SKIPPING};
28use crate::statements::OptionMap;
29use crate::statements::create::{Json2Options, JsonTypeHint};
30
31const JSON2_TYPE_NAME: &str = "JSON2";
32const MAX_AUTO_EXPANDED_PATHS: &str = "max_auto_expanded_paths";
33
34/// Parses a JSON2 type hint path with the same grammar used by `CREATE TABLE`.
35pub fn parse_json2_type_hint_path(path: &str) -> Result<Vec<String>> {
36    let dialect = GreptimeDbDialect {};
37    let mut parser = Parser::new(&dialect)
38        .try_with_sql(path)
39        .context(SyntaxSnafu)?;
40    let path = parse_json2_path(&mut parser)?;
41    ensure!(
42        parser.peek_token().token == Token::EOF,
43        InvalidSqlSnafu {
44            msg: format!(
45                "unexpected token '{}' in JSON2 type hint path",
46                parser.peek_token()
47            )
48        }
49    );
50    Ok(path)
51}
52
53pub(crate) fn parse_json2_type_and_options(
54    parser: &mut Parser<'_>,
55) -> Result<Option<(DataType, Option<Json2Options>)>> {
56    let token = parser.peek_token();
57    let Token::Word(word) = &token.token else {
58        return Ok(None);
59    };
60
61    if !word.value.eq_ignore_ascii_case(JSON2_TYPE_NAME) || word.quote_style.is_some() {
62        return Ok(None);
63    }
64
65    parser.next_token();
66    let data_type = DataType::Custom(ObjectName::from(vec![Ident::new(JSON2_TYPE_NAME)]), vec![]);
67    let options = if parser.consume_token(&Token::LParen) {
68        parse_json2_options(parser)?
69    } else {
70        None
71    };
72
73    Ok(Some((data_type, options)))
74}
75
76fn parse_json2_options(parser: &mut Parser<'_>) -> Result<Option<Json2Options>> {
77    if parser.consume_token(&Token::RParen) {
78        return Ok(None);
79    }
80
81    let mut max_auto_expanded_paths = None;
82    let mut type_hints = Vec::new();
83    loop {
84        let token = parser.peek_token();
85        let is_max_auto_expanded_paths = matches!(
86            &token.token,
87            Token::Word(word)
88                if word.quote_style.is_none()
89                    && word.value.eq_ignore_ascii_case(MAX_AUTO_EXPANDED_PATHS)
90        );
91        if is_max_auto_expanded_paths {
92            parser.next_token();
93            ensure!(
94                max_auto_expanded_paths.is_none(),
95                InvalidSqlSnafu {
96                    msg: format!("duplicated JSON2 option '{MAX_AUTO_EXPANDED_PATHS}'")
97                }
98            );
99            parser.expect_token(&Token::Eq).context(SyntaxSnafu)?;
100
101            let token = parser.next_token();
102            let Token::Number(value, _) = token.token else {
103                return InvalidSqlSnafu {
104                    msg: format!(
105                        "JSON2 option '{MAX_AUTO_EXPANDED_PATHS}' expects a non-negative integer"
106                    ),
107                }
108                .fail();
109            };
110            max_auto_expanded_paths = Some(value.parse::<u32>().map_err(|_| {
111                InvalidSqlSnafu {
112                    msg: format!(
113                        "JSON2 option '{MAX_AUTO_EXPANDED_PATHS}' expects a non-negative integer"
114                    ),
115                }
116                .build()
117            })?);
118        } else {
119            let hint = parse_json2_type_hint(parser)?;
120            ensure_no_path_conflict(&type_hints, &hint.path)?;
121            type_hints.push(hint);
122        }
123
124        if parser.consume_token(&Token::Comma) {
125            if parser.consume_token(&Token::RParen) {
126                break;
127            }
128        } else {
129            parser.expect_token(&Token::RParen).context(SyntaxSnafu)?;
130            break;
131        }
132    }
133
134    Ok(Some(Json2Options {
135        max_auto_expanded_paths,
136        type_hints,
137    }))
138}
139
140fn parse_json2_type_hint(parser: &mut Parser<'_>) -> Result<JsonTypeHint> {
141    let path = parse_json2_path(parser)?;
142    ensure!(
143        path.first().is_none_or(|x| x != JSON2_REMAINDER_FIELD_NAME),
144        InvalidSqlSnafu {
145            msg: format!(
146                "JSON2 type hint path cannot be rooted at reserved field '{JSON2_REMAINDER_FIELD_NAME}'"
147            )
148        }
149    );
150    ensure!(
151        path.len() <= JSON2_MAX_STRUCTURED_DEPTH,
152        InvalidSqlSnafu {
153            msg: format!(
154                "JSON2 type hint path cannot exceed {JSON2_MAX_STRUCTURED_DEPTH} segments"
155            ),
156        }
157    );
158    let data_type = parser.parse_data_type().context(SyntaxSnafu)?;
159    let data_type = validate_json2_type_hint_type(data_type)?;
160
161    let mut inverted_index = false;
162    let mut skipping_index = None;
163    let mut fulltext_index = None;
164
165    loop {
166        if parser.parse_keywords(&[Keyword::NOT, Keyword::NULL])
167            || parser.parse_keyword(Keyword::NULL)
168        {
169            return InvalidSqlSnafu {
170                msg: "JSON2 type hint NULL/NOT NULL is not supported; hinted fields are always nullable"
171                    .to_string(),
172            }
173            .fail();
174        } else if parser.parse_keyword(Keyword::DEFAULT) {
175            return InvalidSqlSnafu {
176                msg: "JSON2 type hint DEFAULT is not supported".to_string(),
177            }
178            .fail();
179        } else if let Token::Word(word) = parser.peek_token().token
180            && word.value.eq_ignore_ascii_case(INVERTED)
181        {
182            parser.next_token();
183            ensure!(
184                parser.parse_keyword(Keyword::INDEX),
185                InvalidSqlSnafu {
186                    msg: format!(
187                        "expect INDEX after INVERTED keyword for JSON2 type hint '{}'",
188                        path.join(".")
189                    )
190                }
191            );
192            ensure!(
193                !inverted_index,
194                InvalidSqlSnafu {
195                    msg: format!(
196                        "duplicated INVERTED INDEX option for JSON2 type hint '{}'",
197                        path.join(".")
198                    )
199                }
200            );
201            ensure!(
202                !path
203                    .iter()
204                    .any(|segment| segment == JSON2_REMAINDER_FIELD_NAME),
205                InvalidSqlSnafu {
206                    msg: format!(
207                        "JSON2 indexed type hint path cannot contain reserved field '{JSON2_REMAINDER_FIELD_NAME}'"
208                    )
209                }
210            );
211            inverted_index = true;
212        } else if let Token::Word(word) = parser.peek_token().token
213            && word.value.eq_ignore_ascii_case(SKIPPING)
214        {
215            parser.next_token();
216            ensure!(
217                parser.parse_keyword(Keyword::INDEX),
218                InvalidSqlSnafu {
219                    msg: "expect INDEX after SKIPPING keyword",
220                }
221            );
222            ensure!(
223                skipping_index.is_none(),
224                InvalidSqlSnafu {
225                    msg: "duplicated SKIPPING INDEX option for JSON2 type hint",
226                }
227            );
228            let options = parser
229                .parse_options(Keyword::WITH)
230                .context(SyntaxSnafu)?
231                .into_iter()
232                .map(crate::util::parse_option_string)
233                .collect::<Result<Vec<_>>>()?;
234            for (key, _) in &options {
235                ensure!(
236                    crate::parsers::utils::validate_column_skipping_index_create_option(key),
237                    InvalidSqlSnafu {
238                        msg: format!("invalid SKIPPING INDEX option: {key}"),
239                    }
240                );
241            }
242            skipping_index = Some(
243                datatypes::schema::SkippingIndexOptions::try_from(
244                    OptionMap::new(options).into_map(),
245                )
246                .context(crate::error::SetSkippingIndexOptionSnafu)?,
247            );
248        } else if parser.parse_keyword(Keyword::FULLTEXT) {
249            ensure!(
250                matches!(data_type, DataType::String(None)),
251                InvalidSqlSnafu {
252                    msg: "JSON2 FULLTEXT index only supports string type hints",
253                }
254            );
255            ensure!(
256                parser.parse_keyword(Keyword::INDEX),
257                InvalidSqlSnafu {
258                    msg: "expect INDEX after FULLTEXT keyword",
259                }
260            );
261            ensure!(
262                fulltext_index.is_none(),
263                InvalidSqlSnafu {
264                    msg: "duplicated FULLTEXT INDEX option for JSON2 type hint",
265                }
266            );
267            let options = parser
268                .parse_options(Keyword::WITH)
269                .context(SyntaxSnafu)?
270                .into_iter()
271                .map(crate::util::parse_option_string)
272                .collect::<Result<Vec<_>>>()?;
273            for (key, _) in &options {
274                ensure!(
275                    crate::parsers::utils::validate_column_fulltext_create_option(key),
276                    InvalidSqlSnafu {
277                        msg: format!("invalid FULLTEXT INDEX option: {key}")
278                    }
279                );
280            }
281            fulltext_index = Some(
282                FulltextOptions::try_from(OptionMap::new(options).into_map())
283                    .context(crate::error::SetFulltextOptionSnafu)?,
284            );
285        } else if matches!(parser.peek_token().token, Token::Comma | Token::RParen) {
286            break;
287        } else {
288            return parser
289                .expected("JSON2 type hint option", parser.peek_token())
290                .context(SyntaxSnafu);
291        }
292    }
293
294    Ok(JsonTypeHint {
295        path,
296        data_type,
297        inverted_index,
298        skipping_index,
299        fulltext_index,
300    })
301}
302
303fn parse_json2_path(parser: &mut Parser<'_>) -> Result<Vec<String>> {
304    let first = parser.parse_identifier().context(SyntaxSnafu)?;
305    let mut path = vec![first.value];
306
307    while parser.consume_token(&Token::Period) {
308        let segment = parser.parse_identifier().context(SyntaxSnafu)?;
309        path.push(segment.value);
310    }
311
312    ensure!(
313        !path.iter().any(|segment| segment.is_empty()),
314        InvalidSqlSnafu {
315            msg: "JSON2 type hint path segment cannot be empty".to_string(),
316        }
317    );
318
319    Ok(path)
320}
321
322fn validate_json2_type_hint_type(data_type: DataType) -> Result<DataType> {
323    match data_type {
324        DataType::Int64
325        | DataType::UInt64
326        | DataType::Float64
327        | DataType::String(None)
328        | DataType::BigInt(None)
329        | DataType::BigIntUnsigned(None)
330        | DataType::Double(ExactNumberInfo::None)
331        | DataType::Boolean => Ok(data_type),
332        _ => InvalidSqlSnafu {
333            msg: format!(
334                "unsupported JSON2 type hint data type: {data_type}; supported types: STRING, BIGINT, BIGINT UNSIGNED, DOUBLE, BOOLEAN; supported aliases: INT64, UINT64, FLOAT64"
335            ),
336        }
337        .fail(),
338    }
339}
340
341fn ensure_no_path_conflict(hints: &[JsonTypeHint], path: &[String]) -> Result<()> {
342    for hint in hints {
343        ensure!(
344            hint.path != path,
345            InvalidSqlSnafu {
346                msg: format!("duplicated JSON2 type hint path '{}'", path.join("."))
347            }
348        );
349        ensure!(
350            !hint.path.starts_with(path) && !path.starts_with(&hint.path),
351            InvalidSqlSnafu {
352                msg: format!(
353                    "JSON2 type hint path '{}' conflicts with '{}'",
354                    path.join("."),
355                    hint.path.join(".")
356                )
357            }
358        );
359    }
360    Ok(())
361}
362
363#[cfg(test)]
364mod tests {
365    use sqlparser::ast::{DataType, ExactNumberInfo};
366
367    use super::parse_json2_type_hint_path;
368    use crate::dialect::GreptimeDbDialect;
369    use crate::parser::{ParseOptions, ParserContext};
370    use crate::statements::create::Column;
371    use crate::statements::statement::Statement;
372
373    fn parse_json2_column(sql: &str) -> Column {
374        let Statement::CreateTable(mut create_table) =
375            ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
376                .unwrap()
377                .remove(0)
378        else {
379            unreachable!()
380        };
381
382        create_table.columns.remove(0)
383    }
384
385    #[test]
386    fn test_json2_fulltext_index() {
387        use datatypes::schema::FulltextBackend;
388        for (options, backend) in [
389            ("", FulltextBackend::Bloom),
390            (
391                "WITH (backend='tantivy', analyzer='Chinese', case_sensitive='true')",
392                FulltextBackend::Tantivy,
393            ),
394            (
395                "WITH (backend='bloom', granularity='8', false_positive_rate='0.05')",
396                FulltextBackend::Bloom,
397            ),
398        ] {
399            let sql = format!(
400                "CREATE TABLE logs (attrs JSON2 (message STRING FULLTEXT INDEX {options} SKIPPING INDEX INVERTED INDEX), ts TIMESTAMP TIME INDEX)"
401            );
402            let column = parse_json2_column(&sql);
403            let options = column.extensions.json2_options.unwrap();
404            let settings = options.build_json_settings().unwrap();
405            let hint = &settings.type_hints()[0];
406            assert_eq!(hint.fulltext_index.as_ref().unwrap().backend, backend);
407            assert!(hint.fulltext_index.as_ref().unwrap().enable);
408            assert!(hint.inverted_index);
409            assert!(hint.skipping_index.is_some());
410            let rebuilt = parse_json2_column(&format!(
411                "CREATE TABLE logs (attrs JSON2 {options}, ts TIMESTAMP TIME INDEX)"
412            ));
413            assert_eq!(
414                settings,
415                rebuilt
416                    .extensions
417                    .json2_options
418                    .unwrap()
419                    .build_json_settings()
420                    .unwrap()
421            );
422        }
423        for hint in [
424            "message BIGINT FULLTEXT INDEX",
425            "message STRING FULLTEXT INDEX FULLTEXT INDEX",
426            "message STRING FULLTEXT",
427            "message STRING FULLTEXT INDEX WITH (analyzer='unknown')",
428            "message STRING FULLTEXT INDEX WITH (backend='unknown')",
429            "message STRING FULLTEXT INDEX WITH (granularity='0')",
430            "message STRING FULLTEXT INDEX WITH (false_positive_rate='0')",
431            "message STRING FULLTEXT INDEX WITH (unknown='1')",
432        ] {
433            let sql = format!("CREATE TABLE logs (attrs JSON2 ({hint}), ts TIMESTAMP TIME INDEX)");
434            let parsed = ParserContext::create_with_dialect(
435                &sql,
436                &GreptimeDbDialect {},
437                ParseOptions::default(),
438            );
439            assert!(parsed.is_err(), "{sql}");
440        }
441    }
442
443    #[test]
444    fn test_json2_skipping_index() {
445        let column = parse_json2_column(
446            "CREATE TABLE logs (attrs JSON2 (service.name STRING SKIPPING INDEX WITH (granularity='8', false_positive_rate='0.05') INVERTED INDEX, status BIGINT SKIPPING INDEX), ts TIMESTAMP TIME INDEX)",
447        );
448        let options = column.extensions.json2_options.as_ref().unwrap();
449        let settings = options.build_json_settings().unwrap();
450        let hint = &settings.type_hints()[0];
451        assert!(hint.inverted_index);
452        assert_eq!(hint.skipping_index.as_ref().unwrap().granularity, 8);
453        assert_eq!(
454            hint.skipping_index.as_ref().unwrap().false_positive_rate(),
455            0.05
456        );
457        assert_eq!(
458            settings.type_hints()[1].skipping_index,
459            Some(datatypes::schema::SkippingIndexOptions::default())
460        );
461        let rebuilt = parse_json2_column(&format!(
462            "CREATE TABLE logs (attrs JSON2 {options}, ts TIMESTAMP TIME INDEX)"
463        ));
464        assert_eq!(
465            settings,
466            rebuilt
467                .extensions
468                .json2_options
469                .unwrap()
470                .build_json_settings()
471                .unwrap()
472        );
473        for option in [
474            "SKIPPING INDEX SKIPPING INDEX",
475            "SKIPPING",
476            "SKIPPING INDEX WITH (granularity='0')",
477            "SKIPPING INDEX WITH (false_positive_rate='0')",
478            "SKIPPING INDEX WITH (type='unknown')",
479            "SKIPPING INDEX WITH (unknown='1')",
480        ] {
481            let sql = format!(
482                "CREATE TABLE logs (attrs JSON2 (a STRING {option}), ts TIMESTAMP TIME INDEX)"
483            );
484            assert!(
485                ParserContext::create_with_dialect(
486                    &sql,
487                    &GreptimeDbDialect {},
488                    ParseOptions::default()
489                )
490                .is_err(),
491                "{sql}"
492            );
493        }
494    }
495
496    #[test]
497    fn test_parse_json2_type_hint_path() {
498        assert_eq!(
499            parse_json2_type_hint_path(r#"attrs."http.status_code""#).unwrap(),
500            vec!["attrs", "http.status_code"]
501        );
502        assert!(parse_json2_type_hint_path("user.id trailing").is_err());
503    }
504
505    #[test]
506    fn test_parse_json2_type_hints() {
507        let column = parse_json2_column(
508            r#"
509CREATE TABLE traces (
510    log_json_data JSON2 (
511        "service.name" STRING INVERTED INDEX,
512        http.method STRING,
513        status_code BIGINT,
514        comment STRING,
515    ),
516    ts TIMESTAMP TIME INDEX,
517)"#,
518        );
519
520        assert!(matches!(
521            column.column_def.data_type,
522            DataType::Custom(_, _)
523        ));
524        let hints = column.extensions.json2_options.unwrap().type_hints;
525        assert_eq!(hints.len(), 4);
526
527        assert_eq!(hints[0].path, vec!["service.name"]);
528        assert_eq!(hints[0].data_type, DataType::String(None));
529        assert!(hints[0].inverted_index);
530
531        assert_eq!(hints[1].path, vec!["http", "method"]);
532        assert_eq!(hints[1].data_type, DataType::String(None));
533        assert!(!hints[1].inverted_index);
534
535        assert_eq!(hints[2].path, vec!["status_code"]);
536        assert_eq!(hints[2].data_type, DataType::BigInt(None));
537
538        assert_eq!(hints[3].path, vec!["comment"]);
539        assert_eq!(hints[3].data_type, DataType::String(None));
540    }
541
542    #[test]
543    fn test_parse_json2_max_auto_expanded_paths() {
544        let column = parse_json2_column(
545            r#"
546CREATE TABLE traces (
547    log_json_data JSON2 (
548        http.method STRING,
549        max_auto_expanded_paths = 0
550    ),
551    ts TIMESTAMP TIME INDEX,
552)"#,
553        );
554
555        let options = column.extensions.json2_options.unwrap();
556        assert_eq!(options.max_auto_expanded_paths, Some(0));
557        assert_eq!(options.type_hints.len(), 1);
558
559        let empty = parse_json2_column(
560            r#"
561CREATE TABLE traces (
562    log_json_data JSON2 (),
563    ts TIMESTAMP TIME INDEX,
564)"#,
565        );
566        assert!(empty.extensions.json2_options.is_none());
567
568        let quoted = parse_json2_column(
569            r#"
570CREATE TABLE traces (
571    log_json_data JSON2 (
572        "max_auto_expanded_paths" STRING,
573        nested."!__remainder__!" STRING
574    ),
575    ts TIMESTAMP TIME INDEX,
576)"#,
577        );
578        let options = quoted.extensions.json2_options.unwrap();
579        assert_eq!(options.max_auto_expanded_paths, None);
580        assert_eq!(options.type_hints.len(), 2);
581    }
582
583    #[test]
584    fn test_parse_json2_max_auto_expanded_paths_rejects_invalid_options() {
585        for options in [
586            "max_auto_expanded_paths = 0, max_auto_expanded_paths = 1",
587            "max_auto_expanded_paths = -1",
588            "max_auto_expanded_paths = 1.5",
589            "max_auto_expanded_paths = 4294967296",
590            r#""!__remainder__!".value STRING"#,
591        ] {
592            let sql = format!(
593                "CREATE TABLE traces (log_json_data JSON2 ({options}), ts TIMESTAMP TIME INDEX)"
594            );
595            assert!(
596                ParserContext::create_with_dialect(
597                    &sql,
598                    &GreptimeDbDialect {},
599                    ParseOptions::default()
600                )
601                .is_err(),
602                "{options}"
603            );
604        }
605    }
606
607    #[test]
608    fn test_parse_json2_type_hint_defaults_to_nullable() {
609        let column = parse_json2_column(
610            r#"
611CREATE TABLE traces (
612    log_json_data JSON2 (http.method STRING),
613    ts TIMESTAMP TIME INDEX,
614)"#,
615        );
616
617        let hints = column.extensions.json2_options.unwrap().type_hints;
618        assert_eq!(hints.len(), 1);
619        assert_eq!(hints[0].data_type, DataType::String(None));
620    }
621
622    #[test]
623    fn test_parse_json2_type_hint_quoted_path_segments() {
624        let column = parse_json2_column(
625            r#"
626CREATE TABLE traces (
627    log_json_data JSON2 (
628        "a".b STRING,
629        "x"."y" STRING,
630        "a.b"."c" STRING,
631        a."b.c" STRING
632    ),
633    ts TIMESTAMP TIME INDEX,
634)"#,
635        );
636
637        let hints = column.extensions.json2_options.unwrap().type_hints;
638        assert_eq!(hints.len(), 4);
639        assert_eq!(hints[0].path, vec!["a", "b"]);
640        assert_eq!(hints[1].path, vec!["x", "y"]);
641        assert_eq!(hints[2].path, vec!["a.b", "c"]);
642        assert_eq!(hints[3].path, vec!["a", "b.c"]);
643    }
644
645    #[test]
646    fn test_parse_json2_type_hint_supported_types() {
647        for (sql_type, expected) in [
648            ("STRING", DataType::String(None)),
649            ("BIGINT", DataType::BigInt(None)),
650            ("BIGINT UNSIGNED", DataType::BigIntUnsigned(None)),
651            ("DOUBLE", DataType::Double(ExactNumberInfo::None)),
652            ("BOOLEAN", DataType::Boolean),
653            ("INT64", DataType::Int64),
654            ("UINT64", DataType::UInt64),
655            ("FLOAT64", DataType::Float64),
656        ] {
657            for sql_type in [sql_type.to_string(), sql_type.to_lowercase()] {
658                let column = parse_json2_column(&format!(
659                    "CREATE TABLE traces (j JSON2 (value {sql_type}), ts TIMESTAMP TIME INDEX)"
660                ));
661                let options = column.extensions.json2_options.unwrap();
662                let settings = options.build_json_settings().unwrap();
663                assert_eq!(settings.type_hints().len(), 1);
664                let hints = options.type_hints;
665                assert_eq!(hints[0].data_type, expected);
666            }
667        }
668    }
669
670    #[test]
671    fn test_parse_json2_type_hint_rejects_unsupported_types() {
672        for sql_type in [
673            "INT2",
674            "INT4",
675            "INT8",
676            "INT16",
677            "INT32",
678            "TINYINT",
679            "SMALLINT",
680            "INT",
681            "INTEGER",
682            "UINT8",
683            "UINT16",
684            "UINT32",
685            "TINYINT UNSIGNED",
686            "SMALLINT UNSIGNED",
687            "INT UNSIGNED",
688            "FLOAT",
689            "REAL",
690            "FLOAT4",
691            "FLOAT8",
692            "FLOAT32",
693            "BOOL",
694            "TEXT",
695            "VARCHAR(10)",
696            "CHAR(10)",
697            "TIMESTAMP",
698            "DECIMAL(10, 2)",
699            "STRING(10)",
700            "BIGINT(10)",
701        ] {
702            for sql in [
703                format!(
704                    "CREATE TABLE traces (j JSON2 (value {sql_type}), ts TIMESTAMP TIME INDEX)"
705                ),
706                format!("ALTER TABLE traces MODIFY COLUMN j JSON2 (value {sql_type})"),
707            ] {
708                let err = ParserContext::create_with_dialect(
709                    &sql,
710                    &GreptimeDbDialect {},
711                    ParseOptions::default(),
712                )
713                .unwrap_err();
714                assert!(
715                    err.to_string().contains(
716                        "supported types: STRING, BIGINT, BIGINT UNSIGNED, DOUBLE, BOOLEAN; supported aliases: INT64, UINT64, FLOAT64"
717                    ),
718                    "{sql}: {err}"
719                );
720            }
721        }
722    }
723
724    #[test]
725    fn test_parse_json2_type_hint_rejects_default() {
726        for default in ["-5", "abs(-1)"] {
727            let sql = format!(
728                "CREATE TABLE traces (log_json_data JSON2 (status_code BIGINT DEFAULT {default}), ts TIMESTAMP TIME INDEX)"
729            );
730            let err = ParserContext::create_with_dialect(
731                &sql,
732                &GreptimeDbDialect {},
733                ParseOptions::default(),
734            )
735            .unwrap_err();
736            assert!(err.to_string().contains("DEFAULT is not supported"));
737        }
738    }
739
740    #[test]
741    fn test_parse_json2_type_hint_rejects_duplicate_path() {
742        let result = ParserContext::create_with_dialect(
743            r#"
744CREATE TABLE traces (
745    log_json_data JSON2 (a.b STRING, a.b BIGINT),
746    ts TIMESTAMP TIME INDEX,
747)"#,
748            &GreptimeDbDialect {},
749            ParseOptions::default(),
750        );
751
752        assert!(result.is_err());
753        assert!(result.unwrap_err().to_string().contains("duplicated"));
754    }
755
756    #[test]
757    fn test_parse_json2_type_hint_rejects_parent_child_path() {
758        let result = ParserContext::create_with_dialect(
759            r#"
760CREATE TABLE traces (
761    log_json_data JSON2 (a STRING, a.b BIGINT),
762    ts TIMESTAMP TIME INDEX,
763)"#,
764            &GreptimeDbDialect {},
765            ParseOptions::default(),
766        );
767
768        assert!(result.is_err());
769        assert!(result.unwrap_err().to_string().contains("conflicts"));
770    }
771
772    #[test]
773    fn test_parse_json2_type_hint_rejects_nullability() {
774        for sql in [
775            r#"
776CREATE TABLE traces (
777    log_json_data JSON2 (a STRING NULL NULL),
778    ts TIMESTAMP TIME INDEX,
779)"#,
780            r#"
781CREATE TABLE traces (
782    log_json_data JSON2 (a STRING NOT NULL NOT NULL),
783    ts TIMESTAMP TIME INDEX,
784)"#,
785            r#"
786CREATE TABLE traces (
787    log_json_data JSON2 (a STRING NOT NULL NULL),
788    ts TIMESTAMP TIME INDEX,
789)"#,
790            r#"
791CREATE TABLE traces (
792    log_json_data JSON2 (a STRING NULL NOT NULL),
793    ts TIMESTAMP TIME INDEX,
794)"#,
795        ] {
796            let result = ParserContext::create_with_dialect(
797                sql,
798                &GreptimeDbDialect {},
799                ParseOptions::default(),
800            );
801
802            assert!(result.is_err());
803            assert!(
804                result
805                    .unwrap_err()
806                    .to_string()
807                    .contains("NULL/NOT NULL is not supported")
808            );
809        }
810    }
811}