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
56 changes: 56 additions & 0 deletions pkg/sql/parser/clickhouse_settings_ttl_format_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2026 GoSQLX Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");

package parser_test

import (
"testing"

"github.com/ajitpratap0/GoSQLX/pkg/gosqlx"
"github.com/ajitpratap0/GoSQLX/pkg/sql/keywords"
)

// TestClickHouseSettingsTTLFormat verifies ClickHouse tail clauses parse for
// both CREATE TABLE and SELECT/INSERT. Regression for #482.
func TestClickHouseSettingsTTLFormat(t *testing.T) {
queries := map[string]string{
"create_table_with_settings": `CREATE TABLE events (
id UInt64,
ts DateTime
) ENGINE = MergeTree()
ORDER BY ts
SETTINGS index_granularity = 8192, storage_policy = 'default'`,

"create_table_with_ttl": `CREATE TABLE logs (
event_date Date,
message String
) ENGINE = MergeTree()
ORDER BY event_date
TTL event_date + INTERVAL 90 DAY`,

"create_table_ttl_then_settings": `CREATE TABLE logs (
event_date Date,
message String
) ENGINE = MergeTree()
ORDER BY event_date
TTL event_date + INTERVAL 90 DAY
SETTINGS index_granularity = 8192`,

"select_with_settings": `SELECT * FROM events
WHERE id > 100
SETTINGS max_threads = 4, distributed_aggregation_memory_efficient = 1`,

"insert_format_values": `INSERT INTO events (id, name) VALUES (1, 'a') FORMAT Values`,

"insert_format_json_each_row": `INSERT INTO events FORMAT JSONEachRow`,
}
for name, q := range queries {
q := q
t.Run(name, func(t *testing.T) {
if _, err := gosqlx.ParseWithDialect(q, keywords.DialectClickHouse); err != nil {
t.Fatalf("parse failed: %v", err)
}
})
}
}
25 changes: 25 additions & 0 deletions pkg/sql/parser/ddl.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,31 @@ func (p *Parser) parseCreateTable(temporary bool) (*ast.CreateTableStatement, er
}
continue
}
if p.isTokenMatch("TTL") {
p.advance()
if err := p.skipClickHouseClauseExpr(); err != nil {
return nil, err
}
continue
}
if p.isTokenMatch("SETTINGS") {
p.advance()
// SETTINGS is a comma-separated list of name=value assignments.
// Consume each k=v pair until the next clause, EOF, or ';'.
for {
t := p.currentToken.Token.Type
val := strings.ToUpper(p.currentToken.Token.Value)
if t == models.TokenTypeEOF || t == models.TokenTypeSemicolon {
break
}
if val == "ORDER" || val == "PARTITION" || val == "PRIMARY" ||
val == "SAMPLE" || val == "TTL" {
break
}
p.advance()
}
continue
}
break
}

Expand Down
26 changes: 26 additions & 0 deletions pkg/sql/parser/dml_insert.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@ func (p *Parser) parseInsertStatement() (ast.Statement, error) {
var values [][]ast.Expression
var query ast.QueryExpression

// ClickHouse-only: INSERT INTO t [(cols)] FORMAT <name> with the data
// payload being external (network / file). Short-circuit here — there is
// no VALUES or SELECT to follow.
if p.dialect == string(keywords.DialectClickHouse) &&
strings.EqualFold(p.currentToken.Token.Value, "FORMAT") {
p.advance() // Consume FORMAT
if p.isIdentifier() || p.isType(models.TokenTypeKeyword) {
p.advance() // Consume format name
}
return &ast.InsertStatement{
TableName: tableName,
Columns: columns,
}, nil
}

switch {
case p.isType(models.TokenTypeSelect):
// INSERT ... SELECT syntax
Expand Down Expand Up @@ -190,6 +205,17 @@ func (p *Parser) parseInsertStatement() (ast.Statement, error) {
}
}

// ClickHouse INSERT ... VALUES (...) FORMAT <name> trailing format hint.
// Parse-only; the trailing payload is not consumed.
if p.dialect == string(keywords.DialectClickHouse) &&
strings.EqualFold(p.currentToken.Token.Value, "FORMAT") {
p.advance() // Consume FORMAT
if p.isIdentifier() || p.isType(models.TokenTypeKeyword) ||
p.isType(models.TokenTypeValues) {
p.advance() // Consume format name (may tokenize as VALUES keyword)
}
}

// Create INSERT statement
return &ast.InsertStatement{
TableName: tableName,
Expand Down
13 changes: 13 additions & 0 deletions pkg/sql/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,19 @@ func (p *Parser) parseStatement() (ast.Statement, error) {
ss.Pos = stmtPos
}
}
// ClickHouse trailing SETTINGS k=v [, k=v]... on SELECT. Parse-only;
// the settings are consumed but not modeled on the AST.
if p.dialect == string(keywords.DialectClickHouse) && p.isTokenMatch("SETTINGS") {
p.advance() // SETTINGS
for {
t := p.currentToken.Token.Type
if t == models.TokenTypeEOF || t == models.TokenTypeSemicolon ||
t == models.TokenTypeRParen {
break
}
p.advance()
}
}
return stmt, nil
case models.TokenTypeInsert:
stmtPos := p.currentLocation()
Expand Down
Loading