Skip to content

Commit d654d51

Browse files
authored
Merge branch 'main' into feat/snowflake-time-travel-483
2 parents d68d731 + 132a13e commit d654d51

File tree

2 files changed

+65
-0
lines changed

2 files changed

+65
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
// TestClickHouseOrderByWithFill verifies ClickHouse's `ORDER BY expr WITH
15+
// FILL [FROM..] [TO..] [STEP..]` modifier parses. Previously the WITH token
16+
// after ORDER BY items was mis-routed to the WITH-CTE parser. Regression
17+
// for #482.
18+
func TestClickHouseOrderByWithFill(t *testing.T) {
19+
queries := map[string]string{
20+
"bare": `SELECT id FROM t ORDER BY id WITH FILL`,
21+
"step_only": `SELECT id FROM t ORDER BY id WITH FILL STEP 1`,
22+
"from_to_step": `SELECT day, count() FROM events GROUP BY day ORDER BY day WITH FILL FROM '2024-01-01' TO '2024-12-31' STEP INTERVAL 1 DAY`,
23+
"multiple_cols": `SELECT a, b FROM t ORDER BY a WITH FILL STEP 1, b`,
24+
"with_desc": `SELECT id FROM t ORDER BY id DESC WITH FILL STEP 1`,
25+
}
26+
for name, q := range queries {
27+
q := q
28+
t.Run(name, func(t *testing.T) {
29+
if _, err := gosqlx.ParseWithDialect(q, keywords.DialectClickHouse); err != nil {
30+
t.Fatalf("parse failed: %v", err)
31+
}
32+
})
33+
}
34+
}

pkg/sql/parser/select_clauses.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,17 @@ func (p *Parser) parseOrderByClause() ([]ast.OrderByExpression, error) {
548548
}
549549
entry.NullsFirst = nullsFirst
550550

551+
// ClickHouse WITH FILL per-entry tail:
552+
// ORDER BY expr [ASC|DESC] WITH FILL [FROM x] [TO y] [STEP z]
553+
// Consume permissively; the clause is not modeled on the AST yet.
554+
if p.dialect == string(keywords.DialectClickHouse) &&
555+
p.isType(models.TokenTypeWith) &&
556+
strings.EqualFold(p.peekToken().Token.Value, "FILL") {
557+
p.advance() // WITH
558+
p.advance() // FILL
559+
p.skipClickHouseWithFillTail()
560+
}
561+
551562
orderByExprs = append(orderByExprs, entry)
552563

553564
if !p.isType(models.TokenTypeComma) {
@@ -558,6 +569,26 @@ func (p *Parser) parseOrderByClause() ([]ast.OrderByExpression, error) {
558569
return orderByExprs, nil
559570
}
560571

572+
// skipClickHouseWithFillTail consumes the optional FROM / TO / STEP arguments
573+
// of a ClickHouse "ORDER BY expr WITH FILL" modifier. Each argument is a
574+
// single expression (possibly an INTERVAL). The tail ends at the next comma
575+
// (more ORDER BY items), next clause keyword, ';', or EOF.
576+
func (p *Parser) skipClickHouseWithFillTail() {
577+
for {
578+
val := strings.ToUpper(p.currentToken.Token.Value)
579+
if val != "FROM" && val != "TO" && val != "STEP" {
580+
return
581+
}
582+
p.advance() // FROM / TO / STEP
583+
// Consume one expression; ignore parse errors so unusual forms
584+
// (INTERVAL '1 day', expressions with function calls, etc.) don't
585+
// surface as parser errors for this permissive skip.
586+
if _, err := p.parseExpression(); err != nil {
587+
return
588+
}
589+
}
590+
}
591+
561592
// parseLimitOffsetClause parses optional LIMIT and/or OFFSET clauses.
562593
// Supports standard "LIMIT n OFFSET m", MySQL "LIMIT offset, count", and
563594
// SQL-99 "OFFSET n ROWS" (ROW/ROWS consumed but value stored).

0 commit comments

Comments
 (0)