Repository: tobymao/sqlglot

Issue:
ClickHouse: fail to parse "WITH <expression> AS <identifier>"
**Description**

`sqlglot` fails to parse `WITH <expression> AS <identifier>`.

**Before you file an issue**

- Make sure you specify the "read" dialect eg. `parse_one(sql, read="spark")` -- Yes, I have specified the "read" dialect.
- Check if the issue still exists on main -- I have cloned and used the latest main branch. But the issue still exists.

**Fully reproducible code snippet**

```python
import sqlglot
from sqlglot.dialects.clickhouse import ClickHouse

query = "WITH 1 AS var SELECT var"
sqlglot.maybe_parse(query, dialect=ClickHouse(), error_level=sqlglot.ErrorLevel.RAISE)
```

**Official Documentation**

Link to [WITH Clause syntax and its example](https://clickhouse.com/docs/en/sql-reference/statements/select/with#syntax) available in the ClickHouse documentation website.


Discussion:
Appreciate the report @herry13, we'll take a look soon.

Resolving patch:
diff --git a/sqlglot/dialects/clickhouse.py b/sqlglot/dialects/clickhouse.py
--- a/sqlglot/dialects/clickhouse.py
+++ b/sqlglot/dialects/clickhouse.py
@@ -15,7 +15,6 @@
     rename_func,
     var_map_sql,
 )
-from sqlglot.errors import ParseError
 from sqlglot.helper import is_int, seq_get
 from sqlglot.tokens import Token, TokenType
 
@@ -386,21 +385,20 @@ def _parse_position(self, haystack_first: bool = False) -> exp.StrPosition:
 
         # https://clickhouse.com/docs/en/sql-reference/statements/select/with/
         def _parse_cte(self) -> exp.CTE:
-            index = self._index
-            try:
-                # WITH <identifier> AS <subquery expression>
-                return super()._parse_cte()
-            except ParseError:
-                # WITH <expression> AS <identifier>
-                self._retreat(index)
+            # WITH <identifier> AS <subquery expression>
+            cte: t.Optional[exp.CTE] = self._try_parse(super()._parse_cte)
 
-                return self.expression(
+            if not cte:
+                # WITH <expression> AS <identifier>
+                cte = self.expression(
                     exp.CTE,
                     this=self._parse_conjunction(),
                     alias=self._parse_table_alias(),
                     scalar=True,
                 )
 
+            return cte
+
         def _parse_join_parts(
             self,
         ) -> t.Tuple[t.Optional[Token], t.Optional[Token], t.Optional[Token]]:
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -15,6 +15,8 @@
     from sqlglot._typing import E, Lit
     from sqlglot.dialects.dialect import Dialect, DialectType
 
+    T = t.TypeVar("T")
+
 logger = logging.getLogger("sqlglot")
 
 OPTIONS_TYPE = t.Dict[str, t.Sequence[t.Union[t.Sequence[str], str]]]
@@ -1055,6 +1057,8 @@ class Parser(metaclass=_Parser):
 
     UNNEST_OFFSET_ALIAS_TOKENS = ID_VAR_TOKENS - SET_OPERATIONS
 
+    SELECT_START_TOKENS = {TokenType.L_PAREN, TokenType.WITH, TokenType.SELECT}
+
     STRICT_CAST = True
 
     PREFIXED_PIVOT_COLUMNS = False
@@ -1346,6 +1350,27 @@ def _parse_command(self) -> exp.Command:
             exp.Command, this=self._prev.text.upper(), expression=self._parse_string()
         )
 
+    def _try_parse(self, parse_method: t.Callable[[], T], retreat: bool = False) -> t.Optional[T]:
+        """
+        Attemps to backtrack if a parse function that contains a try/catch internally raises an error. This behavior can
+        be different depending on the uset-set ErrorLevel, so _try_parse aims to solve this by setting & resetting
+        the parser state accordingly
+        """
+        index = self._index
+        error_level = self.error_level
+
+        self.error_level = ErrorLevel.IMMEDIATE
+        try:
+            this = parse_method()
+        except ParseError:
+            this = None
+        finally:
+            if not this or retreat:
+                self._retreat(index)
+            self.error_level = error_level
+
+        return this
+
     def _parse_comment(self, allow_exists: bool = True) -> exp.Expression:
         start = self._prev
         exists = self._parse_exists() if allow_exists else None
@@ -4430,19 +4455,15 @@ def _parse_lambda(self, alias: bool = False) -> t.Optional[exp.Expression]:
     def _parse_schema(self, this: t.Optional[exp.Expression] = None) -> t.Optional[exp.Expression]:
         index = self._index
 
-        if not self.errors:
-            try:
-                if self._parse_select(nested=True):
-                    return this
-            except ParseError:
-                pass
-            finally:
-                self.errors.clear()
-                self._retreat(index)
-
         if not self._match(TokenType.L_PAREN):
             return this
 
+        # Disambiguate between schema and subquery/CTE, e.g. in INSERT INTO table (<expr>),
+        # expr can be of both types
+        if self._match_set(self.SELECT_START_TOKENS):
+            self._retreat(index)
+            return this
+
         args = self._parse_csv(lambda: self._parse_constraint() or self._parse_field_def())
 
         self._match_r_paren()


Test patch:
diff --git a/tests/dialects/test_clickhouse.py b/tests/dialects/test_clickhouse.py
--- a/tests/dialects/test_clickhouse.py
+++ b/tests/dialects/test_clickhouse.py
@@ -1,5 +1,6 @@
 from sqlglot import exp, parse_one
 from tests.dialects.test_dialect import Validator
+from sqlglot.errors import ErrorLevel
 
 
 class TestClickhouse(Validator):
@@ -417,6 +418,13 @@ def test_cte(self):
         self.assertIsInstance(query.args["with"].expressions[0].this, exp.Subquery)
         self.assertEqual(query.args["with"].expressions[0].alias, "y")
 
+        query = "WITH 1 AS var SELECT var"
+        for error_level in [ErrorLevel.IGNORE, ErrorLevel.RAISE, ErrorLevel.IMMEDIATE]:
+            self.assertEqual(
+                self.parse_one(query, error_level=error_level).sql(dialect=self.dialect),
+                query,
+            )
+
     def test_ternary(self):
         self.validate_all("x ? 1 : 2", write={"clickhouse": "CASE WHEN x THEN 1 ELSE 2 END"})
         self.validate_all(
diff --git a/tests/dialects/test_dialect.py b/tests/dialects/test_dialect.py
--- a/tests/dialects/test_dialect.py
+++ b/tests/dialects/test_dialect.py
@@ -17,8 +17,8 @@
 class Validator(unittest.TestCase):
     dialect = None
 
-    def parse_one(self, sql):
-        return parse_one(sql, read=self.dialect)
+    def parse_one(self, sql, **kwargs):
+        return parse_one(sql, read=self.dialect, **kwargs)
 
     def validate_identity(self, sql, write_sql=None, pretty=False, check_command_warning=False):
         if check_command_warning: