Skip to content

Commit aa1b527

Browse files
kyleconroyclaude
andcommitted
Add support for REFRESH clause in CREATE MATERIALIZED VIEW (#121)
Parse and explain REFRESH AFTER/EVERY interval APPEND TO syntax: - Add REFRESH-related fields to CreateQuery AST - Parse REFRESH type (AFTER/EVERY), interval, unit, APPEND TO, and EMPTY - Output "Refresh strategy definition" and "TimeInterval" in explain Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5a31cd5 commit aa1b527

4 files changed

Lines changed: 82 additions & 5 deletions

File tree

ast/ast.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,12 @@ type CreateQuery struct {
278278
ToDatabase string `json:"to_database,omitempty"` // Target database for materialized views
279279
To string `json:"to,omitempty"` // Target table for materialized views
280280
Populate bool `json:"populate,omitempty"` // POPULATE for materialized views
281+
HasRefresh bool `json:"has_refresh,omitempty"` // Has REFRESH clause
282+
RefreshType string `json:"refresh_type,omitempty"` // AFTER or EVERY
283+
RefreshInterval Expression `json:"refresh_interval,omitempty"` // Interval value
284+
RefreshUnit string `json:"refresh_unit,omitempty"` // SECOND, MINUTE, etc.
285+
RefreshAppend bool `json:"refresh_append,omitempty"` // APPEND TO was specified
286+
Empty bool `json:"empty,omitempty"` // EMPTY keyword was specified
281287
Columns []*ColumnDeclaration `json:"columns,omitempty"`
282288
Indexes []*IndexDefinition `json:"indexes,omitempty"`
283289
Projections []*Projection `json:"projections,omitempty"`

internal/explain/statements.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,10 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string,
237237
if len(n.QuerySettings) > 0 {
238238
children++
239239
}
240+
// Count REFRESH strategy as a child
241+
if n.HasRefresh {
242+
children++ // Refresh strategy definition
243+
}
240244
// For materialized views with TO clause but no storage, count ViewTargets as a child
241245
if n.Materialized && n.To != "" && !hasStorageChild {
242246
children++ // ViewTargets
@@ -353,6 +357,11 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string,
353357
}
354358
}
355359
}
360+
// Output REFRESH strategy for materialized views with REFRESH clause
361+
if n.HasRefresh {
362+
fmt.Fprintf(sb, "%s Refresh strategy definition (children 1)\n", indent)
363+
fmt.Fprintf(sb, "%s TimeInterval\n", indent)
364+
}
356365
// For materialized views, output AsSelect before storage definition
357366
if n.Materialized && n.AsSelect != nil {
358367
// Set context flag to prevent Format from being output at SelectWithUnionQuery level

parser/parser.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2826,6 +2826,72 @@ func (p *Parser) parseCreateView(create *ast.CreateQuery) {
28262826
}
28272827
}
28282828

2829+
// Handle REFRESH clause for materialized views (REFRESH AFTER/EVERY interval APPEND TO target)
2830+
if p.currentIs(token.IDENT) && strings.ToUpper(p.current.Value) == "REFRESH" {
2831+
p.nextToken() // skip REFRESH
2832+
create.HasRefresh = true
2833+
2834+
// Parse refresh timing: AFTER interval or EVERY interval
2835+
if p.currentIs(token.IDENT) {
2836+
upper := strings.ToUpper(p.current.Value)
2837+
if upper == "AFTER" || upper == "EVERY" {
2838+
create.RefreshType = upper
2839+
p.nextToken()
2840+
// Parse interval value and unit
2841+
create.RefreshInterval = p.parseExpression(AND_PREC)
2842+
// Parse interval unit if present as identifier
2843+
if p.currentIs(token.IDENT) {
2844+
unitUpper := strings.ToUpper(p.current.Value)
2845+
if unitUpper == "SECOND" || unitUpper == "MINUTE" || unitUpper == "HOUR" ||
2846+
unitUpper == "DAY" || unitUpper == "WEEK" || unitUpper == "MONTH" || unitUpper == "YEAR" {
2847+
create.RefreshUnit = unitUpper
2848+
p.nextToken()
2849+
}
2850+
}
2851+
}
2852+
}
2853+
2854+
// Handle APPEND TO target - different from regular TO, part of REFRESH strategy
2855+
if p.currentIs(token.IDENT) && strings.ToUpper(p.current.Value) == "APPEND" {
2856+
p.nextToken() // skip APPEND
2857+
create.RefreshAppend = true
2858+
if p.currentIs(token.TO) {
2859+
p.nextToken() // skip TO
2860+
toName := p.parseIdentifierName()
2861+
if p.currentIs(token.DOT) {
2862+
p.nextToken()
2863+
create.ToDatabase = toName
2864+
create.To = p.parseIdentifierName()
2865+
} else {
2866+
create.To = toName
2867+
}
2868+
}
2869+
}
2870+
2871+
// For REFRESH ... APPEND TO target (columns), column definitions come after
2872+
if p.currentIs(token.LPAREN) && len(create.Columns) == 0 {
2873+
p.nextToken()
2874+
for !p.currentIs(token.RPAREN) && !p.currentIs(token.EOF) {
2875+
col := p.parseColumnDeclaration()
2876+
if col != nil {
2877+
create.Columns = append(create.Columns, col)
2878+
}
2879+
if p.currentIs(token.COMMA) {
2880+
p.nextToken()
2881+
} else {
2882+
break
2883+
}
2884+
}
2885+
p.expect(token.RPAREN)
2886+
}
2887+
2888+
// Handle EMPTY keyword
2889+
if p.currentIs(token.IDENT) && strings.ToUpper(p.current.Value) == "EMPTY" {
2890+
create.Empty = true
2891+
p.nextToken()
2892+
}
2893+
}
2894+
28292895
// Parse column definitions (e.g., CREATE VIEW v (x UInt64) AS SELECT ...)
28302896
// For MATERIALIZED VIEW, this can also include INDEX, PROJECTION, and PRIMARY KEY
28312897
if p.currentIs(token.LPAREN) {
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt17": true
4-
}
5-
}
1+
{}

0 commit comments

Comments
 (0)