Repository: AfterShip/clickhouse-sql-parser (go)

Issue:
Parser failed to parse MATERIALIZED view with REFRESH keyword
Parser failed to parse valid clickhouse materrialized view create statement. Here is the clickhouse documentations https://clickhouse.com/docs/materialized-view/refreshable-materialized-view 

```sql
CREATE MATERIALIZED VIEW fresh_mv
REFRESH EVERY 1 HOUR OFFSET 10 MINUTE APPEND TO target_table_name
AS SELECT
              `field_1`,
              `field_2`,
              `field_3`,              
   FROM table_v5
```

Error:
```error
line 2:0 unexpected token: "<keyword>", expected TO or ENGINE
REFRESH EVERY 1 HOUR OFFSET 10 MINUTE APPEND TO target_table_name
^^^^^^^
```


[Example Syntax  ](https://clickhouse.com/docs/sql-reference/statements/create/view#materialized-view)

```sql
CREATE MATERIALIZED VIEW [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
REFRESH EVERY|AFTER interval [OFFSET interval]
[RANDOMIZE FOR interval]
[DEPENDS ON [db.]name [, [db.]name [, ...]]]
[SETTINGS name = value [, name = value [, ...]]]
[APPEND]
[TO[db.]name] [(columns)] [ENGINE = engine]
[EMPTY]
[DEFINER = { user | CURRENT_USER }] [SQL SECURITY { DEFINER | NONE }]
AS SELECT ...
[COMMENT 'comment']

```

Pull request description:
Allow using REFRESH expression in CREATE MV syntax
This closes #156.

Patch:
diff --git a/parser/ast.go b/parser/ast.go
index 58190c6..ab54f11 100644
--- a/parser/ast.go
+++ b/parser/ast.go
@@ -1623,7 +1623,13 @@ type CreateMaterializedView struct {
 	Name         *TableIdentifier
 	IfNotExists  bool
 	OnCluster    *ClusterClause
+	Refresh      *RefreshExpr
+	RandomizeFor *IntervalExpr
+	DependsOn    []*TableIdentifier
+	Settings     *SettingsClause
+	HasAppend    bool
 	Engine       *EngineExpr
+	HasEmpty     bool
 	Destination  *DestinationClause
 	SubQuery     *SubQuery
 	Populate     bool
@@ -1653,6 +1659,30 @@ func (c *CreateMaterializedView) String() string {
 		builder.WriteString(" ")
 		builder.WriteString(c.OnCluster.String())
 	}
+	if c.Refresh != nil {
+		builder.WriteString(" ")
+		builder.WriteString(c.Refresh.String())
+	}
+	if c.RandomizeFor != nil {
+		builder.WriteString(" RANDOMIZE FOR ")
+		builder.WriteString(c.RandomizeFor.String())
+	}
+	if c.DependsOn != nil {
+		builder.WriteString(" DEPENDS ON ")
+		for i, dep := range c.DependsOn {
+			if i > 0 {
+				builder.WriteString(", ")
+			}
+			builder.WriteString(dep.String())
+		}
+	}
+	if c.Settings != nil {
+		builder.WriteString(" ")
+		builder.WriteString(c.Settings.String())
+	}
+	if c.HasAppend {
+		builder.WriteString(" APPEND")
+	}
 	if c.Engine != nil {
 		builder.WriteString(c.Engine.String())
 	}
@@ -1664,6 +1694,9 @@ func (c *CreateMaterializedView) String() string {
 			builder.WriteString(c.Destination.TableSchema.String())
 		}
 	}
+	if c.HasEmpty {
+		builder.WriteString(" EMPTY")
+	}
 	if c.Populate {
 		builder.WriteString(" POPULATE ")
 	}
@@ -1690,6 +1723,28 @@ func (c *CreateMaterializedView) Accept(visitor ASTVisitor) error {
 			return err
 		}
 	}
