Skip to content

Commit 30ee35d

Browse files
ajitpratap0Ajit Pratap Singh
andauthored
feat(parser): Snowflake CREATE STAGE/STREAM/TASK/PIPE/... stubs (#483) (#498)
Co-authored-by: Ajit Pratap Singh <ajitpratapsingh@Ajits-Mac-mini-2655.local>
1 parent 144e3ca commit 30ee35d

File tree

2 files changed

+101
-0
lines changed

2 files changed

+101
-0
lines changed

pkg/sql/parser/ddl.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,60 @@ func (p *Parser) parseCreateStatement() (ast.Statement, error) {
9595
}
9696
return stmt, nil
9797
}
98+
99+
// Snowflake object-type extensions: STAGE, STREAM, TASK, PIPE, FILE FORMAT,
100+
// WAREHOUSE, DATABASE, SCHEMA, ROLE, FUNCTION, PROCEDURE, SEQUENCE.
101+
// Parse-only: record the object kind and name on a DescribeStatement
102+
// placeholder, then consume the rest of the statement body permissively
103+
// until ';' or EOF (tracking balanced parens so embedded expressions with
104+
// semicolons inside string literals round-trip).
105+
if p.dialect == string(keywords.DialectSnowflake) {
106+
kind := strings.ToUpper(p.currentToken.Token.Value)
107+
if kind == "FILE" && strings.EqualFold(p.peekToken().Token.Value, "FORMAT") {
108+
p.advance() // FILE
109+
kind = "FILE FORMAT"
110+
}
111+
switch kind {
112+
case "STAGE", "STREAM", "TASK", "PIPE", "FILE FORMAT",
113+
"WAREHOUSE", "DATABASE", "SCHEMA", "ROLE", "SEQUENCE",
114+
"FUNCTION", "PROCEDURE":
115+
p.advance() // Consume object-kind keyword
116+
// Optional IF NOT EXISTS
117+
if p.isType(models.TokenTypeIf) {
118+
p.advance()
119+
if p.isType(models.TokenTypeNot) {
120+
p.advance()
121+
}
122+
if p.isType(models.TokenTypeExists) {
123+
p.advance()
124+
}
125+
}
126+
// Object name (qualified identifier)
127+
name, _ := p.parseQualifiedName()
128+
// Consume the rest of the statement body until ';' or EOF,
129+
// tracking balanced parens.
130+
depth := 0
131+
for {
132+
t := p.currentToken.Token.Type
133+
if t == models.TokenTypeEOF {
134+
break
135+
}
136+
if t == models.TokenTypeSemicolon && depth == 0 {
137+
break
138+
}
139+
if t == models.TokenTypeLParen {
140+
depth++
141+
} else if t == models.TokenTypeRParen {
142+
depth--
143+
}
144+
p.advance()
145+
}
146+
stub := ast.GetDescribeStatement()
147+
stub.TableName = "CREATE " + kind + " " + name
148+
return stub, nil
149+
}
150+
}
151+
98152
return nil, p.expectedError("TABLE, VIEW, MATERIALIZED VIEW, or INDEX after CREATE")
99153
}
100154

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2026 GoSQLX Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
5+
package parser_test
6+
7+
import (
8+
"testing"
9+
10+
"github.com/ajitpratap0/GoSQLX/pkg/gosqlx"
11+
"github.com/ajitpratap0/GoSQLX/pkg/sql/keywords"
12+
)
13+
14+
// TestSnowflakeCreateObjects verifies Snowflake CREATE statements for object
15+
// types beyond TABLE/VIEW/INDEX parse. These are currently consumed as
16+
// stub statements (body is not modeled on the AST). Regression for #483.
17+
func TestSnowflakeCreateObjects(t *testing.T) {
18+
queries := map[string]string{
19+
"create_stage": `CREATE STAGE my_stage URL='s3://bucket/path' CREDENTIALS=(AWS_KEY_ID='abc' AWS_SECRET_KEY='xyz')`,
20+
21+
"create_file_format": `CREATE FILE FORMAT my_csv TYPE = CSV FIELD_DELIMITER = ','`,
22+
23+
"create_stream": `CREATE STREAM my_stream ON TABLE events`,
24+
25+
"create_task": `CREATE TASK daily_refresh WAREHOUSE = compute_wh SCHEDULE = 'USING CRON 0 0 * * * UTC' AS INSERT INTO t SELECT 1`,
26+
27+
"create_or_replace_pipe": `CREATE OR REPLACE PIPE my_pipe AUTO_INGEST = TRUE AS COPY INTO t FROM @my_stage`,
28+
29+
"create_warehouse": `CREATE WAREHOUSE my_wh WITH WAREHOUSE_SIZE = 'SMALL'`,
30+
31+
"create_database": `CREATE DATABASE my_db`,
32+
33+
"create_schema_qualified": `CREATE SCHEMA analytics.my_schema`,
34+
35+
"create_role": `CREATE ROLE analyst`,
36+
37+
"create_if_not_exists_stage": `CREATE STAGE IF NOT EXISTS my_stage URL='s3://bucket'`,
38+
}
39+
for name, q := range queries {
40+
q := q
41+
t.Run(name, func(t *testing.T) {
42+
if _, err := gosqlx.ParseWithDialect(q, keywords.DialectSnowflake); err != nil {
43+
t.Fatalf("parse failed: %v", err)
44+
}
45+
})
46+
}
47+
}

0 commit comments

Comments
 (0)