Repository: tobymao/sqlglot

Issue:
_parse_column_def adds wrong type of constraint to ColumnDef.constraints list
Hello!

Definition of `exp.ColumnDef` and `exp.ColumnConstraint` for reference and further discussion: 
```
class ColumnDef(Expression):
    arg_types = {
        "this": True,
        "kind": False,
        "constraints": False,
        "exists": False,
        "position": False,
    }

    @property
    def constraints(self) -> t.List[ColumnConstraint]:
        return self.args.get("constraints") or []

    @property
    def kind(self) -> t.Optional[DataType]:
        return self.args.get("kind")
```
```
class ColumnConstraint(Expression):
    arg_types = {"this": False, "kind": True}

    @property
    def kind(self) -> ColumnConstraintKind:
        return self.args["kind"]
```

As you can see `ColumnDef.constraints` should provide `t.List[ColumnConstraint]`, but in `sqlglot.parser.Parser._parse_column_def` both `exp.ComputedColumnConstraint` and `exp.TransformColumnConstraint` are immediately added to the list of constraints on `ColumnDef` level. Issue is that both types I mentioned extend `ColumnConstraintKind` not `ColumnConstraint` class.

As a result unpredictable behavior for the user (error) is noticed while running code from below:
```
from sqlglot import parse_one, exp


if __name__ == '__main__':

    def __check(expression: exp.Expression) -> None:
        for column_def in expression.find_all(exp.ColumnDef):
            for column_constraint in column_def.constraints:
                print(column_constraint.kind)

    __check(parse_one("CREATE TABLE foo (bar INT NOT NULL);", dialect='snowflake'))
    # Above works just fine printing 'NOT NULL'
    
    __check(parse_one("CREATE TABLE foo (bar INT AS (value:bar::INT));", dialect='snowflake'))
    # Above results in error:
    #
    #    print(column_constraint.kind)
    #    ^^^^^^^^^^^^^^^^^^^^^^
    #AttributeError: 'TransformColumnConstraint' object has no attribute 'kind'. Did you mean: 'find'?
```

I would expect that instead 'kind' would be printed. Took me some time to figure it out, but when I made a mistake somewhere in my assumptions and understanding, please let me know how that could be addressed. I noticed that when I was parsing 'NOT NULL' constraint like that:

```
def is_not_null_constraint(constraint: exp.ColumnConstraint) -> bool:
    return isinstance(constraint.kind, exp.NotNullColumnConstraint) and 'allow_null' not in constraint.kind.args
```

Discussion:

Solution patch:
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index 218dc79614..a15dd8155e 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -5359,18 +5359,19 @@ def _parse_column_def(self, this: t.Optional[exp.Expression]) -> t.Optional[exp.
             ("ALIAS", "MATERIALIZED")
         ):
             persisted = self._prev.text.upper() == "MATERIALIZED"
-            constraints.append(
-                self.expression(
-                    exp.ComputedColumnConstraint,
-                    this=self._parse_assignment(),
-                    persisted=persisted or self._match_text_seq("PERSISTED"),
-                    not_null=self._match_pair(TokenType.NOT, TokenType.NULL),
-                )
+            constraint_kind = exp.ComputedColumnConstraint(
+                this=self._parse_assignment(),
+                persisted=persisted or self._match_text_seq("PERSISTED"),
+                not_null=self._match_pair(TokenType.NOT, TokenType.NULL),
             )
+            constraints.append(self.expression(exp.ColumnConstraint, kind=constraint_kind))
         elif kind and self._match_pair(TokenType.ALIAS, TokenType.L_PAREN, advance=False):
             self._match(TokenType.ALIAS)
             constraints.append(
-                self.expression(exp.TransformColumnConstraint, this=self._parse_field())
+                self.expression(
+                    exp.ColumnConstraint,
+                    kind=exp.TransformColumnConstraint(this=self._parse_field()),
+                )
             )
 
         while True:


Test patch:
diff --git a/tests/dialects/test_clickhouse.py b/tests/dialects/test_clickhouse.py
index 2cd272dfe3..cd58132649 100644
--- a/tests/dialects/test_clickhouse.py
+++ b/tests/dialects/test_clickhouse.py
@@ -1016,6 +1016,12 @@ def test_ddl(self):
             pretty=True,
         )
 
+        self.assertIsNotNone(
+            self.validate_identity("CREATE TABLE t1 (a String MATERIALIZED func())").find(
+                exp.ColumnConstraint
+            )
+        )
+
     def test_agg_functions(self):
         def extract_agg_func(query):
             return parse_one(query, read="clickhouse").selects[0].this
diff --git a/tests/dialects/test_snowflake.py b/tests/dialects/test_snowflake.py
index 49f1a4ff73..3e0d600a8d 100644
--- a/tests/dialects/test_snowflake.py
+++ b/tests/dialects/test_snowflake.py
@@ -1423,6 +1423,12 @@ def test_ddl(self):
                     },
                 )
 
+        self.assertIsNotNone(
+            self.validate_identity("CREATE TABLE foo (bar INT AS (foo))").find(
+                exp.TransformColumnConstraint
+            )
+        )
+
     def test_user_defined_functions(self):
         self.validate_all(
             "CREATE FUNCTION a(x DATE, y BIGINT) RETURNS ARRAY LANGUAGE JAVASCRIPT AS $$ SELECT 1 $$",