Skip to content

Commit f5358d8

Browse files
authored
feat(snowflake)!: add support for GET statements (#5019)
* feat(snowflake): add support for GET statements * types in helper method * address review comments * refactor
1 parent c594b63 commit f5358d8

File tree

6 files changed

+66
-2
lines changed

6 files changed

+66
-2
lines changed

sqlglot/dialects/snowflake.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ class Parser(parser.Parser):
512512

513513
STATEMENT_PARSERS = {
514514
**parser.Parser.STATEMENT_PARSERS,
515+
TokenType.GET: lambda self: self._parse_get(),
515516
TokenType.PUT: lambda self: self._parse_put(),
516517
TokenType.SHOW: lambda self: self._parse_show(),
517518
}
@@ -857,6 +858,21 @@ def _parse_put(self) -> exp.Put | exp.Command:
857858
properties=self._parse_properties(),
858859
)
859860

861+
def _parse_get(self) -> exp.Get | exp.Command:
862+
start = self._prev
863+
target = self._parse_location_path()
864+
865+
# Parse as command if unquoted file path
866+
if self._curr.token_type == TokenType.URI_START:
867+
return self._parse_as_command(start)
868+
869+
return self.expression(
870+
exp.Get,
871+
this=self._parse_string(),
872+
target=target,
873+
properties=self._parse_properties(),
874+
)
875+
860876
def _parse_location_property(self) -> exp.LocationProperty:
861877
self._match(TokenType.EQ)
862878
return self.expression(exp.LocationProperty, this=self._parse_location_path())
@@ -937,6 +953,7 @@ class Tokenizer(tokens.Tokenizer):
937953
"CHARACTER VARYING": TokenType.VARCHAR,
938954
"EXCLUDE": TokenType.EXCEPT,
939955
"FILE FORMAT": TokenType.FILE_FORMAT,
956+
"GET": TokenType.GET,
940957
"ILIKE ANY": TokenType.ILIKE_ANY,
941958
"LIKE ANY": TokenType.LIKE_ANY,
942959
"MATCH_CONDITION": TokenType.MATCH_CONDITION,

sqlglot/expressions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3307,6 +3307,11 @@ class Put(Expression):
33073307
arg_types = {"this": True, "target": True, "properties": False}
33083308

33093309

