Skip to content

Commit 1353b79

Browse files
authored
fix(Snowflake): Enhance parity for FILE_FORMAT & CREDENTIALS in CREATE STAGE (#4969)
* feat(Snowflake): enhance parity for FILE_FORMAT in CREATE STAGE * minor fix * add support for CREDENTIALS property in CREATE STAGE expressions * address review comments * move credentialsproperty_sql into base Generator * address more review comments * minor refactoring
1 parent 61bc01c commit 1353b79

File tree

5 files changed

+56
-9
lines changed

5 files changed

+56
-9
lines changed

sqlglot/dialects/snowflake.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,7 @@ class Parser(parser.Parser):
516516

517517
PROPERTY_PARSERS = {
518518
**parser.Parser.PROPERTY_PARSERS,
519+
"CREDENTIALS": lambda self: self._parse_credentials_property(),
519520
"FILE_FORMAT": lambda self: self._parse_file_format_property(),
520521
"LOCATION": lambda self: self._parse_location_property(),
521522
"TAG": lambda self: self._parse_tag(),
@@ -903,8 +904,20 @@ def _parse_foreign_key(self) -> exp.ForeignKey:
903904

904905
def _parse_file_format_property(self) -> exp.FileFormatProperty:
905906
self._match(TokenType.EQ)
907+
if self._match(TokenType.L_PAREN, advance=False):
908+
expressions = self._parse_wrapped_options()
909+
else:
910+
expressions = [self._parse_format_name()]
911+
906912
return self.expression(
907-
exp.FileFormatProperty, expressions=self._parse_wrapped_options()
913+
exp.FileFormatProperty,
914+
expressions=expressions,
915+
)
916+
917+
def _parse_credentials_property(self) -> exp.CredentialsProperty:
918+
return self.expression(
919+
exp.CredentialsProperty,
920+
expressions=self._parse_wrapped_options(),
908921
)
909922

910923
class Tokenizer(tokens.Tokenizer):
@@ -1107,8 +1120,9 @@ class Generator(generator.Generator):
11071120

11081121
PROPERTIES_LOCATION = {
11091122
**generator.Generator.PROPERTIES_LOCATION,
1110-
exp.PartitionedByProperty: exp.Properties.Location.POST_SCHEMA,
1123+
exp.CredentialsProperty: exp.Properties.Location.POST_WITH,
11111124
exp.LocationProperty: exp.Properties.Location.POST_WITH,
1125+
exp.PartitionedByProperty: exp.Properties.Location.POST_SCHEMA,
11121126
exp.SetProperty: exp.Properties.Location.UNSUPPORTED,
11131127
exp.VolatileProperty: exp.Properties.Location.UNSUPPORTED,
11141128
}

sqlglot/expressions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2789,6 +2789,10 @@ class FileFormatProperty(Property):
27892789
arg_types = {"this": False, "expressions": False}
27902790

27912791

2792+
class CredentialsProperty(Property):
2793+
arg_types = {"expressions": True}
2794+
2795+
27922796
class FreespaceProperty(Property):
27932797
arg_types = {"this": True, "percent": False}
27942798

@@ -3134,6 +3138,7 @@ class Properties(Expression):
31343138
"CLUSTERED_BY": ClusteredByProperty,
31353139
"COLLATE": CollateProperty,
31363140
"COMMENT": SchemaCommentProperty,
3141+
"CREDENTIALS": CredentialsProperty,
31373142
"DEFINER": DefinerProperty,
31383143
"DISTKEY": DistKeyProperty,
31393144
"DISTRIBUTED_BY": DistributedByProperty,

sqlglot/generator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ class Generator(metaclass=_Generator):
132132
exp.CommentColumnConstraint: lambda self, e: f"COMMENT {self.sql(e, 'this')}",
133133
exp.ConnectByRoot: lambda self, e: f"CONNECT_BY_ROOT {self.sql(e, 'this')}",
134134
exp.CopyGrantsProperty: lambda *_: "COPY GRANTS",
135+
exp.CredentialsProperty: lambda self,
136+
e: f"CREDENTIALS=({self.expressions(e, 'expressions', sep=' ')})",
135137
exp.DateFormatColumnConstraint: lambda self, e: f"FORMAT {self.sql(e, 'this')}",
136138
exp.DefaultColumnConstraint: lambda self, e: f"DEFAULT {self.sql(e, 'this')}",
137139
exp.DynamicProperty: lambda *_: "DYNAMIC",

sqlglot/parser.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7966,17 +7966,19 @@ def _parse_wrapped_options(self) -> t.List[t.Optional[exp.Expression]]:
79667966
self._match(TokenType.L_PAREN)
79677967

79687968
opts: t.List[t.Optional[exp.Expression]] = []
7969+
option: exp.Expression | None
79697970
while self._curr and not self._match(TokenType.R_PAREN):
79707971
if self._match_text_seq("FORMAT_NAME", "="):
79717972
# The FORMAT_NAME can be set to an identifier for Snowflake and T-SQL
7972-
prop = self.expression(
7973-
exp.Property, this=exp.var("FORMAT_NAME"), value=self._parse_table_parts()
7974-
)
7975-
opts.append(prop)
7973+
option = self._parse_format_name()
79767974
else:
7977-
opts.append(self._parse_property())
7975+
option = self._parse_property()
79787976

7979-
self._match(TokenType.COMMA)
7977+
if option is None:
7978+
self.raise_error("Unable to parse option")
7979+
break
7980+
7981+
opts.append(option)
79807982

79817983
return opts
79827984

@@ -8167,3 +8169,12 @@ def _parse_overlay(self) -> exp.Overlay:
81678169
"for": self._match_text_seq("FOR") and self._parse_bitwise(),
81688170
},
81698171
)
8172+
8173+
def _parse_format_name(self) -> exp.Property:
8174+
# Note: Although not specified in the docs, Snowflake does accept a string/identifier
8175+
# for FILE_FORMAT = <format_name>
8176+
return self.expression(
8177+
exp.Property,
8178+
this=exp.var("FORMAT_NAME"),
8179+
value=self._parse_string() or self._parse_table_parts(),
8180+
)

