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
1 change: 1 addition & 0 deletions parser/keyword_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func TestReservedKeywordInDisambiguatedPositions(t *testing.T) {
"SELECT * FROM t AS from",
"SELECT a FROM db.from",
"SELECT t.from FROM t",
"SELECT kill.id FROM events AS kill",
"SELECT a, limit FROM t",
"SELECT case;",
"SELECT limit",
Expand Down
34 changes: 25 additions & 9 deletions parser/parser_column.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,19 @@ func (p *Parser) parseInfix(expr Expr, precedence int) (Expr, error) {
}, nil
case p.matchTokenKind(TokenKindDot):
_ = p.lexer.consumeToken()
// access column with dot notation
operation := TokenKindDot
hasTypeQualifier := p.tryConsumeTokenKind(TokenKindColon) != nil
if hasTypeQualifier {
// Dynamic JSON subcolumns can pin their result type with
// `.:Type`, for example `json.path.:`Array(JSON)``.
operation = TokenKindDot + TokenKindColon
}

var rightExpr Expr
var err error
if p.matchTokenKind(TokenKindIdent, TokenKindKeyword) {
// After a dot the token can only be a member name, so even
// reserved keywords are accepted (e.g. `t.from`).
if hasTypeQualifier || p.matchTokenKind(TokenKindIdent, TokenKindKeyword) {
// After a dot the token can only be a member name or type
// qualifier, so even reserved keywords are accepted.
rightExpr, err = p.parseAnyKeyword()
} else {
rightExpr, err = p.parseDecimal(p.Pos())
Expand All @@ -202,7 +209,7 @@ func (p *Parser) parseInfix(expr Expr, precedence int) (Expr, error) {
}
return &IndexOperation{
Object: expr,
Operation: TokenKindDot,
Operation: operation,
Index: rightExpr,
}, nil
case p.matchKeyword(KeywordNot):
Expand Down Expand Up @@ -527,7 +534,7 @@ func (p *Parser) peekIsExpressionContinuation() bool {
// 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.
// 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
Expand Down Expand Up @@ -556,9 +563,18 @@ func (p *Parser) isSelectItemTerminatorKeyword() bool {
}

func (p *Parser) parseColumnExpr(pos Pos) (Expr, error) { //nolint:funlen
// 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
// A keyword followed by a dot is unambiguously the left side of a
// qualified column reference, even when it is otherwise reserved (for
// example, `kill.item_id`). INTERVAL must reach its dedicated parser
// first because a dot can also start its numeric operand.
if !p.matchKeyword(KeywordInterval) &&
p.matchTokenKind(TokenKindKeyword) && p.peekTokenKind(TokenKindDot) {
return p.parseIdentOrFunction(pos)
}

// Parse the keyword as an identifier if it 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
Expand Down
13 changes: 8 additions & 5 deletions parser/parser_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -403,9 +403,10 @@ func (p *Parser) parseCreateTable(pos Pos, orReplace bool) (*CreateTable, error)
func (p *Parser) parseIdentOrFunction(_ Pos) (Expr, error) {
var ident *Ident
var err error
if p.matchTokenKind(TokenKindKeyword) && p.peekTokenKind(TokenKindLParen) {
// reserved operator keywords stay callable as ordinary functions:
// and(a, b), or(a, b), in(x, set), like(s, pat), ...
if p.matchTokenKind(TokenKindKeyword) &&
(p.peekTokenKind(TokenKindLParen) || p.peekTokenKind(TokenKindDot)) {
// Reserved keywords remain valid when context proves they are function
// names or the first field of a qualified name.
ident, err = p.parseAnyKeyword()
} else {
ident, err = p.parseIdent()
Expand Down Expand Up @@ -466,7 +467,8 @@ func (p *Parser) parseIdentOrFunction(_ Pos) (Expr, error) {
}, nil
}
return funcExpr, nil
case p.tryConsumeTokenKind(TokenKindDot) != nil:
case p.matchTokenKind(TokenKindDot) && !p.peekTokenKind(TokenKindColon):
_ = p.lexer.consumeToken()
switch {
case p.matchTokenKind(TokenKindIdent, TokenKindKeyword):
fields := []*Ident{ident}
Expand All @@ -478,9 +480,10 @@ func (p *Parser) parseIdentOrFunction(_ Pos) (Expr, error) {
return nil, err
}
fields = append(fields, child)
if p.tryConsumeTokenKind(TokenKindDot) == nil {
if !p.matchTokenKind(TokenKindDot) || p.peekTokenKind(TokenKindColon) {
break
}
_ = p.lexer.consumeToken()
}
return &Path{Fields: fields}, nil
case p.matchTokenKind("*"):
Expand Down
15 changes: 11 additions & 4 deletions parser/precedence_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,10 +520,17 @@ func TestIntervalAsColumnName(t *testing.T) {
}

func TestIntervalOperatorStillParses(t *testing.T) {
expr := parseSelectItemExpr(t, "SELECT INTERVAL 4 DAY")
interval, ok := expr.(*IntervalExpr)
require.True(t, ok, "expected *IntervalExpr, got %T", expr)
require.Equal(t, "DAY", interval.Unit.Name)
for _, sql := range []string{
"SELECT INTERVAL 4 DAY",
"SELECT INTERVAL .1 DAY",
} {
t.Run(sql, func(t *testing.T) {
expr := parseSelectItemExpr(t, sql)
interval, ok := expr.(*IntervalExpr)
require.True(t, ok, "expected *IntervalExpr, got %T", expr)
require.Equal(t, "DAY", interval.Unit.Name)
})
}
}

func TestRepeatedIntervalColumnsParseInPolynomialTime(t *testing.T) {
Expand Down
3 changes: 3 additions & 0 deletions parser/testdata/basic/format/beautify/interval_expr.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
-- Origin SQL:
SELECT INTERVAL 4 DAY;
SELECT INTERVAL .1 DAY;
SELECT INTERVAL a + b DAY;
SELECT INTERVAL toUInt8(1) DAY;
SELECT INTERVAL asc DAY;
Expand All @@ -8,6 +9,8 @@ SELECT INTERVAL asc DAY;
-- Beautify SQL:
SELECT
INTERVAL 4 DAY;
SELECT
INTERVAL .1 DAY;
SELECT
INTERVAL a + b DAY;
SELECT
Expand Down
2 changes: 2 additions & 0 deletions parser/testdata/basic/format/interval_expr.sql
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
-- Origin SQL:
SELECT INTERVAL 4 DAY;
SELECT INTERVAL .1 DAY;
SELECT INTERVAL a + b DAY;
SELECT INTERVAL toUInt8(1) DAY;
SELECT INTERVAL asc DAY;


-- Format SQL:
SELECT INTERVAL 4 DAY;
SELECT INTERVAL .1 DAY;
SELECT INTERVAL a + b DAY;
SELECT INTERVAL toUInt8(1) DAY;
SELECT INTERVAL asc DAY;
1 change: 1 addition & 0 deletions parser/testdata/basic/interval_expr.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
SELECT INTERVAL 4 DAY;
SELECT INTERVAL .1 DAY;
SELECT INTERVAL a + b DAY;
SELECT INTERVAL toUInt8(1) DAY;
SELECT INTERVAL asc DAY;
99 changes: 72 additions & 27 deletions parser/testdata/basic/output/interval_expr.sql.golden.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
},
{
"SelectPos": 23,
"StatementEnd": 48,
"StatementEnd": 45,
"With": null,
"Top": null,
"HasDistinct": false,
Expand All @@ -55,28 +55,73 @@
{
"Expr": {
"IntervalPos": 30,
"Expr": {
"NumPos": 39,
"NumEnd": 41,
"Literal": ".1",
"Base": 10
},
"Unit": {
"Name": "DAY",
"QuoteType": 1,
"NamePos": 42,
"NameEnd": 45
}
},
"Modifiers": [],
"Alias": null
}
],
"From": null,
"Window": null,
"Prewhere": null,
"Where": null,
"GroupBy": null,
"WithTotal": false,
"Having": null,
"OrderBy": null,
"LimitBy": null,
"Limit": null,
"Settings": null,
"Format": null,
"UnionAll": null,
"UnionDistinct": null,
"Except": null,
"Intersect": null
},
{
"SelectPos": 47,
"StatementEnd": 72,
"With": null,
"Top": null,
"HasDistinct": false,
"DistinctOn": null,
"SelectItems": [
{
"Expr": {
"IntervalPos": 54,
"Expr": {
"LeftExpr": {
"Name": "a",
"QuoteType": 1,
"NamePos": 39,
"NameEnd": 40
"NamePos": 63,
"NameEnd": 64
},
"Operation": "+",
"RightExpr": {
"Name": "b",
"QuoteType": 1,
"NamePos": 43,
"NameEnd": 44
"NamePos": 67,
"NameEnd": 68
},
"HasGlobal": false,
"HasNot": false
},
"Unit": {
"Name": "DAY",
"QuoteType": 1,
"NamePos": 45,
"NameEnd": 48
"NamePos": 69,
"NameEnd": 72
}
},
"Modifiers": [],
Expand All @@ -101,35 +146,35 @@
"Intersect": null
},
{
"SelectPos": 50,
"StatementEnd": 80,
"SelectPos": 74,
"StatementEnd": 104,
"With": null,
"Top": null,
"HasDistinct": false,
"DistinctOn": null,
"SelectItems": [
{
"Expr": {
"IntervalPos": 57,
"IntervalPos": 81,
"Expr": {
"Name": {
"Name": "toUInt8",
"QuoteType": 1,
"NamePos": 66,
"NameEnd": 73
"NamePos": 90,
"NameEnd": 97
},
"Params": {
"LeftParenPos": 73,
"RightParenPos": 75,
"LeftParenPos": 97,
"RightParenPos": 99,
"Items": {
"ListPos": 74,
"ListEnd": 75,
"ListPos": 98,
"ListEnd": 99,
"HasDistinct": false,
"Items": [
{
"Expr": {
"NumPos": 74,
"NumEnd": 75,
"NumPos": 98,
"NumEnd": 99,
"Literal": "1",
"Base": 10
},
Expand All @@ -143,8 +188,8 @@
"Unit": {
"Name": "DAY",
"QuoteType": 1,
"NamePos": 77,
"NameEnd": 80
"NamePos": 101,
"NameEnd": 104
}
},
"Modifiers": [],
Expand All @@ -169,27 +214,27 @@
"Intersect": null
},
{
"SelectPos": 82,
"StatementEnd": 105,
"SelectPos": 106,
"StatementEnd": 129,
"With": null,
"Top": null,
"HasDistinct": false,
"DistinctOn": null,
"SelectItems": [
{
"Expr": {
"IntervalPos": 89,
"IntervalPos": 113,
"Expr": {
"Name": "asc",
"QuoteType": 1,
"NamePos": 98,
"NameEnd": 101
"NamePos": 122,
"NameEnd": 125
},
"Unit": {
"Name": "DAY",
"QuoteType": 1,
"NamePos": 102,
"NameEnd": 105
"NamePos": 126,
"NameEnd": 129
}
},
"Modifiers": [],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Origin SQL:
SELECT payload.items.:`Array(JSON)` AS items
FROM source_rows


-- Beautify SQL:
SELECT
payload.items.:`Array(JSON)` AS items
FROM
source_rows;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Origin SQL:
SELECT kill.item_id AS item_id
FROM source_rows AS kill


-- Beautify SQL:
SELECT
kill.item_id AS item_id
FROM
source_rows AS kill;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Origin SQL:
SELECT payload.items.:`Array(JSON)` AS items
FROM source_rows


-- Format SQL:
SELECT payload.items.:`Array(JSON)` AS items FROM source_rows;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Origin SQL:
SELECT kill.item_id AS item_id
FROM source_rows AS kill


-- Format SQL:
SELECT kill.item_id AS item_id FROM source_rows AS kill;
Loading
Loading