Repository: tobymao/sqlglot
Issue category: Bug Report

Issue:
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 */
```


Hints:

Patch:
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,


Test patch:
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