summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/build/changelog/changelog_11.rst19
-rw-r--r--doc/build/changelog/migration_11.rst22
-rw-r--r--lib/sqlalchemy/orm/query.py27
-rw-r--r--lib/sqlalchemy/testing/assertions.py9
-rw-r--r--test/orm/test_query.py68
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'