Skip to content

Commit b263a1d

Browse files
ajitpratap0Ajit Pratap Singh
andauthored
feat(parser): ClickHouse SETTINGS, TTL, and INSERT FORMAT tail clauses (#482) (#489)
Co-authored-by: Ajit Pratap Singh <ajitpratapsingh@Ajits-Mac-mini-2655.local>
1 parent 1fd384b commit b263a1d

File tree

4 files changed

+120
-0
lines changed

4 files changed

+120
-0
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2026 GoSQLX Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
5+
package parser_test
6+
7+
import (
8+
"testing"
9+
10+
"github.com/ajitpratap0/GoSQLX/pkg/gosqlx"
11+
"github.com/ajitpratap0/GoSQLX/pkg/sql/keywords"
12+
)
13+
14+
// TestClickHouseSettingsTTLFormat verifies ClickHouse tail clauses parse for
15+
// both CREATE TABLE and SELECT/INSERT. Regression for #482.
16+
func TestClickHouseSettingsTTLFormat(t *testing.T) {
17+
queries := map[string]string{
18+
"create_table_with_settings": `CREATE TABLE events (
19+
id UInt64,
20+
ts DateTime
21+
) ENGINE = MergeTree()
22+
ORDER BY ts
23+
SETTINGS index_granularity = 8192, storage_policy = 'default'`,
24+
25+
"create_table_with_ttl": `CREATE TABLE logs (
26+
event_date Date,
27+
message String
28+
) ENGINE = MergeTree()
29+
ORDER BY event_date
30+
TTL event_date + INTERVAL 90 DAY`,
31+
32+
"create_table_ttl_then_settings": `CREATE TABLE logs (
33+
event_date Date,
34+
message String
35+
) ENGINE = MergeTree()
36+
ORDER BY event_date
37+
TTL event_date + INTERVAL 90 DAY
38+
SETTINGS index_granularity = 8192`,
39+
40+
"select_with_settings": `SELECT * FROM events
41+
WHERE id > 100
42+
SETTINGS max_threads = 4, distributed_aggregation_memory_efficient = 1`,
43+
44+
"insert_format_values": `INSERT INTO events (id, name) VALUES (1, 'a') FORMAT Values`,
45+
46+
"insert_format_json_each_row": `INSERT INTO events FORMAT JSONEachRow`,
47+
}
48+
for name, q := range queries {
49+
q := q
50+
t.Run(name, func(t *testing.T) {
51+
if _, err := gosqlx.ParseWithDialect(q, keywords.DialectClickHouse); err != nil {
52+
t.Fatalf("parse failed: %v", err)
53+
}
54+
})
55+
}
56+
}

pkg/sql/parser/ddl.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,31 @@ func (p *Parser) parseCreateTable(temporary bool) (*ast.CreateTableStatement, er
297297
}
298298
continue
299299
}
300+
if p.isTokenMatch("TTL") {
301+
p.advance()
302+
if err := p.skipClickHouseClauseExpr(); err != nil {
303+
return nil, err
304+
}
305+
continue
306+
}
307+
if p.isTokenMatch("SETTINGS") {
308+
p.advance()
309+
// SETTINGS is a comma-separated list of name=value assignments.
310+
// Consume each k=v pair until the next clause, EOF, or ';'.
311+
for {
312+
t := p.currentToken.Token.Type
313+
val := strings.ToUpper(p.currentToken.Token.Value)
314+
if t == models.TokenTypeEOF || t == models.TokenTypeSemicolon {
315+
break
316+
}
317+
if val == "ORDER" || val == "PARTITION" || val == "PRIMARY" ||
318+
val == "SAMPLE" || val == "TTL" {
319+
break
320+
}
321+
p.advance()
322+
}
323+
continue
324+
}
300325
break
301326
}
302327

pkg/sql/parser/dml_insert.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,21 @@ func (p *Parser) parseInsertStatement() (ast.Statement, error) {
8383
var values [][]ast.Expression
8484
var query ast.QueryExpression
8585

86+
// ClickHouse-only: INSERT INTO t [(cols)] FORMAT <name> with the data
87+
// payload being external (network / file). Short-circuit here — there is
88+
// no VALUES or SELECT to follow.
89+
if p.dialect == string(keywords.DialectClickHouse) &&
90+
strings.EqualFold(p.currentToken.Token.Value, "FORMAT") {
91+
p.advance() // Consume FORMAT
92+
if p.isIdentifier() || p.isType(models.TokenTypeKeyword) {
93+
p.advance() // Consume format name
94+
}
95+
return &ast.InsertStatement{
96+
TableName: tableName,
97+
Columns: columns,
98+
}, nil
99+
}
100+
86101
switch {
87102
case p.isType(models.TokenTypeSelect):
88103
// INSERT ... SELECT syntax
@@ -190,6 +205,17 @@ func (p *Parser) parseInsertStatement() (ast.Statement, error) {
190205
}
191206
}
192207

208+
// ClickHouse INSERT ... VALUES (...) FORMAT <name> trailing format hint.
209+
// Parse-only; the trailing payload is not consumed.
210+
if p.dialect == string(keywords.DialectClickHouse) &&
211+
strings.EqualFold(p.currentToken.Token.Value, "FORMAT") {
212+
p.advance() // Consume FORMAT
213+
if p.isIdentifier() || p.isType(models.TokenTypeKeyword) ||
214+
p.isType(models.TokenTypeValues) {
215+
p.advance() // Consume format name (may tokenize as VALUES keyword)
216+
}
217+
}
218+
193219
// Create INSERT statement
194220
return &ast.InsertStatement{
195221
TableName: tableName,

pkg/sql/parser/parser.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,19 @@ func (p *Parser) parseStatement() (ast.Statement, error) {
591591
ss.Pos = stmtPos
592592
}
593593
}
594+
// ClickHouse trailing SETTINGS k=v [, k=v]... on SELECT. Parse-only;
595+
// the settings are consumed but not modeled on the AST.
596+
if p.dialect == string(keywords.DialectClickHouse) && p.isTokenMatch("SETTINGS") {
597+
p.advance() // SETTINGS
598+
for {
599+
t := p.currentToken.Token.Type
600+
if t == models.TokenTypeEOF || t == models.TokenTypeSemicolon ||
601+
t == models.TokenTypeRParen {
602+
break
603+
}
604+
p.advance()
605+
}
606+
}
594607
return stmt, nil
595608
case models.TokenTypeInsert:
596609
stmtPos := p.currentLocation()

0 commit comments

Comments
 (0)