Repository: tobymao/sqlglot (2024-05-02 ~ 2024-05-08)

problem_statement: bug(mssql): incorrect syntax for creating quoted temporary table
```python
[ins] In [1]: import sqlglot as sg

[ins] In [2]: sg.__version__
Out[2]: '23.12.3.dev11'

[ins] In [3]: exp = sg.parse_one(
         ...:     "CREATE TEMPORARY TABLE 'temptest' (name VARCHAR);", dialect="duckdb"
         ...: )

[ins] In [4]: exp
Out[4]: 
Create(
  this=Schema(
    this=Table(
      this=Identifier(this=temptest, quoted=True)),
    expressions=[
      ColumnDef(
        this=Identifier(this=name, quoted=False),
        kind=DataType(this=Type.VARCHAR, nested=False))]),
  kind=TABLE,
  properties=Properties(
    expressions=[
      TemporaryProperty()]))

[ins] In [5]: exp.sql(dialect="tsql")
Out[5]: 'CREATE TABLE #[temptest] (name VARCHAR)'
```

`#[temptest]` is invalid syntax.  I am not sure of how to use the `#` temp indicator with a quoted identifier -- it IS possible to create a temporary table that requires quoting but the only way I have figured out how to do it is by specifying the catalog and database explicitly, e.g. 

```sql
CREATE TABLE tempdb.dbo.[quoted@identifer] ([name] VARCHAR(max))
```

**Official Documentation**
https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql?view=sql-server-ver16#syntax-options
https://learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-ver16#classes-of-identifiers

xref: https://github.com/ibis-project/ibis/issues/9095

date: 2024-05-02
patch: diff --git a/sqlglot/dialects/tsql.py b/sqlglot/dialects/tsql.py
index 54fc43187..2e8ddf21d 100644
--- a/sqlglot/dialects/tsql.py
+++ b/sqlglot/dialects/tsql.py
@@ -913,7 +913,7 @@ class TSQL(Dialect):
                 isinstance(prop, exp.TemporaryProperty)
                 for prop in (properties.expressions if properties else [])
             ):
-                sql = f"#{sql}"
+                sql = f"[#{sql[1:]}" if sql.startswith("[") else f"#{sql}"
 
             return sql
 

test_patch: diff --git a/tests/dialects/test_snowflake.py b/tests/dialects/test_snowflake.py
index 3419539af5..c9150ff3ee 100644
--- a/tests/dialects/test_snowflake.py
+++ b/tests/dialects/test_snowflake.py
@@ -10,14 +10,6 @@ class TestSnowflake(Validator):
     dialect = "snowflake"
 
     def test_snowflake(self):
-        self.validate_identity(
-            "MERGE INTO my_db AS ids USING (SELECT new_id FROM my_model WHERE NOT col IS NULL) AS new_ids ON ids.type = new_ids.type AND ids.source = new_ids.source WHEN NOT MATCHED THEN INSERT VALUES (new_ids.new_id)"
-        )
-        self.validate_identity("ALTER TABLE table1 CLUSTER BY (name DESC)")
-        self.validate_identity(
-            "INSERT OVERWRITE TABLE t SELECT 1", "INSERT OVERWRITE INTO t SELECT 1"
-        )
-        self.validate_identity("SELECT rename, replace")
         expr = parse_one("SELECT APPROX_TOP_K(C4, 3, 5) FROM t")
         expr.selects[0].assert_is(exp.AggFunc)
         self.assertEqual(expr.sql(dialect="snowflake"), "SELECT APPROX_TOP_K(C4, 3, 5) FROM t")
