diff --git a/sqlglot/dialects/snowflake.py b/sqlglot/dialects/snowflake.py
index 1d2b246e5d..350f7773b5 100644
--- a/sqlglot/dialects/snowflake.py
+++ b/sqlglot/dialects/snowflake.py
@@ -465,6 +465,12 @@ class Parser(parser.Parser):
         PROPERTY_PARSERS = {
             **parser.Parser.PROPERTY_PARSERS,
             "LOCATION": lambda self: self._parse_location_property(),
+            "TAG": lambda self: self._parse_dict_property(
+                this="TAG",
+                has_kind=False,
+                separator=(TokenType.COMMA, ","),
+                delimiter=(TokenType.EQ, "="),
+            ),
         }

         TYPE_CONVERTERS = {
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index a69e8362f9..de88fef6b8 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -2778,10 +2778,11 @@ class ClusteredByProperty(Property):

 class DictProperty(Property):
-    arg_types = {"this": True, "kind": True, "settings": False}
+    arg_types = {"this": True, "kind": False, "settings": False, "separator": False}

 class DictSubProperty(Property):
+    arg_types = {"this": True, "value": True, "delimiter": False}
     pass

diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index ae9dc35e49..c20e5ed222 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -3720,9 +3720,16 @@ def tonumber_sql(self, expression: exp.ToNumber) -> str:
     def dictproperty_sql(self, expression: exp.DictProperty) -> str:
         this = self.sql(expression, "this")
         kind = self.sql(expression, "kind")
-        settings_sql = self.expressions(expression, key="settings", sep=" ")
-        args = f"({self.sep('')}{settings_sql}{self.seg(')', sep='')}" if settings_sql else "()"
-        return f"{this}({kind}{args})"
+        separator = self.sql(expression, "separator")
+        settings_sql = self.expressions(expression, key="settings", sep=f"{separator} ")
+        if kind:
+            settings_section = (
+                f"({self.sep('')}{settings_sql}{self.seg(')', sep='')}" if settings_sql else "()"
+            )
+            args = f"{kind}{settings_section}"
+        else:
+            args = f"{self.sep('')}{settings_sql}{self.sep(sep='')}"
+        return f"{this} ({args})"

     def dictrange_sql(self, expression: exp.DictRange) -> str:
         this = self.sql(expression, "this")
@@ -3731,7 +3738,8 @@ def dictrange_sql(self, expression: exp.DictRange) -> str:
         return f"{this}(MIN {min} MAX {max})"

     def dictsubproperty_sql(self, expression: exp.DictSubProperty) -> str:
-        return f"{self.sql(expression, 'this')} {self.sql(expression, 'value')}"
+        delimiter = self.sql(expression, "delimiter") or " "
+        return f"{self.sql(expression, 'this')}{delimiter}{self.sql(expression, 'value')}"

     def duplicatekeyproperty_sql(self, expression: exp.DuplicateKeyProperty) -> str:
         return f"DUPLICATE KEY ({self.expressions(expression, flat=True)})"
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index cbbeffe5e1..860425825c 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -936,7 +936,7 @@ class Parser(metaclass=_Parser):
         "INPUT": lambda self: self.expression(exp.InputModelProperty, this=self._parse_schema()),
         "JOURNAL": lambda self, **kwargs: self._parse_journal(**kwargs),
         "LANGUAGE": lambda self: self._parse_property_assignment(exp.LanguageProperty),
-        "LAYOUT": lambda self: self._parse_dict_property(this="LAYOUT"),
+        "LAYOUT": lambda self: self._parse_dict_property(this="LAYOUT", has_kind=True),
         "LIFETIME": lambda self: self._parse_dict_range(this="LIFETIME"),
         "LIKE": lambda self: self._parse_create_like(),
         "LOCATION": lambda self: self._parse_property_assignment(exp.LocationProperty),
@@ -973,7 +973,7 @@ class Parser(metaclass=_Parser):
         "SETTINGS": lambda self: self._parse_settings_property(),
         "SHARING": lambda self: self._parse_property_assignment(exp.SharingProperty),
         "SORTKEY": lambda self: self._parse_sortkey(),
-        "SOURCE": lambda self: self._parse_dict_property(this="SOURCE"),
+        "SOURCE": lambda self: self._parse_dict_property(this="SOURCE", has_kind=True),
         "STABLE": lambda self: self.expression(
             exp.StabilityProperty, this=exp.Literal.string("STABLE")
         ),
@@ -7108,21 +7108,39 @@ def _parse_as_command(self, start: Token) -> exp.Command:
         self._warn_unsupported()
         return exp.Command(this=text[:size], expression=text[size:])

-    def _parse_dict_property(self, this: str) -> exp.DictProperty:
+    def _parse_dict_property(
+        self,
+        this: str,
+        has_kind: bool,
+        separator: t.Optional[t.Tuple[TokenType, str]] = None,
+        delimiter: t.Optional[t.Tuple[TokenType, str]] = None,
+    ) -> exp.DictProperty:
+        """
+        Parses a dictionary property, which is a key-value pair enclosed in parentheses.
+
+        Args:
+            this: The name of the property.
+            has_kind: Whether the property is labeled, e.g. is of the form `PROPERTY (KIND (k1=v1, k2=v2, ...))`.
+            separator: The separator token between key-value pairs, and its string representation.
+            delimiter: The delimiter token between key and value, and its string representation.
+        """
+        separator_token = None
+        separator_str = None
+        if separator:
+            separator_token, separator_str = separator
+
         settings = []

         self._match_l_paren()
-        kind = self._parse_id_var()

-        if self._match(TokenType.L_PAREN):
-            while True:
-                key = self._parse_id_var()
-                value = self._parse_primary()
-
-                if not key and value is None:
-                    break
-                settings.append(self.expression(exp.DictSubProperty, this=key, value=value))
-            self._match(TokenType.R_PAREN)
+        if has_kind:
+            kind = self._parse_id_var()
+            if self._match(TokenType.L_PAREN):
+                settings = self._parse_key_value_list(this, separator_token, delimiter)
+                self._match(TokenType.R_PAREN)
+        else:
+            kind = None
+            settings = self._parse_key_value_list(this, separator_token, delimiter)

         self._match_r_paren()

@@ -7131,8 +7149,39 @@ def _parse_dict_property(self, this: str) -> exp.DictProperty:
             this=this,
             kind=kind.this if kind else None,
             settings=settings,
+            separator=separator_str,
         )

+    def _parse_key_value_list(
+        self,
+        this: str,
+        separator_token: t.Optional[TokenType],
+        delimiter: t.Optional[t.Tuple[TokenType, str]],
+    ) -> t.List[exp.DictSubProperty]:
+        delimiter_token = None
+        delimiter_str = None
+        if delimiter:
+            delimiter_token, delimiter_str = delimiter
+
+        lst = []
+
+        while True:
+            key = self._parse_id_var()
+            if delimiter_token:
+                self._match(delimiter_token)
+            value = self._parse_primary()
+
+            if not key and value is None:
+                break
+            lst.append(
+                self.expression(exp.DictSubProperty, this=key, value=value, delimiter=delimiter_str)
+            )
+
+            if separator_token:
+                self._match(separator_token)
+
+        return lst
+
     def _parse_dict_range(self, this: str) -> exp.DictRange:
         self._match_l_paren()
         has_min = self._match_text_seq("MIN")

diff --git a/tests/dialects/test_clickhouse.py b/tests/dialects/test_clickhouse.py
index 19b3ce3934..65b12bbd58 100644
--- a/tests/dialects/test_clickhouse.py
+++ b/tests/dialects/test_clickhouse.py
@@ -974,11 +974,11 @@ def test_ddl(self):
   amount Float64
 )
 PRIMARY KEY (id)
