Repository: tobymao/sqlglot

Issue:
Redshift COPY command not supported
Parsing a COPY command fails. Example:
```sql
copy customer from 's3://mybucket/customer' iam_role 'arn:aws:iam::0123456789012:role/MyRedshiftRole';
```
taken from https://docs.aws.amazon.com/redshift/latest/dg/copy-parameters-data-source-s3.html#copy-parameters-data-source-s3-examples fails with
```
Invalid expression / Unexpected token. Line 1, Col: 15.
```
It would be useful for my use case that it is supported but I don't know whether if SQLGlot is meant to be able to parse non-standard SQL like this.

Discussion:

Patch:
diff --git a/sqlglot/dialects/redshift.py b/sqlglot/dialects/redshift.py
index bf737ea5..cd50979e 100644
--- a/sqlglot/dialects/redshift.py
+++ b/sqlglot/dialects/redshift.py
@@ -18,12 +18,14 @@ class Redshift(Postgres):
 
         KEYWORDS = {
             **Postgres.Tokenizer.KEYWORDS,  # type: ignore
+            "COPY": TokenType.COMMAND,
             "GEOMETRY": TokenType.GEOMETRY,
             "GEOGRAPHY": TokenType.GEOGRAPHY,
             "HLLSKETCH": TokenType.HLLSKETCH,
             "SUPER": TokenType.SUPER,
             "TIME": TokenType.TIMESTAMP,
             "TIMETZ": TokenType.TIMESTAMPTZ,
+            "UNLOAD": TokenType.COMMAND,
             "VARBYTE": TokenType.VARBINARY,
             "SIMILAR TO": TokenType.SIMILAR_TO,
         }
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index be9d8573..0cbe5c35 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -466,7 +466,7 @@ class Parser(metaclass=_Parser):
         TokenType.SCHEMA_COMMENT: lambda self: self._parse_property_assignment(
             exp.SchemaCommentProperty
         ),
-        TokenType.STORED: lambda self: self._parse_stored(),
+        TokenType.STORED: lambda self: self._parse_property_assignment(exp.FileFormatProperty),
         TokenType.DISTKEY: lambda self: self._parse_distkey(),
         TokenType.DISTSTYLE: lambda self: self._parse_property_assignment(exp.DistStyleProperty),
         TokenType.SORTKEY: lambda self: self._parse_sortkey(),
@@ -480,7 +480,7 @@ class Parser(metaclass=_Parser):
         ),
         TokenType.USING: lambda self: self._parse_property_assignment(exp.TableFormatProperty),
         TokenType.LANGUAGE: lambda self: self._parse_property_assignment(exp.LanguageProperty),
-        TokenType.EXECUTE: lambda self: self._parse_execute_as(),
+        TokenType.EXECUTE: lambda self: self._parse_property_assignment(exp.ExecuteAsProperty),
         TokenType.DETERMINISTIC: lambda self: self.expression(
             exp.VolatilityProperty, this=exp.Literal.string("IMMUTABLE")
         ),
@@ -797,7 +797,7 @@ class Parser(metaclass=_Parser):
         )
 
     def _parse_create(self):
-        replace = self._match(TokenType.OR) and self._match(TokenType.REPLACE)
+        replace = self._match_pair(TokenType.OR, TokenType.REPLACE)
         temporary = self._match(TokenType.TEMPORARY)
         transient = self._match(TokenType.TRANSIENT)
         unique = self._match(TokenType.UNIQUE)
@@ -867,6 +867,7 @@ class Parser(metaclass=_Parser):
 
     def _parse_property_assignment(self, exp_class):
         self._match(TokenType.EQ)
+        self._match(TokenType.ALIAS)
         return self.expression(exp_class, this=self._parse_var_or_string() or self._parse_number())
 
     def _parse_partitioned_by(self):
@@ -876,11 +877,6 @@ class Parser(metaclass=_Parser):
             this=self._parse_schema() or self._parse_bracket(self._parse_field()),
         )
 
-    def _parse_stored(self):
-        self._match(TokenType.ALIAS)
-        self._match(TokenType.EQ)
-        return self.expression(exp.FileFormatProperty, this=self._parse_var_or_string())
-
     def _parse_distkey(self):
         return self.expression(exp.DistKeyProperty, this=self._parse_wrapped(self._parse_var))
 
@@ -926,10 +922,6 @@ class Parser(metaclass=_Parser):
 
         return self.expression(exp.ReturnsProperty, this=value, is_table=is_table)
 
-    def _parse_execute_as(self):
-        self._match(TokenType.ALIAS)
-        return self.expression(exp.ExecuteAsProperty, this=self._parse_var())
-
     def _parse_properties(self):
         properties = []
 
diff --git a/sqlglot/tokens.py b/sqlglot/tokens.py
index dda0e012..0eef73a7 100644
--- a/sqlglot/tokens.py
+++ b/sqlglot/tokens.py
@@ -586,8 +586,8 @@ class Tokenizer(metaclass=_Tokenizer):
         "TRAILING": TokenType.TRAILING,
         "UNBOUNDED": TokenType.UNBOUNDED,
         "UNION": TokenType.UNION,
-        "UNPIVOT": TokenType.UNPIVOT,
         "UNNEST": TokenType.UNNEST,
+        "UNPIVOT": TokenType.UNPIVOT,
         "UPDATE": TokenType.UPDATE,
         "USE": TokenType.USE,
         "USING": TokenType.USING,


Test patch:
diff --git a/tests/dialects/test_redshift.py b/tests/dialects/test_redshift.py
index f9dad397..1943ee38 100644
--- a/tests/dialects/test_redshift.py
+++ b/tests/dialects/test_redshift.py
@@ -74,3 +74,9 @@ class TestRedshift(Validator):
         self.validate_identity(
             "CREATE TABLE sales (salesid INTEGER NOT NULL) DISTKEY(listid) COMPOUND SORTKEY(listid, sellerid)"
         )
+        self.validate_identity(
+            "COPY customer FROM 's3://mybucket/customer' IAM_ROLE 'arn:aws:iam::0123456789012:role/MyRedshiftRole'"
+        )
+        self.validate_identity(
+            "UNLOAD ('select * from venue') TO 's3://mybucket/unload/' IAM_ROLE 'arn:aws:iam::0123456789012:role/MyRedshiftRole'"
+        )