diff options
| -rw-r--r-- | doc/build/changelog/changelog_11.rst | 19 | ||||
| -rw-r--r-- | doc/build/changelog/migration_11.rst | 22 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/query.py | 27 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/assertions.py | 9 | ||||
| -rw-r--r-- | test/orm/test_query.py | 68 |
5 files changed, 135 insertions, 10 deletions
diff --git a/doc/build/changelog/changelog_11.rst b/doc/build/changelog/changelog_11.rst index 81637a0b4..3d29c9eb4 100644 --- a/doc/build/changelog/changelog_11.rst +++ b/doc/build/changelog/changelog_11.rst @@ -22,6 +22,25 @@ :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 ecab73fec..c317b734a 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 e1b920bbb..6b808a701 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 ad0aa4362..8c962d7a3 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 d2f9e4a66..6445ffefd 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' |
