Skip to content
Open
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
14 changes: 7 additions & 7 deletions parser/parser_column.go
Original file line number Diff line number Diff line change
Expand Up @@ -580,13 +580,13 @@ func (p *Parser) parseColumnExpr(pos Pos) (Expr, error) { //nolint:funlen

return interval, nil
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())
literalPos := p.Pos()
savedState := p.lexer.saveState()
_ = p.lexer.consumeToken()
if p.matchTokenKind(TokenKindString) {
return p.parseString(literalPos)
Comment on lines +583 to +587

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the DATE and TIMESTAMP literal type

When a caller parses and formats a typed literal such as SELECT DATE '2024-01-01', this returns a plain StringLiteral after consuming the type keyword, so formatting produces SELECT '2024-01-01' (as the new golden file demonstrates). That changes the expression from a typed Date/Timestamp literal into a String and can alter comparisons, overload resolution, and result schemas; the AST must retain and re-emit the consumed literal type.

Useful? React with 👍 / 👎.

}
p.lexer.restoreState(savedState)
return p.parseIdentOrFunction(pos)
case p.matchKeyword(KeywordCast):
return p.parseColumnCastExpr(pos)
Expand Down Expand Up @@ -1172,7 +1172,7 @@ func (p *Parser) parseSelectItem() (*SelectItem, error) {

modifiers := make([]*FunctionExpr, 0)
for {
if p.matchKeyword(KeywordExcept) || p.matchKeyword(KeywordApply) || p.matchKeyword(KeywordReplace) {
if (p.matchKeyword(KeywordExcept) && !p.peekKeyword(KeywordSelect)) || p.matchKeyword(KeywordApply) || p.matchKeyword(KeywordReplace) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recognize parenthesized EXCEPT query operands

When the right operand is parenthesized, as in SELECT 1 EXCEPT (SELECT 2), the next token is ( rather than SELECT, so this branch consumes EXCEPT as a select-item modifier. The parser consequently leaves SelectQuery.Except nil and formats the statement as SELECT 1 EXCEPT(SELECT 2) instead of representing the set operation, even though parseSelectQuery explicitly supports parenthesized operands; the modifier/set-operation disambiguation must account for this form.

Useful? React with 👍 / 👎.

modifier, err := p.parseFunctionExpr(p.Pos())
if err != nil {
return nil, err
Expand Down
19 changes: 19 additions & 0 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ func TestParser_TableSettingsFunctionExpression(t *testing.T) {
}
}

func TestParser_ContextualKeywords(t *testing.T) {
tests := []string{
"SELECT DATE '2024-01-01'",
"SELECT TIMESTAMP '2024-01-01 00:00:00'",
"SELECT date AS timestamp",
"SELECT * EXCEPT (a) FROM t",
"SELECT 1 EXCEPT SELECT 2",
}

for _, sql := range tests {
t.Run(sql, func(t *testing.T) {
_, err := NewParser(sql).ParseStmts()
require.NoError(t, err)
})
}
}

func TestParser_Compatible(t *testing.T) {
if !*runCompatible {
t.Skip("Compatible test runs only if -compatible is set")
Expand Down Expand Up @@ -285,6 +302,8 @@ func TestParser_InvalidSyntax(t *testing.T) {
// ALL or DISTINCT
"(SELECT 1",
"(SELECT 1) UNION SELECT 2",
"SELECT * EXCEPT FROM t",
"SELECT 1 EXCEPT 2",
// ClickHouse rejects a set operator once SETTINGS is bound to a
// parenthesized group
"(SELECT 1) SETTINGS max_threads=1 UNION ALL SELECT 2",
Expand Down
3 changes: 3 additions & 0 deletions parser/testdata/query/contextual_keywords.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SELECT DATE '2024-01-01', TIMESTAMP '2024-01-01 00:00:00', date AS timestamp;
SELECT * EXCEPT (a) FROM t;
SELECT 1 EXCEPT SELECT 2;
20 changes: 20 additions & 0 deletions parser/testdata/query/format/beautify/contextual_keywords.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- Origin SQL:
SELECT DATE '2024-01-01', TIMESTAMP '2024-01-01 00:00:00', date AS timestamp;
SELECT * EXCEPT (a) FROM t;
SELECT 1 EXCEPT SELECT 2;


-- Beautify SQL:
SELECT
'2024-01-01',
'2024-01-01 00:00:00',
date AS timestamp;
SELECT
* EXCEPT(a)
FROM
t;
SELECT
1
EXCEPT
SELECT
2;
10 changes: 10 additions & 0 deletions parser/testdata/query/format/contextual_keywords.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Origin SQL:
SELECT DATE '2024-01-01', TIMESTAMP '2024-01-01 00:00:00', date AS timestamp;
SELECT * EXCEPT (a) FROM t;
SELECT 1 EXCEPT SELECT 2;


-- Format SQL:
SELECT '2024-01-01', '2024-01-01 00:00:00', date AS timestamp;
SELECT * EXCEPT(a) FROM t;
SELECT 1 EXCEPT SELECT 2;
220 changes: 220 additions & 0 deletions parser/testdata/query/output/contextual_keywords.sql.golden.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
[
{
"SelectPos": 0,
"StatementEnd": 76,
"With": null,
"Top": null,
"HasDistinct": false,
"DistinctOn": null,
"SelectItems": [
{
"Expr": {
"LiteralPos": 7,
"LiteralEnd": 23,
"Literal": "2024-01-01"
},
"Modifiers": [],
"Alias": null
},
{
"Expr": {
"LiteralPos": 26,
"LiteralEnd": 56,
"Literal": "2024-01-01 00:00:00"
},
"Modifiers": [],
"Alias": null
},
{
"Expr": {
"Name": "date",
"QuoteType": 1,
"NamePos": 59,
"NameEnd": 63
},
"Modifiers": [],
"Alias": {
"Name": "timestamp",
"QuoteType": 1,
"NamePos": 67,
"NameEnd": 76
}
}
],
"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": 78,
"StatementEnd": 104,
"With": null,
"Top": null,
"HasDistinct": false,
"DistinctOn": null,
"SelectItems": [
{
"Expr": {
"Name": "*",
"QuoteType": 0,
"NamePos": 85,
"NameEnd": 85
},
"Modifiers": [
{
"Name": {
"Name": "EXCEPT",
"QuoteType": 1,
"NamePos": 87,
"NameEnd": 93
},
"Params": {
"LeftParenPos": 94,
"RightParenPos": 96,
"Items": {
"ListPos": 95,
"ListEnd": 96,
"HasDistinct": false,
"Items": [
{
"Expr": {
"Name": "a",
"QuoteType": 1,
"NamePos": 95,
"NameEnd": 96
},
"Alias": null
}
]
},
"ColumnArgList": null
}
}
],
"Alias": null
}
],
"From": {
"FromPos": 98,
"Expr": {
"Table": {
"TablePos": 103,
"TableEnd": 104,
"Alias": null,
"Expr": {
"Database": null,
"Table": {
"Name": "t",
"QuoteType": 1,
"NamePos": 103,
"NameEnd": 104
}
},
"HasFinal": false
},
"StatementEnd": 104,
"SampleRatio": null,
"HasFinal": false
}
},
"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": 106,
"StatementEnd": 114,
"With": null,
"Top": null,
"HasDistinct": false,
"DistinctOn": null,
"SelectItems": [
{
"Expr": {
"NumPos": 113,
"NumEnd": 114,
"Literal": "1",
"Base": 10
},
"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": {
"SelectPos": 122,
"StatementEnd": 130,
"With": null,
"Top": null,
"HasDistinct": false,
"DistinctOn": null,
"SelectItems": [
{
"Expr": {
"NumPos": 129,
"NumEnd": 130,
"Literal": "2",
"Base": 10
},
"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
},
"Intersect": null
}
]
Loading