mirror of
https://github.com/sqlalchemy/sqlalchemy.git
synced 2026-05-28 19:44:50 -04:00
- Identified an inconsistency when handling :meth:.Query.join to the
same target more than once; it implicitly dedupes only in the case of a relationship join, and due to 🎫`3233`, in 1.0 a join to the same table twice behaves differently than 0.9 in that it no longer erroneously aliases. To help document this change, the verbiage regarding 🎫`3233` in the migration notes has been generalized, and a warning has been added when :meth:`.Query.join` is called against the same target relationship more than once. fixes #3367
This commit is contained in:
Vendored
+13
@@ -18,6 +18,19 @@
|
||||
.. changelog::
|
||||
:version: 1.0.0
|
||||
|
||||
.. change::
|
||||
:tags: bug, orm
|
||||
:tickets: 3367
|
||||
|
||||
Identified an inconsistency when handling :meth:`.Query.join` to the
|
||||
same target more than once; it implicitly dedupes only in the case of
|
||||
a relationship join, and due to :ticket:`3233`, in 1.0 a join
|
||||
to the same table twice behaves differently than 0.9 in that it no
|
||||
longer erroneously aliases. To help document this change,
|
||||
the verbiage regarding :ticket:`3233` in the migration notes has
|
||||
been generalized, and a warning has been added when :meth:`.Query.join`
|
||||
is called against the same target relationship more than once.
|
||||
|
||||
.. change::
|
||||
:tags: bug, orm
|
||||
:tickets: 3364
|
||||
|
||||
Vendored
+78
-11
@@ -1092,18 +1092,83 @@ joined loader options can still be used::
|
||||
|
||||
.. _bug_3233:
|
||||
|
||||
Single inheritance join targets will no longer sometimes implicitly alias themselves
|
||||
------------------------------------------------------------------------------------
|
||||
Changes and fixes in handling of duplicate join targets
|
||||
--------------------------------------------------------
|
||||
|
||||
This is a bug where an unexpected and inconsistent behavior would occur
|
||||
in some scenarios when joining to a single-table-inheritance entity. The
|
||||
difficulty this might cause is that the query is supposed to raise an error,
|
||||
as it is invalid SQL, however the bug would cause an alias to be added which
|
||||
makes the query "work". The issue is confusing because this aliasing
|
||||
is not applied consistently and could change based on the nature of the query
|
||||
preceding the join.
|
||||
Changes here encompass bugs where an unexpected and inconsistent
|
||||
behavior would occur in some scenarios when joining to an entity
|
||||
twice, or to multple single-table entities against the same table,
|
||||
without using a relationship-based ON clause, as well as when joining
|
||||
multiple times to the same target relationship.
|
||||
|
||||
A simple example is::
|
||||
Starting with a mapping as::
|
||||
|
||||
from sqlalchemy import Integer, Column, String, ForeignKey
|
||||
from sqlalchemy.orm import Session, relationship
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class A(Base):
|
||||
__tablename__ = 'a'
|
||||
id = Column(Integer, primary_key=True)
|
||||
bs = relationship("B")
|
||||
|
||||
class B(Base):
|
||||
__tablename__ = 'b'
|
||||
id = Column(Integer, primary_key=True)
|
||||
a_id = Column(ForeignKey('a.id'))
|
||||
|
||||
A query that joins to ``A.bs`` twice::
|
||||
|
||||
print s.query(A).join(A.bs).join(A.bs)
|
||||
|
||||
Will render::
|
||||
|
||||
SELECT a.id AS a_id
|
||||
FROM a JOIN b ON a.id = b.a_id
|
||||
|
||||
The query deduplicates the redundant ``A.bs`` because it is attempting
|
||||
to support a case like the following::
|
||||
|
||||
s.query(A).join(A.bs).\
|
||||
filter(B.foo == 'bar').\
|
||||
reset_joinpoint().join(A.bs, B.cs).filter(C.bar == 'bat')
|
||||
|
||||
That is, the ``A.bs`` is part of a "path". As part of :ticket:`3367`,
|
||||
arriving at the same endpoint twice without it being part of a
|
||||
larger path will now emit a warning::
|
||||
|
||||
SAWarning: Pathed join target A.bs has already been joined to; skipping
|
||||
|
||||
The bigger change involves when joining to an entity without using a
|
||||
relationship-bound path. If we join to ``B`` twice::
|
||||
|
||||
print s.query(A).join(B, B.a_id == A.id).join(B, B.a_id == A.id)
|
||||
|
||||
In 0.9, this would render as follows::
|
||||
|
||||
SELECT a.id AS a_id
|
||||
FROM a JOIN b ON b.a_id = a.id JOIN b AS b_1 ON b_1.a_id = a.id
|
||||
|
||||
This is problematic since the aliasing is implicit and in the case of different
|
||||
ON clauses can lead to unpredictable results.
|
||||
|
||||
In 1.0, no automatic aliasing is applied and we get::
|
||||
|
||||
SELECT a.id AS a_id
|
||||
FROM a JOIN b ON b.a_id = a.id JOIN b ON b.a_id = a.id
|
||||
|
||||
This will raise an error from the database. While it might be nice if
|
||||
the "duplicate join target" acted identically if we joined both from
|
||||
redundant relationships vs. redundant non-relationship based targets,
|
||||
for now we are only changing the behavior in the more serious case where
|
||||
implicit aliasing would have occurred previously, and only emitting a warning
|
||||
in the relationship case. Ultimately, joining to the same thing twice without
|
||||
any aliasing to disambiguate should raise an error in all cases.
|
||||
|
||||
The change also has an impact on single-table inheritance targets. Using
|
||||
a mapping as follows::
|
||||
|
||||
from sqlalchemy import Integer, Column, String, ForeignKey
|
||||
from sqlalchemy.orm import Session, relationship
|
||||
@@ -1151,7 +1216,8 @@ the identical SQL::
|
||||
WHERE a.type IN (:type_2)
|
||||
|
||||
The above SQL is invalid, as it renders "a" within the FROM list twice.
|
||||
The bug however would occur with the second query only and render this instead::
|
||||
However, the implicit aliasing bug would occur with the second query only
|
||||
and render this instead::
|
||||
|
||||
SELECT a.id AS a_id, a.type AS a_type
|
||||
FROM a JOIN b ON b.a_id = a.id JOIN a AS a_1
|
||||
@@ -1173,6 +1239,7 @@ as all the subclasses normally refer to the same table::
|
||||
print s.query(ASub1).join(B, ASub1.b).join(asub2_alias, B.a.of_type(asub2_alias))
|
||||
|
||||
:ticket:`3233`
|
||||
:ticket:`3367`
|
||||
|
||||
|
||||
Deferred Columns No Longer Implicitly Undefer
|
||||
|
||||
@@ -1815,7 +1815,8 @@ class Query(object):
|
||||
# convert to a tuple.
|
||||
keys = (keys,)
|
||||
|
||||
for arg1 in util.to_list(keys):
|
||||
keylist = util.to_list(keys)
|
||||
for idx, arg1 in enumerate(keylist):
|
||||
if isinstance(arg1, tuple):
|
||||
# "tuple" form of join, multiple
|
||||
# tuples are accepted as well. The simpler
|
||||
@@ -1894,6 +1895,11 @@ class Query(object):
|
||||
jp = self._joinpoint[edge].copy()
|
||||
jp['prev'] = (edge, self._joinpoint)
|
||||
self._update_joinpoint(jp)
|
||||
|
||||
if idx == len(keylist) - 1:
|
||||
util.warn(
|
||||
"Pathed join target %s has already "
|
||||
"been joined to; skipping" % prop)
|
||||
continue
|
||||
|
||||
elif onclause is not None and right_entity is None:
|
||||
|
||||
@@ -750,6 +750,17 @@ class JoinTest(QueryTest, AssertsCompiledSQL):
|
||||
filter_by(id=3).outerjoin('orders','address').filter_by(id=1).all()
|
||||
assert [User(id=7, name='jack')] == result
|
||||
|
||||
def test_raises_on_dupe_target_rel(self):
|
||||
User = self.classes.User
|
||||
|
||||
assert_raises_message(
|
||||
sa.exc.SAWarning,
|
||||
"Pathed join target Order.items has already been joined to; "
|
||||
"skipping",
|
||||
lambda: create_session().query(User).outerjoin('orders', 'items').\
|
||||
outerjoin('orders', 'items')
|
||||
)
|
||||
|
||||
def test_from_joinpoint(self):
|
||||
Item, User, Order = (self.classes.Item,
|
||||
self.classes.User,
|
||||
|
||||
Reference in New Issue
Block a user