From: Mike Bayer Date: Tue, 19 Jan 2016 17:44:42 +0000 (-0500) Subject: - The ``str()`` call for :class:`.Query` will now take into account X-Git-Tag: rel_1_1_0b1~84^2~34 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=2a7f37b7b01930fb4e9227e5cab03ea26e0a4b55;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - 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. fixes #3081 --- diff --git a/doc/build/changelog/changelog_11.rst b/doc/build/changelog/changelog_11.rst index 81637a0b47..3d29c9eb43 100644 --- a/doc/build/changelog/changelog_11.rst +++ b/doc/build/changelog/changelog_11.rst @@ -21,6 +21,25 @@ .. 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 diff --git a/doc/build/changelog/migration_11.rst b/doc/build/changelog/migration_11.rst index ecab73fecf..c317b734ae 100644 --- a/doc/build/changelog/migration_11.rst +++ b/doc/build/changelog/migration_11.rst @@ -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 ==================================== diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index e1b920bbb4..6b808a7012 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -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 diff --git a/lib/sqlalchemy/testing/assertions.py b/lib/sqlalchemy/testing/assertions.py index ad0aa43627..8c962d7a3c 100644 --- a/lib/sqlalchemy/testing/assertions.py +++ b/lib/sqlalchemy/testing/assertions.py @@ -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) diff --git a/test/orm/test_query.py b/test/orm/test_query.py index d2f9e4a66b..6445ffefdf 100644 --- a/test/orm/test_query.py +++ b/test/orm/test_query.py @@ -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'