]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- The ``str()`` call for :class:`.Query` will now take into account
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 19 Jan 2016 17:44:42 +0000 (12:44 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 19 Jan 2016 17:44:42 +0000 (12:44 -0500)
the :class:`.Engine` to which the :class:`.Session` is bound, when
generating the string form of the SQL, so that the actual SQL
that would be emitted to the database is shown, if possible.  Previously,
only the engine associated with the :class:`.MetaData` to which the
mappings are associated would be used, if present.  If
no bind can be located either on the :class:`.Session` or on
the :class:`.MetaData` to which the mappings are associated, then
the "default" dialect is used to render the SQL, as was the case
previously.  fixes #3081

doc/build/changelog/changelog_11.rst
doc/build/changelog/migration_11.rst
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/testing/assertions.py
test/orm/test_query.py

index 81637a0b47c006ac355653580e17d4795d21ca0e..3d29c9eb43264814d5471d069188e5e601bd01fe 100644 (file)
 .. changelog::
     :version: 1.1.0b1
 
+    .. change::
+        :tags: feature, orm
+        :tickets: 3081
+
+        The ``str()`` call for :class:`.Query` will now take into account
+        the :class:`.Engine` to which the :class:`.Session` is bound, when
+        generating the string form of the SQL, so that the actual SQL
+        that would be emitted to the database is shown, if possible.  Previously,
+        only the engine associated with the :class:`.MetaData` to which the
+        mappings are associated would be used, if present.  If
+        no bind can be located either on the :class:`.Session` or on
+        the :class:`.MetaData` to which the mappings are associated, then
+        the "default" dialect is used to render the SQL, as was the case
+        previously.
+
+        .. seealso::
+
+            :ref:`change_3081`
+
     .. change::
         :tags: feature, sql
         :tickets: 3501
index ecab73fecf4d38fc5950a373ba049c9423380cd6..c317b734ae8d0601765edd7edcbe3deb8598d2b1 100644 (file)
@@ -16,7 +16,7 @@ What's New in SQLAlchemy 1.1?
     some issues may be moved to later milestones in order to allow
     for a timely release.
 
-    Document last updated: January 14, 2016
+    Document last updated: January 19, 2016
 
 Introduction
 ============
@@ -344,6 +344,26 @@ would have to be compared during the merge.
 
 :ticket:`3601`
 
+.. _change_3081:
+
+Stringify of Query will consult the Session for the correct dialect
+-------------------------------------------------------------------
+
+Calling ``str()`` on a :class:`.Query` object will consult the :class:`.Session`
+for the correct "bind" to use, in order to render the SQL that would be
+passed to the database.  In particular this allows a :class:`.Query` that
+refers to dialect-specific SQL constructs to be renderable, assuming the
+:class:`.Query` is associated with an appropriate :class:`.Session`.
+Previously, this behavior would only take effect if the :class:`.MetaData`
+to which the mappings were associated were itself bound to the target
+:class:`.Engine`.
+
+If neither the underlying :class:`.MetaData` nor the :class:`.Session` are
+associated with any bound :class:`.Engine`, then the fallback to the
+"default" dialect is used to generate the SQL string.
+
+:ticket:`3081`
+
 New Features and Improvements - Core
 ====================================
 
index e1b920bbb4fe2b29516b6953e6b1c142bda16c68..6b808a7012f763c4cd6334fe80124e761b108663 100644 (file)
@@ -2741,22 +2741,37 @@ class Query(object):
             self.session._autoflush()
         return self._execute_and_instances(context)
 
+    def __str__(self):
+        context = self._compile_context()
+        try:
+            bind = self._get_bind_args(
+                context, self.session.get_bind) if self.session else None
+        except sa_exc.UnboundExecutionError:
+            bind = None
+        return str(context.statement.compile(bind))
+
     def _connection_from_session(self, **kw):
-        conn = self.session.connection(
-            **kw)
+        conn = self.session.connection(**kw)
         if self._execution_options:
             conn = conn.execution_options(**self._execution_options)
         return conn
 
     def _execute_and_instances(self, querycontext):
-        conn = self._connection_from_session(
-            mapper=self._bind_mapper(),
-            clause=querycontext.statement,
+        conn = self._get_bind_args(
+            querycontext,
+            self._connection_from_session,
             close_with_result=True)
 
         result = conn.execute(querycontext.statement, self._params)
         return loading.instances(querycontext.query, result, querycontext)
 
+    def _get_bind_args(self, querycontext, fn, **kw):
+        return fn(
+            mapper=self._bind_mapper(),
+            clause=querycontext.statement,
+            **kw
+        )
+
     @property
     def column_descriptions(self):
         """Return metadata about the columns which would be
@@ -3358,8 +3373,6 @@ class Query(object):
                     sql.True_._ifnone(context.whereclause),
                     single_crit)
 
-    def __str__(self):
-        return str(self._compile_context().statement)
 
 from ..sql.selectable import ForUpdateArg
 
index ad0aa436277dba41e9e9105eca29bfacb7b0b906..8c962d7a3c8f777a6e8593ffd342bcd18f52793e 100644 (file)
@@ -245,6 +245,15 @@ def startswith_(a, fragment, msg=None):
         a, fragment)
 
 
+def eq_ignore_whitespace(a, b, msg=None):
+    a = re.sub(r'^\s+?|\n', "", a)
+    a = re.sub(r' {2,}', " ", a)
+    b = re.sub(r'^\s+?|\n', "", b)
+    b = re.sub(r' {2,}', " ", b)
+
+    assert a == b, msg or "%r != %r" % (a, b)
+
+
 def assert_raises(except_cls, callable_, *args, **kw):
     try:
         callable_(*args, **kw)
index d2f9e4a66b1eef3429421d49afaba94188b4bffa..6445ffefdf3d1c140be6372f4ca55d5f9dc6b34a 100644 (file)
@@ -1,7 +1,7 @@
 from sqlalchemy import (
     testing, null, exists, text, union, literal, literal_column, func, between,
     Unicode, desc, and_, bindparam, select, distinct, or_, collate, insert,
-    Integer, String, Boolean, exc as sa_exc, util, cast)
+    Integer, String, Boolean, exc as sa_exc, util, cast, MetaData)
 from sqlalchemy.sql import operators, expression
 from sqlalchemy import column, table
 from sqlalchemy.engine import default
@@ -13,7 +13,8 @@ from sqlalchemy.testing.assertsql import CompiledSQL
 from sqlalchemy.testing.schema import Table, Column
 import sqlalchemy as sa
 from sqlalchemy.testing.assertions import (
-    eq_, assert_raises, assert_raises_message, expect_warnings)
+    eq_, assert_raises, assert_raises_message, expect_warnings,
+    eq_ignore_whitespace)
 from sqlalchemy.testing import fixtures, AssertsCompiledSQL, assert_warnings
 from test.orm import _fixtures
 from sqlalchemy.orm.util import join, with_parent
@@ -210,6 +211,69 @@ class RowTupleTest(QueryTest):
         )
 
 
+class BindSensitiveStringifyTest(fixtures.TestBase):
+    def _fixture(self, bind_to=None):
+        # building a totally separate metadata /mapping here
+        # because we need to control if the MetaData is bound or not
+
+        class User(object):
+            pass
+
+        m = MetaData(bind=bind_to)
+        user_table = Table(
+            'users', m,
+            Column('id', Integer, primary_key=True),
+            Column('name', String(50)))
+
+        mapper(User, user_table)
+        return User
+
+    def _dialect_fixture(self):
+        class MyDialect(default.DefaultDialect):
+            default_paramstyle = 'qmark'
+
+        from sqlalchemy.engine import base
+        return base.Engine(mock.Mock(), MyDialect(), mock.Mock())
+
+    def _test(
+            self, bound_metadata, bound_session,
+            session_present, expect_bound):
+        if bound_metadata or bound_session:
+            eng = self._dialect_fixture()
+        else:
+            eng = None
+
+        User = self._fixture(bind_to=eng if bound_metadata else None)
+
+        s = Session(eng if bound_session else None)
+        q = s.query(User).filter(User.id == 7)
+        if not session_present:
+            q = q.with_session(None)
+
+        eq_ignore_whitespace(
+            str(q),
+            "SELECT users.id AS users_id, users.name AS users_name "
+            "FROM users WHERE users.id = ?" if expect_bound else
+            "SELECT users.id AS users_id, users.name AS users_name "
+            "FROM users WHERE users.id = :id_1"
+        )
+
+    def test_query_unbound_metadata_bound_session(self):
+        self._test(False, True, True, True)
+
+    def test_query_bound_metadata_unbound_session(self):
+        self._test(True, False, True, True)
+
+    def test_query_unbound_metadata_no_session(self):
+        self._test(False, False, False, False)
+
+    def test_query_unbound_metadata_unbound_session(self):
+        self._test(False, False, True, False)
+
+    def test_query_bound_metadata_bound_session(self):
+        self._test(True, True, True, True)
+
+
 class RawSelectTest(QueryTest, AssertsCompiledSQL):
     __dialect__ = 'default'