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
40 changes: 25 additions & 15 deletions parser/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -1406,6 +1406,7 @@ func (c *CreateTable) Accept(visitor ASTVisitor) error {
type CreateMaterializedView struct {
CreatePos Pos // position of CREATE|ATTACH keyword
StatementEnd Pos
OrReplace bool
Name *TableIdentifier
IfNotExists bool
OnCluster *ClusterClause
Expand Down Expand Up @@ -2476,13 +2477,21 @@ type TTLPolicyRule struct {
ToVolume *StringLiteral
ToDisk *StringLiteral
Action *TTLPolicyRuleAction
GroupBy *GroupByClause
Set []*UpdateAssignment
Comment thread
marre marked this conversation as resolved.
}

func (t *TTLPolicyRule) Pos() Pos {
return t.RulePos
}

func (t *TTLPolicyRule) End() Pos {
if len(t.Set) > 0 {
return t.Set[len(t.Set)-1].End()
}
if t.GroupBy != nil {
return t.GroupBy.End()
}
if t.Action != nil {
return t.Action.End()
}
Expand Down Expand Up @@ -2510,29 +2519,32 @@ func (t *TTLPolicyRule) Accept(visitor ASTVisitor) error {
return err
}
}
if t.GroupBy != nil {
if err := t.GroupBy.Accept(visitor); err != nil {
return err
}
}
for _, set := range t.Set {
if err := set.Accept(visitor); err != nil {
return err
}
}
return visitor.VisitTTLPolicyRule(t)
}

type TTLPolicy struct {
Item *TTLPolicyRule
Where *WhereClause
GroupBy *GroupByClause
Item *TTLPolicyRule
Where *WhereClause
}

func (t *TTLPolicy) Pos() Pos {
if t.Item != nil {
return t.Item.Pos()
}
if t.Where != nil {
return t.Where.Pos()
}
return t.GroupBy.Pos()
return t.Where.Pos()
}

func (t *TTLPolicy) End() Pos {
if t.GroupBy != nil {
return t.GroupBy.End()
}
if t.Where != nil {
return t.Where.End()
}
Expand All @@ -2552,11 +2564,6 @@ func (t *TTLPolicy) Accept(visitor ASTVisitor) error {
return err
}
}
if t.GroupBy != nil {
if err := t.GroupBy.Accept(visitor); err != nil {
return err
}
}
return visitor.VisitTTLPolicy(t)
}

Expand All @@ -2571,6 +2578,9 @@ func (t *TTLExpr) Pos() Pos {
}

func (t *TTLExpr) End() Pos {
if t.Policy != nil {
return t.Policy.End()
}
return t.Expr.End()
}

Expand Down
21 changes: 16 additions & 5 deletions parser/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,11 @@ func (c *CreateLiveView) FormatSQL(formatter *Formatter) {
}

func (c *CreateMaterializedView) FormatSQL(formatter *Formatter) {
formatter.WriteString("CREATE MATERIALIZED VIEW ")
formatter.WriteString("CREATE")
if c.OrReplace {
formatter.WriteString(" OR REPLACE")
}
formatter.WriteString(" MATERIALIZED VIEW ")
if c.IfNotExists {
formatter.WriteString("IF NOT EXISTS ")
}
Expand Down Expand Up @@ -2581,10 +2585,6 @@ func (t *TTLPolicy) FormatSQL(formatter *Formatter) {
formatter.WriteByte(whitespace)
formatter.WriteExpr(t.Where)
}
if t.GroupBy != nil {
formatter.WriteByte(whitespace)
formatter.WriteExpr(t.GroupBy)
}
}

func (t *TTLPolicyRule) FormatSQL(formatter *Formatter) {
Expand All @@ -2596,6 +2596,17 @@ func (t *TTLPolicyRule) FormatSQL(formatter *Formatter) {
formatter.WriteExpr(t.ToDisk)
} else if t.Action != nil {
formatter.WriteExpr(t.Action)
} else if t.GroupBy != nil {
formatter.WriteExpr(t.GroupBy)
}
if len(t.Set) > 0 {
formatter.WriteString(" SET ")
for i, set := range t.Set {
if i > 0 {
formatter.WriteString(", ")
}
formatter.WriteExpr(set)
}
}
}

