Skip to content

Commit

Permalink
Support bindparam() with callable for primaryjoin
Browse files Browse the repository at this point in the history
Fixes the comparison of bindparam() objects based on
the "callable" parameter being present which helps to correctly
detect use_get, and also checks for "callable" when detecting
parameters for value substitution and will not impact the
object if present.

Change-Id: I4c93ee5d404d2648dd9835beeae0c5fb67e37d19
Fixes: #3767
  • Loading branch information
zzzeek committed Sep 19, 2016
1 parent 881369b commit 8c3b9d6
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 5 deletions.
11 changes: 11 additions & 0 deletions doc/build/changelog/changelog_11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@
.. changelog::
:version: 1.1.0

.. change::
:tags: bug, orm
:tickets: 3767

The primaryjoin of a :func:`.relationship` construct can now include
a :func:`.bindparam` object that includes a callable function to
generate values. Previously, the lazy loader strategy would
be incompatible with this use, and additionally would fail to correctly
detect if the "use_get" criteria should be used if the primary key
were involved with the bound parameter.

.. change::
:tags: bug, orm
:tickets: 3788
Expand Down
2 changes: 1 addition & 1 deletion lib/sqlalchemy/orm/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ def visit_bindparam(bindparam):
params.append((
bindparam.key, bind_to_col[bindparam._identifying_key],
None))
else:
elif bindparam.callable is None:
params.append((bindparam.key, None, bindparam.value))

criterion = visitors.cloned_traverse(
Expand Down
3 changes: 2 additions & 1 deletion lib/sqlalchemy/sql/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -1145,7 +1145,8 @@ def compare(self, other, **kw):

return isinstance(other, BindParameter) \
and self.type._compare_type_affinity(other.type) \
and self.value == other.value
and self.value == other.value \
and self.callable == other.callable

def __getstate__(self):
"""execute a deferred value for serialization purposes."""
Expand Down
31 changes: 30 additions & 1 deletion test/orm/test_lazy_relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import datetime
from sqlalchemy.orm import attributes, exc as orm_exc, configure_mappers
import sqlalchemy as sa
from sqlalchemy import testing, and_
from sqlalchemy import testing, and_, bindparam
from sqlalchemy import Integer, String, ForeignKey, SmallInteger, Boolean
from sqlalchemy import ForeignKeyConstraint
from sqlalchemy.types import TypeDecorator
Expand Down Expand Up @@ -259,6 +259,35 @@ def test_uselist_false_warning(self):
u1 = s.query(User).filter(User.id == 7).one()
assert_raises(sa.exc.SAWarning, getattr, u1, 'order')

def test_callable_bind(self):
Address, addresses, users, User = (
self.classes.Address,
self.tables.addresses,
self.tables.users,
self.classes.User)

mapper(User, users, properties=dict(
addresses=relationship(
mapper(Address, addresses),
lazy='select',
primaryjoin=and_(
users.c.id == addresses.c.user_id,
users.c.name == bindparam("name", callable_=lambda: "ed")
)
)
))

s = Session()
ed = s.query(User).filter_by(name='ed').one()
eq_(ed.addresses, [
Address(id=2, user_id=8),
Address(id=3, user_id=8),
Address(id=4, user_id=8)
])

fred = s.query(User).filter_by(name='fred').one()
eq_(fred.addresses, []) # fred is missing

def test_one_to_many_scalar(self):
Address, addresses, users, User = (
self.classes.Address,
Expand Down
33 changes: 31 additions & 2 deletions test/sql/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from sqlalchemy.testing import fixtures, is_true, is_false
from sqlalchemy import MetaData, Table, Column, Integer
from sqlalchemy import and_, or_
from sqlalchemy import MetaData, Table, Column, Integer, String
from sqlalchemy import and_, or_, bindparam
from sqlalchemy.sql.elements import ClauseList
from sqlalchemy.sql import operators

Expand Down Expand Up @@ -76,3 +76,32 @@ def test_compare_clauselist_not_assoc_different_operator(self):

is_false(l1.compare(l2))

def test_compare_binds(self):
b1 = bindparam("foo", type_=Integer())
b2 = bindparam("foo", type_=Integer())
b3 = bindparam("bar", type_=Integer())
b4 = bindparam("foo", type_=String())

c1 = lambda: 5 # noqa
c2 = lambda: 6 # noqa

b5 = bindparam("foo", type_=Integer(), callable_=c1)
b6 = bindparam("foo", type_=Integer(), callable_=c2)
b7 = bindparam("foo", type_=Integer(), callable_=c1)

b8 = bindparam("foo", type_=Integer, value=5)
b9 = bindparam("foo", type_=Integer, value=6)

is_false(b1.compare(b5))
is_true(b5.compare(b7))
is_false(b5.compare(b6))
is_true(b1.compare(b2))

# currently not comparing "key", as we often have to compare
# anonymous names. however we should really check for that
is_true(b1.compare(b3))

is_false(b1.compare(b4))
is_false(b1.compare(b8))
is_false(b8.compare(b9))
is_true(b8.compare(b8))

0 comments on commit 8c3b9d6

Please sign in to comment.