@@ -43,6 +35,9 @@ WHERE
   )""",
         )
 
+        self.validate_identity("ALTER TABLE table1 CLUSTER BY (name DESC)")
+        self.validate_identity("SELECT rename, replace")
+        self.validate_identity("ALTER TABLE table1 SET TAG foo.bar = 'baz'")
         self.validate_identity("SELECT TIMEADD(HOUR, 2, CAST('09:05:03' AS TIME))")
         self.validate_identity("SELECT CAST(OBJECT_CONSTRUCT('a', 1) AS MAP(VARCHAR, INT))")
         self.validate_identity("SELECT CAST(OBJECT_CONSTRUCT('a', 1) AS OBJECT(a CHAR NOT NULL))")
@@ -95,6 +90,12 @@ WHERE
         self.validate_identity("SELECT CONVERT_TIMEZONE('UTC', 'America/Los_Angeles', col)")
         self.validate_identity("ALTER TABLE a SWAP WITH b")
         self.validate_identity("SELECT MATCH_CONDITION")
+        self.validate_identity(
+            "MERGE INTO my_db AS ids USING (SELECT new_id FROM my_model WHERE NOT col IS NULL) AS new_ids ON ids.type = new_ids.type AND ids.source = new_ids.source WHEN NOT MATCHED THEN INSERT VALUES (new_ids.new_id)"
+        )
+        self.validate_identity(
+            "INSERT OVERWRITE TABLE t SELECT 1", "INSERT OVERWRITE INTO t SELECT 1"
+        )
         self.validate_identity(
             'DESCRIBE TABLE "SNOWFLAKE_SAMPLE_DATA"."TPCDS_SF100TCL"."WEB_SITE" type=stage'
         )
@@ -104,9 +105,6 @@ WHERE
         self.validate_identity(
             "SELECT * FROM DATA AS DATA_L ASOF JOIN DATA AS DATA_R MATCH_CONDITION (DATA_L.VAL > DATA_R.VAL) ON DATA_L.ID = DATA_R.ID"
         )
-        self.validate_identity(
-            "COPY INTO mytable (col1, col2) FROM 's3://mybucket/data/files' FILES = ('file1', 'file2') PATTERN = 'pattern' FILE_FORMAT = (FORMAT_NAME = my_csv_format) PARSE_HEADER = TRUE"
-        )
         self.validate_identity(
             "REGEXP_REPLACE('target', 'pattern', '\n')",
             "REGEXP_REPLACE('target', 'pattern', '\\n')",
@@ -834,6 +832,18 @@ WHERE
                 "snowflake": "SELECT LISTAGG(col1, ', ') WITHIN GROUP (ORDER BY col2) FROM t",
             },
         )
+        self.validate_all(
+            "SELECT APPROX_PERCENTILE(a, 0.5) FROM t",
+            read={
+                "trino": "SELECT APPROX_PERCENTILE(a, 1, 0.5, 0.001) FROM t",
+                "presto": "SELECT APPROX_PERCENTILE(a, 1, 0.5, 0.001) FROM t",
+            },
+            write={
+                "trino": "SELECT APPROX_PERCENTILE(a, 0.5) FROM t",
+                "presto": "SELECT APPROX_PERCENTILE(a, 0.5) FROM t",
+                "snowflake": "SELECT APPROX_PERCENTILE(a, 0.5) FROM t",
+            },
+        )
 
     def test_null_treatment(self):
         self.validate_all(
@@ -1213,6 +1223,14 @@ WHERE
             "CREATE OR REPLACE FUNCTION my_udtf(foo BOOLEAN) RETURNS TABLE(col1 ARRAY(INT)) AS $$ WITH t AS (SELECT CAST([1, 2, 3] AS ARRAY(INT)) AS c) SELECT c FROM t $$",
             "CREATE OR REPLACE FUNCTION my_udtf(foo BOOLEAN) RETURNS TABLE (col1 ARRAY(INT)) AS ' WITH t AS (SELECT CAST([1, 2, 3] AS ARRAY(INT)) AS c) SELECT c FROM t '",
         )
+        self.validate_identity(
+            "CREATE SEQUENCE seq1 WITH START=1, INCREMENT=1 ORDER",
+            "CREATE SEQUENCE seq1 START=1 INCREMENT BY 1 ORDER",
+        )
+        self.validate_identity(
+            "CREATE SEQUENCE seq1 WITH START=1 INCREMENT=1 ORDER",
+            "CREATE SEQUENCE seq1 START=1 INCREMENT=1 ORDER",
+        )
 
         self.validate_all(
             "CREATE TABLE orders_clone CLONE orders",
@@ -1438,6 +1456,9 @@ FROM persons AS p, LATERAL FLATTEN(input => p.c, path => 'contact') AS _flattene
         )
 
     def test_values(self):
+        select = exp.select("*").from_("values (map(['a'], [1]))")
+        self.assertEqual(select.sql("snowflake"), "SELECT * FROM (SELECT OBJECT_CONSTRUCT('a', 1))")
+
         self.validate_all(
             'SELECT "c0", "c1" FROM (VALUES (1, 2), (3, 4)) AS "t0"("c0", "c1")',
             read={
@@ -1833,3 +1854,18 @@ STORAGE_ALLOWED_LOCATIONS=('s3://mybucket1/path1/', 's3://mybucket2/path2/')""",
 
         expression = annotate_types(expression)
         self.assertEqual(expression.sql(dialect="snowflake"), "SELECT TRY_CAST(FOO() AS TEXT)")
