Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 50 additions & 4 deletions parser/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -4169,11 +4169,18 @@ func (j *JSONPath) String() string {
return builder.String()
}

type JSONTypeHint struct {
Path *JSONPath
Type ColumnType
}

type JSONOption struct {
SkipPath *JSONPath
SkipRegex *StringLiteral
MaxDynamicPaths *NumberLiteral
MaxDynamicTypes *NumberLiteral
// Type hint for specific JSON subcolumn path, e.g., "message String" or "a.b UInt64"
Column *JSONTypeHint
}

func (j *JSONOption) String() string {
Expand All @@ -4196,6 +4203,16 @@ func (j *JSONOption) String() string {
builder.WriteByte('=')
builder.WriteString(j.MaxDynamicTypes.String())
}
if j.Column != nil && j.Column.Path != nil && j.Column.Type != nil {
// add a leading space if there is already content
if builder.Len() > 0 {
builder.WriteByte(' ')
}
builder.WriteString(j.Column.Path.String())
builder.WriteByte(' ')
builder.WriteString(j.Column.Type.String())
}

return builder.String()
}

Expand All @@ -4216,12 +4233,41 @@ func (j *JSONOptions) End() Pos {
func (j *JSONOptions) String() string {
var builder strings.Builder
builder.WriteByte('(')
for i, item := range j.Items {
if i > 0 {
builder.WriteString(", ")
// Ensure stable, readable ordering:
// 1) numeric options (max_dynamic_*), 2) type-hint items, 3) skip options (SKIP, SKIP REGEXP)
// Preserve original relative order within each group.
numericOptionItems := make([]*JSONOption, 0, len(j.Items))
columnItems := make([]*JSONOption, 0, len(j.Items))
skipOptionItems := make([]*JSONOption, 0, len(j.Items))
for _, item := range j.Items {
if item.MaxDynamicPaths != nil || item.MaxDynamicTypes != nil {
numericOptionItems = append(numericOptionItems, item)
continue
}
if item.Column != nil {
columnItems = append(columnItems, item)
continue
}
if item.SkipPath != nil || item.SkipRegex != nil {
skipOptionItems = append(skipOptionItems, item)
continue
}
// Fallback: treat as numeric option to avoid dropping unknown future fields
numericOptionItems = append(numericOptionItems, item)
}

writeItems := func(items []*JSONOption) {
for _, item := range items {
if builder.Len() > 1 { // account for the initial '('
builder.WriteString(", ")
}
builder.WriteString(item.String())
}
builder.WriteString(item.String())
}

writeItems(numericOptionItems)
writeItems(columnItems)
writeItems(skipOptionItems)
builder.WriteByte(')')
return builder.String()
}
Expand Down
46 changes: 45 additions & 1 deletion parser/parser_column.go
Original file line number Diff line number Diff line change
Expand Up @@ -1018,7 +1018,51 @@ func (p *Parser) parseJSONOption() (*JSONOption, error) {
SkipPath: jsonPath,
}, nil
case p.matchTokenKind(TokenKindIdent):
return p.parseJSONMaxDynamicOptions(p.Pos())
// Could be max_dynamic_* option OR a type hint like: a.b String
// Lookahead to see if there's an '=' following the identifier path (max_dynamic_*)
// or if it's a path followed by a ColumnType.
// We'll parse a JSONPath first, then decide.
// Save lexer state by consuming as path greedily using existing helpers.
// Try: if single ident and next is '=' -> max_dynamic_*; else treat as path + type

// Peek next token after current ident without consuming type; we need to
// attempt to parse as max_dynamic_* first as it's existing behavior for a single ident.
// To support dotted paths, we need to capture path, then if '=' exists, it's option; otherwise parse type.
path, err := p.parseJSONPath()
if err != nil {
return nil, err
}
if p.tryConsumeTokenKind(TokenKindSingleEQ) != nil {
// This is a max_dynamic_* option; only valid when path is a single ident of that name
// Reconstruct handling similar to parseJSONMaxDynamicOptions but we already consumed ident and '='
// Determine which option based on the first ident name
if len(path.Idents) != 1 {
return nil, fmt.Errorf("unexpected token kind: %s", p.lastTokenKind())
}
name := path.Idents[0].Name
switch name {
case "max_dynamic_types":
number, err := p.parseNumber(p.Pos())
if err != nil {
return nil, err
}
return &JSONOption{MaxDynamicTypes: number}, nil
case "max_dynamic_paths":
number, err := p.parseNumber(p.Pos())
if err != nil {
return nil, err
}
return &JSONOption{MaxDynamicPaths: number}, nil
default:
return nil, fmt.Errorf("unexpected token kind: %s", p.lastTokenKind())
}
}
// Otherwise, expect a ColumnType as a type hint for the JSON subpath
colType, err := p.parseColumnType(p.Pos())
if err != nil {
return nil, err
}
return &JSONOption{Column: &JSONTypeHint{Path: path, Type: colType}}, nil
default:
return nil, fmt.Errorf("unexpected token kind: %s", p.lastTokenKind())
}
Expand Down
6 changes: 6 additions & 0 deletions parser/testdata/ddl/create_table_json_typehints.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE t (
j JSON(message String, a.b UInt64, max_dynamic_paths=0, SKIP x, SKIP REGEXP 're')
) ENGINE = MergeTree
ORDER BY tuple();


11 changes: 11 additions & 0 deletions parser/testdata/ddl/format/create_table_json_typehints.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Origin SQL:
CREATE TABLE t (
j JSON(message String, a.b UInt64, max_dynamic_paths=0, SKIP x, SKIP REGEXP 're')
) ENGINE = MergeTree
ORDER BY tuple();




-- Format SQL:
CREATE TABLE t (j JSON(max_dynamic_paths=0, message String, a.b UInt64, SKIP x, SKIP REGEXP 're')) ENGINE = MergeTree ORDER BY tuple();
19 changes: 12 additions & 7 deletions parser/testdata/ddl/output/create_table_basic.sql.golden.json
Original file line number Diff line number Diff line change
Expand Up @@ -685,22 +685,24 @@
"SkipRegex": null,
"MaxDynamicPaths": null,
"MaxDynamicTypes": {
"NumPos": 571,
"NumPos": 589,
"NumEnd": 591,
"Literal": "10",
"Base": 10
}
},
"Column": null
},
{
"SkipPath": null,
"SkipRegex": null,
"MaxDynamicPaths": {
"NumPos": 593,
"NumPos": 611,
"NumEnd": 612,
"Literal": "3",
"Base": 10
},
"MaxDynamicTypes": null
"MaxDynamicTypes": null,
"Column": null
},
{
"SkipPath": {
Expand All @@ -715,7 +717,8 @@
},
"SkipRegex": null,
"MaxDynamicPaths": null,
"MaxDynamicTypes": null
"MaxDynamicTypes": null,
"Column": null
},
{
"SkipPath": {
Expand All @@ -742,7 +745,8 @@
},
"SkipRegex": null,
"MaxDynamicPaths": null,
"MaxDynamicTypes": null
"MaxDynamicTypes": null,
"Column": null
},
{
"SkipPath": null,
Expand All @@ -752,7 +756,8 @@
"Literal": "hello"
},
"MaxDynamicPaths": null,
"MaxDynamicTypes": null
"MaxDynamicTypes": null,
"Column": null
}
]
}
Expand Down
Loading