Files
zeekay 4d341a6a1f read back the named tuple ClickHouse prints
A tuple column is declared either way — Tuple(String, Array(String)) or
Tuple(label String, path Array(String)) — and SHOW CREATE TABLE returns whichever
the column carries. Only the unnamed form parsed, so a schema the server printed
itself could not be read back: the parse stopped at the first element's TYPE with
"expected the last token kind is: )" under `label String`.

The named form is a column list, which parseNestedType already reads for Nested,
so this routes to it on one token of lookahead rather than teaching a second
grammar: inside the parens a NAME is followed by its type (an identifier), while
an unnamed element is followed by a comma, the closing paren, or its own
parameters.

Measured on the estate: event.log and event.span each carry a named tuple, and
every logs query against them answered 500 "failed to get logs keys" because the
metadata step reads the table back through this parser.
2026-08-13 19:02:18 -07:00

1286 lines
33 KiB
Go

package parser
import (
"fmt"
"strings"
)
const (
PrecedenceUnknown = iota
PrecedenceOr
PrecedenceAnd
PrecedenceQuery
PrecedenceNot
PrecedenceGlobal
PrecedenceIs
PrecedenceCompare
PrecedenceBetweenLike
precedenceIn
PrecedenceAddSub
PrecedenceMulDivMod
PrecedenceBracket
PrecedenceArrow
PrecedenceDot
PrecedenceDoubleColon
)
func (p *Parser) tryParseColumnComment(pos Pos) (*StringLiteral, error) {
if !p.tryConsumeKeywords(KeywordComment) {
return nil, nil // nolint
}
return p.parseString(pos)
}
func (p *Parser) getNextPrecedence() int {
switch {
case p.matchKeyword(KeywordOr):
return PrecedenceOr
case p.matchKeyword(KeywordAnd):
return PrecedenceAnd
case p.matchKeyword(KeywordIs):
return PrecedenceIs
case p.matchKeyword(KeywordNot):
return PrecedenceNot
case p.matchTokenKind(TokenKindDot):
return PrecedenceDot
case p.matchTokenKind(TokenKindDash):
return PrecedenceDoubleColon
case p.matchTokenKind(TokenKindSingleEQ), p.matchTokenKind(TokenKindLT), p.matchTokenKind(TokenKindLE),
p.matchTokenKind(TokenKindGE), p.matchTokenKind(TokenKindGT), p.matchTokenKind(TokenKindDoubleEQ),
p.matchTokenKind(TokenKindNE), p.matchTokenKind("<>"):
return PrecedenceCompare
case p.matchTokenKind(TokenKindPlus), p.matchTokenKind(TokenKindMinus):
return PrecedenceAddSub
case p.matchTokenKind(TokenKindMul), p.matchTokenKind(TokenKindDiv), p.matchTokenKind(TokenKindMod):
return PrecedenceMulDivMod
case p.matchTokenKind(TokenKindArrow):
return PrecedenceArrow
case p.matchTokenKind(TokenKindLParen), p.matchTokenKind(TokenKindLBracket):
return PrecedenceBracket
case p.matchTokenKind(TokenKindDash):
return PrecedenceDoubleColon
case p.matchTokenKind(TokenKindDot):
return PrecedenceDot
case p.matchKeyword(KeywordBetween), p.matchKeyword(KeywordLike), p.matchKeyword(KeywordIlike):
return PrecedenceBetweenLike
case p.matchKeyword(KeywordIn):
return precedenceIn
case p.matchKeyword(KeywordGlobal):
return PrecedenceGlobal
case p.matchTokenKind(TokenKindQuestionMark):
return PrecedenceQuery
default:
return PrecedenceUnknown
}
}
func (p *Parser) parseInfix(expr Expr, precedence int) (Expr, error) {
switch {
case p.matchTokenKind(TokenKindSingleEQ), p.matchTokenKind(TokenKindLT), p.matchTokenKind(TokenKindLE),
p.matchTokenKind(TokenKindGE), p.matchTokenKind(TokenKindGT),
p.matchTokenKind(TokenKindNE), p.matchTokenKind("<>"),
p.matchTokenKind(TokenKindMinus), p.matchTokenKind(TokenKindPlus), p.matchTokenKind(TokenKindMul),
p.matchTokenKind(TokenKindDiv), p.matchTokenKind(TokenKindMod),
p.matchKeyword(KeywordIn), p.matchKeyword(KeywordLike),
p.matchKeyword(KeywordIlike), p.matchKeyword(KeywordAnd), p.matchKeyword(KeywordOr),
p.matchTokenKind(TokenKindArrow), p.matchTokenKind(TokenKindDoubleEQ):
op := p.last().ToString()
_ = p.lexer.consumeToken()
rightExpr, err := p.parseSubExpr(p.Pos(), precedence)
if err != nil {
return nil, err
}
return &BinaryOperation{
LeftExpr: expr,
Operation: TokenKind(op),
RightExpr: rightExpr,
}, nil
case p.matchTokenKind(TokenKindDash):
_ = p.lexer.consumeToken()
if p.matchTokenKind(TokenKindIdent) && p.last().String == "Tuple" {
name, err := p.parseIdent()
if err != nil {
return nil, err
}
if err := p.expectTokenKind(TokenKindLParen); err != nil {
return nil, err
}
// it's a tuple type definition after "::" operator
rightExpr, err := p.parseNestedType(name, p.Pos())
if err != nil {
return nil, err
}
return &BinaryOperation{
LeftExpr: expr,
Operation: TokenKindDash,
RightExpr: rightExpr,
}, nil
}
rightExpr, err := p.parseSubExpr(p.Pos(), precedence)
if err != nil {
return nil, err
}
return &BinaryOperation{
LeftExpr: expr,
Operation: TokenKindDash,
RightExpr: rightExpr,
}, nil
case p.matchKeyword(KeywordBetween):
return p.parseBetweenClause(expr)
case p.matchKeyword(KeywordGlobal):
_ = p.lexer.consumeToken()
if p.expectKeyword(KeywordIn) != nil {
return nil, fmt.Errorf("expected IN after GLOBAL, got %s", p.lastTokenKind())
}
rightExpr, err := p.parseSubExpr(p.Pos(), precedence)
if err != nil {
return nil, err
}
return &BinaryOperation{
LeftExpr: expr,
Operation: "GLOBAL IN",
RightExpr: rightExpr,
}, nil
case p.matchTokenKind(TokenKindDot):
_ = p.lexer.consumeToken()
// access column with dot notation
var rightExpr Expr
var err error
if p.matchTokenKind(TokenKindIdent) {
rightExpr, err = p.parseIdent()
} else {
rightExpr, err = p.parseDecimal(p.Pos())
}
if err != nil {
return nil, err
}
return &IndexOperation{
Object: expr,
Operation: TokenKindDot,
Index: rightExpr,
}, nil
case p.matchKeyword(KeywordNot):
_ = p.lexer.consumeToken()
switch {
case p.matchKeyword(KeywordIn):
case p.matchKeyword(KeywordLike):
case p.matchKeyword(KeywordIlike):
default:
return nil, fmt.Errorf("expected IN, LIKE or ILIKE after NOT, got %s", p.lastTokenKind())
}
if p.matchKeyword(KeywordBetween) {
return p.parseBetweenClause(expr)
}
op := p.last().ToString()
_ = p.lexer.consumeToken()
rightExpr, err := p.parseSubExpr(p.Pos(), precedence)
if err != nil {
return nil, err
}
return &BinaryOperation{
LeftExpr: expr,
Operation: TokenKind("NOT " + op),
RightExpr: rightExpr,
}, nil
case p.matchTokenKind(TokenKindLBracket):
params, err := p.parseArrayParams(p.Pos())
if err != nil {
return nil, err
}
return &ObjectParams{
Object: expr,
Params: params,
}, nil
case p.matchTokenKind(TokenKindQuestionMark):
return p.parseTernaryExpr(expr)
case p.matchKeyword(KeywordIs):
_ = p.lexer.consumeToken()
isNotNull := p.tryConsumeKeywords(KeywordNot)
if err := p.expectKeyword(KeywordNull); err != nil {
return nil, err
}
if isNotNull {
return &IsNotNullExpr{
IsPos: p.Pos(),
Expr: expr,
}, nil
}
return &IsNullExpr{
IsPos: p.Pos(),
Expr: expr,
}, nil
default:
return nil, fmt.Errorf("unexpected token kind: %s", p.lastTokenKind())
}
}
func (p *Parser) parseExpr(pos Pos) (Expr, error) {
return p.parseSubExpr(pos, PrecedenceUnknown)
}
func (p *Parser) parseSubExpr(pos Pos, precedence int) (Expr, error) {
expr, err := p.parseUnaryExpr(pos)
if err != nil {
return nil, err
}
for !p.lexer.isEOF() {
nextPrecedence := p.getNextPrecedence()
if nextPrecedence <= precedence {
return expr, nil
}
// parse binary operation
expr, err = p.parseInfix(expr, nextPrecedence)
if err != nil {
return nil, err
}
}
return expr, nil
}
func (p *Parser) parseTernaryExpr(condition Expr) (*TernaryOperation, error) {
if err := p.expectTokenKind(TokenKindQuestionMark); err != nil {
return nil, err
}
trueExpr, err := p.parseExpr(p.Pos())
if err != nil {
return nil, err
}
if err := p.expectTokenKind(TokenKindColon); err != nil {
return nil, err
}
falseExpr, err := p.parseExpr(p.Pos())
if err != nil {
return nil, err
}
return &TernaryOperation{
Condition: condition,
TrueExpr: trueExpr,
FalseExpr: falseExpr,
}, nil
}
func (p *Parser) parseColumnExtractExpr(pos Pos) (*ExtractExpr, error) {
if err := p.expectKeyword(KeywordExtract); err != nil {
return nil, err
}
if err := p.expectTokenKind(TokenKindLParen); err != nil {
return nil, err
}
// parse interval
ident, err := p.parseIdent()
if err != nil {
return nil, err
}
if !intervalUnits.Contains(strings.ToUpper(ident.Name)) {
return nil, fmt.Errorf("unknown interval type: <%q>", ident.Name)
}
fromPos := p.Pos()
if err := p.expectKeyword(KeywordFrom); err != nil {
return nil, err
}
expr, err := p.parseExpr(p.Pos())
if err != nil {
return nil, err
}
if err := p.expectTokenKind(TokenKindRParen); err != nil {
return nil, err
}
return &ExtractExpr{
ExtractPos: pos,
Interval: ident,
FromPos: fromPos,
FromExpr: expr,
}, nil
}
func (p *Parser) parseUnaryExpr(pos Pos) (Expr, error) {
op := p.last()
switch {
case p.matchTokenKind(TokenKindPlus),
p.matchTokenKind(TokenKindMinus),
p.matchKeyword(KeywordNot):
_ = p.lexer.consumeToken()
default:
return p.parseColumnExpr(pos)
}
expr, err := p.parseColumnExpr(p.Pos())
if err != nil {
return nil, err
}
return &UnaryExpr{
UnaryPos: pos,
Kind: TokenKind(op.ToString()),
Expr: expr,
}, nil
}
func (p *Parser) peekTokenKind(kind TokenKind) bool {
if p.lexer.isEOF() {
return false
}
token, err := p.lexer.peekToken()
if err != nil || token == nil {
return false
}
return token.Kind == kind
}
func (p *Parser) peekKeyword(keyword string) bool {
if p.lexer.isEOF() {
return false
}
token, err := p.lexer.peekToken()
if err != nil || token == nil {
return false
}
return token.Kind == TokenKindKeyword && strings.EqualFold(token.String, keyword)
}
// isSelectItemTerminatorKeyword checks whether the current token is a keyword
// that begins a clause following the SELECT item list. When true, we should not
// treat the keyword itself as a bare alias.
func (p *Parser) isSelectItemTerminatorKeyword() bool {
switch {
case p.matchKeyword(KeywordFrom):
return true
case p.matchKeyword(KeywordWhere):
return true
case p.matchKeyword(KeywordPrewhere):
return true
case p.matchKeyword(KeywordGroup):
return true
case p.matchKeyword(KeywordHaving):
return true
case p.matchKeyword(KeywordWindow):
return true
case p.matchKeyword(KeywordOrder):
return true
case p.matchKeyword(KeywordLimit):
return true
case p.matchKeyword(KeywordSettings):
return true
case p.matchKeyword(KeywordFormat):
return true
case p.matchKeyword(KeywordUnion):
return true
case p.matchKeyword(KeywordExcept):
return true
default:
return false
}
}
func (p *Parser) parseColumnExpr(pos Pos) (Expr, error) { //nolint:funlen
// Should parse the keyword as an identifier if the keyword is followed by one of comma, `AS`.
// For example: `SELECT 1 as interval GROUP BY interval` is a valid syntax in Datastore.
if p.matchTokenKind(TokenKindKeyword) && (p.peekTokenKind(TokenKindComma) || p.peekKeyword(KeywordAs)) {
return p.parseIdent()
}
switch {
case p.matchKeyword(KeywordInterval):
return p.parseInterval(true)
case p.matchKeyword(KeywordDate), p.matchKeyword(KeywordTimestamp):
nextToken, err := p.lexer.peekToken()
if err != nil {
return nil, err
}
if nextToken != nil && nextToken.Kind == TokenKindString {
return p.parseString(p.Pos())
}
return p.parseIdentOrFunction(pos)
case p.matchKeyword(KeywordCast):
return p.parseColumnCastExpr(pos)
case p.matchKeyword(KeywordCase):
return p.parseColumnCaseExpr(pos)
case p.matchKeyword(KeywordSelect):
return p.parseSelectQuery(pos)
case p.matchKeyword(KeywordExtract):
return p.parseColumnExtractExpr(pos)
case p.matchTokenKind(TokenKindIdent):
return p.parseIdentOrFunction(pos)
case p.matchTokenKind(TokenKindString): // string literal
return p.parseString(pos)
case p.matchTokenKind(TokenKindInt),
p.matchTokenKind(TokenKindFloat): // number literal
return p.parseNumber(pos)
case p.matchTokenKind(TokenKindLParen):
if peek, _ := p.lexer.peekToken(); peek != nil {
if peek.Kind == TokenKindKeyword && strings.EqualFold(peek.String, KeywordSelect) {
return p.parseSubQuery(pos)
}
}
return p.parseFunctionParams(p.Pos())
case p.matchTokenKind("*"):
return p.parseColumnStar(p.Pos())
case p.matchTokenKind(TokenKindLBracket):
return p.parseArrayParams(p.Pos())
case p.matchTokenKind(TokenKindLBrace):
// The map literal string also starts with '{', so we need to check the next token
// to determine if it is a map literal or a query param.
// Treat both identifiers and keywords as identifier-like for placeholders.
// parseIdent accepts keywords-as-ident, so this is safe.
if p.peekTokenKind(TokenKindIdent) || p.peekTokenKind(TokenKindKeyword) {
return p.parseQueryParam(p.Pos())
}
return p.parseMapLiteral(p.Pos())
case p.matchTokenKind(TokenKindDot):
return p.parseNumber(p.Pos())
case p.matchTokenKind(TokenKindQuestionMark):
// Placeholder `?`
_ = p.lexer.consumeToken()
return &PlaceHolder{
PlaceholderPos: pos,
PlaceHolderEnd: pos,
Type: string(TokenKindQuestionMark),
}, nil
default:
return nil, fmt.Errorf("unexpected token kind: %s", p.lastTokenKind())
}
}
func (p *Parser) parseColumnCastExpr(pos Pos) (Expr, error) {
if err := p.expectKeyword(KeywordCast); err != nil {
return nil, err
}
if err := p.expectTokenKind(TokenKindLParen); err != nil {
return nil, err
}
columnExpr, err := p.parseColumnExpr(p.Pos())
if err != nil {
return nil, err
}
var separator string
asPos := p.Pos()
switch {
// CAST(x, T) and CAST(x AS T) are equivalent
case p.matchKeyword(KeywordAs), p.matchTokenKind(","):
separator = p.last().String
_ = p.lexer.consumeToken()
default:
return nil, fmt.Errorf("expected AS or , but got %s", p.lastTokenKind())
}
var asColumnType Expr
// CAST(1 AS 'Float') or CAST(1 AS Float) are equivalent
if p.matchTokenKind(TokenKindString) {
asColumnType, err = p.parseString(p.Pos())
} else {
asColumnType, err = p.parseColumnType(p.Pos())
}
if err != nil {
return nil, err
}
if err := p.expectTokenKind(TokenKindRParen); err != nil {
return nil, err
}
return &CastExpr{
CastPos: pos,
AsPos: asPos,
Separator: separator,
Expr: columnExpr,
AsType: asColumnType,
}, nil
}
func (p *Parser) parseColumnExprListWithLParen(pos Pos) (*ColumnExprList, error) {
return p.parseColumnExprListWithTerm(TokenKindRParen, pos)
}
func (p *Parser) parseColumnExprListWithSquareBracket(pos Pos) (*ColumnExprList, error) {
return p.parseColumnExprListWithTerm(TokenKindRBracket, pos)
}
func (p *Parser) parseColumnExprList(pos Pos) (*ColumnExprList, error) {
return p.parseColumnExprListWithTerm("", pos)
}
func (p *Parser) parseColumnExprListWithTerm(term TokenKind, pos Pos) (*ColumnExprList, error) {
columnExprList := &ColumnExprList{
ListPos: pos,
ListEnd: pos,
}
columnExprList.HasDistinct = p.tryConsumeKeywords(KeywordDistinct)
columnList := make([]Expr, 0)
for !p.lexer.isEOF() || p.last() != nil {
if term != "" && p.matchTokenKind(term) {
break
}
columnExpr, err := p.parseColumnsExpr(p.Pos())
if err != nil {
return nil, err
}
if columnExpr == nil {
break
}
columnList = append(columnList, columnExpr)
if p.tryConsumeTokenKind(TokenKindComma) == nil {
break
}
}
columnExprList.Items = columnList
if len(columnList) > 0 {
columnExprList.ListEnd = columnList[len(columnList)-1].End()
}
return columnExprList, nil
}
func (p *Parser) parseSelectItems() ([]*SelectItem, error) {
selectItems := make([]*SelectItem, 0)
for !p.lexer.isEOF() || p.last() != nil {
selectItem, err := p.parseSelectItem()
if err != nil {
return nil, err
}
if selectItem == nil {
break
}
selectItems = append(selectItems, selectItem)
if p.tryConsumeTokenKind(TokenKindComma) == nil {
break
}
}
return selectItems, nil
}
func (p *Parser) parseInterval(requireKeyword bool) (*IntervalExpr, error) {
var intervalPos Pos
if requireKeyword {
intervalPos = p.Pos()
if err := p.expectKeyword(KeywordInterval); err != nil {
return nil, err
}
}
expr, err := p.parseExpr(p.Pos())
if err != nil {
return nil, err
}
unit, err := p.parseIdent()
if err != nil {
return nil, err
}
if !intervalUnits.Contains(strings.ToUpper(unit.Name)) {
return nil, fmt.Errorf("unknown interval type: <%q>", unit.Name)
}
return &IntervalExpr{
IntervalPos: intervalPos,
Expr: expr,
Unit: unit,
}, nil
}
func (p *Parser) parseFunctionExpr(_ Pos) (*FunctionExpr, error) {
// parse function name
name, err := p.parseIdent()
if err != nil {
return nil, err
}
// parse function params
params, err := p.parseFunctionParams(p.Pos())
if err != nil {
return nil, err
}
return &FunctionExpr{
Name: name,
Params: params,
}, nil
}
func (p *Parser) parseColumnArgList(pos Pos) (*ColumnArgList, error) {
if err := p.expectTokenKind(TokenKindLParen); err != nil {
return nil, err
}
distinct := p.tryConsumeKeywords(KeywordDistinct)
var items []Expr
for !p.lexer.isEOF() && !p.matchTokenKind(TokenKindRParen) {
item, err := p.parseExpr(p.Pos())
if err != nil {
return nil, err
}
items = append(items, item)
if p.tryConsumeTokenKind(TokenKindComma) == nil {
break
}
}
rightParenPos := p.Pos()
if err := p.expectTokenKind(TokenKindRParen); err != nil {
return nil, err
}
return &ColumnArgList{
LeftParenPos: pos,
RightParenPos: rightParenPos,
Distinct: distinct,
Items: items,
}, nil
}
func (p *Parser) parseFunctionParams(pos Pos) (*ParamExprList, error) {
if err := p.expectTokenKind(TokenKindLParen); err != nil {
return nil, err
}
params, err := p.parseColumnExprListWithLParen(p.Pos())
if err != nil {
return nil, err
}
rightParenPos := p.Pos()
if err := p.expectTokenKind(TokenKindRParen); err != nil {
return nil, err
}
paramExprList := &ParamExprList{
LeftParenPos: pos,
RightParenPos: rightParenPos,
Items: params,
}
// For some aggregate functions might support parametric arguments:
// e.g. QUANTILE(0.5)(x) or QUANTILE(0.5, 0.9)(x).
// So we need to have a check if there is another argument list with detecting the left bracket.
if p.matchTokenKind(TokenKindLParen) {
columnArgList, err := p.parseColumnArgList(p.Pos())
if err != nil {
return nil, err
}
paramExprList.ColumnArgList = columnArgList
}
return paramExprList, nil
}
func (p *Parser) parseMapLiteral(pos Pos) (*MapLiteral, error) {
if err := p.expectTokenKind(TokenKindLBrace); err != nil {
return nil, err
}
keyValues := make([]KeyValue, 0)
for !p.lexer.isEOF() && !p.matchTokenKind(TokenKindRBrace) {
key, err := p.parseString(p.Pos())
if err != nil {
return nil, err
}
if err := p.expectTokenKind(TokenKindColon); err != nil {
return nil, err
}
value, err := p.parseExpr(p.Pos())
if err != nil {
return nil, err
}
keyValues = append(keyValues, KeyValue{
Key: *key,
Value: value,
})
if p.tryConsumeTokenKind(TokenKindComma) == nil {
break
}
}
rightBracePos := p.Pos()
if err := p.expectTokenKind(TokenKindRBrace); err != nil {
return nil, err
}
return &MapLiteral{
LBracePos: pos,
RBracePos: rightBracePos,
KeyValues: keyValues,
}, nil
}
func (p *Parser) parseQueryParam(pos Pos) (*QueryParam, error) {
if err := p.expectTokenKind(TokenKindLBrace); err != nil {
return nil, err
}
ident, err := p.parseIdent()
if err != nil {
return nil, err
}
if err := p.expectTokenKind(TokenKindColon); err != nil {
return nil, err
}
columnType, err := p.parseColumnType(p.Pos())
if err != nil {
return nil, err
}
rightBracePos := p.Pos()
if err := p.expectTokenKind(TokenKindRBrace); err != nil {
return nil, err
}
return &QueryParam{
LBracePos: pos,
RBracePos: rightBracePos,
Name: ident,
Type: columnType,
}, nil
}
func (p *Parser) parseArrayParams(pos Pos) (*ArrayParamList, error) {
if err := p.expectTokenKind(TokenKindLBracket); err != nil {
return nil, err
}
params, err := p.parseColumnExprListWithSquareBracket(p.Pos())
if err != nil {
return nil, err
}
rightBracketPos := p.Pos()
if err := p.expectTokenKind(TokenKindRBracket); err != nil {
return nil, err
}
return &ArrayParamList{
LeftBracketPos: pos,
RightBracketPos: rightBracketPos,
Items: params,
}, nil
}
func (p *Parser) parseColumnsExpr(pos Pos) (*ColumnExpr, error) {
expr, err := p.parseExpr(pos)
if err != nil {
return nil, err
}
var alias *Ident
if p.tryConsumeKeywords(KeywordAs) {
alias, err = p.parseIdent()
if err != nil {
return nil, err
}
}
return &ColumnExpr{
Expr: expr,
Alias: alias,
}, nil
}
func (p *Parser) parseSelectItem() (*SelectItem, error) {
expr, err := p.parseExpr(p.Pos())
if err != nil {
return nil, err
}
modifiers := make([]*FunctionExpr, 0)
for {
if p.matchKeyword(KeywordExcept) || p.matchKeyword(KeywordApply) || p.matchKeyword(KeywordReplace) {
modifier, err := p.parseFunctionExpr(p.Pos())
if err != nil {
return nil, err
}
modifiers = append(modifiers, modifier)
} else {
break
}
}
var alias *Ident
switch {
case p.tryConsumeKeywords(KeywordAs):
alias, err = p.parseIdent()
if err != nil {
return nil, err
}
case p.lastTokenKind() == TokenKindKeyword && !p.isSelectItemTerminatorKeyword():
alias, err = p.parseIdent()
if err != nil {
return nil, err
}
default:
alias = p.tryParseIdent()
}
return &SelectItem{
Expr: expr,
Modifiers: modifiers,
Alias: alias,
}, nil
}
func (p *Parser) parseColumnCaseExpr(pos Pos) (*CaseExpr, error) {
// CASE expr
caseExpr := &CaseExpr{CasePos: pos}
if err := p.expectKeyword(KeywordCase); err != nil {
return nil, err
}
// case expr is optional
if !p.matchKeyword(KeywordWhen) {
expr, err := p.parseExpr(p.Pos())
if err != nil {
return nil, err
}
caseExpr.Expr = expr
}
// WHEN expr THEN expr
whenClauses := make([]*WhenClause, 0)
for p.matchKeyword(KeywordWhen) {
whenPos := p.Pos()
_ = p.lexer.consumeToken()
whenCondition, err := p.parseExpr(p.Pos())
if err != nil {
return nil, err
}
thenPos := p.Pos()
if err := p.expectKeyword(KeywordThen); err != nil {
return nil, err
}
thenCondition, err := p.parseExpr(p.Pos())
if err != nil {
return nil, err
}
whenClauses = append(whenClauses, &WhenClause{
WhenPos: whenPos,
ThenPos: thenPos,
When: whenCondition,
Then: thenCondition,
})
}
caseExpr.Whens = whenClauses
// ELSE expr
elsePos := p.Pos()
if p.tryConsumeKeywords(KeywordElse) {
elseExpr, err := p.parseExpr(p.Pos())
if err != nil {
return nil, err
}
caseExpr.ElsePos = elsePos
caseExpr.Else = elseExpr
}
if err := p.expectKeyword(KeywordEnd); err != nil {
return nil, err
}
return caseExpr, nil
}
func (p *Parser) parseColumnType(_ Pos) (ColumnType, error) { // nolint:funlen
ident, err := p.parseIdent()
if err != nil {
return nil, err
}
if p.tryConsumeTokenKind(TokenKindLParen) != nil {
switch {
case p.matchTokenKind(TokenKindIdent):
switch {
case strings.EqualFold(ident.Name, "Nested"):
return p.parseNestedType(ident, p.Pos())
case strings.EqualFold(ident.Name, "Tuple") && p.namedElementsAhead():
// A NAMED tuple is a column list, which is the grammar
// parseNestedType already reads. The unnamed form falls through to
// parseComplexType below, where a bare list of types belongs.
return p.parseNestedType(ident, p.Pos())
case strings.EqualFold(ident.Name, "JSON"):
return p.parseJSONType(ident, p.Pos())
case strings.EqualFold(ident.Name, "QBit"):
return p.parseColumnTypeWithParams(ident, p.Pos())
default:
return p.parseComplexType(ident, p.Pos())
}
case p.matchTokenKind(TokenKindString):
if peekToken, err := p.lexer.peekToken(); err == nil && peekToken.Kind == TokenKindSingleEQ {
// enum values
return p.parseEnumType(ident, p.Pos())
}
// like Datetime('Asia/Dubai')
return p.parseColumnTypeWithParams(ident, p.Pos())
case p.matchTokenKind(TokenKindInt), p.matchTokenKind(TokenKindFloat):
// fixed size
return p.parseColumnTypeWithParams(ident, p.Pos())
default:
return nil, fmt.Errorf("unexpected token kind: %v", p.lastTokenKind())
}
}
return &ScalarType{Name: ident}, nil
}
func (p *Parser) parseColumnPropertyType(_ Pos) (Expr, error) {
ident, err := p.parseIdent()
if err != nil {
return nil, err
}
return &PropertyType{
Name: ident,
}, nil
}
// namedElementsAhead reports whether the type being parsed names its elements —
// Tuple(label String, path Array(String)) rather than Tuple(String, Array(String)).
// ClickHouse writes both, and SHOW CREATE TABLE returns whichever the column was
// declared with, so a parser that reads only one of them cannot read back a schema
// the server itself printed.
//
// The current token is the first token inside the parens. In the NAMED form that is
// an element's name and the next token is its type — an identifier. In the unnamed
// form the first token IS the type, so what follows is a comma, the closing paren,
// or the type's own parameter list. One token of lookahead separates them.
func (p *Parser) namedElementsAhead() bool {
peek, err := p.lexer.peekToken()
return err == nil && peek != nil && peek.Kind == TokenKindIdent
}
func (p *Parser) parseComplexType(name *Ident, pos Pos) (*ComplexType, error) {
subTypes := make([]ColumnType, 0)
for !p.lexer.isEOF() && !p.matchTokenKind(TokenKindRParen) {
subExpr, err := p.parseColumnType(p.Pos())
if err != nil {
return nil, err
}
subTypes = append(subTypes, subExpr)
if p.tryConsumeTokenKind(TokenKindComma) == nil {
break
}
}
rightParenPos := p.Pos()
if err := p.expectTokenKind(TokenKindRParen); err != nil {
return nil, err
}
return &ComplexType{
LeftParenPos: pos,
RightParenPos: rightParenPos,
Name: name,
Params: subTypes,
}, nil
}
func (p *Parser) parseEnumType(name *Ident, pos Pos) (*EnumType, error) {
enumType := &EnumType{
Name: name,
ListPos: pos,
Values: make([]EnumValue, 0),
}
for !p.lexer.isEOF() && !p.matchTokenKind(TokenKindRParen) {
enumValue, err := p.parseEnumValueExpr(p.Pos())
if err != nil {
return nil, err
}
if enumValue == nil {
break
}
enumType.Values = append(enumType.Values, *enumValue)
if p.tryConsumeTokenKind(TokenKindComma) == nil {
break
}
}
if len(enumType.Values) > 0 {
enumType.ListEnd = enumType.Values[len(enumType.Values)-1].Value.NumEnd
}
if err := p.expectTokenKind(TokenKindRParen); err != nil {
return nil, err
}
return enumType, nil
}
func (p *Parser) parseColumnTypeWithParams(name *Ident, pos Pos) (*TypeWithParams, error) {
params := make([]Literal, 0)
param, err := p.parseLiteral(p.Pos())
if err != nil {
return nil, err
}
params = append(params, param)
for !p.lexer.isEOF() && p.tryConsumeTokenKind(TokenKindComma) != nil {
size, err := p.parseLiteral(p.Pos())
if err != nil {
return nil, err
}
params = append(params, size)
}
rightParenPos := p.Pos()
if err := p.expectTokenKind(TokenKindRParen); err != nil {
return nil, err
}
return &TypeWithParams{
Name: name,
LeftParenPos: pos,
RightParenPos: rightParenPos,
Params: params,
}, nil
}
func (p *Parser) parseJSONPath() (*JSONPath, error) {
idents := make([]*Ident, 0)
ident, err := p.parseIdent()
if err != nil {
return nil, err
}
idents = append(idents, ident)
for !p.lexer.isEOF() && p.tryConsumeTokenKind(TokenKindDot) != nil {
ident, err := p.parseIdent()
if err != nil {
return nil, err
}
idents = append(idents, ident)
}
return &JSONPath{
Idents: idents,
}, nil
}
func (p *Parser) parseJSONMaxDynamicOptions(pos Pos) (*JSONOption, error) {
ident, err := p.parseIdent()
if err != nil {
return nil, err
}
if err := p.expectTokenKind(TokenKindSingleEQ); err != nil {
return nil, err
}
switch ident.Name {
case "max_dynamic_types":
number, err := p.parseNumber(pos)
if err != nil {
return nil, err
}
return &JSONOption{MaxDynamicTypes: number}, nil
case "max_dynamic_paths":
number, err := p.parseNumber(pos)
if err != nil {
return nil, err
}
return &JSONOption{MaxDynamicPaths: number}, nil
default:
return nil, fmt.Errorf("unexpected token kind: %s", p.lastTokenKind())
}
}
func (p *Parser) parseJSONOption() (*JSONOption, error) {
switch {
case p.tryConsumeKeywords(KeywordSkip):
if p.tryConsumeKeywords(KeywordRegexp) {
regex, err := p.parseString(p.Pos())
if err != nil {
return nil, err
}
return &JSONOption{
SkipRegex: regex,
}, nil
}
jsonPath, err := p.parseJSONPath()
if err != nil {
return nil, err
}
return &JSONOption{
SkipPath: jsonPath,
}, nil
case p.matchTokenKind(TokenKindIdent):
// Could be max_dynamic_* option OR a type hint like: a.b String
// Lookahead to see if there's an '=' following the identifier path (max_dynamic_*)
// or if it's a path followed by a ColumnType.
// We'll parse a JSONPath first, then decide.
// Save lexer state by consuming as path greedily using existing helpers.
// Try: if single ident and next is '=' -> max_dynamic_*; else treat as path + type
// Peek next token after current ident without consuming type; we need to
// attempt to parse as max_dynamic_* first as it's existing behavior for a single ident.
// To support dotted paths, we need to capture path, then if '=' exists, it's option; otherwise parse type.
path, err := p.parseJSONPath()
if err != nil {
return nil, err
}
if p.tryConsumeTokenKind(TokenKindSingleEQ) != nil {
// This is a max_dynamic_* option; only valid when path is a single ident of that name
// Reconstruct handling similar to parseJSONMaxDynamicOptions but we already consumed ident and '='
// Determine which option based on the first ident name
if len(path.Idents) != 1 {
return nil, fmt.Errorf("unexpected token kind: %s", p.lastTokenKind())
}
name := path.Idents[0].Name
switch name {
case "max_dynamic_types":
number, err := p.parseNumber(p.Pos())
if err != nil {
return nil, err
}
return &JSONOption{MaxDynamicTypes: number}, nil
case "max_dynamic_paths":
number, err := p.parseNumber(p.Pos())
if err != nil {
return nil, err
}
return &JSONOption{MaxDynamicPaths: number}, nil
default:
return nil, fmt.Errorf("unexpected token kind: %s", p.lastTokenKind())
}
}
// Otherwise, expect a ColumnType as a type hint for the JSON subpath
colType, err := p.parseColumnType(p.Pos())
if err != nil {
return nil, err
}
return &JSONOption{Column: &JSONTypeHint{Path: path, Type: colType}}, nil
default:
return nil, fmt.Errorf("unexpected token kind: %s", p.lastTokenKind())
}
}
func (p *Parser) parseJSONType(name *Ident, pos Pos) (*JSONType, error) {
if p.matchTokenKind(TokenKindLParen) {
return &JSONType{Name: name}, nil
}
options := make([]*JSONOption, 0)
for !p.lexer.isEOF() && !p.matchTokenKind(TokenKindRParen) {
option, err := p.parseJSONOption()
if err != nil {
return nil, err
}
options = append(options, option)
if p.tryConsumeTokenKind(",") == nil {
break
}
}
rightParenPos := p.Pos()
if err := p.expectTokenKind(TokenKindRParen); err != nil {
return nil, err
}
return &JSONType{
Name: name,
Options: &JSONOptions{
LParen: pos,
RParen: rightParenPos,
Items: options,
},
}, nil
}
func (p *Parser) parseNestedType(name *Ident, pos Pos) (*NestedType, error) {
columns, err := p.parseTableColumns()
if err != nil {
return nil, err
}
rightParenPos := p.Pos()
if err := p.expectTokenKind(TokenKindRParen); err != nil {
return nil, err
}
return &NestedType{
LeftParenPos: pos,
RightParenPos: rightParenPos,
Name: name,
Columns: columns,
}, nil
}
func (p *Parser) tryParseCompressionCodecs(pos Pos) (*CompressionCodec, error) {
if !p.tryConsumeKeywords(KeywordCodec) {
return nil, nil // nolint
}
if err := p.expectTokenKind(TokenKindLParen); err != nil {
return nil, err
}
// parse codec name
name, err := p.parseIdent()
if err != nil {
return nil, err
}
// parse DELTA if CODEC(Delta, ZSTD(1))
// or CODEC(Delta(9), ZSTD(1)) or CODEC(T64, ZSTD(1))
var codecType *Ident
var typeLevel *NumberLiteral
switch strings.ToUpper(name.Name) {
case "DELTA", "DOUBLEDELTA", "T64", "GORILLA":
codecType = name
// try parse delta level
typeLevel, err = p.tryParseCompressionLevel(p.Pos())
if err != nil {
return nil, err
}
// consume comma
if err := p.expectTokenKind(TokenKindComma); err != nil {
return nil, err
}
name, err = p.parseIdent()
if err != nil {
return nil, err
}
}
var level *NumberLiteral
// TODO: check if the codec name is valid
switch strings.ToUpper(name.Name) {
case "ZSTD", "LZ4HC", "LH4":
level, err = p.tryParseCompressionLevel(p.Pos())
if err != nil {
return nil, err
}
}
rightParenPos := p.End()
if err := p.expectTokenKind(TokenKindRParen); err != nil {
return nil, err
}
return &CompressionCodec{
CodecPos: pos,
RightParenPos: rightParenPos,
Type: codecType,
TypeLevel: typeLevel,
Name: name,
Level: level,
}, nil
}
func (p *Parser) parseEnumValueExpr(pos Pos) (*EnumValue, error) {
name, err := p.parseString(pos)
if err != nil {
return nil, err
}
if err := p.expectTokenKind(TokenKindSingleEQ); err != nil {
return nil, err
}
value, err := p.parseNumber(p.Pos())
if err != nil {
return nil, err
}
return &EnumValue{
Name: name,
Value: value,
}, nil
}
func (p *Parser) parseColumnStar(pos Pos) (*Ident, error) {
if err := p.expectTokenKind("*"); err != nil {
return nil, err
}
return &Ident{
NamePos: pos,
NameEnd: pos,
Name: "*",
}, nil
}
func (p *Parser) tryParseCompressionLevel(pos Pos) (*NumberLiteral, error) {
if p.tryConsumeTokenKind(TokenKindLParen) == nil {
return nil, nil // nolint
}
num, err := p.parseNumber(pos)
if err != nil {
return nil, err
}
if err := p.expectTokenKind(TokenKindRParen); err != nil {
return nil, err
}
return num, nil
}