+
+    def test_copy(self):
+        self.validate_identity(
+            """COPY INTO mytable (col1, col2) FROM 's3://mybucket/data/files' FILES = ('file1', 'file2') PATTERN = 'pattern' file_format = (FORMAT_NAME = my_csv_format NULL_IF = ('str1', 'str2')) PARSE_HEADER = TRUE""",
+            """COPY INTO mytable (col1, col2) FROM 's3://mybucket/data/files' FILES = ('file1', 'file2') PATTERN = 'pattern' FILE_FORMAT = (FORMAT_NAME = my_csv_format NULL_IF = ('str1', 'str2')) PARSE_HEADER = TRUE""",
+        )
+        self.validate_identity(
+            """COPY INTO temp FROM @random_stage/path/ FILE_FORMAT = (TYPE = CSV FIELD_DELIMITER = '|' NULL_IF = () FIELD_OPTIONALLY_ENCLOSED_BY = '"' TIMESTAMP_FORMAT = 'TZHTZM YYYY-MM-DD HH24:MI:SS.FF9' DATE_FORMAT = 'TZHTZM YYYY-MM-DD HH24:MI:SS.FF9' BINARY_FORMAT = BASE64) VALIDATION_MODE = 'RETURN_3_ROWS'"""
+        )
+        self.validate_identity(
+            """COPY INTO load1 FROM @%load1/data1/ FILES = ('test1.csv', 'test2.csv') FORCE = TRUE"""
+        )
+        self.validate_identity(
+            """COPY INTO mytable FROM 'azure://myaccount.blob.core.windows.net/mycontainer/data/files' CREDENTIALS = (AZURE_SAS_TOKEN = 'token') ENCRYPTION = (TYPE = 'AZURE_CSE' MASTER_KEY = 'kPx...') FILE_FORMAT = (FORMAT_NAME = my_csv_format)"""
+        )
diff --git a/tests/dialects/test_tsql.py b/tests/dialects/test_tsql.py
index 5550a40428..1538d47ae3 100644
--- a/tests/dialects/test_tsql.py
+++ b/tests/dialects/test_tsql.py
@@ -780,6 +780,14 @@ class TestTSQL(Validator):
             "CREATE PROCEDURE foo AS BEGIN DELETE FROM bla WHERE foo < CURRENT_TIMESTAMP - 7 END",
             "CREATE PROCEDURE foo AS BEGIN DELETE FROM bla WHERE foo < GETDATE() - 7 END",
         )
