Repository: tobymao/sqlglot

Issue:
SELECT TOP 10 PERCENT doesn't  parse
In T-SQL you can specify PERCENT in TOP expression, e.g. `SELECT TOP  10  PERCENT ...<rest of the query>`.

Here is a [documentation link](https://learn.microsoft.com/en-us/sql/t-sql/queries/top-transact-sql?view=sql-server-ver16) for this.

This currently doesn't parse in SQLGlot, either with dialect="tsql" or with no dialect specified.

The simplest query demonstrating this is `SELECT 10 PERCENT 1;`. 

Thank you for an absolutely wonderful library, BTW - truly a pleasure to work with!

Discussion:

Solution patch:
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index 59a803298e..e1558eea5e 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -2357,8 +2357,7 @@ class Fetch(Expression):
     arg_types = {
         "direction": False,
         "count": False,
-        "percent": False,
-        "with_ties": False,
+        "limit_options": False,
     }
 
 
@@ -2400,7 +2399,21 @@ class Lambda(Expression):
 
 
 class Limit(Expression):
-    arg_types = {"this": False, "expression": True, "offset": False, "expressions": False}
+    arg_types = {
+        "this": False,
+        "expression": True,
+        "offset": False,
+        "limit_options": False,
+        "expressions": False,
+    }
+
+
+class LimitOptions(Expression):
+    arg_types = {
+        "percent": False,
+        "rows": False,
+        "with_ties": False,
+    }
 
 
 class Literal(Condition):
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index c58c7fa37b..bb32c9658a 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -1488,10 +1488,17 @@ def fetch_sql(self, expression: exp.Fetch) -> str:
         direction = f" {direction}" if direction else ""
         count = self.sql(expression, "count")
         count = f" {count}" if count else ""
-        if expression.args.get("percent"):
-            count = f"{count} PERCENT"
-        with_ties_or_only = "WITH TIES" if expression.args.get("with_ties") else "ONLY"
-        return f"{self.seg('FETCH')}{direction}{count} ROWS {with_ties_or_only}"
+        limit_options = self.sql(expression, "limit_options")
+        limit_options = f"{limit_options}" if limit_options else " ROWS ONLY"
+        return f"{self.seg('FETCH')}{direction}{count}{limit_options}"
+
+    def limitoptions_sql(self, expression: exp.LimitOptions) -> str:
+        percent = " PERCENT" if expression.args.get("percent") else ""
+        rows = " ROWS" if expression.args.get("rows") else ""
+        with_ties = " WITH TIES" if expression.args.get("with_ties") else ""
+        if not with_ties and rows:
+            with_ties = " ONLY"
+        return f"{percent}{rows}{with_ties}"
 
     def filter_sql(self, expression: exp.Filter) -> str:
         if self.AGGREGATE_FILTER_SUPPORTED:
@@ -2282,9 +2289,10 @@ def limit_sql(self, expression: exp.Limit, top: bool = False) -> str:
         args_sql = ", ".join(self.sql(e) for e in args)
         args_sql = f"({args_sql})" if top and any(not e.is_number for e in args) else args_sql
         expressions = self.expressions(expression, flat=True)
+        limit_options = self.sql(expression, "limit_options")
         expressions = f" BY {expressions}" if expressions else ""
 
-        return f"{this}{self.seg('TOP' if top else 'LIMIT')} {args_sql}{expressions}"
+        return f"{this}{self.seg('TOP' if top else 'LIMIT')} {args_sql}{limit_options}{expressions}"
 
     def offset_sql(self, expression: exp.Offset) -> str:
         this = self.sql(expression, "this")
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index ba3fd8288d..d55377f3bc 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -4466,6 +4466,13 @@ def _parse_ordered(
             exp.Ordered, this=this, desc=desc, nulls_first=nulls_first, with_fill=with_fill
         )
 
+    def _parse_limit_options(self) -> exp.LimitOptions:
+        percent = self._match(TokenType.PERCENT)
+        rows = self._match_set((TokenType.ROW, TokenType.ROWS))
+        self._match_text_seq("ONLY")
+        with_ties = self._match_text_seq("WITH", "TIES")
+        return self.expression(exp.LimitOptions, percent=percent, rows=rows, with_ties=with_ties)
+
     def _parse_limit(
         self,
         this: t.Optional[exp.Expression] = None,
@@ -4480,7 +4487,10 @@ def _parse_limit(
 
                 if limit_paren:
                     self._match_r_paren()
+
+                limit_options = self._parse_limit_options()
             else:
+                limit_options = None
                 expression = self._parse_term()
 
             if self._match(TokenType.COMMA):
@@ -4495,6 +4505,7 @@ def _parse_limit(
                 expression=expression,
                 offset=offset,
                 comments=comments,
+                limit_options=limit_options,
                 expressions=self._parse_limit_by(),
             )
 
@@ -4505,22 +4516,12 @@ def _parse_limit(
             direction = self._prev.text.upper() if direction else "FIRST"
 
             count = self._parse_field(tokens=self.FETCH_TOKENS)
-            percent = self._match(TokenType.PERCENT)
-
-            self._match_set((TokenType.ROW, TokenType.ROWS))
-
-            only = self._match_text_seq("ONLY")
-            with_ties = self._match_text_seq("WITH", "TIES")
-
-            if only and with_ties:
-                self.raise_error("Cannot specify both ONLY and WITH TIES in FETCH clause")
 
             return self.expression(
                 exp.Fetch,
                 direction=direction,
                 count=count,
-                percent=percent,
-                with_ties=with_ties,
+                limit_options=self._parse_limit_options(),
             )
 
         return this


Test patch:
diff --git a/tests/dialects/test_tsql.py b/tests/dialects/test_tsql.py
index 4ad14fd2dc..bafd4eca4f 100644
--- a/tests/dialects/test_tsql.py
+++ b/tests/dialects/test_tsql.py
@@ -1705,6 +1705,8 @@ def test_top(self):
                 "spark": "SELECT * FROM A LIMIT 3",
             },
         )
+        self.validate_identity("SELECT TOP 10 PERCENT")
+        self.validate_identity("SELECT TOP 10 PERCENT WITH TIES")
 
     def test_format(self):
         self.validate_identity("SELECT FORMAT(foo, 'dddd', 'de-CH')")