Skip to content

Commit 55ed562

Browse files
committed
Fix 27 parser and explain output issues
- Add MarshalJSON for Literal to handle NaN/Inf values in JSON serialization - Support INTERVAL and FORMAT keywords as identifiers when not used as keywords - Handle uint64 numbers that exceed int64 range - Add InsertQuery explain output support - Expand nested DataType parameters in explain output (e.g., Array(DateTime)) - Fix FormatLiteral to handle both int64 and uint64 integer types Test count: 5488 passing (up from 5461)
1 parent c3f8187 commit 55ed562

5 files changed

Lines changed: 110 additions & 9 deletions

File tree

ast/ast.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
package ast
33

44
import (
5+
"encoding/json"
6+
"math"
7+
58
"github.com/kyleconroy/doubleclick/token"
69
)
710

@@ -589,6 +592,42 @@ func (l *Literal) Pos() token.Position { return l.Position }
589592
func (l *Literal) End() token.Position { return l.Position }
590593
func (l *Literal) expressionNode() {}
591594

595+
// MarshalJSON handles special float values (NaN, +Inf, -Inf) that JSON doesn't support.
596+
func (l *Literal) MarshalJSON() ([]byte, error) {
597+
type literalAlias Literal
598+
// Handle special float values
599+
if f, ok := l.Value.(float64); ok {
600+
if math.IsNaN(f) {
601+
return json.Marshal(&struct {
602+
*literalAlias
603+
Value string `json:"value"`
604+
}{
605+
literalAlias: (*literalAlias)(l),
606+
Value: "NaN",
607+
})
608+
}
609+
if math.IsInf(f, 1) {
610+
return json.Marshal(&struct {
611+
*literalAlias
612+
Value string `json:"value"`
613+
}{
614+
literalAlias: (*literalAlias)(l),
615+
Value: "+Inf",
616+
})
617+
}
618+
if math.IsInf(f, -1) {
619+
return json.Marshal(&struct {
620+
*literalAlias
621+
Value string `json:"value"`
622+
}{
623+
literalAlias: (*literalAlias)(l),
624+
Value: "-Inf",
625+
})
626+
}
627+
}
628+
return json.Marshal((*literalAlias)(l))
629+
}
630+
592631
// LiteralType represents the type of a literal.
593632
type LiteralType string
594633

