]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
MSSQL 2014 OFFSET/FETCH syntax support
authorElkin <elkin@raketa.im>
Wed, 5 Feb 2020 14:51:14 +0000 (09:51 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 7 Feb 2020 15:15:10 +0000 (10:15 -0500)
SQL Server OFFSET and FETCH keywords are now used for limit/offset, rather
than using a window function, for SQL Server versions 11 and higher. TOP is
still used for a query that features only LIMIT.   Pull request courtesy
Elkin.

Fixes: #5084
Closes: #5125
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/5125
Pull-request-sha: a45b7f73090d2053e3a7020d4e3d7fabb0c5627d

Change-Id: Id6a01ba30caac87d7d3d92c3903cdfd77fbcee5e

doc/build/changelog/unreleased_14/5084.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/mssql/base.py
test/dialect/mssql/test_compiler.py
test/sql/test_query.py

diff --git a/doc/build/changelog/unreleased_14/5084.rst b/doc/build/changelog/unreleased_14/5084.rst
new file mode 100644 (file)
index 0000000..97b44ae
--- /dev/null
@@ -0,0 +1,8 @@
+.. change::
+    :tags: change, mssql
+    :tickets: 5084
+
+    SQL Server OFFSET and FETCH keywords are now used for limit/offset, rather
+    than using a window function, for SQL Server versions 11 and higher. TOP is
+    still used for a query that features only LIMIT.   Pull request courtesy
+    Elkin.
index 94f6a3303a4947ef77669003b9f723057c4bd848..609a60f7cee1d4b9d76a196287d2c7aa14e0932d 100644 (file)
@@ -1635,8 +1635,45 @@ class MSSQLCompiler(compiler.SQLCompiler):
         return text
 
     def limit_clause(self, select, **kw):
-        # Limit in mssql is after the select keyword
-        return ""
+        """ MSSQL 2012 supports OFFSET/FETCH operators
+            Use it instead subquery with row_number
+
+        """
+
+        if self.dialect._supports_offset_fetch and (
+            (
+                not select._simple_int_limit
+                and select._limit_clause is not None
+            )
+            or (
+                select._offset_clause is not None
+                and not select._simple_int_offset
+                or select._offset
+            )
+        ):
+            # OFFSET are FETCH are options of the ORDER BY clause
+            if not select._order_by_clause.clauses:
+                raise exc.CompileError(
+                    "MSSQL requires an order_by when "
+                    "using an OFFSET or a non-simple "
+                    "LIMIT clause"
+                )
+
+            text = ""
+
+            if select._offset_clause is not None:
+                offset_str = self.process(select._offset_clause, **kw)
+            else:
+                offset_str = '0'
+            text += "\n OFFSET %s ROWS" % offset_str
+
+            if select._limit_clause is not None:
+                text += "\n FETCH NEXT %s ROWS ONLY " % self.process(
+                    select._limit_clause, **kw
+                )
+            return text
+        else:
+            return ""
 
     def visit_try_cast(self, element, **kw):
         return "TRY_CAST (%s AS %s)" % (
@@ -1647,9 +1684,10 @@ class MSSQLCompiler(compiler.SQLCompiler):
     def visit_select(self, select, **kwargs):
         """Look for ``LIMIT`` and OFFSET in a select statement, and if
         so tries to wrap it in a subquery with ``row_number()`` criterion.
+        MSSQL 2012 and above are excluded
 
         """
-        if (
+        if not self.dialect._supports_offset_fetch and (
             (not select._simple_int_limit and select._limit_clause is not None)
             or (
                 select._offset_clause is not None
@@ -2288,6 +2326,7 @@ class MSDialect(default.DefaultDialect):
     non_native_boolean_check_constraint = False
     supports_unicode_binds = True
     postfetch_lastrowid = True
+    _supports_offset_fetch = False
 
     server_version_info = ()
 
@@ -2436,6 +2475,9 @@ class MSDialect(default.DefaultDialect):
                 self.server_version_info >= MS_2012_VERSION
             )
 
+        self._supports_offset_fetch = (
+            self.server_version_info and self.server_version_info[0] >= 11)
+
     def _get_default_schema_name(self, connection):
         if self.server_version_info < MS_2005_VERSION:
             return self.schema_name
index bb5199b00d65a7a647c6380d92286a35eeab531c..07ceb5bf5b78c4455b7a73af322aeff4ba670ddc 100644 (file)
@@ -844,6 +844,29 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
         assert t.c.x in set(c._create_result_map()["x"][1])
         assert t.c.y in set(c._create_result_map()["y"][1])
 
+    def test_limit_offset_using_offset_fetch(self):
+        t = table("t", column("x", Integer), column("y", Integer))
+        dialect_2012 = mssql.base.MSDialect()
+        dialect_2012._supports_offset_fetch = True
+
+        s = select([t]).where(t.c.x == 5).order_by(t.c.y).limit(10).offset(20)
+
+        self.assert_compile(
+            s,
+            "SELECT t.x, t.y "
+            "FROM t "
+            "WHERE t.x = :x_1 ORDER BY t.y "
+            "OFFSET :param_1 ROWS "
+            "FETCH NEXT :param_2 ROWS ONLY ",
+            checkparams={"param_1": 20, "param_2": 10, "x_1": 5},
+            dialect=dialect_2012
+        )
+
+        c = s.compile(dialect=dialect_2012)
+        eq_(len(c._result_columns), 2)
+        assert t.c.x in set(c._create_result_map()["x"][1])
+        assert t.c.y in set(c._create_result_map()["y"][1])
+
     def test_limit_offset_w_ambiguous_cols(self):
         t = table("t", column("x", Integer), column("y", Integer))
 
index 9e56b6489fe0758d876e466faa1bb2b7c73144b3..ce57260b7cfc8aca9782a9a8000066bd8dd08950 100644 (file)
@@ -1013,7 +1013,6 @@ class LimitTest(fixtures.TestBase):
         self.assert_(r[0] != r[1] and r[1] != r[2], repr(r))
 
     @testing.requires.offset
-    @testing.fails_on("mssql", "FIXME: unknown")
     def test_select_distinct_offset(self):
         """Test the interaction between distinct and offset"""