Skip to content

Commit 71cfd12

Browse files
committed
feat(clickhouse): support {name:Type} named parameters
- Update ClickHouse test queries to use {name:Type} parameter syntax instead of ? placeholders for better type inference - Add regex-based parameter detection for named parameters - Add replaceParamsWithNull helper to handle named params in DESCRIBE - Set NotNull: true for parameters to generate non-nullable Go types - Remove unused AST walking code that was replaced by regex approach 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 29cdfb9 commit 71cfd12

File tree

3 files changed

+38
-131
lines changed

3 files changed

+38
-131
lines changed

internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/go/query.sql.go

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
-- name: GetAuthor :one
2-
SELECT id, name, bio FROM authors WHERE id = ?;
2+
SELECT id, name, bio FROM authors WHERE id = {id:UInt64};
33

44
-- name: ListAuthors :many
55
SELECT id, name, bio FROM authors ORDER BY name;
66

77
-- name: CreateAuthor :exec
8-
INSERT INTO authors (id, name, bio) VALUES (?, ?, ?);
8+
INSERT INTO authors (id, name, bio) VALUES ({id:UInt64}, {name:String}, {bio:String});

internal/engine/clickhouse/analyzer/analyze.go

Lines changed: 28 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@ import (
44
"context"
55
"database/sql"
66
"fmt"
7+
"regexp"
78
"strings"
89
"sync"
910

1011
_ "github.com/ClickHouse/clickhouse-go/v2" // ClickHouse driver
11-
dcast "github.com/sqlc-dev/doubleclick/ast"
12-
"github.com/sqlc-dev/doubleclick/parser"
1312

1413
core "github.com/sqlc-dev/sqlc/internal/analysis"
1514
"github.com/sqlc-dev/sqlc/internal/config"
@@ -59,9 +58,9 @@ func (a *Analyzer) Analyze(ctx context.Context, n ast.Node, query string, migrat
5958
if isSelectQuery {
6059
// For ClickHouse, we use DESCRIBE or LIMIT 0 to get column information
6160

62-
// Replace ? placeholders with NULL for introspection
63-
// This allows us to run the query to get column types
64-
preparedQuery := strings.ReplaceAll(query, "?", "NULL")
61+
// Replace all parameter placeholders with NULL for introspection
62+
// This handles both ? placeholders and {name:Type} named parameters
63+
preparedQuery := replaceParamsWithNull(query)
6564

6665
// Use DESCRIBE (query) to get column information
6766
describeQuery := fmt.Sprintf("DESCRIBE (%s)", preparedQuery)
@@ -111,7 +110,7 @@ func (a *Analyzer) Analyze(ctx context.Context, n ast.Node, query string, migrat
111110
Column: &core.Column{
112111
Name: param.Name,
113112
DataType: param.Type,
114-
NotNull: false,
113+
NotNull: true, // Parameters are typically not nullable
115114
},
116115
})
117116
}
@@ -332,38 +331,26 @@ type paramInfo struct {
332331
Type string
333332
}
334333

335-
// detectParameters finds parameters in a ClickHouse query using the doubleclick parser.
334+
// detectParameters finds parameters in a ClickHouse query.
336335
// ClickHouse supports {name:Type} and ? style parameters.
337336
func detectParameters(query string) []paramInfo {
338337
var params []paramInfo
339338

340-
// First, try to find {name:Type} style parameters using the doubleclick parser
341-
ctx := context.Background()
342-
stmts, err := parser.Parse(ctx, strings.NewReader(query))
343-
if err == nil {
344-
// Walk the AST to find Parameter nodes (for {name:Type} style)
345-
for _, stmt := range stmts {
346-
walkStatement(stmt, func(expr dcast.Expression) {
347-
if param, ok := expr.(*dcast.Parameter); ok {
348-
name := param.Name
349-
dataType := "any"
350-
if param.Type != nil {
351-
dataType = normalizeType(param.Type.Name)
352-
}
353-
if name != "" {
354-
// Only add named parameters from the parser
355-
params = append(params, paramInfo{
356-
Name: name,
357-
Type: dataType,
358-
})
359-
}
360-
}
339+
// Find all {name:Type} style parameters using regex
340+
// This is more reliable than AST walking as it works for all statement types
341+
matches := namedParamRegex.FindAllStringSubmatch(query, -1)
342+
for _, match := range matches {
343+
if len(match) >= 3 {
344+
name := match[1]
345+
dataType := normalizeType(match[2])
346+
params = append(params, paramInfo{
347+
Name: name,
348+
Type: dataType,
361349
})
362350
}
363351
}
364352

365-
// Count ? placeholders (the doubleclick parser doesn't fully support these)
366-
// The ? placeholders are added after any named parameters
353+
// Count ? placeholders and add them after any named parameters
367354
count := strings.Count(query, "?")
368355
for i := 0; i < count; i++ {
369356
params = append(params, paramInfo{
@@ -375,97 +362,17 @@ func detectParameters(query string) []paramInfo {
375362
return params
376363
}
377364

378-
// walkStatement walks a statement and calls fn for each expression.
379-
func walkStatement(stmt dcast.Statement, fn func(dcast.Expression)) {
380-
switch s := stmt.(type) {
381-
case *dcast.SelectQuery:
382-
walkSelectQuery(s, fn)
383-
case *dcast.SelectWithUnionQuery:
384-
for _, sel := range s.Selects {
385-
walkStatement(sel, fn)
386-
}
387-
case *dcast.InsertQuery:
388-
if s.Select != nil {
389-
walkStatement(s.Select, fn)
390-
}
391-
}
392-
}
393-
394-
// walkSelectQuery walks a SELECT query and calls fn for each expression.
395-
func walkSelectQuery(s *dcast.SelectQuery, fn func(dcast.Expression)) {
396-
// Walk columns
397-
for _, col := range s.Columns {
398-
walkExpression(col, fn)
399-
}
400-
// Walk WHERE clause
401-
if s.Where != nil {
402-
walkExpression(s.Where, fn)
403-
}
404-
// Walk GROUP BY
405-
for _, g := range s.GroupBy {
406-
walkExpression(g, fn)
407-
}
408-
// Walk HAVING
409-
if s.Having != nil {
410-
walkExpression(s.Having, fn)
411-
}
412-
// Walk ORDER BY
413-
for _, o := range s.OrderBy {
414-
walkExpression(o.Expression, fn)
415-
}
416-
// Walk LIMIT
417-
if s.Limit != nil {
418-
walkExpression(s.Limit, fn)
419-
}
420-
// Walk OFFSET
421-
if s.Offset != nil {
422-
walkExpression(s.Offset, fn)
423-
}
424-
}
425-
426-
// walkExpression walks an expression and calls fn for each sub-expression.
427-
func walkExpression(expr dcast.Expression, fn func(dcast.Expression)) {
428-
if expr == nil {
429-
return
430-
}
431-
fn(expr)
432-
433-
switch e := expr.(type) {
434-
case *dcast.BinaryExpr:
435-
walkExpression(e.Left, fn)
436-
walkExpression(e.Right, fn)
437-
case *dcast.UnaryExpr:
438-
walkExpression(e.Operand, fn)
439-
case *dcast.FunctionCall:
440-
for _, arg := range e.Arguments {
441-
walkExpression(arg, fn)
442-
}
443-
case *dcast.Subquery:
444-
walkStatement(e.Query, fn)
445-
case *dcast.CaseExpr:
446-
if e.Operand != nil {
447-
walkExpression(e.Operand, fn)
448-
}
449-
for _, when := range e.Whens {
450-
walkExpression(when.Condition, fn)
451-
walkExpression(when.Result, fn)
452-
}
453-
if e.Else != nil {
454-
walkExpression(e.Else, fn)
455-
}
456-
case *dcast.InExpr:
457-
walkExpression(e.Expr, fn)
458-
for _, v := range e.List {
459-
walkExpression(v, fn)
460-
}
461-
if e.Query != nil {
462-
walkStatement(e.Query, fn)
463-
}
464-
case *dcast.BetweenExpr:
465-
walkExpression(e.Expr, fn)
466-
walkExpression(e.Low, fn)
467-
walkExpression(e.High, fn)
468-
}
365+
// namedParamRegex matches ClickHouse named parameters like {name:Type}
366+
var namedParamRegex = regexp.MustCompile(`\{(\w+):(\w+)\}`)
367+
368+
// replaceParamsWithNull replaces all parameter placeholders with NULL for query introspection.
369+
// It handles both ? placeholders and {name:Type} named parameters.
370+
func replaceParamsWithNull(query string) string {
371+
// Replace {name:Type} named parameters with NULL
372+
result := namedParamRegex.ReplaceAllString(query, "NULL")
373+
// Also replace ? placeholders with NULL
374+
result = strings.ReplaceAll(result, "?", "NULL")
375+
return result
469376
}
470377

471378
// addLimit0 adds LIMIT 0 to a query for schema introspection.

0 commit comments

Comments
 (0)