diff --git a/ast/ast.go b/ast/ast.go index 0b7cb822f..5c3111d1f 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -920,6 +920,7 @@ type CaseExpr struct { Whens []*WhenClause `json:"whens"` Else Expression `json:"else,omitempty"` Alias string `json:"alias,omitempty"` + QuotedAlias bool `json:"quoted_alias,omitempty"` // true if alias was double-quoted } func (c *CaseExpr) Pos() token.Position { return c.Position } diff --git a/internal/explain/explain.go b/internal/explain/explain.go index 7739112e1..7b1fbdb18 100644 --- a/internal/explain/explain.go +++ b/internal/explain/explain.go @@ -69,6 +69,8 @@ func Node(sb *strings.Builder, node interface{}, depth int) { explainWithElement(sb, n, indent, depth) case *ast.Asterisk: explainAsterisk(sb, n, indent) + case *ast.ColumnsMatcher: + fmt.Fprintf(sb, "%sColumnsRegexpMatcher\n", indent) // Functions case *ast.FunctionCall: diff --git a/internal/explain/format.go b/internal/explain/format.go index c75976705..7cdeab4f8 100644 --- a/internal/explain/format.go +++ b/internal/explain/format.go @@ -47,7 +47,7 @@ func escapeStringLiteral(s string) string { case '\\': sb.WriteString("\\\\\\\\") // backslash becomes four backslashes (\\\\) case '\'': - sb.WriteString("\\\\\\'") // single quote becomes \\\' (escaped backslash + escaped quote) + sb.WriteString("\\\\\\'") // single quote becomes \\\' (three backslashes + quote) case '\n': sb.WriteString("\\\\n") // newline becomes \\n case '\t': diff --git a/internal/explain/functions.go b/internal/explain/functions.go index 84b143983..e8f83bc56 100644 --- a/internal/explain/functions.go +++ b/internal/explain/functions.go @@ -139,6 +139,12 @@ func explainCastExprWithAlias(sb *strings.Builder, n *ast.CastExpr, alias string Node(sb, n.TypeExpr, depth+2) } else { typeStr := FormatDataType(n.Type) + // Only escape if the DataType doesn't have parameters - this means the entire + // type was parsed from a string literal and may contain unescaped quotes. + // If it has parameters, FormatDataType already handles escaping. + if n.Type == nil || len(n.Type.Parameters) == 0 { + typeStr = escapeStringLiteral(typeStr) + } fmt.Fprintf(sb, "%s Literal \\'%s\\'\n", indent, typeStr) } } @@ -711,7 +717,12 @@ func explainIsNullExpr(sb *strings.Builder, n *ast.IsNullExpr, indent string, de } func explainCaseExpr(sb *strings.Builder, n *ast.CaseExpr, indent string, depth int) { - explainCaseExprWithAlias(sb, n, "", indent, depth) + // Only output alias if it's unquoted (ClickHouse doesn't show quoted aliases) + alias := "" + if n.Alias != "" && !n.QuotedAlias { + alias = n.Alias + } + explainCaseExprWithAlias(sb, n, alias, indent, depth) } func explainCaseExprWithAlias(sb *strings.Builder, n *ast.CaseExpr, alias string, indent string, depth int) { diff --git a/lexer/lexer.go b/lexer/lexer.go index d59ee3e7f..eb98e1ee1 100644 --- a/lexer/lexer.go +++ b/lexer/lexer.go @@ -21,9 +21,10 @@ type Lexer struct { // Item represents a lexical token with its value and position. type Item struct { - Token token.Token - Value string - Pos token.Position + Token token.Token + Value string + Pos token.Position + Quoted bool // true if this identifier was double-quoted } // New creates a new Lexer from an io.Reader. @@ -453,7 +454,7 @@ func (l *Lexer) readQuotedIdentifier() Item { sb.WriteRune(l.ch) l.readChar() } - return Item{Token: token.IDENT, Value: sb.String(), Pos: pos} + return Item{Token: token.IDENT, Value: sb.String(), Pos: pos, Quoted: true} } // readUnicodeString reads a string enclosed in Unicode curly quotes (' or ') @@ -497,7 +498,7 @@ func (l *Lexer) readUnicodeQuotedIdentifier(openQuote rune) Item { if l.ch == closeQuote { l.readChar() // skip closing quote } - return Item{Token: token.IDENT, Value: sb.String(), Pos: pos} + return Item{Token: token.IDENT, Value: sb.String(), Pos: pos, Quoted: true} } func (l *Lexer) readBacktickIdentifier() Item { diff --git a/parser/expression.go b/parser/expression.go index ccc3e284d..79698675b 100644 --- a/parser/expression.go +++ b/parser/expression.go @@ -969,6 +969,7 @@ func (p *Parser) parseCase() ast.Expression { p.nextToken() if p.currentIs(token.IDENT) { expr.Alias = p.current.Value + expr.QuotedAlias = p.current.Quoted p.nextToken() } } diff --git a/parser/testdata/00551_parse_or_null/metadata.json b/parser/testdata/00551_parse_or_null/metadata.json index ef120d978..0967ef424 100644 --- a/parser/testdata/00551_parse_or_null/metadata.json +++ b/parser/testdata/00551_parse_or_null/metadata.json @@ -1 +1 @@ -{"todo": true} +{} diff --git a/parser/testdata/02244_casewithexpression_return_type/ast.json b/parser/testdata/02244_casewithexpression_return_type/ast.json index 5731f55ae..b12b3751c 100644 --- a/parser/testdata/02244_casewithexpression_return_type/ast.json +++ b/parser/testdata/02244_casewithexpression_return_type/ast.json @@ -89,7 +89,8 @@ "value": 555555 } }, - "alias": "LONG_COL_0" + "alias": "LONG_COL_0", + "quoted_alias": true } ], "from": { diff --git a/parser/testdata/02910_nullable_enum_cast/metadata.json b/parser/testdata/02910_nullable_enum_cast/metadata.json index ef120d978..0967ef424 100644 --- a/parser/testdata/02910_nullable_enum_cast/metadata.json +++ b/parser/testdata/02910_nullable_enum_cast/metadata.json @@ -1 +1 @@ -{"todo": true} +{}