Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 91 additions & 33 deletions parser/parser_column.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,46 +378,100 @@ func (p *Parser) peekKeyword(keyword string) bool {
return token.Kind == TokenKindKeyword && strings.EqualFold(token.String, keyword)
}

// clauseStarterKeywords lists the keywords that begin a clause following the
// SELECT item list. Single source of truth used by both the terminator check
// (current-token) and the lookahead check (peek), so they cannot drift.
var clauseStarterKeywords = []string{
KeywordFrom, KeywordWhere, KeywordPrewhere, KeywordGroup,
KeywordHaving, KeywordWindow, KeywordOrder, KeywordLimit,
KeywordOffset, KeywordSettings, KeywordFormat, KeywordUnion,
KeywordExcept,
}

// matchClauseStarterKeyword reports whether the current token is one of the
// clause-starter keywords.
func (p *Parser) matchClauseStarterKeyword() bool {
for _, kw := range clauseStarterKeywords {
if p.matchKeyword(kw) {
return true
}
}
return false
}

// peekIsClauseStarterKeyword reports whether the next token is one of the
// clause-starter keywords.
func (p *Parser) peekIsClauseStarterKeyword() bool {
for _, kw := range clauseStarterKeywords {
if p.peekKeyword(kw) {
return true
}
}
return false
}

// peekIsEndOfStatement reports whether the next token is EOF or `;`.
func (p *Parser) peekIsEndOfStatement() bool {
next, err := p.lexer.peekToken()
if err != nil || next == nil {
Comment thread
erezrokah marked this conversation as resolved.
return true
}
return next.Kind == ";"
}

// keywordIsSelectItemIdentifier reports whether the current keyword token is
// being used as a bare column-reference identifier inside a SELECT projection
// rather than starting a clause/expression. This is true when the next token
// is `,`, `AS`, or another clause-starter keyword — any of which prove the
// current keyword cannot legally begin a clause or expression (clause/expr
// starters always require a value/expression next, never another
// clause-starter or a list separator).
//
// ClickHouse server accepts essentially every reserved word as a bare column
// name in a projection; matching that behavior here lets the parser handle
// queries like `SELECT a, limit FROM t` or `SELECT a, from, b FROM t`
// without backtick escaping. Backticked identifiers are tokenized as
// TokenKindIdent (not TokenKindKeyword), so trailing-comma handling for
// keyword-named tables — e.g. `SELECT count(*), FROM `limit`` — is preserved.
//
// End-of-statement (EOF or `;`) is intentionally NOT included here. It's a
// valid disambiguator only in expression position (the current keyword IS
// the projection expression), not in terminator/alias position (where a
// trailing clause-starter keyword like `FROM` at EOF must still be treated
// as a terminator, not a no-AS alias). parseColumnExpr applies the eos
// disambiguator inline.
func (p *Parser) keywordIsSelectItemIdentifier() bool {
if !p.matchTokenKind(TokenKindKeyword) {
return false
}
return p.peekTokenKind(TokenKindComma) ||
Comment thread
erezrokah marked this conversation as resolved.
p.peekKeyword(KeywordAs) ||
p.peekIsClauseStarterKeyword()
}

// 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.
// 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(KeywordOffset):
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:
if p.keywordIsSelectItemIdentifier() {
return false
}
return p.matchClauseStarterKeyword()
}

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 ClickHouse.
if p.matchTokenKind(TokenKindKeyword) && (p.peekTokenKind(TokenKindComma) || p.peekKeyword(KeywordAs)) {
// Should parse the keyword as an identifier if the keyword is followed by
// `,`, `AS`, another clause-starter keyword, or end-of-statement (EOF or
// `;`). ClickHouse accepts most reserved words as bare column names in
// projections (e.g. `SELECT 1 AS interval GROUP BY interval`,
// `SELECT a, case FROM t`, `SELECT case`); a clause/expression starter
// always requires a value/expression next, so the lookahead unambiguously
// identifies these as identifier uses. The end-of-statement disambiguator
// is only valid in expression position, so it's applied inline here
// rather than in keywordIsSelectItemIdentifier (which is shared with the
// terminator/alias check).
if p.keywordIsSelectItemIdentifier() ||
(p.matchTokenKind(TokenKindKeyword) && p.peekIsEndOfStatement()) {
return p.parseIdent()
}
switch {
Expand Down Expand Up @@ -823,6 +877,10 @@ func (p *Parser) parseSelectItem() (*SelectItem, error) {
var alias *Ident
switch {
case p.tryConsumeKeywords(KeywordAs):
// `SELECT 1 AS <reserved-keyword>` works for any keyword because
// matchTokenKind(TokenKindIdent) coerces TokenKindKeyword to ident
// (see parser_common.go matchTokenKind), so parseIdent accepts a
// keyword token here without needing a special-case.
alias, err = p.parseIdent()
if err != nil {
return nil, err
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- Origin SQL:
SELECT id, from, to, where, group, order FROM transfers


-- Beautify SQL:
SELECT
id,
from,
to,
where,
group,
order
FROM
transfers;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Origin SQL:
SELECT limit


-- Beautify SQL:
SELECT
limit;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- Origin SQL:
SELECT id, case, cast, extract, interval FROM events


-- Beautify SQL:
SELECT
id,
case,
cast,
extract,
interval
FROM
events;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Origin SQL:
SELECT 1 AS interval, 2 AS from, 3 AS limit


-- Beautify SQL:
SELECT
1 AS interval,
2 AS from,
3 AS limit;
15 changes: 15 additions & 0 deletions parser/testdata/query/format/beautify/select_keyword_as_column.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- Origin SQL:
SELECT cloud, account, region, name, usage, limit, usage_percentage FROM service_quotas


-- Beautify SQL:
SELECT
cloud,
account,
region,
name,
usage,
limit,
usage_percentage
FROM
service_quotas;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Origin SQL:
SELECT a, limit FROM t


-- Beautify SQL:
SELECT
a,
limit
FROM
t;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Origin SQL:
SELECT interval


-- Beautify SQL:
SELECT
interval;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Origin SQL:
SELECT case;


-- Beautify SQL:
SELECT
case;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Origin SQL:
SELECT count(x), FROM `limit`


-- Beautify SQL:
SELECT
count(x)
FROM
`limit`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Origin SQL:
SELECT id, from, to, where, group, order FROM transfers


-- Format SQL:
SELECT id, from, to, where, group, order FROM transfers;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Origin SQL:
SELECT limit


-- Format SQL:
SELECT limit;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Origin SQL:
SELECT id, case, cast, extract, interval FROM events


-- Format SQL:
SELECT id, case, cast, extract, interval FROM events;
6 changes: 6 additions & 0 deletions parser/testdata/query/format/select_keyword_as_alias.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Origin SQL:
SELECT 1 AS interval, 2 AS from, 3 AS limit


-- Format SQL:
SELECT 1 AS interval, 2 AS from, 3 AS limit;
6 changes: 6 additions & 0 deletions parser/testdata/query/format/select_keyword_as_column.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Origin SQL:
SELECT cloud, account, region, name, usage, limit, usage_percentage FROM service_quotas


-- Format SQL:
SELECT cloud, account, region, name, usage, limit, usage_percentage FROM service_quotas;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Origin SQL:
SELECT a, limit FROM t


-- Format SQL:
SELECT a, limit FROM t;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Origin SQL:
SELECT interval


-- Format SQL:
SELECT interval;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Origin SQL:
SELECT case;


-- Format SQL:
SELECT case;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Origin SQL:
SELECT count(x), FROM `limit`


-- Format SQL:
SELECT count(x) FROM `limit`;
Loading
Loading