]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Emit deprecation warnings for plain text under Session
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 9 Dec 2020 16:39:45 +0000 (11:39 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 11 Dec 2020 20:39:46 +0000 (15:39 -0500)
Deprecation warnings are emitted under "SQLALCHEMY_WARN_20" mode when
passing a plain string to :meth:`_orm.Session.execute`.

It was also considered to have DDL string expressions to include
this as well, however this leaves us with no backwards-compatible
way of handling reflection of elemens, such as an Index() which
reflects "postgresql_where='x > 5'", there's no place for a rule
that will turn those into text() within the reflection process
that would be separate from when the user passes postgresql_where
to the Index.  Not worth it right now.

Fixes: #5754
Change-Id: I8673a79f0e87de0df576b655f39dad0351725ca8

doc/build/changelog/unreleased_14/5754.rst [new file with mode: 0644]
lib/sqlalchemy/sql/coercions.py
test/dialect/postgresql/test_compiler.py
test/orm/test_backref_mutations.py
test/orm/test_deprecations.py
test/orm/test_manytomany.py
test/orm/test_session.py
test/orm/test_transaction.py
test/sql/test_roles.py

diff --git a/doc/build/changelog/unreleased_14/5754.rst b/doc/build/changelog/unreleased_14/5754.rst
new file mode 100644 (file)
index 0000000..3e0e938
--- /dev/null
@@ -0,0 +1,7 @@
+.. change::
+    :tags: bug, sql
+    :tickets: 5754
+
+    Deprecation warnings are emitted under "SQLALCHEMY_WARN_20" mode when
+    passing a plain string to :meth:`_orm.Session.execute`.
+
index 9b3acf5ad7295b5504e90f6f1b8a39c016454fe7..bdd807438ce81a70100ab1d188bc396395b54f3e 100644 (file)
@@ -682,6 +682,10 @@ class DDLExpressionImpl(_Deannotate, _CoerceLiterals, RoleImpl):
     _coerce_consts = True
 
     def _text_coercion(self, element, argname=None):
+        # see #5754 for why we can't easily deprecate this coercion.
+        # essentially expressions like postgresql_where would have to be
+        # text() as they come back from reflection and we don't want to
+        # have text() elements wired into the inspection dictionaries.
         return elements.TextClause(element)
 
 
@@ -769,21 +773,6 @@ class StatementImpl(_NoTextCoercion, RoleImpl):
 class CoerceTextStatementImpl(_CoerceLiterals, RoleImpl):
     __slots__ = ()
 
-    def _dont_literal_coercion(self, element, **kw):
-        if callable(element) and hasattr(element, "__code__"):
-            return lambdas.StatementLambdaElement(
-                element,
-                self._role_class,
-                additional_cache_criteria=kw.get(
-                    "additional_cache_criteria", ()
-                ),
-                tracked=kw["tra"],
-            )
-        else:
-            return super(CoerceTextStatementImpl, self)._literal_coercion(
-                element, **kw
-            )
-
     def _implicit_coercions(
         self, original_element, resolved, argname=None, **kw
     ):
@@ -795,8 +784,12 @@ class CoerceTextStatementImpl(_CoerceLiterals, RoleImpl):
             )
 
     def _text_coercion(self, element, argname=None):
-        # TODO: this should emit deprecation warning,
-        # see deprecation warning in engine/base.py execute()
+        util.warn_deprecated_20(
+            "Using plain strings to indicate SQL statements without using "
+            "the text() construct is  "
+            "deprecated and will be removed in version 2.0.  Ensure plain "
+            "SQL statements are passed using the text() construct."
+        )
         return elements.TextClause(element)
 
 
