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
60 changes: 60 additions & 0 deletions pkg/sql/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,15 @@ func (p *Parser) parseStatement() (ast.Statement, error) {
p.advance()
return p.parsePragmaStatement()
}
// Snowflake stage operations may arrive as keyword tokens depending on
// the active keyword table (LIST, COPY, etc. can be registered).
if p.dialect == string(keywords.DialectSnowflake) {
upper := strings.ToUpper(p.currentToken.Token.Value)
switch upper {
case "COPY", "PUT", "GET", "LIST", "REMOVE", "LS":
return p.parseSnowflakeStageStatement(upper)
}
}
case models.TokenTypeIdentifier:
// PRAGMA may be tokenized as IDENTIFIER when no dialect-specific keyword
// set is active (e.g. when using the default PostgreSQL tokenizer dialect).
Expand All @@ -721,6 +730,16 @@ func (p *Parser) parseStatement() (ast.Statement, error) {
strings.EqualFold(p.currentToken.Token.Value, "USE") {
return p.parseSnowflakeUseStatement()
}
// Snowflake stage operations: COPY INTO, PUT, GET, LIST, REMOVE.
// All tokenize as identifiers; parse-only stubs that consume the
// rest of the statement body.
if p.dialect == string(keywords.DialectSnowflake) {
upper := strings.ToUpper(p.currentToken.Token.Value)
switch upper {
case "COPY", "PUT", "GET", "LIST", "REMOVE", "LS":
return p.parseSnowflakeStageStatement(upper)
}
}
}
return nil, p.expectedError("statement")
}
Expand Down Expand Up @@ -748,6 +767,47 @@ func (p *Parser) parseSnowflakeUseStatement() (ast.Statement, error) {
return stmt, nil
}

// parseSnowflakeStageStatement parses Snowflake stage operations as stubs:
//
// COPY INTO <target> FROM <source> [options]
// PUT file://<path> @<stage>
// GET @<stage> file://<path>
// LIST @<stage> (or LS)
// REMOVE @<stage>/<path>
//
// The statement is consumed token-by-token (tracking balanced parens) until
// ';' or EOF and returned as a DescribeStatement placeholder tagged with the
// operation kind. No AST modeling yet; follow-up work.
func (p *Parser) parseSnowflakeStageStatement(kind string) (ast.Statement, error) {
p.advance() // Consume leading kind token

// COPY INTO: consume the INTO keyword if present.
if kind == "COPY" && p.isType(models.TokenTypeInto) {
p.advance()
}

// Consume the rest of the statement body.
depth := 0
for {
t := p.currentToken.Token.Type
if t == models.TokenTypeEOF {
break
}
if t == models.TokenTypeSemicolon && depth == 0 {
break
}
if t == models.TokenTypeLParen {
depth++
} else if t == models.TokenTypeRParen {
depth--
}
p.advance()
}
stub := ast.GetDescribeStatement()
stub.TableName = kind
return stub, nil
}

// NewParser creates a new parser with optional configuration.
func NewParser(opts ...ParserOption) *Parser {
p := &Parser{}
Expand Down
42 changes: 42 additions & 0 deletions pkg/sql/parser/snowflake_stage_ops_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2026 GoSQLX Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");

package parser_test

import (
"testing"

"github.com/ajitpratap0/GoSQLX/pkg/gosqlx"
"github.com/ajitpratap0/GoSQLX/pkg/sql/keywords"
)

// TestSnowflakeStageOps verifies Snowflake stage operations parse as stubs.
// Regression for #483.
func TestSnowflakeStageOps(t *testing.T) {
queries := map[string]string{
"copy_into_table_with_format": `COPY INTO my_table FROM @my_stage FILE_FORMAT = (TYPE = CSV)`,

"copy_into_named_format": `COPY INTO my_table FROM @my_stage/file.csv FILE_FORMAT = (FORMAT_NAME = my_csv) ON_ERROR = CONTINUE`,

"copy_into_stage_from_table": `COPY INTO @my_stage FROM my_table FILE_FORMAT = (TYPE = PARQUET)`,

"put_to_stage": `PUT file:///tmp/data.csv @my_stage`,

"get_from_stage": `GET @my_stage file:///tmp/output/`,

"list_stage": `LIST @my_stage`,

"remove_from_stage": `REMOVE @my_stage/old_files`,

"ls_alias": `LS @my_stage`,
}
for name, q := range queries {
q := q
t.Run(name, func(t *testing.T) {
if _, err := gosqlx.ParseWithDialect(q, keywords.DialectSnowflake); err != nil {
t.Fatalf("parse failed: %v", err)
}
})
}
}
Loading