]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- move LIMIT/OFFSET rendering to be as bind parameters, for all backends
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 29 Aug 2010 20:35:02 +0000 (16:35 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 29 Aug 2010 20:35:02 +0000 (16:35 -0400)
which support it.  This includes SQLite, MySQL, Postgresql, Firebird,
Oracle (already used binds with ROW NUMBER OVER), MSSQL (when ROW NUMBER
is used, not TOP).   Not included are Informix, Sybase, MaxDB, Access
[ticket:805]
- LIMIT/OFFSET parameters need to stay as literals within SQL
constructs.  This because they may not be renderable as binds on
some backends.

14 files changed:
lib/sqlalchemy/dialects/firebird/base.py
lib/sqlalchemy/dialects/maxdb/base.py
lib/sqlalchemy/dialects/mssql/base.py
lib/sqlalchemy/dialects/mysql/base.py
lib/sqlalchemy/dialects/postgresql/base.py
lib/sqlalchemy/dialects/sqlite/base.py
lib/sqlalchemy/dialects/sybase/base.py
lib/sqlalchemy/sql/compiler.py
test/dialect/test_mysql.py
test/orm/inheritance/test_query.py
test/orm/test_eager_relations.py
test/orm/test_query.py
test/sql/test_compiler.py
test/sql/test_generative.py

index da8bef8c04e5c199f741675154a559770e27580a..04439afb9b472fa6d46d05b0a50572c1a86a5e39 100644 (file)
@@ -263,9 +263,9 @@ class FBCompiler(sql.compiler.SQLCompiler):
 
         result = ""
         if select._limit:
-            result += "FIRST %d "  % select._limit
+            result += "FIRST %s "  % self.process(sql.literal(select._limit))
         if select._offset:
-            result +="SKIP %d "  %  select._offset
+            result +="SKIP %s "  %  self.process(sql.literal(select._offset))
         if select._distinct:
             result += "DISTINCT "
         return result
index 487edc2ca7d92012e1d93ab812d3f2f761c45e03..9a1e10f517fa87a0976426e03c41b061c90888a4 100644 (file)
@@ -603,6 +603,7 @@ class MaxDBCompiler(compiler.SQLCompiler):
     def limit_clause(self, select):
         # The docs say offsets are supported with LIMIT.  But they're not.
         # TODO: maybe emulate by adding a ROWNO/ROWNUM predicate?
+        # TODO: does MaxDB support bind params for LIMIT / TOP ?
         if self.is_subquery():
             # sub queries need TOP
             return ''
index 88ca36dbdf9c474f39dcbb22374a445c6191eb77..089d2f71d6c6b4df2f3b9059181d2e4ce0f9f61d 100644 (file)
@@ -706,9 +706,12 @@ class MSSQLCompiler(compiler.SQLCompiler):
         if select._distinct or select._limit:
             s = select._distinct and "DISTINCT " or ""
             
+            # ODBC drivers and possibly others
+            # don't support bind params in the SELECT clause on SQL Server.
+            # so have to use literal here.
             if select._limit:
                 if not select._offset:
-                    s += "TOP %s " % (select._limit,)
+                    s += "TOP %d " % select._limit
             return s
         return compiler.SQLCompiler.get_select_precolumns(self, select)
 
@@ -738,10 +741,10 @@ class MSSQLCompiler(compiler.SQLCompiler):
 
             limitselect = sql.select([c for c in select.c if
                                         c.key!='mssql_rn'])
-            limitselect.append_whereclause("mssql_rn>%d" % _offset)
+            limitselect.append_whereclause("mssql_rn>%s" % self.process(sql.literal(_offset)))
             if _limit is not None:
-                limitselect.append_whereclause("mssql_rn<=%d" % 
-                                            (_limit + _offset))
+                limitselect.append_whereclause("mssql_rn<=%s" % 
+                                            (self.process(sql.literal(_limit + _offset))))
             return self.process(limitselect, iswrapper=True, **kwargs)
         else:
             return compiler.SQLCompiler.visit_select(self, select, **kwargs)
index a2d3748f330d54f816b0887521bc4d3b22b5907f..d526d74e8ca66ba47bc2b9927a1114304732b181 100644 (file)
@@ -1226,10 +1226,12 @@ class MySQLCompiler(compiler.SQLCompiler):
             # artificial limit if one wasn't provided
             if limit is None:
                 limit = 18446744073709551615
-            return ' \n LIMIT %s, %s' % (offset, limit)
+            return ' \n LIMIT %s, %s' % (
+                                self.process(sql.literal(offset)), 
+                                self.process(sql.literal(limit)))
         else:
             # No offset provided, so just use the limit
-            return ' \n LIMIT %s' % (limit,)
+            return ' \n LIMIT %s' % (self.process(sql.literal(limit)),)
 
     def visit_update(self, update_stmt):
         self.stack.append({'from': set([update_stmt.table])})
index 89769b8c0a66f151394d01b47b544fa50658d936..768fbcb4db2bc0e01d635588caf781d3ab5ac588 100644 (file)
@@ -374,11 +374,11 @@ class PGCompiler(compiler.SQLCompiler):
     def limit_clause(self, select):
         text = ""
         if select._limit is not None:
-            text +=  " \n LIMIT " + str(select._limit)
+            text +=  " \n LIMIT " + self.process(sql.literal(select._limit))
         if select._offset is not None:
             if select._limit is None:
                 text += " \n LIMIT ALL"
-            text += " OFFSET " + str(select._offset)
+            text += " OFFSET " + self.process(sql.literal(select._offset))
         return text
 
     def get_select_precolumns(self, select):
index b84b18e68e01326dbe8a311fc86737b958455f9a..7bd6d51f3089dfa4ba5c035a2ac88f684787f21c 100644 (file)
@@ -222,13 +222,13 @@ class SQLiteCompiler(compiler.SQLCompiler):
     def limit_clause(self, select):
         text = ""
         if select._limit is not None:
-            text +=  " \n LIMIT " + str(select._limit)
+            text +=  "\n LIMIT " + self.process(sql.literal(select._limit))
         if select._offset is not None:
             if select._limit is None:
-                text += " \n LIMIT -1"
-            text += " OFFSET " + str(select._offset)
+                text += "\n LIMIT " + self.process(sql.literal(-1))
+            text += " OFFSET " + self.process(sql.literal(select._offset))
         else:
-            text += " OFFSET 0"
+            text += " OFFSET " + self.process(sql.literal(0))
         return text
 
     def for_update_clause(self, select):
index b0b1bbff4d167a0385cce967a3eb6c1127020f02..e3d0d582dd61f654da994b4cee8ade9af6810b68 100644 (file)
@@ -271,6 +271,8 @@ class SybaseSQLCompiler(compiler.SQLCompiler):
 
     def get_select_precolumns(self, select):
         s = select._distinct and "DISTINCT " or ""
+        # TODO: don't think Sybase supports
+        # bind params for FIRST / TOP
         if select._limit:
             #if select._limit == 1:
                 #s += "FIRST "
index fcff5e35500494d31267cb60ca88b95c612b5b80..584e43a889f39f41050f350550f808b7961d3274 100644 (file)
@@ -808,11 +808,11 @@ class SQLCompiler(engine.Compiled):
     def limit_clause(self, select):
         text = ""
         if select._limit is not None:
-            text +=  " \n LIMIT " + str(select._limit)
+            text +=  "\n LIMIT " + self.process(sql.literal(select._limit))
         if select._offset is not None:
             if select._limit is None:
-                text += " \n LIMIT -1"
-            text += " OFFSET " + str(select._offset)
+                text += "\n LIMIT -1"
+            text += " OFFSET " + self.process(sql.literal(select._offset))
         return text
 
     def visit_table(self, table, asfrom=False, ashint=False, fromhints=None, **kwargs):
index 7c4cc2309f05f51321b1ab7647f34e2e6cc06d4b..0701c46abfd214bfd258756e52a6fb356d719506 100644 (file)
@@ -1046,14 +1046,18 @@ class SQLTest(TestBase, AssertsCompiledSQL):
 
         self.assert_compile(
             select([t]).limit(10).offset(20),
-            "SELECT t.col1, t.col2 FROM t  LIMIT 20, 10"
+            "SELECT t.col1, t.col2 FROM t  LIMIT %s, %s",
+            {'param_1':20, 'param_2':10}
             )
         self.assert_compile(
             select([t]).limit(10),
-            "SELECT t.col1, t.col2 FROM t  LIMIT 10")
+            "SELECT t.col1, t.col2 FROM t  LIMIT %s", 
+            {'param_1':10})
+            
         self.assert_compile(
             select([t]).offset(10),
-            "SELECT t.col1, t.col2 FROM t  LIMIT 10, 18446744073709551615"
+            "SELECT t.col1, t.col2 FROM t  LIMIT %s, %s",
+            {'param_1':10, 'param_2':18446744073709551615}
             )
     
     def test_varchar_raise(self):