internal/explain/explain.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ func Node(sb *strings.Builder, node interface{}, depth int) {
9797
explainExtractExpr(sb, n, indent, depth)
9898

9999
// DDL statements
100+
case *ast.InsertQuery:
101+
explainInsertQuery(sb, n, indent, depth)
100102
case *ast.CreateQuery:
101103
explainCreateQuery(sb, n, indent, depth)
102104
case *ast.DropQuery:

internal/explain/format.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,18 @@ import (
1111
func FormatLiteral(lit *ast.Literal) string {
1212
switch lit.Type {
1313
case ast.LiteralInteger:
14-
val := lit.Value.(int64)
15-
if val >= 0 {
14+
// Handle both int64 and uint64 values
15+
switch val := lit.Value.(type) {
16+
case int64:
17+
if val >= 0 {
18+
return fmt.Sprintf("UInt64_%d", val)
19+
}
20+
return fmt.Sprintf("Int64_%d", val)
21+
case uint64:
1622
return fmt.Sprintf("UInt64_%d", val)
23+
default:
24+
return fmt.Sprintf("UInt64_%v", lit.Value)
1725
}
18-
return fmt.Sprintf("Int64_%d", val)
1926
case ast.LiteralFloat:
2027
val := lit.Value.(float64)
2128
return fmt.Sprintf("Float64_%v", val)

internal/explain/statements.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,35 @@ import (
77
"github.com/kyleconroy/doubleclick/ast"
88
)
99

10+
func explainInsertQuery(sb *strings.Builder, n *ast.InsertQuery, indent string, depth int) {
11+
// Count children
12+
children := 0
13+
if n.Function != nil {
14+
children++
15+
} else if n.Table != "" {
16+
children++ // Table identifier
17+
}
18+
if n.Select != nil {
19+
children++
20+
}
21+
// Note: InsertQuery uses 3 spaces after name in ClickHouse explain
22+
fmt.Fprintf(sb, "%sInsertQuery (children %d)\n", indent, children)
23+
24+
if n.Function != nil {
25+
Node(sb, n.Function, depth+1)
26+
} else if n.Table != "" {
27+
name := n.Table
28+
if n.Database != "" {
29+
name = n.Database + "." + n.Table
30+
}
31+
fmt.Fprintf(sb, "%s Identifier %s\n", indent, name)
32+
}
33+
34+
if n.Select != nil {
35+
Node(sb, n.Select, depth+1)
36+
}
37+
}
38+
1039
func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string, depth int) {
1140
name := n.Table
1241
if n.View != "" {
@@ -139,6 +168,15 @@ func explainDescribeQuery(sb *strings.Builder, n *ast.DescribeQuery, indent stri
139168
}
140169

141170
func explainDataType(sb *strings.Builder, n *ast.DataType, indent string, depth int) {
171+
// Check if type has nested DataType parameters that should be expanded
172+
hasNestedTypes := false
173+
for _, p := range n.Parameters {
174+
if _, ok := p.(*ast.DataType); ok {
175+
hasNestedTypes = true
176+
break
177+
}
178+
}
179+
142180
// Check if type has complex parameters (expressions, not just literals/types)
143181
hasComplexParams := false
144182
for _, p := range n.Parameters {
@@ -152,8 +190,8 @@ func explainDataType(sb *strings.Builder, n *ast.DataType, indent string, depth
152190
break
153191
}
154192

155-
if hasComplexParams && len(n.Parameters) > 0 {
156-
// Complex parameters need to be output as children
193+
if (hasNestedTypes || hasComplexParams) && len(n.Parameters) > 0 {
194+
// Nested types and complex parameters need to be output as children
157195
fmt.Fprintf(sb, "%sDataType %s (children %d)\n", indent, n.Name, 1)
158196
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.Parameters))
159197
for _, p := range n.Parameters {

parser/expression.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,13 @@ func (p *Parser) parsePrefixExpression() ast.Expression {
206206
case token.EXTRACT:
207207
return p.parseExtract()
208208
case token.INTERVAL:
209-
return p.parseInterval()
209+
// INTERVAL can be a literal (INTERVAL 1 DAY) or identifier reference
210+
// Check if next token can start an interval value
211+
if p.peekIs(token.NUMBER) || p.peekIs(token.LPAREN) || p.peekIs(token.MINUS) || p.peekIs(token.STRING) {
212+
return p.parseInterval()
213+
}
214+
// Otherwise treat as identifier
215+
return p.parseKeywordAsIdentifier()
210216
case token.EXISTS:
211217
return p.parseExists()
212218
case token.PARAM:
@@ -230,7 +236,8 @@ func (p *Parser) parsePrefixExpression() ast.Expression {
230236
if p.peekIs(token.LPAREN) {
231237
return p.parseKeywordAsFunction()
232238
}
233-
return nil
239+
// format as identifier (e.g., format='Parquet' in function args)
240+
return p.parseKeywordAsIdentifier()
234241
default:
235242
// Handle other keywords that can be used as function names or identifiers
236243
if p.current.Token.IsKeyword() {
@@ -572,10 +579,18 @@ func (p *Parser) parseNumber() ast.Expression {
572579
lit.Value = f
573580
}
574581
} else {
582+
// Try signed int64 first
575583
i, err := strconv.ParseInt(value, 10, 64)
576584
if err != nil {
577-
lit.Type = ast.LiteralString
578-
lit.Value = value
585+
// Try unsigned uint64 for large positive numbers
586+
u, uerr := strconv.ParseUint(value, 10, 64)
587+
if uerr != nil {
588+
lit.Type = ast.LiteralString
589+
lit.Value = value
590+
} else {
591+
lit.Type = ast.LiteralInteger
592+
lit.Value = u // Store as uint64
593+
}
579594
} else {
580595
lit.Type = ast.LiteralInteger
581596
lit.Value = i

0 commit comments

Comments
 (0)