Repository: tobymao/sqlglot

Issue:
`WITH foo AS MATERIALIZED (` fails in Postgres
This is a valid Postgres query:

```sql
with data as materialized (
select a.n/100 as x, 10*a.n/30 as y from generate_series(1, 1000) as a(n)
) select * from data
```

But it fails to parse because of `materialized`:

```python
from sqlglot import parse_one  # using 23.0.1

p = parse_one(
    """
with data as materialized (
select a.n/100 as x, 10*a.n/30 as y from generate_series(1, 1000) as a(n)
) select * from data
""",
    dialect="postgres",
)
```

```bash
$ python test.py
Traceback (most recent call last):
  File "/Users/beto/Projects/github/superset/test.py", line 3, in <module>
    p = parse_one(
  File "/Users/beto/.pyenv/versions/superset-3.9.2/lib/python3.9/site-packages/sqlglot/__init__.py", line 124, in parse_one
    result = dialect.parse(sql, **opts)
  File "/Users/beto/.pyenv/versions/superset-3.9.2/lib/python3.9/site-packages/sqlglot/dialects/dialect.py", line 490, in parse
    return self.parser(**opts).parse(self.tokenize(sql), sql)
  File "/Users/beto/.pyenv/versions/superset-3.9.2/lib/python3.9/site-packages/sqlglot/parser.py", line 1153, in parse
    return self._parse(
  File "/Users/beto/.pyenv/versions/superset-3.9.2/lib/python3.9/site-packages/sqlglot/parser.py", line 1219, in _parse
    expressions.append(parse_method(self))
  File "/Users/beto/.pyenv/versions/superset-3.9.2/lib/python3.9/site-packages/sqlglot/parser.py", line 1427, in _parse_statement
    expression = self._parse_set_operations(expression) if expression else self._parse_select()
  File "/Users/beto/.pyenv/versions/superset-3.9.2/lib/python3.9/site-packages/sqlglot/parser.py", line 2426, in _parse_select
    cte = self._parse_with()
  File "/Users/beto/.pyenv/versions/superset-3.9.2/lib/python3.9/site-packages/sqlglot/parser.py", line 2532, in _parse_with
    expressions.append(self._parse_cte())
  File "/Users/beto/.pyenv/versions/superset-3.9.2/lib/python3.9/site-packages/sqlglot/parser.py", line 2550, in _parse_cte
    exp.CTE, this=self._parse_wrapped(self._parse_statement), alias=alias
  File "/Users/beto/.pyenv/versions/superset-3.9.2/lib/python3.9/site-packages/sqlglot/parser.py", line 5548, in _parse_wrapped
    self.raise_error("Expecting (")
  File "/Users/beto/.pyenv/versions/superset-3.9.2/lib/python3.9/site-packages/sqlglot/parser.py", line 1263, in raise_error
    raise error
sqlglot.errors.ParseError: Expecting (. Line 2, Col: 25.

with data as materialized (
select a.n/100 as x, 10*a.n/30 as y from generate_series(1, 1000) as a(n)
) select * from data
```


Discussion:

Resolving patch:
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -1404,7 +1404,12 @@ class WithinGroup(Expression):
 # clickhouse supports scalar ctes
 # https://clickhouse.com/docs/en/sql-reference/statements/select/with
 class CTE(DerivedTable):
-    arg_types = {"this": True, "alias": True, "scalar": False}
+    arg_types = {
+        "this": True,
+        "alias": True,
+        "scalar": False,
+        "materialized": False,
+    }
 
 
 class TableAlias(Expression):
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -1049,7 +1049,14 @@ def with_sql(self, expression: exp.With) -> str:
 
     def cte_sql(self, expression: exp.CTE) -> str:
         alias = self.sql(expression, "alias")
-        return f"{alias} AS {self.wrap(expression)}"
+
+        materialized = expression.args.get("materialized")
+        if materialized is False:
+            materialized = "NOT MATERIALIZED "
+        elif materialized:
+            materialized = "MATERIALIZED "
+
+        return f"{alias} AS {materialized or ''}{self.wrap(expression)}"
 
     def tablealias_sql(self, expression: exp.TableAlias) -> str:
         alias = self.sql(expression, "this")
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -2546,8 +2546,19 @@ def _parse_cte(self) -> exp.CTE:
             self.raise_error("Expected CTE to have alias")
 
         self._match(TokenType.ALIAS)
+
+        if self._match_text_seq("NOT", "MATERIALIZED"):
+            materialized = False
+        elif self._match_text_seq("MATERIALIZED"):
+            materialized = True
+        else:
+            materialized = None
+
         return self.expression(
-            exp.CTE, this=self._parse_wrapped(self._parse_statement), alias=alias
+            exp.CTE,
+            this=self._parse_wrapped(self._parse_statement),
+            alias=alias,
+            materialized=materialized,
         )
 
     def _parse_table_alias(


Test patch:
diff --git a/tests/dialects/test_postgres.py b/tests/dialects/test_postgres.py
--- a/tests/dialects/test_postgres.py
+++ b/tests/dialects/test_postgres.py
@@ -40,13 +40,6 @@ def test_postgres(self):
         self.validate_identity("CAST(x AS DATEMULTIRANGE)")
         self.validate_identity("SELECT ARRAY[1, 2, 3] @> ARRAY[1, 2]")
         self.validate_identity("SELECT ARRAY[1, 2, 3] <@ ARRAY[1, 2]")
-        self.validate_all(
-            "SELECT ARRAY[1, 2, 3] && ARRAY[1, 2]",
-            write={
-                "": "SELECT ARRAY_OVERLAPS(ARRAY(1, 2, 3), ARRAY(1, 2))",
-                "postgres": "SELECT ARRAY[1, 2, 3] && ARRAY[1, 2]",
-            },
-        )
         self.validate_identity("x$")
         self.validate_identity("SELECT ARRAY[1, 2, 3]")
         self.validate_identity("SELECT ARRAY(SELECT 1)")
@@ -70,6 +63,9 @@ def test_postgres(self):
         self.validate_identity("EXEC AS myfunc @id = 123", check_command_warning=True)
         self.validate_identity("SELECT CURRENT_USER")
         self.validate_identity("SELECT * FROM ONLY t1")
+        self.validate_identity(
+            "WITH t1 AS MATERIALIZED (SELECT 1), t2 AS NOT MATERIALIZED (SELECT 2) SELECT * FROM t1, t2"
+        )
         self.validate_identity(
             """LAST_VALUE("col1") OVER (ORDER BY "col2" RANGE BETWEEN INTERVAL '1 DAY' PRECEDING AND '1 month' FOLLOWING)"""
         )
@@ -310,6 +306,13 @@ def test_postgres(self):
         )
         self.validate_identity("SELECT * FROM t1*", "SELECT * FROM t1")
 
+        self.validate_all(
+            "SELECT ARRAY[1, 2, 3] && ARRAY[1, 2]",
+            write={
+                "": "SELECT ARRAY_OVERLAPS(ARRAY(1, 2, 3), ARRAY(1, 2))",
+                "postgres": "SELECT ARRAY[1, 2, 3] && ARRAY[1, 2]",
+            },
+        )
         self.validate_all(
             "SELECT JSON_EXTRACT_PATH_TEXT(x, k1, k2, k3) FROM t",
             read={