@@ -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.
337336func 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