+
+        self.validate_all(
+            "CREATE TABLE [#temptest] (name VARCHAR)",
+            read={
+                "duckdb": "CREATE TEMPORARY TABLE 'temptest' (name VARCHAR)",
+                "tsql": "CREATE TABLE [#temptest] (name VARCHAR)",
+            },
+        )
         self.validate_all(
             "CREATE TABLE tbl (id INTEGER IDENTITY PRIMARY KEY)",
             read={


----

problem_statement: APPROX_PERCENTILE is translated from Trino to APPROX_QUANTILE in Snowflake, which does not exist in Snowflake
**Fully reproducible code snippet**
```
>>> sql = "SELECT APPROX_PERCENTILE(a,1, 0.5, 0.001) from t"
>>> select = sqlglot.parse_one(sql, "trino")
>>> select.sql("snowflake")
'SELECT APPROX_QUANTILE(a, 0.5, 0.001, 1) FROM t'
```
It should be translated to `APPROX_PERCENTILE`

**Official Documentation**
https://docs.snowflake.com/en/sql-reference/functions/approx_percentile

date: 2024-05-08
patch: diff --git a/sqlglot/dialects/snowflake.py b/sqlglot/dialects/snowflake.py
index c87f416d9..538cb9cb4 100644
--- a/sqlglot/dialects/snowflake.py
+++ b/sqlglot/dialects/snowflake.py
@@ -345,6 +345,7 @@ class Snowflake(Dialect):
 
         FUNCTIONS = {
             **parser.Parser.FUNCTIONS,
+            "APPROX_PERCENTILE": exp.ApproxQuantile.from_arg_list,
             "ARRAYAGG": exp.ArrayAgg.from_arg_list,
             "ARRAY_CONSTRUCT": exp.Array.from_arg_list,
             "ARRAY_CONTAINS": lambda args: exp.ArrayContains(
@@ -440,7 +441,7 @@ class Snowflake(Dialect):
 
         PROPERTY_PARSERS = {
             **parser.Parser.PROPERTY_PARSERS,
-            "LOCATION": lambda self: self._parse_location(),
+            "LOCATION": lambda self: self._parse_location_property(),
         }
 
         SHOW_PARSERS = {
@@ -676,10 +677,13 @@ class Snowflake(Dialect):
             self._match_text_seq("WITH")
             return self.expression(exp.SwapTable, this=self._parse_table(schema=True))
 
-        def _parse_location(self) -> exp.LocationProperty:
+        def _parse_location_property(self) -> exp.LocationProperty:
             self._match(TokenType.EQ)
             return self.expression(exp.LocationProperty, this=self._parse_location_path())
 
+        def _parse_file_location(self) -> t.Optional[exp.Expression]:
+            return self._parse_table_parts()
+
         def _parse_location_path(self) -> exp.Var:
             parts = [self._advance_any(ignore_reserved=True)]
 
@@ -859,7 +863,10 @@ class Snowflake(Dialect):
         }
 
         UNSUPPORTED_VALUES_EXPRESSIONS = {
+            exp.Map,
+            exp.StarMap,
             exp.Struct,
+            exp.VarMap,
         }
 
         def values_sql(self, expression: exp.Values, values_as_table: bool = True) -> str:
@@ -1037,3 +1044,19 @@ class Snowflake(Dialect):
                     values.append(e)
 
             return self.func("OBJECT_CONSTRUCT", *flatten(zip(keys, values)))
+
+        def copyparameter_sql(self, expression: exp.CopyParameter) -> str:
+            option = self.sql(expression, "this").upper()
+            if option == "FILE_FORMAT":
+                values = self.expressions(expression, key="expression", flat=True, sep=" ")
+                return f"{option} = ({values})"
+
+            return super().copyparameter_sql(expression)
+
+        def approxquantile_sql(self, expression: exp.ApproxQuantile) -> str:
+            if expression.args.get("weight") or expression.args.get("accuracy"):
+                self.unsupported(
+                    "APPROX_PERCENTILE with weight and/or accuracy arguments are not supported in Snowflake"
+                )
+
+            return self.func("APPROX_PERCENTILE", expression.this, expression.args.get("quantile"))


----

problem_statement: Allow comma-separated values for `CREATE SEQUENCE` properties (Snowflake)
Allow comma-separated values for `CREATE SEQUENCE` properties in Snowflake queries.

This query can be parsed successfully (results is an instance of `exp.Create`):
```
    query = "CREATE SEQUENCE seq1 WITH START=1 INCREMENT=1 ORDER"
    result = parse_one(query, dialect="snowflake")
    print(type(result))

    --> <class 'sqlglot.expressions.Create'>
```

... whereas the following query cannot be fully parsed and is returned as a generic `exp.Command` as fallback:
```
    query = "CREATE SEQUENCE seq1 WITH START=1, INCREMENT=1 ORDER"
    result = parse_one(query, dialect="snowflake")
    print(type(result))

    --> <class 'sqlglot.expressions.Command'>
```

(note - the only subtle difference is the comma `,` between the `START` and `INCREMENT` properties)

Note that the second query syntax may not be covered in the Snowflake docs, but it is indeed a valid syntax that can be executed against Snowflake (and some Snowflake client apps/SDKs are generating queries that are using this syntax, unfortunately).
date: 2024-05-08
patch: diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index 9969e95ca..8f5050ee3 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -266,6 +266,7 @@ class Parser(metaclass=_Parser):
         TokenType.UNKNOWN,
         TokenType.NULL,
         TokenType.NAME,
+        TokenType.TDIGEST,
         *ENUM_TYPE_TOKENS,
         *NESTED_TYPE_TOKENS,
         *AGGREGATE_TYPE_TOKENS,
@@ -1470,7 +1471,7 @@ class Parser(metaclass=_Parser):
         if self._match_set(self.STATEMENT_PARSERS):
             return self.STATEMENT_PARSERS[self._prev.token_type](self)
 
-        if self._match_set(Tokenizer.COMMANDS):
+        if self._match_set(self.dialect.tokenizer.COMMANDS):
             return self._parse_command()
 
         expression = self._parse_expression()
@@ -1588,7 +1589,15 @@ class Parser(metaclass=_Parser):
                     if return_:
                         expression = self.expression(exp.Return, this=expression)
         elif create_token.token_type == TokenType.INDEX:
-            this = self._parse_index(index=self._parse_id_var())
+            # Postgres allows anonymous indexes, eg. CREATE INDEX IF NOT EXISTS ON t(c)
+            if not self._match(TokenType.ON):
+                index = self._parse_id_var()
+                anonymous = False
+            else:
+                index = None
+                anonymous = True
+
+            this = self._parse_index(index=index, anonymous=anonymous)
         elif create_token.token_type in self.DB_CREATABLES:
             table_parts = self._parse_table_parts(
                 schema=True, is_db_reference=create_token.token_type == TokenType.SCHEMA
@@ -1669,6 +1678,7 @@ class Parser(metaclass=_Parser):
         index = self._index
 
         while self._curr:
+            self._match(TokenType.COMMA)
             if self._match_text_seq("INCREMENT"):
                 self._match_text_seq("BY")
                 self._match_text_seq("=")
@@ -3020,10 +3030,9 @@ class Parser(metaclass=_Parser):
         )
 
     def _parse_index(
-        self,
-        index: t.Optional[exp.Expression] = None,
+        self, index: t.Optional[exp.Expression] = None, anonymous: bool = False
     ) -> t.Optional[exp.Index]:
-        if index:
+        if index or anonymous:
             unique = None
             primary = None
             amp = None
@@ -4315,7 +4324,9 @@ class Parser(metaclass=_Parser):
 
             this = self._parse_query_modifiers(seq_get(expressions, 0))
 
-            if isinstance(this, exp.UNWRAPPED_QUERIES):
+            if not this and self._match(TokenType.R_PAREN, advance=False):
+                this = self.expression(exp.Tuple)
+            elif isinstance(this, exp.UNWRAPPED_QUERIES):
                 this = self._parse_set_operations(
                     self._parse_subquery(this=this, parse_alias=False)
                 )
@@ -5964,7 +5975,7 @@ class Parser(metaclass=_Parser):
         if kind in ("GLOBAL", "SESSION") and self._match_text_seq("TRANSACTION"):
             return self._parse_set_transaction(global_=kind == "GLOBAL")
 
-        left = self._parse_primary() or self._parse_id_var()
+        left = self._parse_primary() or self._parse_column()
         assignment_delimiter = self._match_texts(("=", "TO"))
 
         if not left or (self.SET_REQUIRES_ASSIGNMENT_DELIMITER and not assignment_delimiter):
@@ -6305,6 +6316,15 @@ class Parser(metaclass=_Parser):
 
         return self.expression(exp.WithOperator, this=this, op=op)
 
+    def _parse_wrapped_options(self) -> t.List[t.Optional[exp.Expression]]:
+        opts = []
+        self._match(TokenType.EQ)
+        self._match(TokenType.L_PAREN)
+        while self._curr and not self._match(TokenType.R_PAREN):
+            opts.append(self._parse_conjunction())
+            self._match(TokenType.COMMA)
+        return opts
+
     def _parse_copy_parameters(self) -> t.List[exp.CopyParameter]:
         sep = TokenType.COMMA if self.dialect.COPY_PARAMS_ARE_CSV else None
 
@@ -6312,12 +6332,19 @@ class Parser(metaclass=_Parser):
         while self._curr and not self._match(TokenType.R_PAREN, advance=False):
             option = self._parse_unquoted_field()
             value = None
+
             # Some options are defined as functions with the values as params
             if not isinstance(option, exp.Func):
+                prev = self._prev.text.upper()
                 # Different dialects might separate options and values by white space, "=" and "AS"
                 self._match(TokenType.EQ)
                 self._match(TokenType.ALIAS)
-                value = self._parse_unquoted_field()
+
+                if prev == "FILE_FORMAT" and self._match(TokenType.L_PAREN):
+                    # Snowflake FILE_FORMAT case
+                    value = self._parse_wrapped_options()
+                else:
+                    value = self._parse_unquoted_field()
 
             param = self.expression(exp.CopyParameter, this=option, expression=value)
             options.append(param)
@@ -6328,24 +6355,18 @@ class Parser(metaclass=_Parser):
         return options
 
     def _parse_credentials(self) -> t.Optional[exp.Credentials]:
-        def parse_options():
-            opts = []
-            self._match(TokenType.EQ)
-            self._match(TokenType.L_PAREN)
-            while self._curr and not self._match(TokenType.R_PAREN):
-                opts.append(self._parse_conjunction())
-            return opts
-
         expr = self.expression(exp.Credentials)
 
         if self._match_text_seq("STORAGE_INTEGRATION", advance=False):
             expr.set("storage", self._parse_conjunction())
         if self._match_text_seq("CREDENTIALS"):
             # Snowflake supports CREDENTIALS = (...), while Redshift CREDENTIALS <string>
-            creds = parse_options() if self._match(TokenType.EQ) else self._parse_field()
+            creds = (
+                self._parse_wrapped_options() if self._match(TokenType.EQ) else self._parse_field()
+            )
             expr.set("credentials", creds)
         if self._match_text_seq("ENCRYPTION"):
-            expr.set("encryption", parse_options())
+            expr.set("encryption", self._parse_wrapped_options())
         if self._match_text_seq("IAM_ROLE"):
             expr.set("iam_role", self._parse_field())
         if self._match_text_seq("REGION"):
@@ -6353,7 +6374,10 @@ class Parser(metaclass=_Parser):
 
         return expr
 
-    def _parse_copy(self):
+    def _parse_file_location(self) -> t.Optional[exp.Expression]:
+        return self._parse_field()
+
+    def _parse_copy(self) -> exp.Copy | exp.Command:
         start = self._prev
 
         self._match(TokenType.INTO)
@@ -6366,7 +6390,7 @@ class Parser(metaclass=_Parser):
 
         kind = self._match(TokenType.FROM) or not self._match_text_seq("TO")
 
-        files = self._parse_csv(self._parse_conjunction)
+        files = self._parse_csv(self._parse_file_location)
         credentials = self._parse_credentials()
 
         self._match_text_seq("WITH")