Skip to content

Commit 341967c

Browse files
Add support of the type hint parsing in JSON column type (#196)
1 parent ee69fcc commit 341967c

File tree

6 files changed

+328
-12
lines changed

6 files changed

+328
-12
lines changed

parser/ast.go

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4169,11 +4169,18 @@ func (j *JSONPath) String() string {
41694169
return builder.String()
41704170
}
41714171

4172+
type JSONTypeHint struct {
4173+
Path *JSONPath
4174+
Type ColumnType
4175+
}
4176+
41724177
type JSONOption struct {
41734178
SkipPath *JSONPath
41744179
SkipRegex *StringLiteral
41754180
MaxDynamicPaths *NumberLiteral
41764181
MaxDynamicTypes *NumberLiteral
4182+
// Type hint for specific JSON subcolumn path, e.g., "message String" or "a.b UInt64"
4183+
Column *JSONTypeHint
41774184
}
41784185

41794186
func (j *JSONOption) String() string {
@@ -4196,6 +4203,16 @@ func (j *JSONOption) String() string {
41964203
builder.WriteByte('=')
41974204
builder.WriteString(j.MaxDynamicTypes.String())
41984205
}
4206+
if j.Column != nil && j.Column.Path != nil && j.Column.Type != nil {
4207+
// add a leading space if there is already content
4208+
if builder.Len() > 0 {
4209+
builder.WriteByte(' ')
4210+
}
4211+
builder.WriteString(j.Column.Path.String())
4212+
builder.WriteByte(' ')
4213+
builder.WriteString(j.Column.Type.String())
4214+
}
4215+
41994216
return builder.String()
42004217
}
42014218

@@ -4216,12 +4233,41 @@ func (j *JSONOptions) End() Pos {
42164233
func (j *JSONOptions) String() string {
42174234
var builder strings.Builder
42184235
builder.WriteByte('(')
4219-
for i, item := range j.Items {
4220-
if i > 0 {
4221-
builder.WriteString(", ")
4236+
// Ensure stable, readable ordering:
4237+
// 1) numeric options (max_dynamic_*), 2) type-hint items, 3) skip options (SKIP, SKIP REGEXP)
4238+
// Preserve original relative order within each group.
4239+
numericOptionItems := make([]*JSONOption, 0, len(j.Items))
4240+
columnItems := make([]*JSONOption, 0, len(j.Items))
4241+
skipOptionItems := make([]*JSONOption, 0, len(j.Items))
4242+
for _, item := range j.Items {
4243+
if item.MaxDynamicPaths != nil || item.MaxDynamicTypes != nil {
4244+
numericOptionItems = append(numericOptionItems, item)
4245+
continue
4246+
}
4247+
if item.Column != nil {
4248+
columnItems = append(columnItems, item)
4249+
continue
4250+
}
4251+
if item.SkipPath != nil || item.SkipRegex != nil {
4252+
skipOptionItems = append(skipOptionItems, item)
4253+
continue
4254+
}
4255+
// Fallback: treat as numeric option to avoid dropping unknown future fields
4256+
numericOptionItems = append(numericOptionItems, item)
4257+
}
4258+
4259+
writeItems := func(items []*JSONOption) {
4260+
for _, item := range items {
4261+
if builder.Len() > 1 { // account for the initial '('
4262+
builder.WriteString(", ")
4263+
}
4264+
builder.WriteString(item.String())
42224265
}
4223-
builder.WriteString(item.String())
42244266
}
4267+
4268+
writeItems(numericOptionItems)
4269+
writeItems(columnItems)
4270+
writeItems(skipOptionItems)
42254271
builder.WriteByte(')')
42264272
return builder.String()
42274273
}

parser/parser_column.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1018,7 +1018,51 @@ func (p *Parser) parseJSONOption() (*JSONOption, error) {
10181018
SkipPath: jsonPath,
10191019
}, nil
10201020
case p.matchTokenKind(TokenKindIdent):
1021-
return p.parseJSONMaxDynamicOptions(p.Pos())
1021+
// Could be max_dynamic_* option OR a type hint like: a.b String
1022+
// Lookahead to see if there's an '=' following the identifier path (max_dynamic_*)
1023+
// or if it's a path followed by a ColumnType.
1024+
// We'll parse a JSONPath first, then decide.
1025+
// Save lexer state by consuming as path greedily using existing helpers.
1026+
// Try: if single ident and next is '=' -> max_dynamic_*; else treat as path + type
1027+
1028+
// Peek next token after current ident without consuming type; we need to
1029+
// attempt to parse as max_dynamic_* first as it's existing behavior for a single ident.
1030+
// To support dotted paths, we need to capture path, then if '=' exists, it's option; otherwise parse type.
1031+
path, err := p.parseJSONPath()
1032+
if err != nil {
1033+
return nil, err
1034+
}
1035+
if p.tryConsumeTokenKind(TokenKindSingleEQ) != nil {
1036+
// This is a max_dynamic_* option; only valid when path is a single ident of that name
1037+
// Reconstruct handling similar to parseJSONMaxDynamicOptions but we already consumed ident and '='
1038+
// Determine which option based on the first ident name
1039+
if len(path.Idents) != 1 {
1040+
return nil, fmt.Errorf("unexpected token kind: %s", p.lastTokenKind())
1041+
}
1042+
name := path.Idents[0].Name
1043+
switch name {
1044+
case "max_dynamic_types":
1045+
number, err := p.parseNumber(p.Pos())
1046+
if err != nil {
1047+
return nil, err
1048+
}
1049+
return &JSONOption{MaxDynamicTypes: number}, nil
1050+
case "max_dynamic_paths":
1051+
number, err := p.parseNumber(p.Pos())
1052+
if err != nil {
1053+
return nil, err
1054+
}
1055+
return &JSONOption{MaxDynamicPaths: number}, nil
1056+
default:
1057+
return nil, fmt.Errorf("unexpected token kind: %s", p.lastTokenKind())
1058+
}
1059+
}
1060+
// Otherwise, expect a ColumnType as a type hint for the JSON subpath
1061+
colType, err := p.parseColumnType(p.Pos())
1062+
if err != nil {
1063+
return nil, err
1064+
}
1065+
return &JSONOption{Column: &JSONTypeHint{Path: path, Type: colType}}, nil
10221066
default:
10231067
return nil, fmt.Errorf("unexpected token kind: %s", p.lastTokenKind())
10241068
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CREATE TABLE t (
2+
j JSON(message String, a.b UInt64, max_dynamic_paths=0, SKIP x, SKIP REGEXP 're')
3+
) ENGINE = MergeTree
4+
ORDER BY tuple();
5+
6+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- Origin SQL:
2+
CREATE TABLE t (
3+
j JSON(message String, a.b UInt64, max_dynamic_paths=0, SKIP x, SKIP REGEXP 're')
4+
) ENGINE = MergeTree
5+
ORDER BY tuple();
6+
7+
8+
9+
10+
-- Format SQL:
11+
CREATE TABLE t (j JSON(max_dynamic_paths=0, message String, a.b UInt64, SKIP x, SKIP REGEXP 're')) ENGINE = MergeTree ORDER BY tuple();

parser/testdata/ddl/output/create_table_basic.sql.golden.json

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -685,22 +685,24 @@
685685
"SkipRegex": null,
686686
"MaxDynamicPaths": null,
687687
"MaxDynamicTypes": {
688-
"NumPos": 571,
688+
"NumPos": 589,
689689
"NumEnd": 591,
690690
"Literal": "10",
691691
"Base": 10
692-
}
692+
},
693+
"Column": null
693694
},
694695
{
695696
"SkipPath": null,
696697
"SkipRegex": null,
697698
"MaxDynamicPaths": {
698-
"NumPos": 593,
699+
"NumPos": 611,
699700
"NumEnd": 612,
700701
"Literal": "3",
701702
"Base": 10
702703
},
703-
"MaxDynamicTypes": null
704+
"MaxDynamicTypes": null,
705+
"Column": null
704706
},
705707
{
706708
"SkipPath": {
@@ -715,7 +717,8 @@
715717
},
716718
"SkipRegex": null,
717719
"MaxDynamicPaths": null,
718-
"MaxDynamicTypes": null
720+
"MaxDynamicTypes": null,
721+
"Column": null
719722
},
720723
{
721724
"SkipPath": {
@@ -742,7 +745,8 @@
742745
},
743746
"SkipRegex": null,
744747
"MaxDynamicPaths": null,
745-
"MaxDynamicTypes": null
748+
"MaxDynamicTypes": null,
749+
"Column": null
746750
},
747751
{
748752
"SkipPath": null,
@@ -752,7 +756,8 @@
752756
"Literal": "hello"
753757
},
754758
"MaxDynamicPaths": null,
755-
"MaxDynamicTypes": null
759+
"MaxDynamicTypes": null,
760+
"Column": null
756761
}
757762
]
758763
}

0 commit comments

Comments
 (0)