Expand Down
136 changes: 126 additions & 10 deletions parser/parser_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,19 @@ func (p *Parser) parseDDL(pos Pos) (DDL, error) {
switch {
case p.matchKeyword(KeywordCreate),
p.matchKeyword(KeywordAttach):
isAttach := p.matchKeyword(KeywordAttach)
_ = p.lexer.consumeToken()
orReplace := p.tryConsumeKeywords(KeywordOr, KeywordReplace)
if orReplace && !p.matchOneOfKeywords(KeywordTemporary, KeywordTable, KeywordView, KeywordFunction, KeywordDictionary) {
return nil, fmt.Errorf("expected keyword: TEMPORARY|TABLE|VIEW|FUNCTION|DICTIONARY, but got %q", p.currentTokenString())
if orReplace {
// MATERIALIZED VIEW accepts OR REPLACE only under CREATE;
// ClickHouse rejects an ATTACH OR REPLACE combination.
if isAttach {
if !p.matchOneOfKeywords(KeywordTemporary, KeywordTable, KeywordView, KeywordFunction, KeywordDictionary) {
return nil, fmt.Errorf("expected keyword: TEMPORARY|TABLE|VIEW|FUNCTION|DICTIONARY, but got %q", p.currentTokenString())
}
} else if !p.matchOneOfKeywords(KeywordTemporary, KeywordTable, KeywordView, KeywordFunction, KeywordDictionary, KeywordMaterialized) {
return nil, fmt.Errorf("expected keyword: TEMPORARY|TABLE|VIEW|FUNCTION|DICTIONARY|MATERIALIZED, but got %q", p.currentTokenString())
}
}
switch {
case p.matchKeyword(KeywordNamed):
Expand All @@ -28,7 +37,7 @@ func (p *Parser) parseDDL(pos Pos) (DDL, error) {
case p.matchKeyword(KeywordFunction):
return p.parseCreateFunction(pos, orReplace)
case p.matchKeyword(KeywordMaterialized):
return p.parseCreateMaterializedView(pos)
return p.parseCreateMaterializedView(pos, orReplace)
case p.matchKeyword(KeywordLive):
return p.parseCreateLiveView(pos)
case p.matchKeyword(KeywordView):
Expand Down Expand Up @@ -1211,6 +1220,7 @@ func (p *Parser) parseTTLClause(pos Pos, allowMultiValues bool) ([]*TTLExpr, err

func (p *Parser) tryParseTTLPolicy(pos Pos) (*TTLPolicy, error) {
var rule *TTLPolicyRule
var where *WhereClause
switch {
case p.tryConsumeKeywords(KeywordTo):
if p.tryConsumeKeywords(KeywordDisk) {
Expand All @@ -1229,6 +1239,7 @@ func (p *Parser) tryParseTTLPolicy(pos Pos) (*TTLPolicy, error) {
return nil, fmt.Errorf("unexpected token: %q, expected DISK or VOLUME", p.currentTokenKind())
}
case p.matchKeyword(KeywordDelete), p.matchKeyword(KeywordRecompress):
isDelete := p.matchKeyword(KeywordDelete)
token := p.current()
_ = p.lexer.consumeToken()
action := &TTLPolicyRuleAction{
Expand All @@ -1242,23 +1253,128 @@ func (p *Parser) tryParseTTLPolicy(pos Pos) (*TTLPolicy, error) {
}
action.Codec = codec
rule = &TTLPolicyRule{RulePos: pos, Action: action}
// A TTL WHERE clause belongs only to the DELETE action; ClickHouse
// rejects it after RECOMPRESS, TO DISK/VOLUME, and GROUP BY, so it
// is left unconsumed for the statement parser in those cases.
if isDelete {
where, err = p.tryParseWhereClause(p.Pos())
if err != nil {
return nil, err
}
}
case p.matchKeyword(KeywordGroup):
groupBy, err := p.parseTTLPolicyGroupBy(pos)
if err != nil {
return nil, err
}
rule = groupBy
default:
return nil, nil // nolint
}
policy := &TTLPolicy{Item: rule}
return &TTLPolicy{Item: rule, Where: where}, nil
}

where, err := p.tryParseWhereClause(p.Pos())
if err != nil {
// parseTTLPolicyGroupBy parses the TTL GROUP BY action: a plain key
// expression list, optionally followed by SET <col> = <expr> assignments.
//
// ClickHouse rejects the query-level GROUP BY forms (ALL,
// CUBE/ROLLUP/GROUPING SETS, WITH CUBE/ROLLUP/TOTALS) inside a TTL, so the
// keys are parsed directly as a list instead of reusing parseGroupByClause
// and then discarding those forms: building the clause from the expected
// shape keeps any future query-level GROUP BY sugar out of the TTL grammar
// as well.
func (p *Parser) parseTTLPolicyGroupBy(pos Pos) (*TTLPolicyRule, error) {
if err := p.expectKeyword(KeywordGroup); err != nil {
return nil, err
}
if err := p.expectKeyword(KeywordBy); err != nil {
return nil, err
}
policy.Where = where
keys := &ColumnExprList{ListPos: p.Pos()}
for {
var key Expr
var err error
if p.matchTokenKind(TokenKindKeyword) {
// Bare keywords (e.g. ALL) are valid TTL GROUP BY keys even
// when followed by SET or a closing engine clause;
// parseColumnExpr only reads a keyword as an identifier for a
// narrower lookahead, so fall back to it when an expression
// cannot start here.
savedState := p.lexer.saveState()
key, err = p.parseExpr(p.Pos())
if err != nil {
p.lexer.restoreState(savedState)
key, err = p.parseAnyKeyword()
}
} else {
key, err = p.parseExpr(p.Pos())
}
if err != nil {
return nil, err
}
keys.Items = append(keys.Items, key)
keys.ListEnd = key.End()
if p.tryConsumeTokenKind(TokenKindComma) == nil {
Comment thread
marre marked this conversation as resolved.
break
}
}
rule := &TTLPolicyRule{
RulePos: pos,
GroupBy: &GroupByClause{
GroupByPos: pos,
GroupByEnd: keys.End(),
Expr: keys,
},
}
if p.tryConsumeKeywords(KeywordSet) {
set, err := p.parseTTLPolicySet(p.Pos())
if err != nil {
return nil, err
}
rule.Set = append(rule.Set, set)
for {
// A comma either continues the SET assignment list or starts
// the next TTL expression of a multi-value TTL clause; consume
// it and probe for another assignment, rolling both back when
// none follows so parseTTLClause can treat the comma as a rule
// separator.
savedState := p.lexer.saveState()
if p.tryConsumeTokenKind(TokenKindComma) == nil {
break
}
set, err := p.parseTTLPolicySet(p.Pos())
if err != nil {
p.lexer.restoreState(savedState)
break
}
rule.Set = append(rule.Set, set)
}
}
return rule, nil
}

groupBy, err := p.tryParseGroupByClause(p.Pos())
// parseTTLPolicySet parses one TTL SET assignment: <col> = <expr>.
// Unlike parseUpdateAssignment, the right-hand side is read with full
// expression precedence: a TTL SET has no trailing clause like the IN
// PARTITION that bounds an ALTER TABLE UPDATE assignment, so expressions
// such as `SET x = max(y) > 0` are valid here.
func (p *Parser) parseTTLPolicySet(pos Pos) (*UpdateAssignment, error) {
column, err := p.ParseNestedIdentifier(p.Pos())
if err != nil {
return nil, err
}
if err := p.expectTokenKind(TokenKindSingleEQ); err != nil {
return nil, err
}
expr, err := p.parseExpr(p.Pos())
if err != nil {
return nil, err
}
policy.GroupBy = groupBy
return policy, nil
return &UpdateAssignment{
AssignmentPos: pos,
Column: column,
Expr: expr,
}, nil
}

func (p *Parser) parseTTLExpr(pos Pos) (*TTLExpr, error) {
Expand Down
23 changes: 23 additions & 0 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,29 @@ func TestParser_InvalidSyntax(t *testing.T) {
"CREATE TABLE t (x String DEFAULT CAST(a +, 'String'))",
"CREATE TABLE t (x String MATERIALIZED a +)",
"CREATE TABLE t (x String ALIAS a +)",
// A TTL GROUP BY action only accepts a plain expression list; the
// query-level modifiers are syntax errors for ClickHouse in a TTL.
// (GROUP BY ALL and CUBE/ROLLUP(...) read as ordinary key
// expressions there and are only rejected semantically.)
"CREATE TABLE t (id UInt64, created DateTime) ENGINE = MergeTree() ORDER BY id TTL created + INTERVAL 1 DAY GROUP BY id WITH TOTALS",
"CREATE TABLE t (id UInt64, created DateTime) ENGINE = MergeTree() ORDER BY id TTL created + INTERVAL 1 DAY GROUP BY id WITH ROLLUP",
"CREATE TABLE t (id UInt64, created DateTime) ENGINE = MergeTree() ORDER BY id TTL created + INTERVAL 1 DAY GROUP BY id WITH CUBE",
"CREATE TABLE t (id UInt64, created DateTime) ENGINE = MergeTree() ORDER BY id TTL created + INTERVAL 1 DAY GROUP BY GROUPING SETS (id)",
// OR REPLACE is only accepted for MATERIALIZED VIEW under CREATE,
// never under ATTACH
"ATTACH OR REPLACE MATERIALIZED VIEW mv TO dest AS SELECT * FROM src",
// A TTL GROUP BY key list is greedy: after the keys the comma
// continues the list, so a trailing TTL action after a
// comma-separated key is rejected by ClickHouse (unlike after a
// complete SET assignment, where the comma starts the next TTL
// rule).
"CREATE TABLE t (id UInt64, created DateTime) ENGINE = MergeTree() ORDER BY id TTL created + INTERVAL 1 DAY GROUP BY id, created + INTERVAL 2 DAY DELETE",
// A TTL WHERE clause belongs only to the DELETE action; ClickHouse
// rejects it after GROUP BY, RECOMPRESS, and TO DISK/VOLUME.
"CREATE TABLE t (id UInt64, created DateTime, x UInt64) ENGINE = MergeTree() ORDER BY id TTL created + INTERVAL 1 DAY GROUP BY id SET x = sum(x) WHERE id > 0",
"CREATE TABLE t (id UInt64, created DateTime) ENGINE = MergeTree() ORDER BY id TTL created + INTERVAL 1 DAY GROUP BY id WHERE id > 0",
"CREATE TABLE t (id UInt64, created DateTime) ENGINE = MergeTree() ORDER BY id TTL created + INTERVAL 1 DAY RECOMPRESS CODEC(ZSTD(1)) WHERE id > 0",
"CREATE TABLE t (id UInt64, created DateTime) ENGINE = MergeTree() ORDER BY id TTL created + INTERVAL 1 DAY TO VOLUME 'v1' WHERE id > 0",
// Invalid ARRAY JOIN types (only ARRAY JOIN, LEFT ARRAY JOIN, and INNER ARRAY JOIN are valid)
"SELECT * FROM t RIGHT ARRAY JOIN arr AS a", // RIGHT ARRAY JOIN not supported
"SELECT * FROM t FULL ARRAY JOIN arr AS a", // FULL ARRAY JOIN not supported
Expand Down
6 changes: 4 additions & 2 deletions parser/parser_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@ import "fmt"
// [DEFINER = { user | CURRENT_USER }] [SQL SECURITY { DEFINER | NONE }]
// AS SELECT ...
// [COMMENT 'comment']
func (p *Parser) parseCreateMaterializedView(pos Pos) (*CreateMaterializedView, error) {
//
//nolint:funlen
func (p *Parser) parseCreateMaterializedView(pos Pos, orReplace bool) (*CreateMaterializedView, error) {
if err := p.expectKeyword(KeywordMaterialized); err != nil {
return nil, err
}
if err := p.expectKeyword(KeywordView); err != nil {
return nil, err
}

createMaterializedView := &CreateMaterializedView{CreatePos: pos}
createMaterializedView := &CreateMaterializedView{CreatePos: pos, OrReplace: orReplace}

// parse IF NOT EXISTS clause if exists
var err error
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE OR REPLACE MATERIALIZED VIEW mv TO dest AS SELECT * FROM src;
11 changes: 11 additions & 0 deletions parser/testdata/ddl/create_table_ttl_group_by.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE t (id UInt64, created DateTime, x UInt64) ENGINE = MergeTree() ORDER BY id TTL created + INTERVAL 1 DAY GROUP BY id SET x = sum(x);

CREATE TABLE t (id UInt64, created DateTime, x UInt64) ENGINE = MergeTree() ORDER BY id TTL created + INTERVAL 1 DAY GROUP BY id;

CREATE TABLE t (id UInt64, created DateTime, x UInt64, total UInt64) ENGINE = MergeTree() ORDER BY (id, created) TTL created + INTERVAL 1 DAY GROUP BY id, created SET x = sum(x), total = count();

CREATE TABLE t (id UInt64, created DateTime, x UInt64) ENGINE = MergeTree() ORDER BY id TTL created + INTERVAL 1 DAY GROUP BY id SET x = sum(x), created + INTERVAL 2 DAY DELETE;

CREATE TABLE t (id UInt64, created DateTime, x UInt64) ENGINE = MergeTree() ORDER BY id TTL created + INTERVAL 1 DAY GROUP BY ALL SET x = sum(x);

CREATE TABLE t (id UInt64, created DateTime, x UInt64, y UInt64) ENGINE = MergeTree() ORDER BY id TTL created + INTERVAL 1 DAY GROUP BY id SET x = max(y) > 0;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Origin SQL:
CREATE OR REPLACE MATERIALIZED VIEW mv TO dest AS SELECT * FROM src;

-- Beautify SQL:
CREATE OR REPLACE MATERIALIZED VIEW mv
TO dest
AS
SELECT
*
FROM
src;
Loading
Loading