-SOURCE(CLICKHOUSE(
+SOURCE (CLICKHOUSE(
   TABLE 'discounts'
 ))
 LIFETIME(MIN 1 MAX 1000)
-LAYOUT(RANGE_HASHED(
+LAYOUT (RANGE_HASHED(
   range_lookup_strategy 'max'
 ))
 RANGE(MIN discount_start_date MAX discount_end_date)""",
@@ -1004,10 +1004,10 @@ def test_ddl(self):
   cca2 String DEFAULT '??'
 )
 PRIMARY KEY (prefix)
-SOURCE(CLICKHOUSE(
+SOURCE (CLICKHOUSE(
   TABLE 'my_ip_addresses'
 ))
-LAYOUT(IP_TRIE())
+LAYOUT (IP_TRIE())
 LIFETIME(MIN 0 MAX 3600)""",
             },
             pretty=True,
@@ -1030,10 +1030,10 @@ def test_ddl(self):
   name String
 )
 PRIMARY KEY (key)
-SOURCE(CLICKHOUSE(
+SOURCE (CLICKHOUSE(
   TABLE 'polygons_test_table'
 ))
-LAYOUT(POLYGON(
+LAYOUT (POLYGON(
   STORE_POLYGON_KEY_COLUMN 1
 ))
 LIFETIME(MIN 0 MAX 0)""",
diff --git a/tests/dialects/test_snowflake.py b/tests/dialects/test_snowflake.py
index 157947df46..8058bcf6de 100644
--- a/tests/dialects/test_snowflake.py
+++ b/tests/dialects/test_snowflake.py
@@ -1479,13 +1479,20 @@ def test_ddl(self):
                 "snowflake": "CREATE OR REPLACE TRANSIENT TABLE a (id INT)",
             },
         )
-
         self.validate_all(
             "CREATE TABLE a (b INT)",
             read={"teradata": "CREATE MULTISET TABLE a (b INT)"},
             write={"snowflake": "CREATE TABLE a (b INT)"},
         )

+        self.validate_identity("CREATE TABLE a TAG (key1='value_1', key2='value_2')")
+        self.validate_all(
+            "CREATE TABLE a TAG (key1='value_1')",
+            read={
+                "snowflake": "CREATE TABLE a WITH TAG (key1='value_1')",
+            },
+        )
+
         for action in ("SET", "DROP"):
             with self.subTest(f"ALTER COLUMN {action} NOT NULL"):
                 self.validate_all(

Support Snowflake WITH TAG syntax
**Is your feature request related to a problem? Please describe.**
This library does not support snowflake's WITH TAG syntax for setting tags while creating an object.

For example,
```
sqlglot.parse_one("CREATE VIEW my_view WITH TAG (my_tag = 'tag')", dialect="snowflake")
'CREATE VIEW my_view WITH TAG (my_tag = 'tag')' contains unsupported syntax. Falling back to parsing as a 'Command'.
> Command(this=CREATE, expression=VIEW my_view WITH TAG (my_tag = 'tag'))
```
Note that multiple tags can be specified in the same statement.

**Describe the solution you'd like**
Parse the above statement into something like:
```
Create(
  this=Table(
    this=Identifier(this=my_view, quoted=False)),
  kind=VIEW,
  actions=[tag=[
        Paren(
          this=EQ(
            this=Column(
              this=Identifier(this=my_tag, quoted=False)),
            expression=Literal(this=tag, is_string=True)))])
  ]
)
```
This was pulled from an `ALTER VIEW ... SET TAG` statement, idk if this is actually the correct Create object. My primary desire is to avoid the Command parsing fallback.

**Additional context**
Snowflake CREATE TABLE reference: https://docs.snowflake.com/en/sql-reference/sql/create-table
Example CREATE WAREHOUSE statement: https://docs.snowflake.com/en/user-guide/object-tagging

There are similar `WITH ...` syntaxes in snowflake, e.g. `WITH ROW ACCESS POLICY` and `WITH AGGREGATION POLICY`, or `WITH MASKING POLICY` and `WITH PROJECTION POLICY` for columns. This feature request is not specifically asking for support for that syntax.

This is low priority for us right now, but we'll be happy to accept a well-crafted & tested PR. Shouldn't be too hard to support.


diff --git a/sqlglot/dialects/dialect.py b/sqlglot/dialects/dialect.py
index 38a3723f24..2fbd4e6864 100644
--- a/sqlglot/dialects/dialect.py
+++ b/sqlglot/dialects/dialect.py
@@ -397,6 +397,13 @@ class Dialect(metaclass=_Dialect):
     ARRAY_AGG_INCLUDES_NULLS: t.Optional[bool] = True
     """Whether ArrayAgg needs to filter NULL values."""

+    PROMOTE_TO_INFERRED_DATETIME_TYPE = False
+    """
+    This flag is used in the optimizer's canonicalize rule and determines whether x will be promoted
+    to the literal's type in x::DATE < '2020-01-01 12:05:03' (i.e., DATETIME). When false, the literal
+    is cast to x's type to match it instead.
+    """
+
     REGEXP_EXTRACT_DEFAULT_GROUP = 0
     """The default value for the capturing group."""

diff --git a/sqlglot/dialects/mysql.py b/sqlglot/dialects/mysql.py
index 1404f0fc32..ce266d5c11 100644
--- a/sqlglot/dialects/mysql.py
+++ b/sqlglot/dialects/mysql.py
@@ -145,6 +145,8 @@ def func(self: MySQL.Generator, expression: exp.Func) -> str:

 class MySQL(Dialect):
+    PROMOTE_TO_INFERRED_DATETIME_TYPE = True
+
     # https://dev.mysql.com/doc/refman/8.0/en/identifiers.html
     IDENTIFIERS_CAN_START_WITH_DIGIT = True

diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index a69e8362f9..3f771bc98d 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -7847,6 +7847,7 @@ def cast(
         types_are_equivalent = type_mapping.get(
             existing_cast_type, existing_cast_type.value
         ) == type_mapping.get(new_cast_type, new_cast_type.value)
+
         if expr.is_type(data_type) or types_are_equivalent:
             return expr

diff --git a/sqlglot/optimizer/canonicalize.py b/sqlglot/optimizer/canonicalize.py
index fd0026228b..b8a399505b 100644
--- a/sqlglot/optimizer/canonicalize.py
+++ b/sqlglot/optimizer/canonicalize.py
@@ -4,10 +4,12 @@
 import typing as t

 from sqlglot import exp
+from sqlglot.dialects.dialect import Dialect, DialectType
 from sqlglot.helper import is_date_unit, is_iso_date, is_iso_datetime
+from sqlglot.optimizer.annotate_types import TypeAnnotator

-def canonicalize(expression: exp.Expression) -> exp.Expression:
+def canonicalize(expression: exp.Expression, dialect: DialectType = None) -> exp.Expression:
     """Converts a sql expression into a standard form.

     This method relies on annotate_types because many of the
@@ -17,10 +19,12 @@ def canonicalize(expression: exp.Expression) -> exp.Expression:
         expression: The expression to canonicalize.
     """

+    dialect = Dialect.get_or_raise(dialect)
+
     def _canonicalize(expression: exp.Expression) -> exp.Expression:
         expression = add_text_to_concat(expression)
         expression = replace_date_funcs(expression)
-        expression = coerce_type(expression)
+        expression = coerce_type(expression, dialect.PROMOTE_TO_INFERRED_DATETIME_TYPE)
         expression = remove_redundant_casts(expression)
         expression = ensure_bools(expression, _replace_int_predicate)
         expression = remove_ascending_order(expression)
@@ -68,11 +72,11 @@ def replace_date_funcs(node: exp.Expression) -> exp.Expression:
 )

-def coerce_type(node: exp.Expression) -> exp.Expression:
+def coerce_type(node: exp.Expression, promote_to_inferred_datetime_type: bool) -> exp.Expression:
     if isinstance(node, COERCIBLE_DATE_OPS):
-        _coerce_date(node.left, node.right)
+        _coerce_date(node.left, node.right, promote_to_inferred_datetime_type)
     elif isinstance(node, exp.Between):
-        _coerce_date(node.this, node.args["low"])
+        _coerce_date(node.this, node.args["low"], promote_to_inferred_datetime_type)
     elif isinstance(node, exp.Extract) and not node.expression.type.is_type(
         *exp.DataType.TEMPORAL_TYPES
     ):
@@ -128,17 +132,48 @@ def remove_ascending_order(expression: exp.Expression) -> exp.Expression:
     return expression

-def _coerce_date(a: exp.Expression, b: exp.Expression) -> None:
+def _coerce_date(
+    a: exp.Expression,
+    b: exp.Expression,
+    promote_to_inferred_datetime_type: bool,
+) -> None:
     for a, b in itertools.permutations([a, b]):
         if isinstance(b, exp.Interval):
             a = _coerce_timeunit_arg(a, b.unit)
+
+        a_type = a.type
         if (
-            a.type
-            and a.type.this in exp.DataType.TEMPORAL_TYPES
-            and b.type
-            and b.type.this in exp.DataType.TEXT_TYPES
+            not a_type
+            or a_type.this not in exp.DataType.TEMPORAL_TYPES
+            or not b.type
+            or b.type.this not in exp.DataType.TEXT_TYPES
         ):
-            _replace_cast(b, exp.DataType.Type.DATETIME)
+            continue
+
+        if promote_to_inferred_datetime_type:
+            if b.is_string:
+                date_text = b.name
+                if is_iso_date(date_text):
+                    b_type = exp.DataType.Type.DATE
+                elif is_iso_datetime(date_text):
+                    b_type = exp.DataType.Type.DATETIME
+                else:
+                    b_type = a_type.this
+            else:
+                # If b is not a datetime string, we conservatively promote it to a DATETIME,
+                # in order to ensure there are no surprising truncations due to downcasting
+                b_type = exp.DataType.Type.DATETIME
+
+            target_type = (
+                b_type if b_type in TypeAnnotator.COERCES_TO.get(a_type.this, {}) else a_type
+            )
+        else:
+            target_type = a_type
+
+        if target_type != a_type:
+            _replace_cast(a, target_type)
+
+        _replace_cast(b, target_type)

 def _coerce_timeunit_arg(arg: exp.Expression, unit: t.Optional[exp.Expression]) -> exp.Expression:
@@ -168,7 +203,7 @@ def _coerce_datediff_args(node: exp.DateDiff) -> None:
             e.replace(exp.cast(e.copy(), to=exp.DataType.Type.DATETIME))

-def _replace_cast(node: exp.Expression, to: exp.DataType.Type) -> None:
+def _replace_cast(node: exp.Expression, to: exp.DATA_TYPE) -> None:
     node.replace(exp.cast(node.copy(), to=to))

diff --git a/tests/fixtures/optimizer/canonicalize.sql b/tests/fixtures/optimizer/canonicalize.sql
index 66c6c95c93..3610f1628d 100644
--- a/tests/fixtures/optimizer/canonicalize.sql
+++ b/tests/fixtures/optimizer/canonicalize.sql
@@ -2,7 +2,7 @@ SELECT w.d + w.e AS c FROM w AS w;
 SELECT CONCAT("w"."d", "w"."e") AS "c" FROM "w" AS "w";

 SELECT CAST(w.d AS DATE) > w.e AS a FROM w AS w;
-SELECT CAST("w"."d" AS DATE) > CAST("w"."e" AS DATETIME) AS "a" FROM "w" AS "w";
+SELECT CAST("w"."d" AS DATE) > CAST("w"."e" AS DATE) AS "a" FROM "w" AS "w";

 SELECT CAST(1 AS VARCHAR) AS a FROM w AS w;
 SELECT CAST(1 AS VARCHAR) AS "a" FROM "w" AS "w";
@@ -102,7 +102,7 @@ DATEDIFF('2023-01-01', '2023-01-02', DAY);
 DATEDIFF(CAST('2023-01-01' AS DATETIME), CAST('2023-01-02' AS DATETIME), DAY);

 SELECT "t"."d" > '2023-01-01' AS "d" FROM "temporal" AS "t";
-SELECT "t"."d" > CAST('2023-01-01' AS DATETIME) AS "d" FROM "temporal" AS "t";
+SELECT "t"."d" > CAST('2023-01-01' AS DATE) AS "d" FROM "temporal" AS "t";

 SELECT "t"."d" > CAST('2023-01-01' AS DATETIME) AS "d" FROM "temporal" AS "t";
 SELECT "t"."d" > CAST('2023-01-01' AS DATETIME) AS "d" FROM "temporal" AS "t";
@@ -110,6 +110,17 @@ SELECT "t"."d" > CAST('2023-01-01' AS DATETIME) AS "d" FROM "temporal" AS "t";
 SELECT "t"."t" > '2023-01-01 00:00:01' AS "t" FROM "temporal" AS "t";
 SELECT "t"."t" > CAST('2023-01-01 00:00:01' AS DATETIME) AS "t" FROM "temporal" AS "t";

+WITH "t" AS (SELECT CAST("ext"."created_at" AS TIMESTAMP) AS "created_at" FROM "ext" AS "ext") SELECT "t"."created_at" > '2024-10-01 12:05:02' AS "col" FROM "t" AS "t";
+WITH "t" AS (SELECT CAST("ext"."created_at" AS TIMESTAMP) AS "created_at" FROM "ext" AS "ext") SELECT "t"."created_at" > CAST('2024-10-01 12:05:02' AS TIMESTAMP) AS "col" FROM "t" AS "t";
+
+# dialect: mysql
+SELECT `t`.`d` < '2023-01-01 00:00:01' AS `col` FROM `temporal` AS `t`;
+SELECT CAST(`t`.`d` AS DATETIME) < CAST('2023-01-01 00:00:01' AS DATETIME) AS `col` FROM `temporal` AS `t`;
+
+# dialect: mysql
+SELECT CAST(`t`.`some_col` AS DATE) < CAST(`t`.`other_col` AS CHAR) AS `col` FROM `other_table` AS `t`;
+SELECT CAST(CAST(`t`.`some_col` AS DATE) AS DATETIME) < CAST(CAST(`t`.`other_col` AS CHAR) AS DATETIME) AS `col` FROM `other_table` AS `t`;
+
 --------------------------------------
 -- Remove redundant casts
 --------------------------------------
diff --git a/tests/fixtures/optimizer/tpc-ds/tpc-ds.sql b/tests/fixtures/optimizer/tpc-ds/tpc-ds.sql
index 290d2760b8..59bc432fdd 100644
--- a/tests/fixtures/optimizer/tpc-ds/tpc-ds.sql
+++ b/tests/fixtures/optimizer/tpc-ds/tpc-ds.sql
@@ -736,8 +736,8 @@ WITH "salesreturns" AS (
     "date_dim"."d_date" AS "d_date"
   FROM "date_dim" AS "date_dim"
   WHERE
-    CAST("date_dim"."d_date" AS DATETIME) <= CAST('2002-09-05' AS DATE)
-    AND CAST("date_dim"."d_date" AS DATETIME) >= CAST('2002-08-22' AS DATE)
+    CAST("date_dim"."d_date" AS DATE) <= CAST('2002-09-05' AS DATE)
+    AND CAST("date_dim"."d_date" AS DATE) >= CAST('2002-08-22' AS DATE)
 ), "ssr" AS (
   SELECT
     "store"."s_store_id" AS "s_store_id",
@@ -1853,8 +1853,8 @@ SELECT
 FROM "web_sales" AS "web_sales"
 JOIN "date_dim" AS "date_dim"
   ON "date_dim"."d_date_sk" = "web_sales"."ws_sold_date_sk"
-  AND CAST("date_dim"."d_date" AS DATETIME) <= CAST('2000-06-10' AS DATE)
-  AND CAST("date_dim"."d_date" AS DATETIME) >= CAST('2000-05-11' AS DATE)
+  AND CAST("date_dim"."d_date" AS DATE) <= CAST('2000-06-10' AS DATE)
+  AND CAST("date_dim"."d_date" AS DATE) >= CAST('2000-05-11' AS DATE)
 JOIN "item" AS "item"
   ON "item"."i_category" IN ('Home', 'Men', 'Women')
   AND "item"."i_item_sk" = "web_sales"."ws_item_sk"
@@ -2422,7 +2422,7 @@ JOIN "date_dim" AS "date_dim"
   AND "date_dim"."d_date" >= '2002-3-01'
   AND (
     CAST('2002-3-01' AS DATE) + INTERVAL '60' DAY
-  ) >= CAST("date_dim"."d_date" AS DATETIME)
+  ) >= CAST("date_dim"."d_date" AS DATE)
 WHERE
   "_u_3"."_u_4" IS NULL
   AND ARRAY_ANY("_u_0"."_u_2", "_x" -> "cs1"."cs_warehouse_sk" <> "_x")
@@ -2731,8 +2731,8 @@ SELECT
 FROM "catalog_sales" AS "catalog_sales"
 JOIN "date_dim" AS "date_dim"
   ON "catalog_sales"."cs_sold_date_sk" = "date_dim"."d_date_sk"
-  AND CAST("date_dim"."d_date" AS DATETIME) <= CAST('2001-03-05' AS DATE)
-  AND CAST("date_dim"."d_date" AS DATETIME) >= CAST('2001-02-03' AS DATE)
+  AND CAST("date_dim"."d_date" AS DATE) <= CAST('2001-03-05' AS DATE)
+  AND CAST("date_dim"."d_date" AS DATE) >= CAST('2001-02-03' AS DATE)
 JOIN "item" AS "item"
   ON "catalog_sales"."cs_item_sk" = "item"."i_item_sk"
   AND "item"."i_category" IN ('Children', 'Women', 'Electronics')
@@ -2811,8 +2811,8 @@ WITH "x" AS (
   FROM "inventory" AS "inventory"
   JOIN "date_dim" AS "date_dim"
     ON "date_dim"."d_date_sk" = "inventory"."inv_date_sk"
-    AND CAST("date_dim"."d_date" AS DATETIME) <= CAST('2000-06-12' AS DATE)
-    AND CAST("date_dim"."d_date" AS DATETIME) >= CAST('2000-04-13' AS DATE)
+    AND CAST("date_dim"."d_date" AS DATE) <= CAST('2000-06-12' AS DATE)
+    AND CAST("date_dim"."d_date" AS DATE) >= CAST('2000-04-13' AS DATE)
   JOIN "item" AS "item"
     ON "inventory"."inv_item_sk" = "item"."i_item_sk"
     AND "item"."i_current_price" <= 1.49
@@ -3944,7 +3944,7 @@ WITH "catalog_sales_2" AS (
   FROM "date_dim" AS "date_dim"
   WHERE
     "date_dim"."d_date" >= '2001-03-04'
-    AND CAST("date_dim"."d_date" AS DATETIME) <= CAST('2001-06-02' AS DATE)
+    AND CAST("date_dim"."d_date" AS DATE) <= CAST('2001-06-02' AS DATE)
 ), "_u_0" AS (
   SELECT
     1.3 * AVG("catalog_sales"."cs_ext_discount_amt") AS "_col_0",
@@ -4510,8 +4510,8 @@ JOIN "inventory" AS "inventory"
   AND "inventory"."inv_quantity_on_hand" >= 100
 JOIN "date_dim" AS "date_dim"
   ON "date_dim"."d_date_sk" = "inventory"."inv_date_sk"
-  AND CAST("date_dim"."d_date" AS DATETIME) <= CAST('1999-05-05' AS DATE)
-  AND CAST("date_dim"."d_date" AS DATETIME) >= CAST('1999-03-06' AS DATE)
+  AND CAST("date_dim"."d_date" AS DATE) <= CAST('1999-05-05' AS DATE)
+  AND CAST("date_dim"."d_date" AS DATE) >= CAST('1999-03-06' AS DATE)
 WHERE
   "item"."i_current_price" <= 50
   AND "item"."i_current_price" >= 20
@@ -4787,8 +4787,8 @@ LEFT JOIN "catalog_returns" AS "catalog_returns"
   AND "catalog_returns"."cr_order_number" = "catalog_sales"."cs_order_number"
 JOIN "date_dim" AS "date_dim"
   ON "catalog_sales"."cs_sold_date_sk" = "date_dim"."d_date_sk"
-  AND CAST("date_dim"."d_date" AS DATETIME) <= CAST('2002-07-01' AS DATE)
-  AND CAST("date_dim"."d_date" AS DATETIME) >= CAST('2002-05-02' AS DATE)
+  AND CAST("date_dim"."d_date" AS DATE) <= CAST('2002-07-01' AS DATE)
+  AND CAST("date_dim"."d_date" AS DATE) >= CAST('2002-05-02' AS DATE)
 JOIN "item" AS "item"
   ON "catalog_sales"."cs_item_sk" = "item"."i_item_sk"
   AND "item"."i_current_price" <= 1.49
@@ -10318,8 +10318,8 @@ WITH "date_dim_2" AS (
     "date_dim"."d_date" AS "d_date"
   FROM "date_dim" AS "date_dim"
   WHERE
-    CAST("date_dim"."d_date" AS DATETIME) <= CAST('2001-09-15' AS DATE)
-    AND CAST("date_dim"."d_date" AS DATETIME) >= CAST('2001-08-16' AS DATE)
+    CAST("date_dim"."d_date" AS DATE) <= CAST('2001-09-15' AS DATE)
+    AND CAST("date_dim"."d_date" AS DATE) >= CAST('2001-08-16' AS DATE)
 ), "store_2" AS (
   SELECT
     "store"."s_store_sk" AS "s_store_sk"
@@ -10828,8 +10828,8 @@ WITH "date_dim_2" AS (
     "date_dim"."d_date" AS "d_date"
   FROM "date_dim" AS "date_dim"
   WHERE
-    CAST("date_dim"."d_date" AS DATETIME) <= CAST('2000-09-25' AS DATE)
-    AND CAST("date_dim"."d_date" AS DATETIME) >= CAST('2000-08-26' AS DATE)
+    CAST("date_dim"."d_date" AS DATE) <= CAST('2000-09-25' AS DATE)
+    AND CAST("date_dim"."d_date" AS DATE) >= CAST('2000-08-26' AS DATE)
 ), "item_2" AS (
   SELECT
     "item"."i_item_sk" AS "i_item_sk",
@@ -11109,8 +11109,8 @@ JOIN "store_sales" AS "store_sales"
   ON "item"."i_item_sk" = "store_sales"."ss_item_sk"
 JOIN "date_dim" AS "date_dim"
   ON "date_dim"."d_date_sk" = "inventory"."inv_date_sk"
-  AND CAST("date_dim"."d_date" AS DATETIME) <= CAST('1998-06-26' AS DATE)
-  AND CAST("date_dim"."d_date" AS DATETIME) >= CAST('1998-04-27' AS DATE)
+  AND CAST("date_dim"."d_date" AS DATE) <= CAST('1998-06-26' AS DATE)
+  AND CAST("date_dim"."d_date" AS DATE) >= CAST('1998-04-27' AS DATE)
 WHERE
   "item"."i_current_price" <= 93
   AND "item"."i_current_price" >= 63
@@ -12180,7 +12180,7 @@ WITH "web_sales_2" AS (
   FROM "date_dim" AS "date_dim"
   WHERE
     "date_dim"."d_date" >= '2002-03-29'
-    AND CAST("date_dim"."d_date" AS DATETIME) <= CAST('2002-06-27' AS DATE)
+    AND CAST("date_dim"."d_date" AS DATE) <= CAST('2002-06-27' AS DATE)
 ), "_u_0" AS (
   SELECT
     1.3 * AVG("web_sales"."ws_ext_discount_amt") AS "_col_0",
@@ -12321,7 +12321,7 @@ JOIN "date_dim" AS "date_dim"
   AND "date_dim"."d_date_sk" = "ws1"."ws_ship_date_sk"
   AND (
     CAST('2000-3-01' AS DATE) + INTERVAL '60' DAY
-  ) >= CAST("date_dim"."d_date" AS DATETIME)
+  ) >= CAST("date_dim"."d_date" AS DATE)
 JOIN "web_site" AS "web_site"
   ON "web_site"."web_company_name" = 'pri'
   AND "web_site"."web_site_sk" = "ws1"."ws_web_site_sk"
@@ -12411,7 +12411,7 @@ JOIN "date_dim" AS "date_dim"
   AND "date_dim"."d_date_sk" = "ws1"."ws_ship_date_sk"
   AND (
     CAST('2000-4-01' AS DATE) + INTERVAL '60' DAY
-  ) >= CAST("date_dim"."d_date" AS DATETIME)
+  ) >= CAST("date_dim"."d_date" AS DATE)
 JOIN "web_site" AS "web_site"
   ON "web_site"."web_company_name" = 'pri'
   AND "web_site"."web_site_sk" = "ws1"."ws_web_site_sk"
@@ -12595,8 +12595,8 @@ SELECT
 FROM "store_sales" AS "store_sales"
 JOIN "date_dim" AS "date_dim"
   ON "date_dim"."d_date_sk" = "store_sales"."ss_sold_date_sk"
-  AND CAST("date_dim"."d_date" AS DATETIME) <= CAST('2000-06-17' AS DATE)
-  AND CAST("date_dim"."d_date" AS DATETIME) >= CAST('2000-05-18' AS DATE)
+  AND CAST("date_dim"."d_date" AS DATE) <= CAST('2000-06-17' AS DATE)
+  AND CAST("date_dim"."d_date" AS DATE) >= CAST('2000-05-18' AS DATE)
 JOIN "item" AS "item"
   ON "item"."i_category" IN ('Men', 'Home', 'Electronics')
   AND "item"."i_item_sk" = "store_sales"."ss_item_sk"
diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py
index 0fa4ff6ccc..4a41e4a774 100644
--- a/tests/test_optimizer.py
+++ b/tests/test_optimizer.py
@@ -132,7 +132,6 @@ def check_file(
         func,
         pretty=False,
         execute=False,
-        set_dialect=False,
         only=None,
         **kwargs,
     ):
@@ -158,7 +157,7 @@ def check_file(
                         validate_qualify_columns
                     )

-                if set_dialect and dialect:
+                if dialect:
                     func_kwargs["dialect"] = dialect

                 future = pool.submit(parse_and_optimize, func, sql, dialect, **func_kwargs)
@@ -207,7 +206,6 @@ def test_optimize(self, logger):
             pretty=True,
             execute=True,
             schema=schema,
-            set_dialect=True,
         )

     def test_isolate_table_selects(self):
@@ -235,7 +233,6 @@ def test_qualify_tables(self):
             optimizer.qualify_tables.qualify_tables,
             db="db",
             catalog="c",
-            set_dialect=True,
         )

     def test_normalize(self):
@@ -446,11 +443,8 @@ def test_qualify_columns(self, logger):
             qualify_columns,
             execute=True,
             schema=self.schema,
-            set_dialect=True,
-        )
-        self.check_file(
-            "qualify_columns_ddl", qualify_columns, schema=self.schema, set_dialect=True
         )
+        self.check_file("qualify_columns_ddl", qualify_columns, schema=self.schema)

     def test_qualify_columns__with_invisible(self):
         schema = MappingSchema(self.schema, {"x": {"a"}, "y": {"b"}, "z": {"b"}})
@@ -475,7 +469,6 @@ def test_normalize_identifiers(self):
         self.check_file(
             "normalize_identifiers",
             optimizer.normalize_identifiers.normalize_identifiers,
-            set_dialect=True,
         )

         self.assertEqual(optimizer.normalize_identifiers.normalize_identifiers("a%").sql(), '"a%"')
@@ -484,14 +477,13 @@ def test_quote_identifiers(self):
         self.check_file(
             "quote_identifiers",
             optimizer.qualify_columns.quote_identifiers,
-            set_dialect=True,
         )

     def test_pushdown_projection(self):
         self.check_file("pushdown_projections", pushdown_projections, schema=self.schema)

     def test_simplify(self):
-        self.check_file("simplify", simplify, set_dialect=True)
+        self.check_file("simplify", simplify)

         expression = parse_one("SELECT a, c, b FROM table1 WHERE 1 = 1")
         self.assertEqual(simplify(simplify(expression.find(exp.Where))).sql(), "WHERE TRUE")

Bigquery annotate_types incorrectly sets TIMESTAMP as DATETIME even when schema type is provided
```python
import sqlglot
import sqlglot.errors
from sqlglot.optimizer import optimize

dialect = "bigquery"
schema = {"test": {"products": {"created_at": "TIMESTAMP"}}}
expression = sqlglot.parse_one("SELECT * FROM test.products WHERE created_at > '2024-10-01'", read=dialect)
optimized_expression = optimize(
    expression,
    dialect=dialect,
    schema=schema,
)
formatted_sql = optimized_expression.sql(dialect=dialect, pretty=True)
print("@formatted_sql", formatted_sql)
```
formatted_sql is
```SQL
SELECT
  `products`.`created_at` AS `created_at`
FROM `test`.`products` AS `products`
WHERE
  `products`.`created_at` > CAST('2024-10-01' AS DATETIME)
```

which cannot be ran due to
No matching signature for operator < for argument types: TIMESTAMP, DATETIME.

```
diff --git a/sqlglot/optimizer/canonicalize.py b/sqlglot/optimizer/canonicalize.py
index fd002622..4382dc70 100644
--- a/sqlglot/optimizer/canonicalize.py
+++ b/sqlglot/optimizer/canonicalize.py
@@ -138,7 +138,7 @@ def _coerce_date(a: exp.Expression, b: exp.Expression) -> None:
             and b.type
             and b.type.this in exp.DataType.TEXT_TYPES
         ):
-            _replace_cast(b, exp.DataType.Type.DATETIME)
+            _replace_cast(b, a.type)
```

cc @barakalon


diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index 5fa7d1ef13..6e7b21a6cc 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -5109,9 +5109,8 @@ def _parse_column_ops(self, this: t.Optional[exp.Expression]) -> t.Optional[exp.
             else:
                 field = self._parse_field(any_token=True, anonymous_func=True)

-            if isinstance(field, exp.Func) and this:
-                # bigquery allows function calls like x.y.count(...)
-                # SAFE.SUBSTR(...)
+            if isinstance(field, (exp.Func, exp.Window)) and this:
+                # BQ & snowflake allow function calls like x.y.count(...), SAFE.SUBSTR(...) etc
                 # https://cloud.google.com/bigquery/docs/reference/standard-sql/functions-reference#function_call_rules
                 this = exp.replace_tree(
                     this,
@@ -5135,6 +5134,11 @@ def _parse_column_ops(self, this: t.Optional[exp.Expression]) -> t.Optional[exp.
                     db=this.args.get("table"),
                     catalog=this.args.get("db"),
                 )
+            elif isinstance(field, exp.Window):
+                # Move the exp.Dot's to the window's function
+                window_func = self.expression(exp.Dot, this=this, expression=field.this)
+                field.set("this", window_func)
+                this = field
             else:
                 this = self.expression(exp.Dot, this=this, expression=field)

diff --git a/tests/dialects/test_snowflake.py b/tests/dialects/test_snowflake.py
index 515a07c4f4..157947df46 100644
--- a/tests/dialects/test_snowflake.py
+++ b/tests/dialects/test_snowflake.py
@@ -2250,3 +2250,13 @@ def test_grant(self):
         self.validate_identity(
             "GRANT ALL PRIVILEGES ON FUNCTION mydb.myschema.ADD5(number) TO ROLE analyst"
         )
+
+    def test_window_function_arg(self):
+        query = "SELECT * FROM TABLE(db.schema.FUNC(a) OVER ())"
+
+        ast = self.parse_one(query)
+        window = ast.find(exp.Window)
+
+        self.assertEqual(ast.sql("snowflake"), query)
+        self.assertEqual(len(list(ast.find_all(exp.Column))), 1)
+        self.assertEqual(window.this.sql("snowflake"), "db.schema.FUNC(a)")

User defined function recognized as column (Snowflake dialect)
This query:
`select COL1,COL2 from some_table,TABLE(SOME_DB.SOME_SCHEMA.TABLE_FUNC(value1, value2) over (PARTITION BY value1))`
`p = sqlglot.parse_one(query, dialect=sqlglot.Dialects.SNOWFLAKE)`
creates this ast:
`Select(
  expressions=[
    Column(
      this=Identifier(this=COL1, quoted=False)),
    Column(
      this=Identifier(this=COL2, quoted=False))],
  from=From(
    this=Table(
      this=Identifier(this=some_table, quoted=False))),
  joins=[
    Join(
      this=Table(
        this=Anonymous(
          this=TABLE,
          expressions=[
            Column(
              this=Window(
                this=Anonymous(
                  this=TABLE_FUNC,
                  expressions=[
                    Column(
                      this=Identifier(this=value1, quoted=False)),
                    Column(
                      this=Identifier(this=value2, quoted=False))]),
                partition_by=[
                  Column(
                    this=Identifier(this=value1, quoted=False))],
                over=OVER),
              table=Identifier(this=SOME_SCHEMA, quoted=False),
              db=Identifier(this=SOME_DB, quoted=False))])))])`

Inside the anonymous TABLE (which is also a function that returns values as a table you can select on) we can see the expressions contain only one expression which should be another function but is recognized as a Column.

Verified this still happens on 25.31.4.
dialect: Snowflake


diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index a2f118fb5c..5fa7d1ef13 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -1738,9 +1738,13 @@ def _parse_drop(self, exists: bool = False) -> exp.Drop | exp.Command:

         concurrently = self._match_text_seq("CONCURRENTLY")
         if_exists = exists or self._parse_exists()
-        table = self._parse_table_parts(
-            schema=True, is_db_reference=self._prev.token_type == TokenType.SCHEMA
-        )
+
+        if kind == "COLUMN":
+            this = self._parse_column()
+        else:
+            this = self._parse_table_parts(
+                schema=True, is_db_reference=self._prev.token_type == TokenType.SCHEMA
+            )

         cluster = self._parse_on_property() if self._match(TokenType.ON) else None

@@ -1752,7 +1756,7 @@ def _parse_drop(self, exists: bool = False) -> exp.Drop | exp.Command:
         return self.expression(
             exp.Drop,
             exists=if_exists,
-            this=table,
+            this=this,
             expressions=expressions,
             kind=self.dialect.CREATABLE_KIND_MAPPING.get(kind) or kind,
             temporary=temporary,

diff --git a/tests/test_parser.py b/tests/test_parser.py
index ba1240c4f7..b60d719341 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -879,3 +879,8 @@ def test_odbc_date_literals(self):
             expr = parse_one(sql)
             self.assertIsInstance(expr, exp.Insert)
             self.assertIsInstance(expr.expression.expressions[0].expressions[0], cls)
+
+    def test_drop_column(self):
+        ast = parse_one("ALTER TABLE tbl DROP COLUMN col")
+        self.assertEqual(len(list(ast.find_all(exp.Table))), 1)
+        self.assertEqual(len(list(ast.find_all(exp.Column))), 1)

Simple query, wrong tables list
Very simple query, wrong tables list! it considers `activity_id` as a table!

```python
sql='ALTER TABLE ride DROP COLUMN activity_id'
list(sqlglot.parse_one(sql, read='mysql').find_all(sqlglot.exp.Table))
# list(sqlglot.parse_one(sql, read='mysql').find_all(sqlglot.exp.Table))
#  0 = {Table} Table(\n  this=Identifier(this=ride, quoted=False))
#  1 = {Table} Table(\n  this=Identifier(this=activity_id, quoted=False))
```


diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index 613c3b39fc..f578d79ed1 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -301,7 +301,7 @@ def copy(self):
         """
         return deepcopy(self)

-    def add_comments(self, comments: t.Optional[t.List[str]] = None) -> None:
+    def add_comments(self, comments: t.Optional[t.List[str]] = None, prepend: bool = False) -> None:
         if self.comments is None:
             self.comments = []

@@ -313,7 +313,12 @@ def add_comments(self, comments: t.Optional[t.List[str]] = None) -> None:
                         k, *v = kv.split("=")
                         value = v[0].strip() if v else True
                         self.meta[k.strip()] = value
-                self.comments.append(comment)
+
+                if not prepend:
+                    self.comments.append(comment)
+
+            if prepend:
+                self.comments = comments + self.comments

     def pop_comments(self) -> t.List[str]:
         comments = self.comments or []
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index a55712c01e..498c4f549e 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -593,6 +593,7 @@ class Generator(metaclass=_Generator):
     WITH_SEPARATED_COMMENTS: t.Tuple[t.Type[exp.Expression], ...] = (
         exp.Command,
         exp.Create,
+        exp.Describe,
         exp.Delete,
         exp.Drop,
         exp.From,
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index df68d4a152..a2f118fb5c 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -806,7 +806,7 @@ class Parser(metaclass=_Parser):
             kind=self._parse_var_from_options(self.USABLES, raise_unmatched=False),
             this=self._parse_table(schema=False),
         ),
-        TokenType.SEMICOLON: lambda self: self.expression(exp.Semicolon),
+        TokenType.SEMICOLON: lambda self: exp.Semicolon(),
     }

     UNARY_PARSERS = {
@@ -1715,7 +1715,10 @@ def _parse_statement(self) -> t.Optional[exp.Expression]:
             return None

         if self._match_set(self.STATEMENT_PARSERS):
-            return self.STATEMENT_PARSERS[self._prev.token_type](self)
+            comments = self._prev_comments
+            stmt = self.STATEMENT_PARSERS[self._prev.token_type](self)
+            stmt.add_comments(comments, prepend=True)
+            return stmt

         if self._match_set(self.dialect.tokenizer.COMMANDS):
             return self._parse_command()
@@ -1748,7 +1751,6 @@ def _parse_drop(self, exists: bool = False) -> exp.Drop | exp.Command:

         return self.expression(
             exp.Drop,
-            comments=start.comments,
             exists=if_exists,
             this=table,
             expressions=expressions,
@@ -1772,7 +1774,6 @@ def _parse_exists(self, not_: bool = False) -> t.Optional[bool]:
     def _parse_create(self) -> exp.Create | exp.Command:
         # Note: this can't be None because we've matched a statement parser
         start = self._prev
-        comments = self._prev_comments

         replace = (
             start.token_type == TokenType.REPLACE
@@ -1919,7 +1920,6 @@ def extend_props(temp_props: t.Optional[exp.Properties]) -> None:
         create_kind_text = create_token.text.upper()
         return self.expression(
             exp.Create,
-            comments=comments,
             this=this,
             kind=self.dialect.CREATABLE_KIND_MAPPING.get(create_kind_text) or create_kind_text,
             replace=replace,
@@ -2659,7 +2659,7 @@ def parse_conditional_insert() -> t.Optional[exp.ConditionalInsert]:
         )

     def _parse_insert(self) -> t.Union[exp.Insert, exp.MultitableInserts]:
-        comments = ensure_list(self._prev_comments)
+        comments = []
         hint = self._parse_hint()
         overwrite = self._match(TokenType.OVERWRITE)
         ignore = self._match(TokenType.IGNORE)
@@ -2845,7 +2845,6 @@ def _parse_delete(self) -> exp.Delete:
         # This handles MySQL's "Multiple-Table Syntax"
         # https://dev.mysql.com/doc/refman/8.0/en/delete.html
         tables = None
-        comments = self._prev_comments
         if not self._match(TokenType.FROM, advance=False):
             tables = self._parse_csv(self._parse_table) or None

@@ -2853,7 +2852,6 @@ def _parse_delete(self) -> exp.Delete:

         return self.expression(
             exp.Delete,
-            comments=comments,
             tables=tables,
             this=self._match(TokenType.FROM) and self._parse_table(joins=True),
             using=self._match(TokenType.USING) and self._parse_table(joins=True),
@@ -2864,13 +2862,11 @@ def _parse_delete(self) -> exp.Delete:
         )

     def _parse_update(self) -> exp.Update:
-        comments = self._prev_comments
         this = self._parse_table(joins=True, alias_tokens=self.UPDATE_ALIAS_TOKENS)
         expressions = self._match(TokenType.SET) and self._parse_csv(self._parse_equality)
         returning = self._parse_returning()
         return self.expression(
             exp.Update,
-            comments=comments,
             **{  # type: ignore
                 "this": this,
                 "expressions": expressions,

diff --git a/tests/dialects/test_mysql.py b/tests/dialects/test_mysql.py
index 52b04eae84..d7aef4f8d9 100644
--- a/tests/dialects/test_mysql.py
+++ b/tests/dialects/test_mysql.py
@@ -81,6 +81,10 @@ def test_ddl(self):
         self.validate_identity(
             "CREATE OR REPLACE VIEW my_view AS SELECT column1 AS `boo`, column2 AS `foo` FROM my_table WHERE column3 = 'some_value' UNION SELECT q.* FROM fruits_table, JSON_TABLE(Fruits, '$[*]' COLUMNS(id VARCHAR(255) PATH '$.$id', value VARCHAR(255) PATH '$.value')) AS q",
         )
+        self.validate_identity(
+            "/*left*/ EXPLAIN SELECT /*hint*/ col FROM t1 /*right*/",
+            "/* left */ DESCRIBE /* hint */ SELECT col FROM t1 /* right */",
+        )
         self.validate_identity(
             "CREATE TABLE t (name VARCHAR)",
             "CREATE TABLE t (name TEXT)",
diff --git a/tests/fixtures/identity.sql b/tests/fixtures/identity.sql
index 33199de377..0e13a64fd6 100644
--- a/tests/fixtures/identity.sql
+++ b/tests/fixtures/identity.sql
@@ -882,4 +882,4 @@ GRANT SELECT ON TABLE tbl TO user
 GRANT SELECT, INSERT ON FUNCTION tbl TO user
 GRANT SELECT ON orders TO ROLE PUBLIC
 GRANT SELECT ON nation TO alice WITH GRANT OPTION
-GRANT DELETE ON SCHEMA finance TO bob
\ No newline at end of file
+GRANT DELETE ON SCHEMA finance TO bob

Issue with Comment Handling in `Describe` Output
Hi,

I encountered an issue with the [`Describe`](https://sqlglot.com/sqlglot/expressions.html#Describe) functionality in `sqlglot`. The comments in the SQL query are not being preserved as expected.

Here’s a minimal example to illustrate the problem:

```python
import os
# os.environ['SQLGLOTRS_TOKENIZER'] = '0'

import importlib.metadata
import sqlglot

package_name = 'sqlglot'
version = importlib.metadata.version(package_name)
print(f"{package_name} version: {version}\n")

sql = '/*left*/ EXPLAIN SELECT /*hint*/ col FROM t1 /*right*/'

ast = sqlglot.parse(sql, read='mysql')

print(ast)
print(list(map(lambda x: x.sql(dialect='mysql'), ast)))
```

### Output:
```
sqlglot version: 25.30.0

[Describe(
  this=Select(
    expressions=[
      Column(
        this=Identifier(this=col, quoted=False))],
    from=From(
      this=Table(
        this=Identifier(this=t1, quoted=False),
        _comments=[
          right])),
    _comments=[
      hint]))]
['DESCRIBE /* hint */ SELECT col FROM t1 /* right */']
```

### Expected Output:
```sql
/*left*/ DESCRIBE /* hint */ SELECT col FROM t1 /* right */
```

### Actual Output:
```sql
DESCRIBE /* hint */ SELECT col FROM t1 /* right */
```


diff --git a/sqlglot/dialects/duckdb.py b/sqlglot/dialects/duckdb.py
index bf1abe2f1d..a183a883f5 100644
--- a/sqlglot/dialects/duckdb.py
+++ b/sqlglot/dialects/duckdb.py
@@ -156,18 +156,24 @@ def _struct_sql(self: DuckDB.Generator, expression: exp.Struct) -> str:

     # BigQuery allows inline construction such as "STRUCT<a STRING, b INTEGER>('str', 1)" which is
     # canonicalized to "ROW('str', 1) AS STRUCT(a TEXT, b INT)" in DuckDB
-    # The transformation to ROW will take place if a cast to STRUCT / ARRAY of STRUCTs is found
+    # The transformation to ROW will take place if:
+    #  1. The STRUCT itself does not have proper fields (key := value) as a "proper" STRUCT would
+    #  2. A cast to STRUCT / ARRAY of STRUCTs is found
     ancestor_cast = expression.find_ancestor(exp.Cast)
-    is_struct_cast = ancestor_cast and any(
-        casted_type.is_type(exp.DataType.Type.STRUCT)
-        for casted_type in ancestor_cast.find_all(exp.DataType)
+    is_bq_inline_struct = (
+        (expression.find(exp.PropertyEQ) is None)
+        and ancestor_cast
+        and any(
+            casted_type.is_type(exp.DataType.Type.STRUCT)
+            for casted_type in ancestor_cast.find_all(exp.DataType)
+        )
     )

     for i, expr in enumerate(expression.expressions):
         is_property_eq = isinstance(expr, exp.PropertyEQ)
         value = expr.expression if is_property_eq else expr

-        if is_struct_cast:
+        if is_bq_inline_struct:
             args.append(self.sql(value))
         else:
             key = expr.name if is_property_eq else f"_{i}"
@@ -175,7 +181,7 @@ def _struct_sql(self: DuckDB.Generator, expression: exp.Struct) -> str:

     csv_args = ", ".join(args)

-    return f"ROW({csv_args})" if is_struct_cast else f"{{{csv_args}}}"
+    return f"ROW({csv_args})" if is_bq_inline_struct else f"{{{csv_args}}}"

 def _datatype_sql(self: DuckDB.Generator, expression: exp.DataType) -> str:

diff --git a/tests/dialects/test_duckdb.py b/tests/dialects/test_duckdb.py
index 5b3b2a4ff4..3d4fe9cc4a 100644
--- a/tests/dialects/test_duckdb.py
+++ b/tests/dialects/test_duckdb.py
@@ -1154,6 +1154,7 @@ def test_cast(self):
         self.validate_identity("CAST(x AS BINARY)", "CAST(x AS BLOB)")
         self.validate_identity("CAST(x AS VARBINARY)", "CAST(x AS BLOB)")
         self.validate_identity("CAST(x AS LOGICAL)", "CAST(x AS BOOLEAN)")
+        self.validate_identity("""CAST({'i': 1, 's': 'foo'} AS STRUCT("s" TEXT, "i" INT))""")
         self.validate_identity(
             "CAST(ROW(1, ROW(1)) AS STRUCT(number BIGINT, row STRUCT(number BIGINT)))"
         )
@@ -1163,11 +1164,11 @@ def test_cast(self):
         )
         self.validate_identity(
             "CAST([[STRUCT_PACK(a := 1)]] AS STRUCT(a BIGINT)[][])",
-            "CAST([[ROW(1)]] AS STRUCT(a BIGINT)[][])",
+            "CAST([[{'a': 1}]] AS STRUCT(a BIGINT)[][])",
         )
         self.validate_identity(
             "CAST([STRUCT_PACK(a := 1)] AS STRUCT(a BIGINT)[])",
-            "CAST([ROW(1)] AS STRUCT(a BIGINT)[])",
+            "CAST([{'a': 1}] AS STRUCT(a BIGINT)[])",
         )
         self.validate_identity(
             "STRUCT_PACK(a := 'b')::json",
@@ -1175,7 +1176,7 @@ def test_cast(self):
         )
         self.validate_identity(
             "STRUCT_PACK(a := 'b')::STRUCT(a TEXT)",
-            "CAST(ROW('b') AS STRUCT(a TEXT))",
+            "CAST({'a': 'b'} AS STRUCT(a TEXT))",
         )

         self.validate_all(

BUG(duckdb): Compiling STRUCTs throws away field name info, which is sometimes needed
**Fully reproducible code snippet**
```python
import sqlglot
print(sqlglot.__version__)
# 25.28.1.dev12

inp = """SELECT CAST({'i':1, 's':'foo'} AS STRUCT("s" TEXT, "i" INT));"""
e = sqlglot.parse_one(inp, "duckdb")
actual = e.sql("duckdb")
print(actual)
# SELECT CAST(ROW(1, 'foo') AS STRUCT("s" TEXT, "i" INT));
```

If you run `actual` on https://shell.duckdb.org/, you get "Conversion Error: Could not convert string 'foo' to INT32".
This is because the names of the fields is important, and the simple `(ROW(1, 'foo')` that is currently given as output isn't adequate for this usage.

I would expect that `actual == inp`. I think this should work even if the struct values are columns instead of literals, eg `{'i':my_int_col, 's':my_str_col}` should roundtrip exactly.

I originally found this in [this ibis issue](https://github.com/ibis-project/ibis/issues/10417), but I think this should be fixed at the sqlglot level. Curious if you will agree.


diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index c6b5198977..11a58346aa 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -3267,8 +3267,10 @@ def _parse_join(
             kwargs["on"] = self._parse_assignment()
         elif self._match(TokenType.USING):
             kwargs["using"] = self._parse_using_identifiers()
-        elif not isinstance(kwargs["this"], exp.Unnest) and not (
-            kind and kind.token_type == TokenType.CROSS
+        elif (
+            not (outer_apply or cross_apply)
+            and not isinstance(kwargs["this"], exp.Unnest)
+            and not (kind and kind.token_type == TokenType.CROSS)
         ):
             index = self._index
             joins: t.Optional[list] = list(self._parse_joins())

diff --git a/tests/test_parser.py b/tests/test_parser.py
index f06f1e11b5..ff82e08cc5 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -699,77 +699,19 @@ def test_pivot_columns(self):

     def test_parse_nested(self):
         now = time.time()
-        query = parse_one(
-            """
-            SELECT *
-            FROM a
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            LEFT JOIN b ON a.id = b.id
-            """
-        )
-
+        query = parse_one("SELECT * FROM a " + ("LEFT JOIN b ON a.id = b.id " * 38))
         self.assertIsNotNone(query)
+        self.assertLessEqual(time.time() - now, 0.1)

-        query = parse_one(
-            """
-            SELECT *
-            FROM a
-            LEFT JOIN UNNEST(ARRAY[])
-            LEFT JOIN UNNEST(ARRAY[])
-            LEFT JOIN UNNEST(ARRAY[])
-            LEFT JOIN UNNEST(ARRAY[])
-            LEFT JOIN UNNEST(ARRAY[])
-            LEFT JOIN UNNEST(ARRAY[])
-            LEFT JOIN UNNEST(ARRAY[])
-            LEFT JOIN UNNEST(ARRAY[])
-            LEFT JOIN UNNEST(ARRAY[])
-            LEFT JOIN UNNEST(ARRAY[])
-            LEFT JOIN UNNEST(ARRAY[])
-            LEFT JOIN UNNEST(ARRAY[])
-            LEFT JOIN UNNEST(ARRAY[])
-            LEFT JOIN UNNEST(ARRAY[])
-            LEFT JOIN UNNEST(ARRAY[])
-            """
-        )
+        now = time.time()
+        query = parse_one("SELECT * FROM a " + ("LEFT JOIN UNNEST(ARRAY[]) " * 15))
+        self.assertIsNotNone(query)
+        self.assertLessEqual(time.time() - now, 0.1)

+        now = time.time()
+        query = parse_one("SELECT * FROM a " + ("OUTER APPLY (SELECT * FROM b) " * 30))
         self.assertIsNotNone(query)
-        self.assertLessEqual(time.time() - now, 0.2)
+        self.assertLessEqual(time.time() - now, 0.1)

     def test_parse_properties(self):
         self.assertEqual(

Parsing APPLY doubles execution time per APPLY statement
SQLGlot versions: tested with 25.9.0 and 25.8.1

Dialect: "tsql"
Description: When parsing SQL using an APPLY statement, each successive APPLY seems to double parsing time.

I was parsing a (really badly written) SQL query and noticed my code sort of just stopped. I realized that the query had an excessive number of APPLY statements (a really ridiculous number). I broke it down and wrote a dummy query where I slowly started adding apply statements one by one and noticed the query times doubled every time I added an APPLY statement.

Some quick numbers slowly adding 0..15 APPLY statements to the query:

- Testing with 0 APPLY statements
- test_apply executed in 0.001636 seconds
- Testing with 1 APPLY statements
- test_apply executed in 0.002330 seconds
- Testing with 2 APPLY statements
- test_apply executed in 0.004563 seconds
- Testing with 3 APPLY statements
- test_apply executed in 0.009903 seconds
- Testing with 4 APPLY statements
- test_apply executed in 0.019378 seconds
- Testing with 5 APPLY statements
- test_apply executed in 0.035283 seconds
- Testing with 6 APPLY statements
- test_apply executed in 0.065637 seconds
- Testing with 7 APPLY statements
- test_apply executed in 0.128544 seconds
- Testing with 8 APPLY statements
- test_apply executed in 0.258765 seconds
- Testing with 9 APPLY statements
- test_apply executed in 0.499099 seconds
- Testing with 10 APPLY statements
- test_apply executed in 1.008456 seconds
- Testing with 11 APPLY statements
- test_apply executed in 2.009916 seconds
- Testing with 12 APPLY statements
- test_apply executed in 3.927725 seconds
- Testing with 13 APPLY statements
- test_apply executed in 7.998525 seconds
- Testing with 14 APPLY statements
- test_apply executed in 16.154987 seconds
- Testing with 15 APPLY statements
- test_apply executed in 31.792223 seconds

Each time I add one more APPLY, it takes 2x longer than the prior run.

I'm attaching my sample "apply generator" that just spins out N APPLY blocks and does some very basic timing.

```
from sqlglot import parse_one
import time

num_apply = 15

def generate_apply(apply_n=0):

    apply_template = """
        OUTER APPLY (
            SELECT TOP 1
                *
            FROM my_other_catalog.my_other_schema.my_other_table_{i} b
            WHERE
                b.id = a.id
        ORDER BY
            b.a_date desc
        ) b{i}
    """
    apply = ""
    for i in range(apply_n):
        apply = apply + eval(f'f"""{apply_template}"""')

    query_template = f"""
    SELECT
        *
    FROM
        my_catalog.my_schema.my_table a
    {apply}
    """

    return query_template

def timing_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} executed in {end_time - start_time:.6f} seconds")
        return result
    return wrapper

@timing_decorator
def test_apply(sql):
    x = parse_one(sql=sql, dialect="tsql")

for i in range(num_apply+1):
    sql = generate_apply(apply_n=i)
    print(f"Testing with {i} APPLY statements")
    #print(sql)
    test_apply(sql=sql)
```

Also tested this using a LEFT JOIN LATERAL and dialect set to "databricks" - same timing
Thanks for the detailed report, I can reproduce these times. We'll take a look.


diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index 229af18aec..8f5050ee31 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -1678,6 +1678,7 @@ def _parse_sequence_properties(self) -> t.Optional[exp.SequenceProperties]:
         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("=")

diff --git a/tests/dialects/test_snowflake.py b/tests/dialects/test_snowflake.py
index d03412cace..c9150ff3ee 100644
--- a/tests/dialects/test_snowflake.py
+++ b/tests/dialects/test_snowflake.py
@@ -1223,6 +1223,14 @@ def test_ddl(self):
             "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",

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).


diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index b0a25167ba..51022bbe6b 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -5974,7 +5974,7 @@ def _parse_set_item_assignment(
         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):

diff --git a/tests/dialects/test_snowflake.py b/tests/dialects/test_snowflake.py
index dae835574b..97b83ab930 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 @@ def test_snowflake(self):
   )""",
         )

+        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 @@ def test_snowflake(self):
         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'
         )
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 6bcdb643a5..1b9639a785 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -503,7 +503,7 @@ def test_set_expression(self):

         self.assertIsInstance(set_item, exp.SetItem)
         self.assertIsInstance(set_item.this, exp.EQ)
-        self.assertIsInstance(set_item.this.this, exp.Identifier)
+        self.assertIsInstance(set_item.this.this, exp.Column)
         self.assertIsInstance(set_item.this.expression, exp.Literal)

         self.assertEqual(set_item.args.get("kind"), "SESSION")

SET TAG identifier containing dots is parsed as Command
Would love for an identifier containing dots to be parsed as a `SetItem` ie: like an identifier without dots.

Without dots, is parsed as SetItem:
```
❯ sqlglot.parse_one("ALTER TABLE table1 SET TAG foo='baz'", read="snowflake")
AlterTable(
  this=Table(
    this=Identifier(this=table1, quoted=False)),
  actions=[
    Set(
      expressions=[
        SetItem(
          this=EQ(
            this=Identifier(this=foo, quoted=False),
            expression=Literal(this=baz, is_string=True)))],
      unset=False,
      tag=True)])
```

vs with dots:

```
❯ sqlglot.parse_one("ALTER TABLE table1 SET TAG foo.bar='baz'", read="snowflake")
AlterTable(
  this=Table(
    this=Identifier(this=table1, quoted=False)),
  actions=[
    Command(this=TAG, expression=foo.bar='baz')])
```

In sqlglot 15 this was parsed as SetTag:
```
❯ python -c 'import sqlglot; print(repr(sqlglot.parse_one("ALTER TABLE table1 SET TAG foo.bar=''baz''", read="snowflake")));'
(ALTERTABLE this:
  (TABLE this:
    (IDENTIFIER this: table1, quoted: False)), actions:
  (SETTAG expressions:
    (EQ this:
      (COLUMN this:
        (IDENTIFIER this: bar, quoted: False), table:
        (IDENTIFIER this: foo, quoted: False)), expression:
      (LITERAL this: baz, is_string: True)), unset: False))
```


diff --git a/sqlglot/dialects/snowflake.py b/sqlglot/dialects/snowflake.py
index 6022132354..0ffe4cf0a3 100644
--- a/sqlglot/dialects/snowflake.py
+++ b/sqlglot/dialects/snowflake.py
@@ -48,10 +48,12 @@ def _builder(args: t.List) -> exp.Func:
                     return exp.UnixToTime(this=value, scale=seq_get(args, 1))
                 if not is_float(value.this):
                     return build_formatted_time(exp.StrToTime, "snowflake")(args)
-            if kind == exp.DataType.Type.DATE and not int_value:
-                formatted_exp = build_formatted_time(exp.TsOrDsToDate, "snowflake")(args)
-                formatted_exp.set("safe", safe)
-                return formatted_exp
+
+        if len(args) == 2 and kind == exp.DataType.Type.DATE:
+            formatted_exp = build_formatted_time(exp.TsOrDsToDate, "snowflake")(args)
+            formatted_exp.set("safe", safe)
+            return formatted_exp
+
         return exp.Anonymous(this=name, expressions=args)

     return _builder

diff --git a/tests/dialects/test_snowflake.py b/tests/dialects/test_snowflake.py
index 19a1eb58a9..00e6169ac0 100644
--- a/tests/dialects/test_snowflake.py
+++ b/tests/dialects/test_snowflake.py
@@ -985,6 +985,17 @@ def test_timestamps(self):
             "SELECT CAST('2019-02-28' AS DATE) + INTERVAL '1 day, 1 year'",
         )

+        self.validate_identity("DATE(x)").assert_is(exp.Anonymous)
+        self.validate_identity("TO_DATE(x)").assert_is(exp.Anonymous)
+        self.validate_identity("TRY_TO_DATE(x)").assert_is(exp.Anonymous)
+
+        self.validate_all(
+            "TO_DATE(x, 'MM-DD-YYYY')",
+            write={
+                "snowflake": "TO_DATE(x, 'mm-DD-yyyy')",
+                "duckdb": "CAST(STRPTIME(x, '%m-%d-%Y') AS DATE)",
+            },
+        )
         self.validate_all(
             "DATE('01-01-2000', 'MM-DD-YYYY')",
             write={

feat(snowflake): Adding support for DATE, TO_DATE, TRY_TO_DATE functions
Fixes #3152

Add support for the mentioned functions by:

- Converting all of them to `exp.TsOrDsToDate` expressions if the format is present _or_ to date casts otherwise
- Adding a new argument `safe` in `exp.TsOrDsToDate` to preserve the roundtrip of `TRY_TO_DATE`, which has a different error handling behavior compared to `TO_DATE / DATE`

For the transpilation issue #3152, no change was needed in DuckDB as it already transpiles the aforementioned expression to `strptime` (note that it produces a `TIMESTAMP`, thus the cast to `DATE`):

```
>>> import sqlglot
>>> sqlglot.transpile("SELECT try_to_date('01-01-1999', 'MM-DD-YYYY')", read="snowflake", write="duckdb")
SELECT CAST(STRPTIME('01-01-1999', '%m-%d-%Y') AS DATE)
```

Docs
-----------

1. [Snowflake TRY_TO_DATE](https://docs.snowflake.com/en/sql-reference/functions/try_to_date)
2. [Snowflake DATE / TO_DATE](https://docs.snowflake.com/en/sql-reference/functions/to_date)
3. [DuckDB strptime](https://duckdb.org/docs/sql/functions/dateformat.html#strptime-examples)

the command above works great.
```
>>> import sqlglot
>>> sqlglot.transpile("SELECT try_to_date('01-01-1999', 'MM-DD-YYYY')", read="snowflake", write="duckdb")
SELECT CAST(STRPTIME('01-01-1999', '%m-%d-%Y') AS DATE)
```

but seems this command still not work, when the parameter is a column_name instead of a string
```
>>> sqlglot.transpile("SELECT try_to_date(x, 'MMDDYYYY')", read="snowflake", write="duckdb")
SELECT TRY_TO_DATE(x, 'MMDDYYYY')
```
@VaggelisD this is because we've reused the logic that we have for `TO_TIMESTAMP`. In that function you can basically have both a numeric expression and a string expression as the first argument, so we couldn't safely convert it to a `StrToTime` without having a literal. It seems though that for `TO_DATE` and `TRY_TO_DATE` there's only one variant with two arguments - the `<str>, <fmt>` one - so it should be safe to convert it accordingly. Wanna take a look?


diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py
index 9178458b140..7d75cac9885 100644
--- a/sphinx/builders/linkcheck.py
+++ b/sphinx/builders/linkcheck.py
@@ -13,7 +13,7 @@
 from queue import PriorityQueue, Queue
 from threading import Thread
 from typing import TYPE_CHECKING, NamedTuple, cast
-from urllib.parse import unquote, urlparse, urlsplit, urlunparse
+from urllib.parse import quote, unquote, urlparse, urlsplit, urlunparse

 from docutils import nodes
 from requests.exceptions import ConnectionError, HTTPError, SSLError, TooManyRedirects
@@ -409,6 +409,7 @@ def _check_uri(self, uri: str, hyperlink: Hyperlink) -> tuple[str, str, int]:
                     if rex.match(req_url):
                         anchor = ''
                         break
+            anchor = unquote(anchor)

         # handle non-ASCII URIs
         try:
@@ -446,7 +447,7 @@ def _check_uri(self, uri: str, hyperlink: Hyperlink) -> tuple[str, str, int]:
                 ) as response:
                     if (self.check_anchors and response.ok and anchor
                             and not contains_anchor(response, anchor)):
-                        raise Exception(__(f'Anchor {anchor!r} not found'))
+                        raise Exception(__(f'Anchor {quote(anchor)!r} not found'))

                 # Copy data we need from the (closed) response
                 status_code = response.status_code
@@ -592,7 +593,7 @@ def _get_request_headers(

 def contains_anchor(response: Response, anchor: str) -> bool:
     """Determine if an anchor is contained within an HTTP response."""
-    parser = AnchorCheckParser(unquote(anchor))
+    parser = AnchorCheckParser(anchor)
     # Read file in chunks. If we find a matching anchor, we break
     # the loop early in hopes not to have to download the whole thing.
     for chunk in response.iter_content(chunk_size=4096, decode_unicode=True):

diff --git a/tests/roots/test-linkcheck-anchors-ignore-for-url/index.rst b/tests/roots/test-linkcheck-anchors-ignore-for-url/index.rst
index df287b4c425..02969b63e31 100644
--- a/tests/roots/test-linkcheck-anchors-ignore-for-url/index.rst
+++ b/tests/roots/test-linkcheck-anchors-ignore-for-url/index.rst
@@ -1,5 +1,6 @@
 * `Example valid url, no anchor <http://localhost:7777/valid>`_
 * `Example valid url, valid anchor <http://localhost:7777/valid#valid-anchor>`_
+* `Example valid url, valid quotable anchor <http://localhost:7777/valid#py:module::urllib.parse>`_
 * `Example valid url, invalid anchor <http://localhost:7777/valid#invalid-anchor>`_
 * `Example ignored url, no anchor <http://localhost:7777/ignored>`_
 * `Example ignored url, invalid anchor <http://localhost:7777/ignored#invalid-anchor>`_
diff --git a/tests/test_builders/test_build_linkcheck.py b/tests/test_builders/test_build_linkcheck.py
index c8d8515af16..f3ff64c083e 100644
--- a/tests/test_builders/test_build_linkcheck.py
+++ b/tests/test_builders/test_build_linkcheck.py
@@ -295,7 +295,7 @@ def test_anchors_ignored_for_url(app):

     attrs = ('filename', 'lineno', 'status', 'code', 'uri', 'info')
     data = [json.loads(x) for x in content.splitlines()]
-    assert len(data) == 7
+    assert len(data) == 8
     assert all(all(attr in row for attr in attrs) for row in data)

     # rows may be unsorted due to network latency or
@@ -304,6 +304,7 @@ def test_anchors_ignored_for_url(app):

     assert rows[f'http://{address}/valid']['status'] == 'working'
     assert rows[f'http://{address}/valid#valid-anchor']['status'] == 'working'
+    assert rows['http://localhost:7777/valid#py:module::urllib.parse']['status'] == 'broken'
     assert rows[f'http://{address}/valid#invalid-anchor'] == {
         'status': 'broken',
         'info': "Anchor 'invalid-anchor' not found",

linkcheck performance: downloading page multiple times when checking anchors
### Problem
- If my sphinx documentation contains multiple links with anchors to a web page with multiple anchors, it will download the page multiple times, once per anchor to check
- This scales very badly.  If I have many hundreds or thousands of anchors (e.g. for automatically generated documentation), it might download several megabytes × the number of links.  This can end up being multiple gigabytes

#### Procedure to reproduce the problem

- create a document with links to anchors on the same web page
- run the link checker; it will fetch the page multiple times

#### Expected results

- I would suggest that the link checker could cache the anchors on webpages, so that it only downloads each page once, and only checks each link once.  It could build a dictionary of pages to check, and store the anchors as a list or dict within it?  Since we know up front which of our links have anchors, we can skip storing them when we know it's unnecessary.
- There may be other better ways of doing this; I'm not familiar with the internals of the link checker.

### Reproducible project / your project
- https://github.com/openmicroscopy/bioformats/tree/develop/docs/sphinx
- contains lots of links to https://www.openmicroscopy.org/Schemas/Documentation/Generated/OME-2016-06/ome_xsd.html

### Environment info
- OS: Any
- Python version: Any
- Sphinx version: Any


diff --git a/prowler/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days.py b/prowler/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days.py
index 3de61166c5a..225b92381df 100644
--- a/prowler/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days.py
+++ b/prowler/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days.py
@@ -49,7 +49,7 @@ def execute(self) -> Check_Report_AWS:
                         old_access_keys = True
                         report = Check_Report_AWS(self.metadata())
                         report.region = iam_client.region
-                        report.resource_id = user["user"]
+                        report.resource_id = f"{user['user']}-access-key-1"
                         report.resource_arn = user["arn"]
                         report.resource_tags = user_tags
                         report.status = "FAIL"
@@ -66,7 +66,7 @@ def execute(self) -> Check_Report_AWS:
                         old_access_keys = True
                         report = Check_Report_AWS(self.metadata())
                         report.region = iam_client.region
-                        report.resource_id = user["user"]
+                        report.resource_id = f"{user['user']}-access-key-2"
                         report.resource_arn = user["arn"]
                         report.resource_tags = user_tags
                         report.status = "FAIL"

diff --git a/tests/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days_test.py b/tests/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days_test.py
index 57c697062f2..8205cd583c1 100644
--- a/tests/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days_test.py
+++ b/tests/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days_test.py
@@ -21,13 +21,16 @@ def test_user_no_access_keys(self):

         aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])

-        with mock.patch(
-            "prowler.providers.common.provider.Provider.get_global_provider",
-            return_value=aws_provider,
-        ), mock.patch(
-            "prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client",
-            new=IAM(aws_provider),
-        ) as service_client:
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=aws_provider,
+            ),
+            mock.patch(
+                "prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client",
+                new=IAM(aws_provider),
+            ) as service_client,
+        ):
             from prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days import (
                 iam_rotate_access_key_90_days,
             )
@@ -62,13 +65,16 @@ def test_user_access_key_1_not_rotated(self):

         aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])

-        with mock.patch(
-            "prowler.providers.common.provider.Provider.get_global_provider",
-            return_value=aws_provider,
-        ), mock.patch(
-            "prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client",
-            new=IAM(aws_provider),
-        ) as service_client:
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=aws_provider,
+            ),
+            mock.patch(
+                "prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client",
+                new=IAM(aws_provider),
+            ) as service_client,
+        ):
             from prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days import (
                 iam_rotate_access_key_90_days,
             )
@@ -86,7 +92,7 @@ def test_user_access_key_1_not_rotated(self):
                 result[0].status_extended
                 == f"User {user} has not rotated access key 1 in over 90 days (100 days)."
             )
-            assert result[0].resource_id == user
+            assert result[0].resource_id == f"{user}-access-key-1"
             assert result[0].resource_arn == arn
             assert result[0].region == AWS_REGION_US_EAST_1
             assert result[0].resource_tags == [{"Key": "test-tag", "Value": "test"}]
@@ -106,13 +112,16 @@ def test_user_access_key_2_not_rotated(self):

         aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])

-        with mock.patch(
-            "prowler.providers.common.provider.Provider.get_global_provider",
-            return_value=aws_provider,
-        ), mock.patch(
-            "prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client",
-            new=IAM(aws_provider),
-        ) as service_client:
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=aws_provider,
+            ),
+            mock.patch(
+                "prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client",
+                new=IAM(aws_provider),
+            ) as service_client,
+        ):
             from prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days import (
                 iam_rotate_access_key_90_days,
             )
@@ -130,7 +139,7 @@ def test_user_access_key_2_not_rotated(self):
                 result[0].status_extended
                 == f"User {user} has not rotated access key 2 in over 90 days (100 days)."
             )
-            assert result[0].resource_id == user
+            assert result[0].resource_id == f"{user}-access-key-2"
             assert result[0].resource_arn == arn
             assert result[0].region == AWS_REGION_US_EAST_1
             assert result[0].resource_tags == [{"Key": "test-tag", "Value": "test"}]
@@ -150,13 +159,16 @@ def test_user_both_access_keys_not_rotated(self):

         aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])

-        with mock.patch(
-            "prowler.providers.common.provider.Provider.get_global_provider",
-            return_value=aws_provider,
-        ), mock.patch(
-            "prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client",
-            new=IAM(aws_provider),
-        ) as service_client:
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=aws_provider,
+            ),
+            mock.patch(
+                "prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client",
+                new=IAM(aws_provider),
+            ) as service_client,
+        ):
             from prowler.providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days import (
                 iam_rotate_access_key_90_days,
             )
@@ -179,7 +191,7 @@ def test_user_both_access_keys_not_rotated(self):
                 result[0].status_extended
                 == f"User {user} has not rotated access key 1 in over 90 days (100 days)."
             )
-            assert result[0].resource_id == user
+            assert result[0].resource_id == f"{user}-access-key-1"
             assert result[0].resource_arn == arn
             assert result[0].region == AWS_REGION_US_EAST_1
             assert result[0].resource_tags == [{"Key": "test-tag", "Value": "test"}]
@@ -188,7 +200,7 @@ def test_user_both_access_keys_not_rotated(self):
                 result[1].status_extended
                 == f"User {user} has not rotated access key 2 in over 90 days (100 days)."
             )
-            assert result[1].resource_id == user
+            assert result[1].resource_id == f"{user}-access-key-2"
             assert result[1].resource_arn == arn
             assert result[1].region == AWS_REGION_US_EAST_1
             assert result[1].resource_tags == [{"Key": "test-tag", "Value": "test"}]

`finding_uid` is not unique in `iam_rotate_access_key_90_days`
### Steps to Reproduce

python prowler.py aws -s iam

### Expected behavior

The program output CSV file with the same finding_uid like that
prowler-aws-iam_rotate_access_key_90_days-<AWS ACCOUNT_ID>-us-east-1-<IAM_USER>

This IAM_USER has 2 access keys

### Actual Result with Screenshots or Logs

No log

### How did you install Prowler?

Cloning the repository from github.com (git clone)

### Environment Resource

8. Other (Local PC)

### OS used

2. MacOS

### Prowler version

Prowler 5.1.0 (You are running the latest version, yay!)

### Pip version

pip 23.3

### Context

The source code of [finding.py ](https://github.com/prowler-cloud/prowler/blob/master/prowler/lib/outputs/finding.py#L238-L241)

```
output_data["uid"] = (
                f"prowler-{provider.type}-{check_output.check_metadata.CheckID}-{output_data['account_uid']}-"
                f"{output_data['region']}-{output_data['resource_name']}"
            )
```

Do we need add UUID to above line to avoid problem?

Hello @banv, that's a great catch!

The Prowler Finding UID has the above format for historical reasons and can't be an UUID since it can't be changed because it'll introduce a breaking change. I think in this case the check needs to be adjusted to add a index for each access key, like `prowler-aws-iam_rotate_access_key_90_days--us-east-1-<IAM_USER>-access-key-[0,1]`.

With that we'll achieve uniqueness in the `finding_uid`. Let me know what you think about it.

Thanks for using Prowler! 🚀


diff --git a/prowler/providers/aws/services/wafv2/wafv2_service.py b/prowler/providers/aws/services/wafv2/wafv2_service.py
index 85feed76f62..c867691b771 100644
--- a/prowler/providers/aws/services/wafv2/wafv2_service.py
+++ b/prowler/providers/aws/services/wafv2/wafv2_service.py
@@ -150,6 +150,22 @@ def _get_web_acl(self, acl: str):
                         else:
                             acl.rules.append(new_rule)

+                    firewall_manager_managed_rg = get_web_acl.get("WebACL", {}).get(
+                        "PreProcessFirewallManagerRuleGroups", []
+                    ) + get_web_acl.get("WebACL", {}).get(
+                        "PostProcessFirewallManagerRuleGroups", []
+                    )
+
+                    for rule in firewall_manager_managed_rg:
+                        acl.rule_groups.append(
+                            Rule(
+                                name=rule.get("Name", ""),
+                                cloudwatch_metrics_enabled=rule.get(
+                                    "VisibilityConfig", {}
+                                ).get("CloudWatchMetricsEnabled", False),
+                            )
+                        )
+
                 except Exception as error:
                     logger.error(
                         f"{acl.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
@@ -193,13 +209,6 @@ class Rule(BaseModel):
     cloudwatch_metrics_enabled: bool = False

-class FirewallManagerRuleGroup(BaseModel):
-    """Model representing a rule group for the Web ACL."""
-
-    name: str
-    cloudwatch_metrics_enabled: bool = False
-
-
 class WebAclv2(BaseModel):
     """Model representing a Web ACL for WAFv2."""

diff --git a/tests/providers/aws/services/wafv2/wafv2_webacl_with_rules/wafv2_webacl_with_rules_test.py b/tests/providers/aws/services/wafv2/wafv2_webacl_with_rules/wafv2_webacl_with_rules_test.py
index 6d476d0f9be..19cde553a77 100644
--- a/tests/providers/aws/services/wafv2/wafv2_webacl_with_rules/wafv2_webacl_with_rules_test.py
+++ b/tests/providers/aws/services/wafv2/wafv2_webacl_with_rules/wafv2_webacl_with_rules_test.py
@@ -1,10 +1,61 @@
 from unittest import mock
+from unittest.mock import patch

+import botocore
 from boto3 import client
 from moto import mock_aws

 from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider

+# Original botocore _make_api_call function
+orig = botocore.client.BaseClient._make_api_call
+
+FM_RG_NAME = "test-firewall-managed-rule-group"
+FM_RG_ARN = "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test-firewall-managed-rule-group"
+
+
+# Mocked botocore _make_api_call function
+def mock_make_api_call(self, operation_name, kwarg):
+    if operation_name == "ListWebACLs":
+        return {
+            "WebACLs": [
+                {
+                    "Name": FM_RG_NAME,
+                    "Id": FM_RG_NAME,
+                    "ARN": FM_RG_ARN,
+                }
+            ]
+        }
+    elif operation_name == "GetWebACL":
+        return {
+            "WebACL": {
+                "PostProcessFirewallManagerRuleGroups": [
+                    {
+                        "Name": FM_RG_NAME,
+                        "VisibilityConfig": {
+                            "SampledRequestsEnabled": True,
+                            "CloudWatchMetricsEnabled": True,
+                            "MetricName": "web-acl-test-metric",
+                        },
+                    }
+                ]
+            }
+        }
+    elif operation_name == "ListResourcesForWebACL":
+        return {
+            "ResourceArns": [
+                FM_RG_ARN,
+            ]
+        }
+    elif operation_name == "ListTagsForResource":
+        return {
+            "TagInfoForResource": {
+                "ResourceARN": FM_RG_ARN,
+                "TagList": [{"Key": "Name", "Value": FM_RG_NAME}],
+            }
+        }
+    return orig(self, operation_name, kwarg)
+

 class Test_wafv2_webacl_with_rules:
     @mock_aws
@@ -13,12 +64,15 @@ def test_no_web_acls(self):

         aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])

-        with mock.patch(
-            "prowler.providers.common.provider.Provider.get_global_provider",
-            return_value=aws_provider,
-        ), mock.patch(
-            "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client",
-            new=WAFv2(aws_provider),
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=aws_provider,
+            ),
+            mock.patch(
+                "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client",
+                new=WAFv2(aws_provider),
+            ),
         ):
             from prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules import (
                 wafv2_webacl_with_rules,
@@ -69,12 +123,15 @@ def test_wafv2_web_acl_with_rule(self):

         aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])

-        with mock.patch(
-            "prowler.providers.common.provider.Provider.get_global_provider",
-            return_value=aws_provider,
-        ), mock.patch(
-            "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client",
-            new=WAFv2(aws_provider),
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=aws_provider,
+            ),
+            mock.patch(
+                "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client",
+                new=WAFv2(aws_provider),
+            ),
         ):
             from prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules import (
                 wafv2_webacl_with_rules,
@@ -137,12 +194,15 @@ def test_wafv2_web_acl_with_rule_group(self):

         aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])

-        with mock.patch(
-            "prowler.providers.common.provider.Provider.get_global_provider",
-            return_value=aws_provider,
-        ), mock.patch(
-            "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client",
-            new=WAFv2(aws_provider),
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=aws_provider,
+            ),
+            mock.patch(
+                "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client",
+                new=WAFv2(aws_provider),
+            ),
         ):
             from prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules import (
                 wafv2_webacl_with_rules,
@@ -161,6 +221,43 @@ def test_wafv2_web_acl_with_rule_group(self):
             assert result[0].region == AWS_REGION_US_EAST_1
             assert result[0].resource_tags == [{"Key": "Name", "Value": waf_name}]

+    @patch(
+        "botocore.client.BaseClient._make_api_call",
+        new=mock_make_api_call,
+    )
+    @mock_aws
+    def test_wafv2_web_acl_with_firewall_manager_managed_rule_group(self):
+        from prowler.providers.aws.services.wafv2.wafv2_service import WAFv2
+
+        aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
+
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=aws_provider,
+            ),
+            mock.patch(
+                "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client",
+                new=WAFv2(aws_provider),
+            ),
+        ):
+            from prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules import (
+                wafv2_webacl_with_rules,
+            )
+
+            check = wafv2_webacl_with_rules()
+            result = check.execute()
+            assert len(result) == 1
+            assert result[0].status == "PASS"
+            assert (
+                result[0].status_extended
+                == f"AWS WAFv2 Web ACL {FM_RG_NAME} does have rules or rule groups attached."
+            )
+            assert result[0].resource_id == FM_RG_NAME
+            assert result[0].resource_arn == FM_RG_ARN
+            assert result[0].region == AWS_REGION_US_EAST_1
+            assert result[0].resource_tags == [{"Key": "Name", "Value": FM_RG_NAME}]
+
     @mock_aws
     def test_wafv2_web_acl_without_rule_or_rule_group(self):
         wafv2_client = client("wafv2", region_name=AWS_REGION_US_EAST_1)
@@ -184,12 +281,15 @@ def test_wafv2_web_acl_without_rule_or_rule_group(self):

         aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])

-        with mock.patch(
-            "prowler.providers.common.provider.Provider.get_global_provider",
-            return_value=aws_provider,
-        ), mock.patch(
-            "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client",
-            new=WAFv2(aws_provider),
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=aws_provider,
+            ),
+            mock.patch(
+                "prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules.wafv2_client",
+                new=WAFv2(aws_provider),
+            ),
         ):
             from prowler.providers.aws.services.wafv2.wafv2_webacl_with_rules.wafv2_webacl_with_rules import (
                 wafv2_webacl_with_rules,

False positive on `wafv2_webacl_with_rules` when when ACL provisioned with AWS Firewall Manager
### Steps to Reproduce

1. Depoloy ACL with AWS Firewall Manager including any rule group
2. Run prowler against AWS account where ACL is provisioned with Firewall Manager
3. Prowler will add that ACL as failed because the list of managed rules is empty, but it has AWS Firewall Manager provisioned rules

### Expected behavior

it's expected that ACL is managed centrally and may not have rules added directly

### Actual Result with Screenshots or Logs

![image](https://github.com/user-attachments/assets/bc4da90e-caf6-4748-9fd1-1cef8e4e41f4)

![image](https://github.com/user-attachments/assets/54640d49-5e58-44c6-9801-3c1bd1fe9343)

### How did you install Prowler?

From pip package (pip install prowler)

### Environment Resource

4. EKS

### OS used

1. Amazon Linux 2023

### Prowler version

5.0.0

### Pip version

24.0

### Context

_No response_

Hello @ivan-morhun , we will review it as soon as possible and get back to you once we have an update.

Thanks for using Prowler 🚀