index 9e944ca6fd81c8ae37c6472b08782feeb955df69..900337cf11a0171cdb06c344ec6eb42ca525975e 100644 (file)
@@ -1191,11 +1191,12 @@ class SelfReferentialM2MTest(_base.MappedTest, AssertsCompiledSQL):
         "anon_1.parent_cls AS anon_1_parent_cls, anon_2.parent_id AS anon_2_parent_id, "\
         "anon_2.child2_id AS anon_2_child2_id, anon_2.parent_cls AS anon_2_parent_cls FROM "\
         "(SELECT parent.id AS parent_id, child1.id AS child1_id, parent.cls AS parent_cls FROM parent "\
-        "JOIN child1 ON parent.id = child1.id  LIMIT 1) AS anon_1 LEFT OUTER JOIN secondary AS secondary_1 "\
+        "JOIN child1 ON parent.id = child1.id LIMIT :param_1) AS anon_1 LEFT OUTER JOIN secondary AS secondary_1 "\
         "ON anon_1.parent_id = secondary_1.right_id LEFT OUTER JOIN (SELECT parent.id AS parent_id, "\
         "parent.cls AS parent_cls, child2.id AS child2_id FROM parent JOIN child2 ON parent.id = child2.id) "\
-        "AS anon_2 ON anon_2.parent_id = secondary_1.left_id"
-        , dialect=default.DefaultDialect())
+        "AS anon_2 ON anon_2.parent_id = secondary_1.left_id",
+        {'param_1':1},
+        dialect=default.DefaultDialect())
 
         # another way to check
         assert q.limit(1).with_labels().subquery().count().scalar() == 1
