Repository: tobymao/sqlglot

Issue:
Concerns about Translating SQLite Queries to TSQL
1. **Offset Issue:**
   
   When attempting to convert an SQLite query to TSQL using the provided code snippet:

   ```python
   import sqlglot

   sql_a = "SELECT name, (SELECT COUNT(*) FROM orders WHERE orders.customer_id = customers.id) AS order_count FROM customers LIMIT 5, 10"

   sql_b = sqlglot.transpile(sql_a, read="sqlite", write="tsql")[0]

   print(sql_b)
   ```

   The resulting TSQL output is as follows:

   ```sql
   SELECT TOP 10 name, (SELECT COUNT(*) FROM orders WHERE orders.customer_id = customers.id) AS order_count FROM customers
   ```

   It appears that the translated TSQL does not include information about the offset of 5. Instead, it only selects the top 10 rows, neglecting the fact that it should select 10 rows after an offset of 5. Could you please verify if this behavior is intended?

2. **Completeness of Translation:**

   I had a question about the thoroughness of the translation process from SQLite to TSQL. Does the transpiler cover all possible cases of SQLite queries that are incompatible with TSQL syntax? Are there any known clauses or scenarios where the transpiler fails to convert the syntax accurately?



Discussion:
Hey, thanks for the report. I'll take a look, this seems like a bug.

> I had a question about the thoroughness of the translation process from SQLite to TSQL. Does the transpiler cover all possible cases of SQLite queries that are incompatible with TSQL syntax? Are there any known clauses or scenarios where the transpiler fails to convert the syntax accurately?

The transpiler probably does not cover all possible cases as both dialects are huge and there may even be features that are not transpilable at all. It does cover a very large surface though, so I expect that for more "standard" queries you won't have any issues. There's no documentation about missing functionality, but feel free to file issues and / or PRs if you come across bugs in the future so we can improve.

Resolving patch:
diff --git a/sqlglot/dialects/tsql.py b/sqlglot/dialects/tsql.py
--- a/sqlglot/dialects/tsql.py
+++ b/sqlglot/dialects/tsql.py
@@ -810,6 +810,22 @@ class Generator(generator.Generator):
             exp.VolatileProperty: exp.Properties.Location.UNSUPPORTED,
         }
 
+        def select_sql(self, expression: exp.Select) -> str:
+            if expression.args.get("offset"):
+                if not expression.args.get("order"):
+                    # ORDER BY is required in order to use OFFSET in a query, so we use
+                    # a noop order by, since we don't really care about the order.
+                    # See: https://www.microsoftpressstore.com/articles/article.aspx?p=2314819
+                    expression.order_by(exp.select(exp.null()).subquery(), copy=False)
+
+                limit = expression.args.get("limit")
+                if isinstance(limit, exp.Limit):
+                    # TOP and OFFSET can't be combined, we need use FETCH instead of TOP
+                    # we replace here because otherwise TOP would be generated in select_sql
+                    limit.replace(exp.Fetch(direction="FIRST", count=limit.expression))
+
+            return super().select_sql(expression)
+
         def convert_sql(self, expression: exp.Convert) -> str:
             name = "TRY_CONVERT" if expression.args.get("safe") else "CONVERT"
             return self.func(
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -2125,24 +2125,13 @@ def matchrecognize_sql(self, expression: exp.MatchRecognize) -> str:
         return f"{self.seg('MATCH_RECOGNIZE')} {self.wrap(body)}{alias}"
 
     def query_modifiers(self, expression: exp.Expression, *sqls: str) -> str:
-        limit: t.Optional[exp.Fetch | exp.Limit] = expression.args.get("limit")
-
-        # If the limit is generated as TOP, we need to ensure it's not generated twice
-        with_offset_limit_modifiers = not isinstance(limit, exp.Limit) or not self.LIMIT_IS_TOP
+        limit = expression.args.get("limit")
 
         if self.LIMIT_FETCH == "LIMIT" and isinstance(limit, exp.Fetch):
             limit = exp.Limit(expression=exp.maybe_copy(limit.args.get("count")))
         elif self.LIMIT_FETCH == "FETCH" and isinstance(limit, exp.Limit):
             limit = exp.Fetch(direction="FIRST", count=exp.maybe_copy(limit.expression))
 
-        fetch = isinstance(limit, exp.Fetch)
-
-        offset_limit_modifiers = (
-            self.offset_limit_modifiers(expression, fetch, limit)
-            if with_offset_limit_modifiers
-            else []
-        )
-
         options = self.expressions(expression, key="options")
         if options:
             options = f" OPTION{self.wrap(options)}"
@@ -2159,7 +2148,7 @@ def query_modifiers(self, expression: exp.Expression, *sqls: str) -> str:
             self.sql(expression, "having"),
             *[gen(self, expression) for gen in self.AFTER_HAVING_MODIFIER_TRANSFORMS.values()],
             self.sql(expression, "order"),
-            *offset_limit_modifiers,
+            *self.offset_limit_modifiers(expression, isinstance(limit, exp.Fetch), limit),
             *self.after_limit_modifiers(expression),
             options,
             sep="",
@@ -2190,12 +2179,13 @@ def select_sql(self, expression: exp.Select) -> str:
         distinct = self.sql(expression, "distinct")
         distinct = f" {distinct}" if distinct else ""
         kind = self.sql(expression, "kind")
+
         limit = expression.args.get("limit")
-        top = (
-            self.limit_sql(limit, top=True)
-            if isinstance(limit, exp.Limit) and self.LIMIT_IS_TOP
-            else ""
-        )
+        if isinstance(limit, exp.Limit) and self.LIMIT_IS_TOP:
+            top = self.limit_sql(limit, top=True)
+            limit.pop()
+        else:
+            top = ""
 
         expressions = self.expressions(expression)
 


Test patch:
diff --git a/tests/dialects/test_tsql.py b/tests/dialects/test_tsql.py
--- a/tests/dialects/test_tsql.py
+++ b/tests/dialects/test_tsql.py
@@ -272,6 +272,28 @@ def test_tsql(self):
             "SELECT [x].[y] FROM foo",
         )
 
+        self.validate_all(
+            "SELECT * FROM t ORDER BY (SELECT NULL) OFFSET 2 ROWS",
+            read={
+                "postgres": "SELECT * FROM t OFFSET 2",
+            },
+            write={
+                "postgres": "SELECT * FROM t ORDER BY (SELECT NULL) NULLS FIRST OFFSET 2",
+                "tsql": "SELECT * FROM t ORDER BY (SELECT NULL) OFFSET 2 ROWS",
+            },
+        )
+        self.validate_all(
+            "SELECT * FROM t ORDER BY (SELECT NULL) OFFSET 5 ROWS FETCH FIRST 10 ROWS ONLY",
+            read={
+                "duckdb": "SELECT * FROM t LIMIT 10 OFFSET 5",
+                "sqlite": "SELECT * FROM t LIMIT 5, 10",
+                "tsql": "SELECT * FROM t ORDER BY (SELECT NULL) OFFSET 5 ROWS FETCH FIRST 10 ROWS ONLY",
+            },
+            write={
+                "duckdb": "SELECT * FROM t ORDER BY (SELECT NULL) NULLS FIRST LIMIT 10 OFFSET 5",
+                "sqlite": "SELECT * FROM t ORDER BY (SELECT NULL) LIMIT 10 OFFSET 5",
+            },
+        )
         self.validate_all(
             "SELECT CAST([a].[b] AS SMALLINT) FROM foo",
             write={