Skip to content
Open
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
4 changes: 4 additions & 0 deletions pkg/sql/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@ func (i Identifier) Children() []Node { return nil }
type FunctionCall struct {
Name string
Arguments []Expression // Renamed from Args for consistency
Parameters []Expression // ClickHouse parametric aggregates: quantile(0.5)(x) — params before args
Over *WindowSpec // For window functions
Distinct bool
Filter Expression // WHERE clause for aggregate functions
Expand All @@ -705,6 +706,9 @@ func (f *FunctionCall) expressionNode() {}
func (f FunctionCall) TokenLiteral() string { return f.Name }
func (f FunctionCall) Children() []Node {
children := nodifyExpressions(f.Arguments)
if len(f.Parameters) > 0 {
children = append(children, nodifyExpressions(f.Parameters)...)
}
if f.Over != nil {
children = append(children, f.Over)
}
Expand Down
74 changes: 74 additions & 0 deletions pkg/sql/parser/clickhouse_parametric_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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/ast"
"github.com/ajitpratap0/GoSQLX/pkg/sql/keywords"
)

// TestClickHouseParametricAggregates verifies that ClickHouse parametric
// aggregates of the form `funcName(params)(args)` parse. Regression for #482.
func TestClickHouseParametricAggregates(t *testing.T) {
queries := map[string]string{
"quantile_tdigest": `SELECT quantileTDigest(0.95)(value) FROM events`,
"top_k": `SELECT topK(10)(name) FROM users`,
"quantiles": `SELECT quantiles(0.5, 0.9, 0.99)(latency_ms) FROM requests`,
"with_group_by": `SELECT category, quantileTDigest(0.99)(price) FROM products GROUP BY category`,
}
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)
}
})
}
}

// TestClickHouseParametricAggregates_ASTShape verifies that the Parameters
// field is populated and reachable via the visitor pattern.
func TestClickHouseParametricAggregates_ASTShape(t *testing.T) {
q := `SELECT quantileTDigest(0.95)(value) FROM events`
tree, err := gosqlx.ParseWithDialect(q, keywords.DialectClickHouse)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
// Walk the tree until we find a FunctionCall node and verify both
// Parameters and Arguments are populated, and Children() exposes both.
var found bool
var visit func(n ast.Node)
visit = func(n ast.Node) {
if n == nil || found {
return
}
if fc, ok := n.(*ast.FunctionCall); ok && fc.Name == "quantileTDigest" {
if len(fc.Parameters) != 1 {
t.Fatalf("Parameters: want 1, got %d", len(fc.Parameters))
}
if len(fc.Arguments) != 1 {
t.Fatalf("Arguments: want 1, got %d", len(fc.Arguments))
}
// Children() must include both the argument and the parameter.
if len(fc.Children()) < 2 {
t.Fatalf("Children(): want >=2 (args + params), got %d", len(fc.Children()))
}
found = true
return
}
for _, c := range n.Children() {
visit(c)
}
}
for _, stmt := range tree.Statements {
visit(stmt)
}
if !found {
t.Fatal("did not find quantileTDigest FunctionCall in AST")
}
}
31 changes: 31 additions & 0 deletions pkg/sql/parser/window.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/ajitpratap0/GoSQLX/pkg/models"
"github.com/ajitpratap0/GoSQLX/pkg/sql/ast"
"github.com/ajitpratap0/GoSQLX/pkg/sql/keywords"
)

// SUM(salary) OVER (PARTITION BY dept ORDER BY date ROWS UNBOUNDED PRECEDING) -> window function with frame
Expand Down Expand Up @@ -153,6 +154,36 @@ func (p *Parser) parseFunctionCall(funcName string) (*ast.FunctionCall, error) {
OrderBy: orderByExprs,
}

// ClickHouse parametric aggregates: funcName(params)(args).
// e.g. quantileTDigest(0.95)(value), topK(10)(name).
// What we just parsed becomes Parameters; the next paren group is the
// real arguments. Gated to ClickHouse to avoid false positives.
if p.dialect == string(keywords.DialectClickHouse) && p.isType(models.TokenTypeLParen) {
funcCall.Parameters = funcCall.Arguments
funcCall.Arguments = nil
p.advance() // Consume second (
if !p.isType(models.TokenTypeRParen) {
for {
arg, err := p.parseExpression()
if err != nil {
return nil, err
}
funcCall.Arguments = append(funcCall.Arguments, arg)
if p.isType(models.TokenTypeComma) {
p.advance()
} else if p.isType(models.TokenTypeRParen) {
break
} else {
return nil, p.expectedError(", or )")
}
}
}
if !p.isType(models.TokenTypeRParen) {
return nil, p.expectedError(")")
}
p.advance() // Consume second )
}

// Check for IGNORE NULLS / RESPECT NULLS (SQL:2016 null treatment).
// Used by LAG, LEAD, FIRST_VALUE, LAST_VALUE, NTH_VALUE in Snowflake,
// Oracle, BigQuery, etc. IGNORE arrives as TokenTypeKeyword; RESPECT is
Expand Down
Loading