index 6dc782e8ef4f088b28120eb648f6bbd484f7dddd..1763b210b2bd1df28aedce46791b10d8ee633786 100644 (file)
@@ -458,7 +458,7 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
         idx3 = Index(
             "test_idx2",
             tbl.c.data,
-            postgresql_where="data > 'a' AND data < 'b''s'",
+            postgresql_where=text("data > 'a' AND data < 'b''s'"),
         )
         self.assert_compile(
             schema.CreateIndex(idx3),
index 8647ddb785d56da172aa6c883a982fecce5df8ff..c873f46c70cc00b085482503167c92edf1f54870 100644 (file)
@@ -10,6 +10,7 @@ UPDATE in the database.
 """
 
 from sqlalchemy import testing
+from sqlalchemy import text
 from sqlalchemy.orm import attributes
 from sqlalchemy.orm import backref
 from sqlalchemy.orm import mapper
@@ -727,7 +728,7 @@ class M2MCollectionMoveTest(_fixtures.FixtureTest):
         eq_(i1.keywords, [k1, k2])
 
         # prove it didn't flush
-        eq_(session.scalar("select count(*) from item_keywords"), 1)
+        eq_(session.scalar(text("select count(*) from item_keywords")), 1)
 
         # the pending collection was removed
         assert (
index 4bd921af45bcdab2a50b82ea559894e770fab7d9..d5cfca83a865be8e4a0e1b75cbf1bf45e86684b5 100644 (file)
@@ -2003,6 +2003,33 @@ class SessionTest(fixtures.RemovesEvents, _LocalFixture):
         ):
             s1.transaction
 
+    def test_textual_execute(self, connection):
+        """test that Session.execute() converts to text()"""
+
+        users = self.tables.users
+
+        with Session(bind=connection) as sess:
+            sess.execute(users.insert(), dict(id=7, name="jack"))
+
+            with testing.expect_deprecated_20(
+                "Using plain strings to indicate SQL statements "
+                "without using the text"
+            ):
+                # use :bindparam style
+                eq_(
+                    sess.execute(
+                        "select * from users where id=:id", {"id": 7}
+                    ).fetchall(),
+                    [(7, "jack")],
+                )
+
+            with testing.expect_deprecated_20(
+                "Using plain strings to indicate SQL statements "
+                "without using the text"
+            ):
+                # use :bindparam style
+                eq_(sess.scalar("select id from users where id=:id", {"id": 7}), 7)
+
     def test_session_str(self):
         s1 = Session(testing.db)
         str(s1)
index 226415e34bc99ae9affc5710523b2ab7ba50359b..8b51d7e20375f8852d9c811312512c9eb987448b 100644 (file)
@@ -387,7 +387,7 @@ class M2MTest(fixtures.MappedTest):
         p1.place_id
         p1.transitions
 
-        sess.execute("delete from place_input", mapper=Place)
+        sess.execute(place_input.delete(), mapper=Place)
         p1.place_id = 7
 
         assert_raises_message(
@@ -400,7 +400,7 @@ class M2MTest(fixtures.MappedTest):
 
         p1.place_id
         p1.transitions
-        sess.execute("delete from place_input", mapper=Place)
+        sess.execute(place_input.delete(), mapper=Place)
         p1.transitions.remove(t1)
         assert_raises_message(
             orm_exc.StaleDataError,
index d2838e5bf854cf866c931224259ae08c127c7d2e..759d78ba5248e593cc96a83b54ae445106b76d3f 100644 (file)
@@ -9,6 +9,7 @@ from sqlalchemy import select
 from sqlalchemy import Sequence
 from sqlalchemy import String
 from sqlalchemy import testing
+from sqlalchemy import text
 from sqlalchemy.orm import attributes
 from sqlalchemy.orm import backref
 from sqlalchemy.orm import close_all_sessions
@@ -57,24 +58,6 @@ class ExecutionTest(_fixtures.FixtureTest):
         finally:
             seq.drop(connection)
 
-    def test_textual_execute(self, connection):
-        """test that Session.execute() converts to text()"""
-
-        users = self.tables.users
-
-        with Session(bind=connection) as sess:
-            sess.execute(users.insert(), dict(id=7, name="jack"))
-
-            # use :bindparam style
-            eq_(
-                sess.execute(
-                    "select * from users where id=:id", {"id": 7}
-                ).fetchall(),
-                [(7, "jack")],
-            )
-
-            # use :bindparam style
-            eq_(sess.scalar("select id from users where id=:id", {"id": 7}), 7)
 
     def test_parameter_execute(self):
         users = self.tables.users
@@ -555,44 +538,39 @@ class SessionStateTest(_fixtures.FixtureTest):
         User, users = self.classes.User, self.tables.users
 
         mapper(User, users)
-        try:
-            sess = create_session(autocommit=False, autoflush=True)
+        with create_session(autocommit=False, autoflush=True) as sess:
             u = User()
             u.name = "ed"
             sess.add(u)
             u2 = sess.query(User).filter_by(name="ed").one()
-            assert u2 is u
-            assert (
+            is_(u2, u)
+            eq_(
                 sess.execute(
-                    "select count(1) from users",
+                    text("select count(1) from users"),
                     bind_arguments=dict(mapper=User),
-                ).scalar()
-                == 1
+                ).scalar(),
+                1,
             )
-            assert (
+            eq_(
                 testing.db.connect()
                 .exec_driver_sql("select count(1) from users")
-                .scalar()
-                == 0
+                .scalar(),
+                0,
             )
             sess.commit()
-            assert (
+            eq_(
                 sess.execute(
-                    "select count(1) from users",
+                    text("select count(1) from users"),
                     bind_arguments=dict(mapper=User),
-                ).scalar()
-                == 1
+                ).scalar(),
+                1,
             )
-            assert (
+            eq_(
                 testing.db.connect()
                 .exec_driver_sql("select count(1) from users")
-                .scalar()
-                == 1
+                .scalar(),
+                1,
             )
-            sess.close()
-        except Exception:
-            sess.rollback()
-            raise
 
     @engines.close_open_connections
     def test_autoflush_2(self):
@@ -1938,11 +1916,15 @@ class SessionInterface(fixtures.TestBase):
 
         raises_("connection", bind_arguments=dict(mapper=user_arg))
 
-        raises_("execute", "SELECT 1", bind_arguments=dict(mapper=user_arg))
+        raises_(
+            "execute", text("SELECT 1"), bind_arguments=dict(mapper=user_arg)
+        )
 
         raises_("get_bind", mapper=user_arg)
 
-        raises_("scalar", "SELECT 1", bind_arguments=dict(mapper=user_arg))
+        raises_(
+            "scalar", text("SELECT 1"), bind_arguments=dict(mapper=user_arg)
+        )
 
         eq_(
             watchdog,
index 248f334cf6a520e7af53e9f15258b52ff496a2dd..6eda6fbb6821bbca3d543127991306eb6b1e89e3 100644 (file)
@@ -11,6 +11,7 @@ from sqlalchemy import select
 from sqlalchemy import String
 from sqlalchemy import Table
 from sqlalchemy import testing
+from sqlalchemy import text
 from sqlalchemy.future import Engine
 from sqlalchemy.orm import attributes
 from sqlalchemy.orm import create_session
@@ -602,7 +603,9 @@ class SessionTransactionTest(fixtures.RemovesEvents, FixtureTest):
 
         s1 = Session(eng)
 
-        assert_raises_message(Exception, "failure", s1.execute, "select 1")
+        assert_raises_message(
+            Exception, "failure", s1.execute, text("select 1")
+        )
 
         conn, fairy = state[0]
         assert not fairy.is_valid
@@ -630,7 +633,9 @@ class SessionTransactionTest(fixtures.RemovesEvents, FixtureTest):
         s1 = Session(eng)
 
         s1.begin_nested()
-        assert_raises_message(Exception, "failure", s1.execute, "select 1")
+        assert_raises_message(
+            Exception, "failure", s1.execute, text("select 1")
+        )
 
         conn, fairy = state[0]
         assert fairy.is_valid
@@ -710,7 +715,7 @@ class SessionTransactionTest(fixtures.RemovesEvents, FixtureTest):
 
         @event.listens_for(sess, "after_commit")
         def go(session):
-            session.execute("select 1")
+            session.execute(text("select 1"))
 
         assert_raises_message(
             sa_exc.InvalidRequestError,
@@ -729,7 +734,7 @@ class SessionTransactionTest(fixtures.RemovesEvents, FixtureTest):
             "This session is in 'prepared' state; no further "
             "SQL can be emitted within this transaction.",
             sess.execute,
-            "select 1",
+            text("select 1"),
         )
 
     def test_no_sql_during_rollback(self):
@@ -739,7 +744,7 @@ class SessionTransactionTest(fixtures.RemovesEvents, FixtureTest):
 
         @event.listens_for(sess, "after_rollback")
         def go(session):
-            session.execute("select 1")
+            session.execute(text("select 1"))
 
         assert_raises_message(
             sa_exc.InvalidRequestError,
@@ -911,7 +916,7 @@ class SessionTransactionTest(fixtures.RemovesEvents, FixtureTest):
         conn = mock.Mock(engine=bind)
         bind.connect = mock.Mock(return_value=conn)
         sess = Session(bind=bind)
-        sess.execute("select 1")
+        sess.execute(text("select 1"))
         with expect_warnings(
             "Connection is already established for the "
             "given bind; execution_options ignored"
index 8759bbb22fc252540f8c2423902ea3a9c9de72fe..0ef90e89e974b3ec74a3c011c5f544eed46a89de 100644 (file)
@@ -173,11 +173,14 @@ class RoleTest(fixtures.TestBase):
         )
 
     def test_statement_text_coercion(self):
-        is_true(
-            expect(
-                roles.CoerceTextStatementRole, "select * from table"
-            ).compare(text("select * from table"))
-        )
+        with testing.expect_deprecated_20(
+            "Using plain strings to indicate SQL statements"
+        ):
+            is_true(
+                expect(
+                    roles.CoerceTextStatementRole, "select * from table"
+                ).compare(text("select * from table"))
+            )
 
     def test_select_statement_no_text_coercion(self):
         assert_raises_message(