3310+
# https://docs.snowflake.com/en/sql-reference/sql/get
3311+
class Get(Expression):
3312+
arg_types = {"this": True, "target": True, "properties": False}
3313+
3314+
33103315
class Table(Expression):
33113316
arg_types = {
33123317
"this": False,

sqlglot/generator.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ class Generator(metaclass=_Generator):
146146
exp.Except: lambda self, e: self.set_operations(e),
147147
exp.ExternalProperty: lambda *_: "EXTERNAL",
148148
exp.Floor: lambda self, e: self.ceil_floor(e),
149+
exp.Get: lambda self, e: self.get_put_sql(e),
149150
exp.GlobalProperty: lambda *_: "GLOBAL",
150151
exp.HeapProperty: lambda *_: "HEAP",
151152
exp.IcebergProperty: lambda *_: "ICEBERG",
@@ -175,6 +176,7 @@ class Generator(metaclass=_Generator):
175176
exp.PivotAny: lambda self, e: f"ANY{self.sql(e, 'this')}",
176177
exp.ProjectionPolicyColumnConstraint: lambda self,
177178
e: f"PROJECTION POLICY {self.sql(e, 'this')}",
179+
exp.Put: lambda self, e: self.get_put_sql(e),
178180
exp.RemoteWithConnectionModelProperty: lambda self,
179181
e: f"REMOTE WITH CONNECTION {self.sql(e, 'this')}",
180182
exp.ReturnsProperty: lambda self, e: (
@@ -4903,9 +4905,16 @@ def show_sql(self, expression: exp.Show) -> str:
49034905
self.unsupported("Unsupported SHOW statement")
49044906
return ""
49054907

4906-
def put_sql(self, expression: exp.Put) -> str:
4908+
def get_put_sql(self, expression: exp.Put | exp.Get) -> str:
4909+
# Snowflake GET/PUT statements:
4910+
# PUT <file> <internalStage> <properties>
4911+
# GET <internalStage> <file> <properties>
49074912
props = expression.args.get("properties")
49084913
props_sql = self.properties(props, prefix=" ", sep=" ", wrapped=False) if props else ""
49094914
this = self.sql(expression, "this")
49104915
target = self.sql(expression, "target")
4911-
return f"PUT {this} {target}{props_sql}"
4916+
4917+
if isinstance(expression, exp.Put):
4918+
return f"PUT {this} {target}{props_sql}"
4919+
else:
4920+
return f"GET {target} {this}{props_sql}"

sqlglot/parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ class Parser(metaclass=_Parser):
512512
TokenType.FINAL,
513513
TokenType.FORMAT,
514514
TokenType.FULL,
515+
TokenType.GET,
515516
TokenType.IDENTIFIER,
516517
TokenType.IS,
517518
TokenType.ISNULL,

sqlglot/tokens.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ class TokenType(AutoName):
290290
FROM = auto()
291291
FULL = auto()
292292
FUNCTION = auto()
293+
GET = auto()
293294
GLOB = auto()
294295
GLOBAL = auto()
295296
GRANT = auto()

tests/dialects/test_snowflake.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ def test_snowflake(self):
110110
"SELECT 1 put",
111111
"SELECT 1 AS put",
112112
)
113+
self.validate_identity(
114+
"SELECT 1 get",
115+
"SELECT 1 AS get",
116+
)
113117
self.validate_identity(
114118
"WITH t (SELECT 1 AS c) SELECT c FROM t",
115119
"WITH t AS (SELECT 1 AS c) SELECT c FROM t",
@@ -2465,6 +2469,33 @@ def test_put_to_stage(self):
24652469
check_command_warning=True,
24662470
)
24672471

2472+
def test_get_from_stage(self):
2473+
self.validate_identity('GET @"my_DB"."schEMA1"."MYstage" \'file:///dir/tmp.csv\'')
2474+
self.validate_identity("GET @s1/test 'file:///dir/tmp.csv'")
2475+
2476+
# GET with file path and stage ref containing spaces (wrapped in single quotes)
2477+
ast = parse_one("GET '@s1/my folder' 'file://my file.txt'", read="snowflake")
2478+
self.assertIsInstance(ast, exp.Get)
2479+
self.assertEqual(ast.args["target"], exp.Var(this="'@s1/my folder'"))
2480+
self.assertEqual(ast.this, exp.Literal(this="file://my file.txt", is_string=True))
2481+
self.assertEqual(ast.sql("snowflake"), "GET '@s1/my folder' 'file://my file.txt'")
2482+
2483+
# expression with additional properties
2484+
ast = parse_one("GET @stage1/folder 'file:///tmp/my.txt' PARALLEL = 1", read="snowflake")
2485+
self.assertIsInstance(ast, exp.Get)
2486+
self.assertEqual(ast.args["target"], exp.Var(this="@stage1/folder"))
2487+
self.assertEqual(ast.this, exp.Literal(this="file:///tmp/my.txt", is_string=True))
2488+
properties = ast.args.get("properties")
2489+
props_dict = {prop.this.this: prop.args["value"].this for prop in properties.expressions}
2490+
self.assertEqual(props_dict, {"PARALLEL": "1"})
2491+
2492+
# the unquoted URI variant is not fully supported yet
2493+
self.validate_identity("GET @%table file:///dir/tmp.csv", check_command_warning=True)
2494+
self.validate_identity(
2495+
"GET @s1/test file:///dir/tmp.csv PARALLEL=1",
2496+
check_command_warning=True,
2497+
)
2498+
24682499
def test_querying_semi_structured_data(self):
24692500
self.validate_identity("SELECT $1")
24702501
self.validate_identity("SELECT $1.elem")

0 commit comments

Comments
 (0)