index b96014a575257c9289261e2081fe2a2e1f766bca..9ac214df49875a52c5859394edcadd0dd0968cb8 100644 (file)
@@ -671,8 +671,9 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             "orders_1_address_id, orders_1.description AS orders_1_description, orders_1.isopen AS orders_1_isopen "
             "FROM (SELECT users.id AS users_id, users.name AS users_name "
             "FROM users "
-            " LIMIT 10) AS anon_1 LEFT OUTER JOIN orders AS orders_1 ON anon_1.users_id = orders_1.user_id"
-            ,use_default_dialect=True
+            "LIMIT :param_1) AS anon_1 LEFT OUTER JOIN orders AS orders_1 ON anon_1.users_id = orders_1.user_id",
+            {'param_1':10},
+            use_default_dialect=True
         )
 
         self.assert_compile(
@@ -680,8 +681,9 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             "SELECT orders.id AS orders_id, orders.user_id AS orders_user_id, orders.address_id AS "
             "orders_address_id, orders.description AS orders_description, orders.isopen AS orders_isopen, "
             "users_1.id AS users_1_id, users_1.name AS users_1_name FROM orders LEFT OUTER JOIN users AS "
-            "users_1 ON users_1.id = orders.user_id  LIMIT 10"
-            ,use_default_dialect=True
+            "users_1 ON users_1.id = orders.user_id LIMIT :param_1",
+            {'param_1':10},
+            use_default_dialect=True
         )
 
         self.assert_compile(
@@ -689,8 +691,9 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             "SELECT orders.id AS orders_id, orders.user_id AS orders_user_id, orders.address_id AS "
             "orders_address_id, orders.description AS orders_description, orders.isopen AS orders_isopen, "
             "users_1.id AS users_1_id, users_1.name AS users_1_name FROM orders JOIN users AS "
-            "users_1 ON users_1.id = orders.user_id  LIMIT 10"
-            ,use_default_dialect=True
+            "users_1 ON users_1.id = orders.user_id LIMIT :param_1",
+            {'param_1':10},
+            use_default_dialect=True
         )
 
         self.assert_compile(
@@ -700,10 +703,11 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             "addresses_1.email_address AS addresses_1_email_address, orders_1.id AS orders_1_id, "
             "orders_1.user_id AS orders_1_user_id, orders_1.address_id AS orders_1_address_id, "
             "orders_1.description AS orders_1_description, orders_1.isopen AS orders_1_isopen FROM "
-            "(SELECT users.id AS users_id, users.name AS users_name FROM users  LIMIT 10) AS anon_1 "
+            "(SELECT users.id AS users_id, users.name AS users_name FROM users LIMIT :param_1) AS anon_1 "
             "LEFT OUTER JOIN orders AS orders_1 ON anon_1.users_id = orders_1.user_id LEFT OUTER JOIN "
-            "addresses AS addresses_1 ON addresses_1.id = orders_1.address_id"
-            ,use_default_dialect=True
+            "addresses AS addresses_1 ON addresses_1.id = orders_1.address_id",
+            {'param_1':10},
+            use_default_dialect=True
         )
 
         self.assert_compile(
@@ -730,9 +734,10 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
             "orders_1.description AS orders_1_description, orders_1.isopen AS orders_1_isopen "
             "FROM (SELECT users.id AS users_id, users.name AS users_name "
             "FROM users "
-            " LIMIT 10) AS anon_1 LEFT OUTER JOIN orders AS orders_1 ON anon_1.users_id = "
-            "orders_1.user_id JOIN addresses AS addresses_1 ON addresses_1.id = orders_1.address_id"
-            ,use_default_dialect=True
+            "LIMIT :param_1) AS anon_1 LEFT OUTER JOIN orders AS orders_1 ON anon_1.users_id = "
+            "orders_1.user_id JOIN addresses AS addresses_1 ON addresses_1.id = orders_1.address_id",
+            {'param_1':10},
+            use_default_dialect=True
         )
         
     @testing.resolve_artifact_names
