Skip to content

Commit d50de22

Browse files
committed
Support TTL elements with WHERE conditions
Added TTLElement struct to store both the TTL expression and the optional WHERE condition. Updated parser to correctly parse multiple TTL elements separated by commas, including proper handling of SET clause comma separation (SET assignments vs new TTL elements). Fixes tests: - 01622_multiple_ttls (stmt3, stmt11) - 03236_create_query_ttl_where (stmt2) - 03636_empty_projection_block (stmt1) - 03622_ttl_infos_where (stmt3) - 02932_set_ttl_where (stmt2)
1 parent 76f3e3b commit d50de22

File tree

8 files changed

+165
-71
lines changed

8 files changed

+165
-71
lines changed

ast/ast.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,11 +496,22 @@ type TTLClause struct {
496496
Position token.Position `json:"-"`
497497
Expression Expression `json:"expression"`
498498
Expressions []Expression `json:"expressions,omitempty"` // Additional TTL expressions (for multiple TTL elements)
499+
Elements []*TTLElement `json:"elements,omitempty"` // TTL elements with WHERE conditions
499500
}
500501

501502
func (t *TTLClause) Pos() token.Position { return t.Position }
502503
func (t *TTLClause) End() token.Position { return t.Position }
503504

505+
// TTLElement represents a single TTL element with optional WHERE condition.
506+
type TTLElement struct {
507+
Position token.Position `json:"-"`
508+
Expr Expression `json:"expr"`
509+
Where Expression `json:"where,omitempty"` // WHERE condition for DELETE
510+
}
511+
512+
func (t *TTLElement) Pos() token.Position { return t.Position }
513+
func (t *TTLElement) End() token.Position { return t.Position }
514+
504515
// DropQuery represents a DROP statement.
505516
type DropQuery struct {
506517
Position token.Position `json:"-"`

internal/explain/statements.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -478,14 +478,30 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string,
478478
Node(sb, n.SampleBy, storageChildDepth)
479479
}
480480
if n.TTL != nil {
481-
// Count total TTL elements (1 for Expression + len(Expressions))
482-
ttlCount := 1 + len(n.TTL.Expressions)
483-
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", storageIndent, ttlCount)
484-
fmt.Fprintf(sb, "%s TTLElement (children 1)\n", storageIndent)
485-
Node(sb, n.TTL.Expression, storageChildDepth+2)
486-
for _, expr := range n.TTL.Expressions {
481+
// Use Elements if available (has WHERE conditions), otherwise use legacy Expression/Expressions
482+
if len(n.TTL.Elements) > 0 {
483+
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", storageIndent, len(n.TTL.Elements))
484+
for _, elem := range n.TTL.Elements {
485+
children := 1
486+
if elem.Where != nil {
487+
children = 2
488+
}
489+
fmt.Fprintf(sb, "%s TTLElement (children %d)\n", storageIndent, children)
490+
Node(sb, elem.Expr, storageChildDepth+2)
491+
if elem.Where != nil {
492+
Node(sb, elem.Where, storageChildDepth+2)
493+
}
494+
}
495+
} else {
496+
// Legacy: use Expression/Expressions
497+
ttlCount := 1 + len(n.TTL.Expressions)
498+
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", storageIndent, ttlCount)
487499
fmt.Fprintf(sb, "%s TTLElement (children 1)\n", storageIndent)
488-
Node(sb, expr, storageChildDepth+2)
500+
Node(sb, n.TTL.Expression, storageChildDepth+2)
501+
for _, expr := range n.TTL.Expressions {
502+
fmt.Fprintf(sb, "%s TTLElement (children 1)\n", storageIndent)
503+
Node(sb, expr, storageChildDepth+2)
504+
}
489505
}
490506
}
491507
if len(n.Settings) > 0 {

parser/parser.go

Lines changed: 126 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2656,54 +2656,25 @@ func (p *Parser) parseTableOptions(create *ast.CreateQuery) {
26562656
case p.currentIs(token.TTL):
26572657
p.nextToken()
26582658
create.TTL = &ast.TTLClause{
2659-
Position: p.current.Pos,
2660-
Expression: p.parseExpression(ALIAS_PREC), // Use ALIAS_PREC for AS SELECT
2661-
}
2662-
// Skip RECOMPRESS CODEC(...) if present
2663-
p.skipTTLModifiers()
2664-
// Parse additional TTL elements (comma-separated)
2665-
for p.currentIs(token.COMMA) {
2666-
p.nextToken() // skip comma
2667-
expr := p.parseExpression(ALIAS_PREC)
2668-
create.TTL.Expressions = append(create.TTL.Expressions, expr)
2669-
// Skip RECOMPRESS CODEC(...) if present
2670-
p.skipTTLModifiers()
2659+
Position: p.current.Pos,
26712660
}
2672-
// Handle TTL GROUP BY x SET y = max(y) syntax
2673-
if p.currentIs(token.GROUP) {
2674-
p.nextToken()
2675-
if p.currentIs(token.BY) {
2661+
// Parse TTL elements (comma-separated)
2662+
for {
2663+
elem := p.parseTTLElement()
2664+
create.TTL.Elements = append(create.TTL.Elements, elem)
2665+
if p.currentIs(token.COMMA) {
26762666
p.nextToken()
2677-
// Parse GROUP BY expressions (can have multiple, comma separated)
2678-
for {
2679-
p.parseExpression(ALIAS_PREC)
2680-
if p.currentIs(token.COMMA) {
2681-
p.nextToken()
2682-
} else {
2683-
break
2684-
}
2685-
}
2667+
} else {
2668+
break
26862669
}
26872670
}
2688-
// Handle SET clause in TTL (aggregation expressions for TTL GROUP BY)
2689-
if p.currentIs(token.SET) {
2690-
p.nextToken()
2691-
// Parse SET expressions until we hit a keyword or end
2692-
for !p.currentIs(token.SETTINGS) && !p.currentIs(token.AS) && !p.currentIs(token.WHERE) && !p.currentIs(token.SEMICOLON) && !p.currentIs(token.EOF) {
2693-
p.parseExpression(ALIAS_PREC)
2694-
if p.currentIs(token.COMMA) {
2695-
p.nextToken()
2696-
} else {
2697-
break
2698-
}
2671+
// Keep backward compatibility with Expression/Expressions fields
2672+
if len(create.TTL.Elements) > 0 {
2673+
create.TTL.Expression = create.TTL.Elements[0].Expr
2674+
for i := 1; i < len(create.TTL.Elements); i++ {
2675+
create.TTL.Expressions = append(create.TTL.Expressions, create.TTL.Elements[i].Expr)
26992676
}
27002677
}
2701-
// Handle WHERE clause in TTL (conditional deletion)
2702-
if p.currentIs(token.WHERE) {
2703-
p.nextToken()
2704-
// Parse WHERE condition
2705-
p.parseExpression(ALIAS_PREC)
2706-
}
27072678
case p.currentIs(token.SETTINGS):
27082679
p.nextToken()
27092680
create.Settings = p.parseSettingsList()
@@ -8068,6 +8039,119 @@ func (p *Parser) parseTransactionControl() *ast.TransactionControlQuery {
80688039
return query
80698040
}
80708041

8042+
// parseTTLElement parses a single TTL element: expression [DELETE] [WHERE condition] [GROUP BY ...] [SET ...]
8043+
func (p *Parser) parseTTLElement() *ast.TTLElement {
8044+
elem := &ast.TTLElement{
8045+
Position: p.current.Pos,
8046+
Expr: p.parseExpression(ALIAS_PREC),
8047+
}
8048+
// Skip RECOMPRESS CODEC(...), DELETE, TO DISK, TO VOLUME (but not WHERE)
8049+
p.skipTTLModifiersExceptWhere()
8050+
// Handle WHERE clause for this TTL element (conditional deletion)
8051+
if p.currentIs(token.WHERE) {
8052+
p.nextToken()
8053+
elem.Where = p.parseExpression(ALIAS_PREC)
8054+
}
8055+
// Handle GROUP BY x SET y = max(y) syntax (skip for now, already parsed in Where or just skip)
8056+
if p.currentIs(token.GROUP) {
8057+
p.nextToken()
8058+
if p.currentIs(token.BY) {
8059+
p.nextToken()
8060+
for {
8061+
p.parseExpression(ALIAS_PREC)
8062+
if p.currentIs(token.COMMA) {
8063+
p.nextToken()
8064+
} else {
8065+
break
8066+
}
8067+
}
8068+
}
8069+
}
8070+
// Handle SET clause - assignments are comma separated (id = expr, id = expr, ...)
8071+
// We need to distinguish between:
8072+
// - Comma continuing SET: followed by IDENT = pattern
8073+
// - Comma starting new TTL: followed by expression (like d + toIntervalYear(...))
8074+
if p.currentIs(token.SET) {
8075+
p.nextToken()
8076+
for {
8077+
// Parse assignment expression: id = expr
8078+
p.parseExpression(ALIAS_PREC)
8079+
// Check for comma
8080+
if p.currentIs(token.COMMA) {
8081+
// Look ahead to check pattern. We need to see: COMMA IDENT EQ
8082+
// Save state to peek ahead
8083+
savedCurrent := p.current
8084+
savedPeek := p.peek
8085+
p.nextToken() // skip comma to see what follows
8086+
isSetContinuation := false
8087+
if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() {
8088+
if p.peekIs(token.EQ) {
8089+
// It's another SET assignment (id = expr)
8090+
isSetContinuation = true
8091+
}
8092+
}
8093+
if isSetContinuation {
8094+
// Continue parsing SET assignments (already consumed comma)
8095+
continue
8096+
}
8097+
// Not a SET assignment - restore state so caller sees the comma
8098+
p.current = savedCurrent
8099+
p.peek = savedPeek
8100+
break
8101+
}
8102+
// No comma, end of SET clause
8103+
break
8104+
}
8105+
}
8106+
return elem
8107+
}
8108+
8109+
// skipTTLModifiersExceptWhere skips TTL modifiers but stops at WHERE
8110+
func (p *Parser) skipTTLModifiersExceptWhere() {
8111+
for {
8112+
// Skip RECOMPRESS CODEC(...)
8113+
if p.currentIs(token.IDENT) && strings.ToUpper(p.current.Value) == "RECOMPRESS" {
8114+
p.nextToken()
8115+
if p.currentIs(token.IDENT) && strings.ToUpper(p.current.Value) == "CODEC" {
8116+
p.nextToken()
8117+
if p.currentIs(token.LPAREN) {
8118+
depth := 1
8119+
p.nextToken()
8120+
for depth > 0 && !p.currentIs(token.EOF) {
8121+
if p.currentIs(token.LPAREN) {
8122+
depth++
8123+
} else if p.currentIs(token.RPAREN) {
8124+
depth--
8125+
}
8126+
p.nextToken()
8127+
}
8128+
}
8129+
}
8130+
continue
8131+
}
8132+
// Skip DELETE (TTL ... DELETE)
8133+
if p.currentIs(token.DELETE) {
8134+
p.nextToken()
8135+
continue
8136+
}
8137+
// Skip TO DISK 'name' or TO VOLUME 'name'
8138+
if p.currentIs(token.TO) {
8139+
p.nextToken()
8140+
if p.currentIs(token.IDENT) {
8141+
upper := strings.ToUpper(p.current.Value)
8142+
if upper == "DISK" || upper == "VOLUME" {
8143+
p.nextToken()
8144+
if p.currentIs(token.STRING) {
8145+
p.nextToken()
8146+
}
8147+
continue
8148+
}
8149+
}
8150+
}
8151+
break
8152+
}
8153+
}
8154+
80718155
// skipTTLModifiers skips TTL modifiers like RECOMPRESS CODEC(...), DELETE, TO DISK, TO VOLUME
80728156
func (p *Parser) skipTTLModifiers() {
80738157
for {
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"explain_todo": {
3-
"stmt11": true,
4-
"stmt3": true
3+
"stmt11": true
54
}
65
}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt2": true
4-
}
5-
}
1+
{}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt2": true
4-
}
5-
}
1+
{}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt3": true
4-
}
5-
}
1+
{}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt1": true
4-
}
5-
}
1+
{}

0 commit comments

Comments
 (0)