+	if c.Refresh != nil {
+		if err := c.Refresh.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	if c.RandomizeFor != nil {
+		if err := c.RandomizeFor.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	if c.DependsOn != nil {
+		for _, dep := range c.DependsOn {
+			if err := dep.Accept(visitor); err != nil {
+				return err
+			}
+		}
+	}
+	if c.Settings != nil {
+		if err := c.Settings.Accept(visitor); err != nil {
+			return err
+		}
+	}
 	if c.Engine != nil {
 		if err := c.Engine.Accept(visitor); err != nil {
 			return err
@@ -2744,6 +2799,55 @@ func (t *TTLPolicyRuleAction) Accept(visitor ASTVisitor) error {
 	return visitor.VisitTTLPolicyItemAction(t)
 }
 
+type RefreshExpr struct {
+	RefreshPos Pos
+	Frequency  string // EVERY|AFTER
+	Interval   *IntervalExpr
+	Offset     *IntervalExpr
+}
+
+func (r *RefreshExpr) Pos() Pos {
+	return r.RefreshPos
+}
+
+func (r *RefreshExpr) End() Pos {
+	if r.Offset != nil {
+		return r.Offset.End()
+	}
+	return r.Interval.End()
+}
+
+func (r *RefreshExpr) String() string {
+	var builder strings.Builder
+	builder.WriteString("REFRESH ")
+	builder.WriteString(r.Frequency)
+	if r.Interval != nil {
+		builder.WriteString(" ")
+		builder.WriteString(r.Interval.String())
+	}
+	if r.Offset != nil {
+		builder.WriteString(" OFFSET ")
+		builder.WriteString(r.Offset.String())
+	}
+	return builder.String()
+}
+
+func (r *RefreshExpr) Accept(visitor ASTVisitor) error {
+	visitor.enter(r)
+	defer visitor.leave(r)
+	if r.Interval != nil {
+		if err := r.Interval.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	if r.Offset != nil {
+		if err := r.Offset.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	return visitor.VisitRefreshExpr(r)
+}
+
 type TTLPolicyRule struct {
 	RulePos  Pos
 	ToVolume *StringLiteral
@@ -4108,13 +4212,18 @@ func (e *EnumType) Type() string {
 }
 
 type IntervalExpr struct {
+	// INTERVAL keyword position which might be omitted(IntervalPos = 0)
 	IntervalPos Pos
-	Expr        Expr
-	Unit        *Ident
+
+	Expr Expr
+	Unit *Ident
 }
 
 func (i *IntervalExpr) Pos() Pos {
-	return i.IntervalPos
+	if i.IntervalPos != 0 {
+		return i.IntervalPos
+	}
+	return i.Expr.Pos()
 }
 
 func (i *IntervalExpr) End() Pos {
@@ -4123,7 +4232,9 @@ func (i *IntervalExpr) End() Pos {
 
 func (i *IntervalExpr) String() string {
 	var builder strings.Builder
-	builder.WriteString("INTERVAL ")
+	if i.IntervalPos != 0 {
+		builder.WriteString("INTERVAL ")
+	}
 	builder.WriteString(i.Expr.String())
 	builder.WriteByte(' ')
 	builder.WriteString(i.Unit.String())
diff --git a/parser/ast_visitor.go b/parser/ast_visitor.go
index 06b8875..4969cdc 100644
--- a/parser/ast_visitor.go
+++ b/parser/ast_visitor.go
@@ -64,6 +64,7 @@ type ASTVisitor interface {
 	VisitTTLPolicy(expr *TTLPolicy) error
 	VisitTTLPolicyRule(expr *TTLPolicyRule) error
 	VisitTTLPolicyItemAction(expr *TTLPolicyRuleAction) error
+	VisitRefreshExpr(expr *RefreshExpr) error
 	VisitOrderByExpr(expr *OrderExpr) error
 	VisitOrderByListExpr(expr *OrderByClause) error
 	VisitSettingsExpr(expr *SettingExprList) error
@@ -621,6 +622,13 @@ func (v *DefaultASTVisitor) VisitTTLPolicyItemAction(expr *TTLPolicyRuleAction)
 	return nil
 }
 
+func (v *DefaultASTVisitor) VisitRefreshExpr(expr *RefreshExpr) error {
+	if v.Visit != nil {
+		return v.Visit(expr)
+	}
+	return nil
+}
+
 func (v *DefaultASTVisitor) VisitOrderByExpr(expr *OrderExpr) error {
 	if v.Visit != nil {
 		return v.Visit(expr)
diff --git a/parser/keyword.go b/parser/keyword.go
index 64eb4b0..14cfd81 100644
--- a/parser/keyword.go
+++ b/parser/keyword.go
@@ -10,6 +10,7 @@ const (
 	KeywordAnd          = "AND"
 	KeywordAnti         = "ANTI"
 	KeywordAny          = "ANY"
+	KeywordAppend       = "APPEND"
 	KeywordApply        = "APPLY"
 	KeywordArray        = "ARRAY"
 	KeywordAs           = "AS"
@@ -48,6 +49,7 @@ const (
 	KeywordDefault      = "DEFAULT"
 	KeywordDelay        = "DELAY"
 	KeywordDelete       = "DELETE"
+	KeywordDepends      = "DEPENDS"
 	KeywordDesc         = "DESC"
 	KeywordDescending   = "DESCENDING"
 	KeywordDescribe     = "DESCRIBE"
@@ -62,10 +64,12 @@ const (
 	KeywordDNS          = "DNS"
 	KeywordElse         = "ELSE"
 	KeywordEmbedded     = "EMBEDDED"
+	KeywordEmpty        = "EMPTY"
 	KeywordEnd          = "END"
 	KeywordEngine       = "ENGINE"
 	KeywordEstimate     = "ESTIMATE"
 	KeywordEvents       = "EVENTS"
+	KeywordEvery        = "EVERY"
 	KeywordExcept       = "EXCEPT"
 	KeywordExists       = "EXISTS"
 	KeywordExplain      = "EXPLAIN"
@@ -159,6 +163,7 @@ const (
 	KeywordQuery        = "QUERY"
 	KeywordQueues       = "QUEUES"
 	KeywordQuota        = "QUOTA"
+	KeywordRandomize    = "RANDOMIZE"
 	KeywordRange        = "RANGE"
 	KeywordRecompress   = "RECOMPRESS"
 	KeywordRefresh      = "REFRESH"
@@ -241,6 +246,7 @@ var keywords = NewSet(
 	KeywordAnd,
 	KeywordAnti,
 	KeywordAny,
+	KeywordAppend,
 	KeywordApply,
 	KeywordArray,
 	KeywordAs,
@@ -279,6 +285,7 @@ var keywords = NewSet(
 	KeywordDefault,
 	KeywordDelay,
 	KeywordDelete,
+	KeywordDepends,
 	KeywordDesc,
 	KeywordDescending,
 	KeywordDescribe,
@@ -295,7 +302,10 @@ var keywords = NewSet(
 	KeywordEnd,
 	KeywordEngine,
 	KeywordEstimate,
+	KeywordEmbedded,
+	KeywordEmpty,
 	KeywordEvents,
+	KeywordEvery,
 	KeywordExcept,
 	KeywordExists,
 	KeywordExplain,
@@ -389,6 +399,7 @@ var keywords = NewSet(
 	KeywordQuery,
 	KeywordQueues,
 	KeywordQuota,
+	KeywordRandomize,
 	KeywordRange,
 	KeywordRecompress,
 	KeywordRefresh,
diff --git a/parser/parser_column.go b/parser/parser_column.go
index cc060d0..9d438f7 100644
--- a/parser/parser_column.go
+++ b/parser/parser_column.go
@@ -274,7 +274,7 @@ func (p *Parser) parseColumnExtractExpr(pos Pos) (*ExtractExpr, error) {
 	if err != nil {
 		return nil, err
 	}
-	if !intervalType.Contains(strings.ToUpper(ident.Name)) {
+	if !intervalUnits.Contains(strings.ToUpper(ident.Name)) {
 		return nil, fmt.Errorf("unknown interval type: <%q>", ident.Name)
 	}
 
@@ -352,7 +352,7 @@ func (p *Parser) parseColumnExpr(pos Pos) (Expr, error) { //nolint:funlen
 	}
 	switch {
 	case p.matchKeyword(KeywordInterval):
-		return p.parseColumnExprInterval(pos)
+		return p.parseInterval(true)
 	case p.matchKeyword(KeywordDate), p.matchKeyword(KeywordTimestamp):
 		nextToken, err := p.lexer.peekToken()
 		if err != nil {
@@ -517,30 +517,30 @@ func (p *Parser) parseSelectItems() ([]*SelectItem, error) {
 	return selectItems, nil
 }
 
-// Syntax: INTERVAL expr interval
-func (p *Parser) parseColumnExprInterval(pos Pos) (Expr, error) {
-	if err := p.expectKeyword(KeywordInterval); err != nil {
-		return nil, err
+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
+		}
 	}
-
-	// store the column expr if it needs
-	columnExpr, err := p.parseExpr(p.Pos())
+	expr, err := p.parseExpr(p.Pos())
 	if err != nil {
 		return nil, err
 	}
 
-	// parse interval
-	ident, err := p.parseIdent()
+	unit, err := p.parseIdent()
 	if err != nil {
 		return nil, err
 	}
-	if !intervalType.Contains(strings.ToUpper(ident.Name)) {
-		return nil, fmt.Errorf("unknown interval type: <%q>", ident.Name)
+	if !intervalUnits.Contains(strings.ToUpper(unit.Name)) {
+		return nil, fmt.Errorf("unknown interval type: <%q>", unit.Name)
 	}
 	return &IntervalExpr{
-		IntervalPos: pos,
-		Expr:        columnExpr,
-		Unit:        ident,
+		IntervalPos: intervalPos,
+		Expr:        expr,
+		Unit:        unit,
 	}, nil
 }
 
diff --git a/parser/parser_view.go b/parser/parser_view.go
index ac8dbcf..672d710 100644
--- a/parser/parser_view.go
+++ b/parser/parser_view.go
@@ -2,6 +2,20 @@ package parser
 
 import "fmt"
 
+// parseCreateMaterializedView parses a CREATE MATERIALIZED VIEW statement.
+//
+// The syntax is as follows:
+// CREATE MATERIALIZED VIEW [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
+// REFRESH EVERY|AFTER interval [OFFSET interval]
+// [RANDOMIZE FOR interval]
+// [DEPENDS ON [db.]name [, [db.]name [, ...]]]
+// [SETTINGS name = value [, name = value [, ...]]]
+// [APPEND]
+// [TO[db.]name] [(columns)] [ENGINE = engine]
+// [EMPTY]
+// [DEFINER = { user | CURRENT_USER }] [SQL SECURITY { DEFINER | NONE }]
+// AS SELECT ...
+// [COMMENT 'comment']
 func (p *Parser) parseCreateMaterializedView(pos Pos) (*CreateMaterializedView, error) {
 	if err := p.expectKeyword(KeywordMaterialized); err != nil {
 		return nil, err
@@ -25,13 +39,48 @@ func (p *Parser) parseCreateMaterializedView(pos Pos) (*CreateMaterializedView,
 	}
 	createMaterializedView.Name = tableIdentifier
 
-	// parse ON CLUSTER clause if exists
 	onCluster, err := p.tryParseClusterClause(p.Pos())
 	if err != nil {
 		return nil, err
 	}
 	createMaterializedView.OnCluster = onCluster
 
+	refreshExpr, err := p.tryParseRefreshExpr(p.Pos())
+	if err != nil {
+		return nil, err
+	}
+	createMaterializedView.Refresh = refreshExpr
+
+	if p.tryConsumeKeywords(KeywordRandomize, KeywordFor) {
+		randomizeFor, err := p.parseInterval(false)
+		if err != nil {
+			return nil, err
+		}
+		createMaterializedView.RandomizeFor = randomizeFor
+	}
+	if p.tryConsumeKeywords(KeywordDepends, KeywordOn) {
+		dependsOnTables := make([]*TableIdentifier, 0)
+		table, err := p.parseTableIdentifier(p.Pos())
+		if err != nil {
+			return nil, err
+		}
+		dependsOnTables = append(dependsOnTables, table)
+		for p.matchTokenKind(TokenKindComma) {
+			table, err := p.parseTableIdentifier(p.Pos())
+			if err != nil {
+				return nil, err
+			}
+			dependsOnTables = append(dependsOnTables, table)
+		}
+		createMaterializedView.DependsOn = dependsOnTables
+	}
+	settings, err := p.tryParseSettingsClause(p.Pos())
+	if err != nil {
+		return nil, err
+	}
+	createMaterializedView.Settings = settings
+	createMaterializedView.HasAppend = p.tryConsumeKeywords(KeywordAppend)
+
 	switch {
 	case p.matchKeyword(KeywordTo):
 		destination, err := p.parseDestinationClause(p.Pos())
@@ -62,6 +111,8 @@ func (p *Parser) parseCreateMaterializedView(pos Pos) (*CreateMaterializedView,
 	default:
 		return nil, fmt.Errorf("unexpected token: %q, expected TO or ENGINE", p.lastTokenKind())
 	}
+	createMaterializedView.HasEmpty = p.tryConsumeKeywords(KeywordEmpty)
+
 	if p.tryConsumeKeywords(KeywordAs) {
 		subQuery, err := p.parseSubQuery(p.Pos())
 		if err != nil {
@@ -79,6 +130,37 @@ func (p *Parser) parseCreateMaterializedView(pos Pos) (*CreateMaterializedView,
 	return createMaterializedView, nil
 }
 
+func (p *Parser) tryParseRefreshExpr(pos Pos) (*RefreshExpr, error) {
+	if !p.tryConsumeKeywords(KeywordRefresh) {
+		return nil, nil // nolint
+	}
+
+	// REFRESH EVERY|AFTER interval
+	refreshExpr := &RefreshExpr{RefreshPos: pos}
+	if !p.matchOneOfKeywords(KeywordEvery, KeywordAfter) {
+		return nil, fmt.Errorf("expected EVERY or AFTER, but got %q", p.lastTokenKind())
+	}
+	refreshExpr.Frequency = p.last().String
+	_ = p.lexer.consumeToken()
+
+	interval, err := p.parseInterval(false)
+	if err != nil {
+		return nil, err
+	}
+	refreshExpr.Interval = interval
+
+	// [OFFSET interval]
+	if p.tryConsumeKeywords(KeywordOffset) {
+		offset, err := p.parseInterval(false)
+		if err != nil {
+			return nil, err
+		}
+		refreshExpr.Offset = offset
+	}
+
+	return refreshExpr, nil
+}
+
 // (ATTACH | CREATE) (OR REPLACE)? VIEW (IF NOT EXISTS)? tableIdentifier uuidClause? clusterClause? tableSchemaClause? subqueryClause
 func (p *Parser) parseCreateView(pos Pos, orReplace bool) (*CreateView, error) {
 	createView := &CreateView{CreatePos: pos, OrReplace: orReplace}
diff --git a/parser/type.go b/parser/type.go
index d40336a..f2eefd7 100644
--- a/parser/type.go
+++ b/parser/type.go
@@ -1,3 +1,3 @@
 package parser
 
-var intervalType = NewSet("SECOND", "MINUTE", "HOUR", "DAY", "WEEK", "MONTH", "QUARTER", "YEAR")
+var intervalUnits = NewSet("SECOND", "MINUTE", "HOUR", "DAY", "WEEK", "MONTH", "QUARTER", "YEAR")


Test patch:
diff --git a/parser/testdata/ddl/create_materialized_view_with_refresh.sql b/parser/testdata/ddl/create_materialized_view_with_refresh.sql
new file mode 100644
index 0000000..60d3021
--- /dev/null
+++ b/parser/testdata/ddl/create_materialized_view_with_refresh.sql
@@ -0,0 +1,15 @@
+CREATE MATERIALIZED VIEW fresh_mv
+REFRESH EVERY 1 HOUR OFFSET 10 MINUTE
+RANDOMIZE FOR 1 SECOND
+DEPENDS ON  table_v5
+SETTINGS
+    randomize_for = 1,
+    randomize_offset = 10,
+    randomize_period = 1
+APPEND TO target_table_name
+EMPTY
+AS SELECT
+    `field_1`,
+    `field_2`,
+    `field_3`,
+FROM table_v5
\ No newline at end of file
diff --git a/parser/testdata/ddl/format/create_materialized_view_with_refresh.sql b/parser/testdata/ddl/format/create_materialized_view_with_refresh.sql
new file mode 100644
index 0000000..7dbb387
--- /dev/null
+++ b/parser/testdata/ddl/format/create_materialized_view_with_refresh.sql
@@ -0,0 +1,19 @@
+-- Origin SQL:
+CREATE MATERIALIZED VIEW fresh_mv
+REFRESH EVERY 1 HOUR OFFSET 10 MINUTE
+RANDOMIZE FOR 1 SECOND
+DEPENDS ON  table_v5
+SETTINGS
+    randomize_for = 1,
+    randomize_offset = 10,
+    randomize_period = 1
+APPEND TO target_table_name
+EMPTY
+AS SELECT
+    `field_1`,
+    `field_2`,
+    `field_3`,
+FROM table_v5
+
+-- Format SQL:
+CREATE MATERIALIZED VIEW fresh_mv REFRESH EVERY 1 HOUR OFFSET 10 MINUTE RANDOMIZE FOR 1 SECOND DEPENDS ON table_v5 SETTINGS randomize_for=1, randomize_offset=10, randomize_period=1 APPEND TO target_table_name EMPTY AS SELECT `field_1`, `field_2`, `field_3`, FROM AS table_v5;
diff --git a/parser/testdata/ddl/output/bug_001.sql.golden.json b/parser/testdata/ddl/output/bug_001.sql.golden.json
index 49ac5f1..a1b6131 100644
--- a/parser/testdata/ddl/output/bug_001.sql.golden.json
+++ b/parser/testdata/ddl/output/bug_001.sql.golden.json
@@ -25,7 +25,13 @@
         "Literal": "default_cluster"
       }
     },
+    "Refresh": null,
+    "RandomizeFor": null,
+    "DependsOn": null,
+    "Settings": null,
+    "HasAppend": false,
     "Engine": null,
+    "HasEmpty": false,
     "Destination": {
       "ToPos": 89,
       "TableIdentifier": {
diff --git a/parser/testdata/ddl/output/create_materialized_view_basic.sql.golden.json b/parser/testdata/ddl/output/create_materialized_view_basic.sql.golden.json
index a6cc772..28d211c 100644
--- a/parser/testdata/ddl/output/create_materialized_view_basic.sql.golden.json
+++ b/parser/testdata/ddl/output/create_materialized_view_basic.sql.golden.json
@@ -25,7 +25,13 @@
         "Literal": "default_cluster"
       }
     },
+    "Refresh": null,
+    "RandomizeFor": null,
+    "DependsOn": null,
+    "Settings": null,
+    "HasAppend": false,
     "Engine": null,
+    "HasEmpty": false,
     "Destination": {
       "ToPos": 78,
       "TableIdentifier": {
diff --git a/parser/testdata/ddl/output/create_materialized_view_with_empty_table_schema.sql.golden.json b/parser/testdata/ddl/output/create_materialized_view_with_empty_table_schema.sql.golden.json
index 5810051..6a0c4f4 100644
--- a/parser/testdata/ddl/output/create_materialized_view_with_empty_table_schema.sql.golden.json
+++ b/parser/testdata/ddl/output/create_materialized_view_with_empty_table_schema.sql.golden.json
@@ -26,6 +26,11 @@
         "NameEnd": 59
       }
     },
+    "Refresh": null,
+    "RandomizeFor": null,
+    "DependsOn": null,
+    "Settings": null,
+    "HasAppend": false,
     "Engine": {
       "EnginePos": 60,
       "EngineEnd": 190,
@@ -137,6 +142,7 @@
         ]
       }
     },
+    "HasEmpty": false,
     "Destination": null,
     "SubQuery": {
       "HasParen": false,
diff --git a/parser/testdata/ddl/output/create_materialized_view_with_refresh.sql.golden.json b/parser/testdata/ddl/output/create_materialized_view_with_refresh.sql.golden.json
new file mode 100644
index 0000000..4e17a5a
--- /dev/null
+++ b/parser/testdata/ddl/output/create_materialized_view_with_refresh.sql.golden.json
@@ -0,0 +1,218 @@
+[
+  {
+    "CreatePos": 0,
+    "StatementEnd": 302,
+    "Name": {
+      "Database": null,
+      "Table": {
+        "Name": "fresh_mv",
+        "QuoteType": 1,
+        "NamePos": 25,
+        "NameEnd": 33
+      }
+    },
+    "IfNotExists": false,
+    "OnCluster": null,
+    "Refresh": {
+      "RefreshPos": 34,
+      "Frequency": "EVERY",
+      "Interval": {
+        "IntervalPos": 0,
+        "Expr": {
+          "NumPos": 48,
+          "NumEnd": 49,
+          "Literal": "1",
+          "Base": 10
+        },
+        "Unit": {
+          "Name": "HOUR",
+          "QuoteType": 1,
+          "NamePos": 50,
+          "NameEnd": 54
+        }
+      },
+      "Offset": {
+        "IntervalPos": 0,
+        "Expr": {
+          "NumPos": 62,
+          "NumEnd": 64,
+          "Literal": "10",
+          "Base": 10
+        },
+        "Unit": {
+          "Name": "MINUTE",
+          "QuoteType": 1,
+          "NamePos": 65,
+          "NameEnd": 71
+        }
+      }
+    },
+    "RandomizeFor": {
+      "IntervalPos": 0,
+      "Expr": {
+        "NumPos": 86,
+        "NumEnd": 87,
+        "Literal": "1",
+        "Base": 10
+      },
+      "Unit": {
+        "Name": "SECOND",
+        "QuoteType": 1,
+        "NamePos": 88,
+        "NameEnd": 94
+      }
+    },
+    "DependsOn": [
+      {
+        "Database": null,
+        "Table": {
+          "Name": "table_v5",
+          "QuoteType": 1,
+          "NamePos": 107,
+          "NameEnd": 115
+        }
+      }
+    ],
+    "Settings": {
+      "SettingsPos": 116,
+      "ListEnd": 199,
+      "Items": [
+        {
+          "SettingsPos": 129,
+          "Name": {
+            "Name": "randomize_for",
+            "QuoteType": 1,
+            "NamePos": 129,
+            "NameEnd": 142
+          },
+          "Expr": {
+            "NumPos": 145,
+            "NumEnd": 146,
+            "Literal": "1",
+            "Base": 10
+          }
+        },
+        {
+          "SettingsPos": 152,
+          "Name": {
+            "Name": "randomize_offset",
+            "QuoteType": 1,
+            "NamePos": 152,
+            "NameEnd": 168
+          },
+          "Expr": {
+            "NumPos": 171,
+            "NumEnd": 173,
+            "Literal": "10",
+            "Base": 10
+          }
+        },
+        {
+          "SettingsPos": 179,
+          "Name": {
+            "Name": "randomize_period",
+            "QuoteType": 1,
+            "NamePos": 179,
+            "NameEnd": 195
+          },
+          "Expr": {
+            "NumPos": 198,
+            "NumEnd": 199,
+            "Literal": "1",
+            "Base": 10
+          }
+        }
+      ]
+    },
+    "HasAppend": true,
+    "Engine": null,
+    "HasEmpty": true,
+    "Destination": {
+      "ToPos": 207,
+      "TableIdentifier": {
+        "Database": null,
+        "Table": {
+          "Name": "target_table_name",
+          "QuoteType": 1,
+          "NamePos": 210,
+          "NameEnd": 227
+        }
+      },
+      "TableSchema": null
+    },
+    "SubQuery": {
+      "HasParen": false,
+      "Select": {
+        "SelectPos": 237,
+        "StatementEnd": 302,
+        "With": null,
+        "Top": null,
+        "SelectItems": [
+          {
+            "Expr": {
+              "Name": "field_1",
+              "QuoteType": 3,
+              "NamePos": 249,
+              "NameEnd": 256
+            },
+            "Modifiers": [],
+            "Alias": null
+          },
+          {
+            "Expr": {
+              "Name": "field_2",
+              "QuoteType": 3,
+              "NamePos": 264,
+              "NameEnd": 271
+            },
+            "Modifiers": [],
+            "Alias": null
+          },
+          {
+            "Expr": {
+              "Name": "field_3",
+              "QuoteType": 3,
+              "NamePos": 279,
+              "NameEnd": 286
+            },
+            "Modifiers": [],
+            "Alias": null
+          },
+          {
+            "Expr": {
+              "Name": "FROM",
+              "QuoteType": 1,
+              "NamePos": 289,
+              "NameEnd": 293
+            },
+            "Modifiers": [],
+            "Alias": {
+              "Name": "table_v5",
+              "QuoteType": 1,
+              "NamePos": 294,
+              "NameEnd": 302
+            }
+          }
+        ],
+        "From": null,
+        "ArrayJoin": 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
+      }
+    },
+    "Populate": false,
+    "Comment": null
+  }
+]
\ No newline at end of file
diff --git a/parser/testdata/ddl/output/create_mv_with_not_op.sql.golden.json b/parser/testdata/ddl/output/create_mv_with_not_op.sql.golden.json
index 444962d..530c564 100644
--- a/parser/testdata/ddl/output/create_mv_with_not_op.sql.golden.json
+++ b/parser/testdata/ddl/output/create_mv_with_not_op.sql.golden.json
@@ -25,7 +25,13 @@
         "Literal": "default_cluster"
       }
     },
+    "Refresh": null,
+    "RandomizeFor": null,
+    "DependsOn": null,
+    "Settings": null,
+    "HasAppend": false,
     "Engine": null,
+    "HasEmpty": false,
     "Destination": {
       "ToPos": 77,
       "TableIdentifier": {
diff --git a/parser/testdata/ddl/output/create_mv_with_order_by.sql.golden.json b/parser/testdata/ddl/output/create_mv_with_order_by.sql.golden.json
index 2934ef4..0119301 100644
--- a/parser/testdata/ddl/output/create_mv_with_order_by.sql.golden.json
+++ b/parser/testdata/ddl/output/create_mv_with_order_by.sql.golden.json
@@ -13,6 +13,11 @@
     },
     "IfNotExists": true,
     "OnCluster": null,
+    "Refresh": null,
+    "RandomizeFor": null,
+    "DependsOn": null,
+    "Settings": null,
+    "HasAppend": false,
     "Engine": {
       "EnginePos": 47,
       "EngineEnd": 106,
@@ -89,6 +94,7 @@
         ]
       }
     },
+    "HasEmpty": false,
     "Destination": null,
     "SubQuery": {
       "HasParen": false,
@@ -166,6 +172,11 @@
     },
     "IfNotExists": true,
     "OnCluster": null,
+    "Refresh": null,
+    "RandomizeFor": null,
+    "DependsOn": null,
+    "Settings": null,
+    "HasAppend": false,
     "Engine": {
       "EnginePos": 185,
       "EngineEnd": 230,
@@ -211,6 +222,7 @@
       "Settings": null,
       "OrderBy": null
     },
+    "HasEmpty": false,
     "Destination": null,
     "SubQuery": {
       "HasParen": false,