@@ -1331,7 +1336,7 @@ class SelfReferentialEagerTest(_base.MappedTest):
         self.assert_sql_execution(testing.db, go, 
             CompiledSQL(
                 "SELECT nodes.id AS nodes_id, nodes.parent_id AS nodes_parent_id, nodes.data AS nodes_data FROM nodes "
-                "WHERE nodes.data = :data_1 ORDER BY nodes.id  LIMIT 1 OFFSET 0",
+                "WHERE nodes.data = :data_1 ORDER BY nodes.id LIMIT :param_1 OFFSET :param_2",
                 {'data_1': 'n1'}
             )
         )
index cc2f046cd38feb6c195596d48bb00011ac8fd83f..65be1e00a9816dfee3f6346de8ceeb573d68caba 100644 (file)
@@ -725,7 +725,7 @@ class SliceTest(QueryTest):
 
         assert create_session().query(User).filter(User.id==27).first() is None
 
-    @testing.fails_on_everything_except('sqlite')
+    @testing.only_on('sqlite', 'testing execution but db-specific syntax')
     def test_limit_offset_applies(self):
         """Test that the expected LIMIT/OFFSET is applied for slices.
         
@@ -738,15 +738,15 @@ class SliceTest(QueryTest):
         q = sess.query(User)
         
         self.assert_sql(testing.db, lambda: q[10:20], [
-            ("SELECT users.id AS users_id, users.name AS users_name FROM users  LIMIT 10 OFFSET 10", {})
+            ("SELECT users.id AS users_id, users.name AS users_name FROM users LIMIT :param_1 OFFSET :param_2", {'param_1':10, 'param_2':10})
         ])
 
         self.assert_sql(testing.db, lambda: q[:20], [
-            ("SELECT users.id AS users_id, users.name AS users_name FROM users  LIMIT 20 OFFSET 0", {})
+            ("SELECT users.id AS users_id, users.name AS users_name FROM users LIMIT :param_1 OFFSET :param_2", {'param_1':20, 'param_2':0})
         ])
 
         self.assert_sql(testing.db, lambda: q[5:], [
-            ("SELECT users.id AS users_id, users.name AS users_name FROM users  LIMIT -1 OFFSET 5", {})
+            ("SELECT users.id AS users_id, users.name AS users_name FROM users LIMIT :param_1 OFFSET :param_2", {'param_1':-1, 'param_2':5})
         ])
 
         self.assert_sql(testing.db, lambda: q[2:2], [])
@@ -3721,7 +3721,8 @@ class SelfReferentialTest(_base.MappedTest, AssertsCompiledSQL):
             "nodes_1.id = nodes.parent_id JOIN nodes AS nodes_2 "
             "ON nodes_2.id = nodes_1.parent_id "
             "WHERE nodes.data = :data_1 AND nodes_1.data = :data_2 AND "
-            "nodes_2.data = :data_3) AS anon_1  LIMIT 1",
+            "nodes_2.data = :data_3) AS anon_1 LIMIT :param_1",
+            {'param_1':1},
             use_default_dialect=True
         )
         
index b7e5d0953137c34bcba73771f0ecf065d770bb8d..09432e1d42d78b4c9654898ba7122169c6cb4f01 100644 (file)
@@ -1271,7 +1271,6 @@ sq.myothertable_othername AS sq_myothertable_othername FROM (" + sqstring + ") A
 
         assert u1.corresponding_column(table2.c.otherid) is u1.c.myid
         
-        # TODO - why is there an extra space before the LIMIT ?
         self.assert_compile(
             union(
                 select([table1.c.myid, table1.c.name]),
@@ -1282,7 +1281,8 @@ sq.myothertable_othername AS sq_myothertable_othername FROM (" + sqstring + ") A
             ),
             "SELECT mytable.myid, mytable.name "
             "FROM mytable UNION SELECT myothertable.otherid, myothertable.othername "
-            "FROM myothertable ORDER BY myid  LIMIT 5 OFFSET 10"
+            "FROM myothertable ORDER BY myid LIMIT :param_1 OFFSET :param_2",
+            {'param_1':5, 'param_2':10}
         )
 
         self.assert_compile(
@@ -1330,7 +1330,9 @@ sq.myothertable_othername AS sq_myothertable_othername FROM (" + sqstring + ") A
         # self_group() is honored
         self.assert_compile(
             union(s.order_by("foo").self_group(), s.order_by("bar").limit(10).self_group()), 
-            "(SELECT foo, bar ORDER BY foo) UNION (SELECT foo, bar ORDER BY bar  LIMIT 10)"
+            "(SELECT foo, bar ORDER BY foo) UNION (SELECT foo, bar ORDER BY bar LIMIT :param_1)",
+            {'param_1':10}
+            
         )
         
     def test_compound_grouping(self):
@@ -1661,7 +1663,8 @@ sq.myothertable_othername AS sq_myothertable_othername FROM (" + sqstring + ") A
             ),
             "SELECT mytable.myid, mytable.name, mytable.description, myothertable.otherid, myothertable.othername FROM mytable "\
             "JOIN myothertable ON mytable.myid = myothertable.otherid WHERE myothertable.otherid IN (SELECT myothertable.otherid "\
-            "FROM myothertable ORDER BY myothertable.othername  LIMIT 10) ORDER BY mytable.myid"
+            "FROM myothertable ORDER BY myothertable.othername LIMIT :param_1) ORDER BY mytable.myid",
+            {'param_1':10}
         )
 
     def test_tuple(self):
index 5457c7a7974e3029662d4e0e649d2c6ac9b2efe3..26f9c1ad0a94c81d9b98fe2315507822549ce54c 100644 (file)
@@ -621,22 +621,27 @@ class ClauseAdapterTest(TestBase, AssertsCompiledSQL):
         s2 = select([s1]).limit(5).offset(10).alias()
 
         self.assert_compile(sql_util.ClauseAdapter(s2).traverse(s1),
-            "SELECT foo.col1, foo.col2, foo.col3 FROM (SELECT table1.col1 AS col1, table1.col2 AS col2, table1.col3 AS col3 FROM table1) AS foo  LIMIT 5 OFFSET 10")
+            "SELECT foo.col1, foo.col2, foo.col3 FROM (SELECT table1.col1 AS col1, table1.col2 AS col2, table1.col3 AS col3 FROM table1) AS foo LIMIT :param_1 OFFSET :param_2",
+            {'param_1':5, 'param_2':10}
+            )
 
         j = s1.outerjoin(t2, s1.c.col1==t2.c.col1)
         self.assert_compile(sql_util.ClauseAdapter(s2).traverse(j).select(),
             "SELECT anon_1.col1, anon_1.col2, anon_1.col3, table2.col1, table2.col2, table2.col3 FROM "\
             "(SELECT foo.col1 AS col1, foo.col2 AS col2, foo.col3 AS col3 FROM "\
-            "(SELECT table1.col1 AS col1, table1.col2 AS col2, table1.col3 AS col3 FROM table1) AS foo  LIMIT 5 OFFSET 10) AS anon_1 "\
-            "LEFT OUTER JOIN table2 ON anon_1.col1 = table2.col1")
+            "(SELECT table1.col1 AS col1, table1.col2 AS col2, table1.col3 AS col3 FROM table1) AS foo LIMIT :param_1 OFFSET :param_2) AS anon_1 "\
+            "LEFT OUTER JOIN table2 ON anon_1.col1 = table2.col1",
+            {'param_1':5, 'param_2':10}
+            )
 
         talias = t1.alias('bar')
         j = s1.outerjoin(talias, s1.c.col1==talias.c.col1)
         self.assert_compile(sql_util.ClauseAdapter(s2).traverse(j).select(),
             "SELECT anon_1.col1, anon_1.col2, anon_1.col3, bar.col1, bar.col2, bar.col3 FROM "\
             "(SELECT foo.col1 AS col1, foo.col2 AS col2, foo.col3 AS col3 FROM "\
-            "(SELECT table1.col1 AS col1, table1.col2 AS col2, table1.col3 AS col3 FROM table1) AS foo  LIMIT 5 OFFSET 10) AS anon_1 "\
-            "LEFT OUTER JOIN table1 AS bar ON anon_1.col1 = bar.col1")
+            "(SELECT table1.col1 AS col1, table1.col2 AS col2, table1.col3 AS col3 FROM table1) AS foo LIMIT :param_1 OFFSET :param_2) AS anon_1 "\
+            "LEFT OUTER JOIN table1 AS bar ON anon_1.col1 = bar.col1",
+            {'param_1':5, 'param_2':10})
     
     def test_functions(self):
         self.assert_compile(sql_util.ClauseAdapter(t1.alias()).traverse(func.count(t1.c.col1)), "count(table1_1.col1)")