tests/dialects/test_snowflake.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from unittest import mock
22

3-
from sqlglot import UnsupportedError, exp, parse_one
3+
from sqlglot import UnsupportedError, exp, parse_one, ParseError
44
from sqlglot.optimizer.normalize_identifiers import normalize_identifiers
55
from sqlglot.optimizer.qualify_columns import quote_identifiers
66
from tests.dialects.test_dialect import Validator
@@ -1484,6 +1484,21 @@ def test_ddl(self):
14841484
self.validate_identity(
14851485
"CREATE TEMPORARY STAGE stage1 FILE_FORMAT=(TYPE=PARQUET)"
14861486
).this.assert_is(exp.Table)
1487+
self.validate_identity(
1488+
"CREATE STAGE stage1 FILE_FORMAT='format1'",
1489+
"CREATE STAGE stage1 FILE_FORMAT=(FORMAT_NAME='format1')",
1490+
)
1491+
self.validate_identity("CREATE STAGE stage1 FILE_FORMAT=(FORMAT_NAME=stage1.format1)")
1492+
self.validate_identity("CREATE STAGE stage1 FILE_FORMAT=(FORMAT_NAME='stage1.format1')")
1493+
self.validate_identity(
1494+
"CREATE STAGE stage1 FILE_FORMAT=schema1.format1",
1495+
"CREATE STAGE stage1 FILE_FORMAT=(FORMAT_NAME=schema1.format1)",
1496+
)
1497+
with self.assertRaises(ParseError):
1498+
self.parse_one("CREATE STAGE stage1 FILE_FORMAT=123", dialect="snowflake")
1499+
self.validate_identity(
1500+
"CREATE STAGE s1 URL='s3://bucket-123' FILE_FORMAT=(TYPE='JSON') CREDENTIALS=(aws_key_id='test' aws_secret_key='test')"
1501+
)
14871502
self.validate_identity(
14881503
"CREATE OR REPLACE TAG IF NOT EXISTS cost_center COMMENT='cost_center tag'"
14891504
).this.assert_is(exp.Identifier)

0 commit comments

Comments
 (0)