From 3201167e8236f19c36e9120cf38a209a1f0c64dd Mon Sep 17 00:00:00 2001 From: Riley Wilburn Date: Sun, 25 Jan 2026 20:07:37 -0800 Subject: [PATCH 1/2] Add support for nested named/unnamed tuple types --- parser/parser_column.go | 125 ++++- .../ddl/create_table_with_tuple_fields.sql | 10 + .../format/create_table_with_tuple_fields.sql | 15 + ...te_table_with_tuple_fields.sql.golden.json | 460 ++++++++++++++++++ 4 files changed, 608 insertions(+), 2 deletions(-) create mode 100644 parser/testdata/ddl/create_table_with_tuple_fields.sql create mode 100644 parser/testdata/ddl/format/create_table_with_tuple_fields.sql create mode 100644 parser/testdata/ddl/output/create_table_with_tuple_fields.sql.golden.json diff --git a/parser/parser_column.go b/parser/parser_column.go index c3566f4..74bf4f5 100644 --- a/parser/parser_column.go +++ b/parser/parser_column.go @@ -901,11 +901,16 @@ func (p *Parser) parseColumnCaseExpr(pos Pos) (*CaseExpr, error) { return caseExpr, nil } -func (p *Parser) parseColumnType(_ Pos) (ColumnType, error) { // nolint:funlen +func (p *Parser) parseColumnType(_ Pos) (ColumnType, error) { ident, err := p.parseIdent() if err != nil { return nil, err } + + return p.parseColumnTypeArgs(ident) +} + +func (p *Parser) parseColumnTypeArgs(ident *Ident) (ColumnType, error) { // nolint:funlen if lParen := p.tryConsumeTokenKind(TokenKindLParen); lParen != nil { switch { case p.matchTokenKind(TokenKindIdent): @@ -1185,10 +1190,11 @@ func (p *Parser) parseJSONType(name *Ident, pos Pos) (*JSONType, error) { } func (p *Parser) parseNestedType(name *Ident, pos Pos) (*NestedType, error) { - columns, err := p.parseTableColumns() + columns, err := p.parseNestedTypeFields() if err != nil { return nil, err } + rightParenPos := p.Pos() if err := p.expectTokenKind(TokenKindRParen); err != nil { return nil, err @@ -1201,6 +1207,121 @@ func (p *Parser) parseNestedType(name *Ident, pos Pos) (*NestedType, error) { }, nil } +func (p *Parser) parseNestedTypeFields() ([]Expr, error) { + switch { + case p.lexer.isEOF() || p.matchTokenKind(TokenKindRParen): + // Cases like `Tuple()` + return []Expr{}, nil + case p.matchTokenKind(TokenKindIdent): + ident, err := p.parseIdent() + if err != nil { + return nil, err + } + + if p.matchTokenKind(TokenKindIdent) { + // Cases like `Tuple(a Int, b String)` or `Nested(a Int, b String)` + return p.parseNestedTypeFieldsWithNames(ident) + } + + // Cases like `Tuple(Int, String)` + return p.parseNestedTypeFieldsWithoutNames(ident) + default: + return nil, fmt.Errorf("unexpected token kind: %s", p.lastTokenKind()) + } +} + +func (p *Parser) parseNestedTypeFieldsWithNames(firstIdent *Ident) ([]Expr, error) { + columns := make([]Expr, 0) + + columnType, err := p.parseColumnType(p.Pos()) + if err != nil { + return nil, err + } + + columns = append(columns, &ColumnDef{ + NamePos: firstIdent.Pos(), + Name: &NestedIdentifier{ + Ident: firstIdent, + }, + Type: columnType, + ColumnEnd: columnType.End(), + }) + + if p.tryConsumeTokenKind(TokenKindComma) == nil { + return columns, nil + } + + for !p.lexer.isEOF() && !p.matchTokenKind(TokenKindRParen) { + column, err := p.parseNestedTypeFieldWithName() + if err != nil { + return nil, err + } + if column == nil { + break + } + columns = append(columns, column) + + if p.tryConsumeTokenKind(TokenKindComma) == nil { + break + } + } + + return columns, nil +} + +func (p *Parser) parseNestedTypeFieldsWithoutNames(firstIdent *Ident) ([]Expr, error) { + columns := make([]Expr, 0) + + column, err := p.parseColumnTypeArgs(firstIdent) + if err != nil { + return nil, err + } + + columns = append(columns, column) + + if p.tryConsumeTokenKind(TokenKindComma) == nil { + return columns, nil + } + + for !p.lexer.isEOF() && !p.matchTokenKind(TokenKindRParen) { + column, err := p.parseColumnType(p.Pos()) + if err != nil { + return nil, err + } + if column == nil { + break + } + columns = append(columns, column) + + if p.tryConsumeTokenKind(TokenKindComma) == nil { + break + } + } + + return columns, nil +} + +func (p *Parser) parseNestedTypeFieldWithName() (Expr, error) { + name, err := p.parseIdent() + if err != nil { + return nil, err + } + + columnType, err := p.parseColumnType(p.Pos()) + if err != nil { + return nil, err + } + + return &ColumnDef{ + NamePos: name.Pos(), + Name: &NestedIdentifier{ + Ident: name, + }, + Type: columnType, + ColumnEnd: columnType.End(), + }, nil +} + func (p *Parser) tryParseCompressionCodecs(pos Pos) (*CompressionCodec, error) { if !p.tryConsumeKeywords(KeywordCodec) { return nil, nil // nolint diff --git a/parser/testdata/ddl/create_table_with_tuple_fields.sql b/parser/testdata/ddl/create_table_with_tuple_fields.sql new file mode 100644 index 0000000..a32dd46 --- /dev/null +++ b/parser/testdata/ddl/create_table_with_tuple_fields.sql @@ -0,0 +1,10 @@ +CREATE TABLE t0 on cluster default_cluster +( + `tup0` Tuple(), + `tup1` Tuple(String, Int64), + `tup2` Tuple(String, Tuple(String, String)), + `tup3` Tuple(a String, cd Tuple(c String, d String)) +) +ENGINE = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}', '{replica}') +ORDER BY (tup1, tup2, tup3) +SETTINGS index_granularity = 8192; diff --git a/parser/testdata/ddl/format/create_table_with_tuple_fields.sql b/parser/testdata/ddl/format/create_table_with_tuple_fields.sql new file mode 100644 index 0000000..db288c1 --- /dev/null +++ b/parser/testdata/ddl/format/create_table_with_tuple_fields.sql @@ -0,0 +1,15 @@ +-- Origin SQL: +CREATE TABLE t0 on cluster default_cluster +( + `tup0` Tuple(), + `tup1` Tuple(String, Int64), + `tup2` Tuple(String, Tuple(String, String)), + `tup3` Tuple(a String, cd Tuple(c String, d String)) +) +ENGINE = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}', '{replica}') +ORDER BY (tup1, tup2, tup3) +SETTINGS index_granularity = 8192; + + +-- Format SQL: +CREATE TABLE t0 ON CLUSTER default_cluster (`tup0` Tuple(), `tup1` Tuple(String, Int64), `tup2` Tuple(String, Tuple(String, String)), `tup3` Tuple(a String, cd Tuple(c String, d String))) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}', '{replica}') ORDER BY (tup1, tup2, tup3) SETTINGS index_granularity=8192; diff --git a/parser/testdata/ddl/output/create_table_with_tuple_fields.sql.golden.json b/parser/testdata/ddl/output/create_table_with_tuple_fields.sql.golden.json new file mode 100644 index 0000000..d39c1d5 --- /dev/null +++ b/parser/testdata/ddl/output/create_table_with_tuple_fields.sql.golden.json @@ -0,0 +1,460 @@ +[ + { + "CreatePos": 0, + "StatementEnd": 347, + "OrReplace": false, + "Name": { + "Database": null, + "Table": { + "Name": "t0", + "QuoteType": 1, + "NamePos": 13, + "NameEnd": 15 + } + }, + "IfNotExists": false, + "UUID": null, + "OnCluster": { + "OnPos": 16, + "Expr": { + "Name": "default_cluster", + "QuoteType": 1, + "NamePos": 27, + "NameEnd": 42 + } + }, + "TableSchema": { + "SchemaPos": 43, + "SchemaEnd": 204, + "Columns": [ + { + "NamePos": 50, + "ColumnEnd": 62, + "Name": { + "Ident": { + "Name": "tup0", + "QuoteType": 3, + "NamePos": 50, + "NameEnd": 54 + }, + "DotIdent": null + }, + "Type": { + "LeftParenPos": 61, + "RightParenPos": 62, + "Name": { + "Name": "Tuple", + "QuoteType": 1, + "NamePos": 56, + "NameEnd": 61 + }, + "Params": null + }, + "NotNull": null, + "Nullable": null, + "DefaultExpr": null, + "MaterializedExpr": null, + "AliasExpr": null, + "Codec": null, + "TTL": null, + "Comment": null, + "CompressionCodec": null + }, + { + "NamePos": 70, + "ColumnEnd": 95, + "Name": { + "Ident": { + "Name": "tup1", + "QuoteType": 3, + "NamePos": 70, + "NameEnd": 74 + }, + "DotIdent": null + }, + "Type": { + "LeftParenPos": 82, + "RightParenPos": 95, + "Name": { + "Name": "Tuple", + "QuoteType": 1, + "NamePos": 76, + "NameEnd": 81 + }, + "Columns": [ + { + "Name": { + "Name": "String", + "QuoteType": 1, + "NamePos": 82, + "NameEnd": 88 + } + }, + { + "Name": { + "Name": "Int64", + "QuoteType": 1, + "NamePos": 90, + "NameEnd": 95 + } + } + ] + }, + "NotNull": null, + "Nullable": null, + "DefaultExpr": null, + "MaterializedExpr": null, + "AliasExpr": null, + "Codec": null, + "TTL": null, + "Comment": null, + "CompressionCodec": null + }, + { + "NamePos": 103, + "ColumnEnd": 144, + "Name": { + "Ident": { + "Name": "tup2", + "QuoteType": 3, + "NamePos": 103, + "NameEnd": 107 + }, + "DotIdent": null + }, + "Type": { + "LeftParenPos": 115, + "RightParenPos": 144, + "Name": { + "Name": "Tuple", + "QuoteType": 1, + "NamePos": 109, + "NameEnd": 114 + }, + "Columns": [ + { + "Name": { + "Name": "String", + "QuoteType": 1, + "NamePos": 115, + "NameEnd": 121 + } + }, + { + "LeftParenPos": 129, + "RightParenPos": 143, + "Name": { + "Name": "Tuple", + "QuoteType": 1, + "NamePos": 123, + "NameEnd": 128 + }, + "Columns": [ + { + "Name": { + "Name": "String", + "QuoteType": 1, + "NamePos": 129, + "NameEnd": 135 + } + }, + { + "Name": { + "Name": "String", + "QuoteType": 1, + "NamePos": 137, + "NameEnd": 143 + } + } + ] + } + ] + }, + "NotNull": null, + "Nullable": null, + "DefaultExpr": null, + "MaterializedExpr": null, + "AliasExpr": null, + "Codec": null, + "TTL": null, + "Comment": null, + "CompressionCodec": null + }, + { + "NamePos": 152, + "ColumnEnd": 202, + "Name": { + "Ident": { + "Name": "tup3", + "QuoteType": 3, + "NamePos": 152, + "NameEnd": 156 + }, + "DotIdent": null + }, + "Type": { + "LeftParenPos": 164, + "RightParenPos": 202, + "Name": { + "Name": "Tuple", + "QuoteType": 1, + "NamePos": 158, + "NameEnd": 163 + }, + "Columns": [ + { + "NamePos": 164, + "ColumnEnd": 172, + "Name": { + "Ident": { + "Name": "a", + "QuoteType": 1, + "NamePos": 164, + "NameEnd": 165 + }, + "DotIdent": null + }, + "Type": { + "Name": { + "Name": "String", + "QuoteType": 1, + "NamePos": 166, + "NameEnd": 172 + } + }, + "NotNull": null, + "Nullable": null, + "DefaultExpr": null, + "MaterializedExpr": null, + "AliasExpr": null, + "Codec": null, + "TTL": null, + "Comment": null, + "CompressionCodec": null + }, + { + "NamePos": 174, + "ColumnEnd": 201, + "Name": { + "Ident": { + "Name": "cd", + "QuoteType": 1, + "NamePos": 174, + "NameEnd": 176 + }, + "DotIdent": null + }, + "Type": { + "LeftParenPos": 183, + "RightParenPos": 201, + "Name": { + "Name": "Tuple", + "QuoteType": 1, + "NamePos": 177, + "NameEnd": 182 + }, + "Columns": [ + { + "NamePos": 183, + "ColumnEnd": 191, + "Name": { + "Ident": { + "Name": "c", + "QuoteType": 1, + "NamePos": 183, + "NameEnd": 184 + }, + "DotIdent": null + }, + "Type": { + "Name": { + "Name": "String", + "QuoteType": 1, + "NamePos": 185, + "NameEnd": 191 + } + }, + "NotNull": null, + "Nullable": null, + "DefaultExpr": null, + "MaterializedExpr": null, + "AliasExpr": null, + "Codec": null, + "TTL": null, + "Comment": null, + "CompressionCodec": null + }, + { + "NamePos": 193, + "ColumnEnd": 201, + "Name": { + "Ident": { + "Name": "d", + "QuoteType": 1, + "NamePos": 193, + "NameEnd": 194 + }, + "DotIdent": null + }, + "Type": { + "Name": { + "Name": "String", + "QuoteType": 1, + "NamePos": 195, + "NameEnd": 201 + } + }, + "NotNull": null, + "Nullable": null, + "DefaultExpr": null, + "MaterializedExpr": null, + "AliasExpr": null, + "Codec": null, + "TTL": null, + "Comment": null, + "CompressionCodec": null + } + ] + }, + "NotNull": null, + "Nullable": null, + "DefaultExpr": null, + "MaterializedExpr": null, + "AliasExpr": null, + "Codec": null, + "TTL": null, + "Comment": null, + "CompressionCodec": null + } + ] + }, + "NotNull": null, + "Nullable": null, + "DefaultExpr": null, + "MaterializedExpr": null, + "AliasExpr": null, + "Codec": null, + "TTL": null, + "Comment": null, + "CompressionCodec": null + } + ], + "AliasTable": null, + "TableFunction": null + }, + "Engine": { + "EnginePos": 206, + "EngineEnd": 347, + "Name": "ReplicatedMergeTree", + "Params": { + "LeftParenPos": 234, + "RightParenPos": 284, + "Items": { + "ListPos": 236, + "ListEnd": 283, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "LiteralPos": 236, + "LiteralEnd": 270, + "Literal": "/clickhouse/tables/{layer}-{shard}" + }, + "Alias": null + }, + { + "Expr": { + "LiteralPos": 274, + "LiteralEnd": 283, + "Literal": "{replica}" + }, + "Alias": null + } + ] + }, + "ColumnArgList": null + }, + "PrimaryKey": null, + "PartitionBy": null, + "SampleBy": null, + "TTL": null, + "Settings": { + "SettingsPos": 314, + "ListEnd": 347, + "Items": [ + { + "SettingsPos": 323, + "Name": { + "Name": "index_granularity", + "QuoteType": 1, + "NamePos": 323, + "NameEnd": 340 + }, + "Expr": { + "NumPos": 343, + "NumEnd": 347, + "Literal": "8192", + "Base": 10 + } + } + ] + }, + "OrderBy": { + "OrderPos": 286, + "ListEnd": 312, + "Items": [ + { + "OrderPos": 286, + "Expr": { + "LeftParenPos": 295, + "RightParenPos": 312, + "Items": { + "ListPos": 296, + "ListEnd": 312, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "Name": "tup1", + "QuoteType": 1, + "NamePos": 296, + "NameEnd": 300 + }, + "Alias": null + }, + { + "Expr": { + "Name": "tup2", + "QuoteType": 1, + "NamePos": 302, + "NameEnd": 306 + }, + "Alias": null + }, + { + "Expr": { + "Name": "tup3", + "QuoteType": 1, + "NamePos": 308, + "NameEnd": 312 + }, + "Alias": null + } + ] + }, + "ColumnArgList": null + }, + "Alias": null, + "Direction": "", + "Fill": null + } + ], + "Interpolate": null + } + }, + "SubQuery": null, + "TableFunction": null, + "HasTemporary": false, + "Comment": null + } +] \ No newline at end of file From 39a9543743f3a2c728fd83479570308d4e1bb827 Mon Sep 17 00:00:00 2001 From: Riley Wilburn Date: Wed, 28 Jan 2026 13:18:26 -0800 Subject: [PATCH 2/2] rename firstIdent --- parser/parser_column.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/parser/parser_column.go b/parser/parser_column.go index 74bf4f5..c0d9d47 100644 --- a/parser/parser_column.go +++ b/parser/parser_column.go @@ -1230,7 +1230,7 @@ func (p *Parser) parseNestedTypeFields() ([]Expr, error) { } } -func (p *Parser) parseNestedTypeFieldsWithNames(firstIdent *Ident) ([]Expr, error) { +func (p *Parser) parseNestedTypeFieldsWithNames(columnName *Ident) ([]Expr, error) { columns := make([]Expr, 0) columnType, err := p.parseColumnType(p.Pos()) @@ -1239,9 +1239,9 @@ func (p *Parser) parseNestedTypeFieldsWithNames(firstIdent *Ident) ([]Expr, erro } columns = append(columns, &ColumnDef{ - NamePos: firstIdent.Pos(), + NamePos: columnName.Pos(), Name: &NestedIdentifier{ - Ident: firstIdent, + Ident: columnName, }, Type: columnType, ColumnEnd: columnType.End(), @@ -1269,10 +1269,10 @@ func (p *Parser) parseNestedTypeFieldsWithNames(firstIdent *Ident) ([]Expr, erro return columns, nil } -func (p *Parser) parseNestedTypeFieldsWithoutNames(firstIdent *Ident) ([]Expr, error) { +func (p *Parser) parseNestedTypeFieldsWithoutNames(columnType *Ident) ([]Expr, error) { columns := make([]Expr, 0) - column, err := p.parseColumnTypeArgs(firstIdent) + column, err := p.parseColumnTypeArgs(columnType) if err != nil { return nil, err }