From bb6ec17708e56170bcccf7e32703d0ba37b160d2 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 22 Feb 2008 23:17:15 +0000 Subject: [PATCH] - the value of a bindparam() can be a callable, in which case it's evaluated at statement execution time to get the value. - expressions used in filter(), filter_by() and others, when they make usage of a clause generated from a relation using the identity of a child object (e.g. filter(Parent.child==)), evaluate the actual primary key value of at execution time so that the autoflush step of the Query can complete, thereby populating the PK value of in the case that was pending. - cleanup of attributes.get_committed_value() to never return the NO_VALUE value; evaluates to None --- CHANGES | 16 ++++++++++++---- lib/sqlalchemy/orm/attributes.py | 5 ++++- lib/sqlalchemy/orm/strategies.py | 4 +++- lib/sqlalchemy/orm/unitofwork.py | 4 ++-- lib/sqlalchemy/sql/compiler.py | 13 +++++++++++-- test/orm/attributes.py | 20 ++++++++++++++++++++ test/orm/session.py | 15 +++++++++++++++ 7 files changed, 67 insertions(+), 10 deletions(-) diff --git a/CHANGES b/CHANGES index c9030a6ba8..78112999c1 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,9 @@ CHANGES - sql - can again create aliases of selects against textual FROM clauses, [ticket:975] + - the value of a bindparam() can be a callable, in which + case it's evaluated at statement execution time to + get the value. - orm - any(), has(), contains(), attribute level == and != now @@ -33,10 +36,15 @@ CHANGES - preventive code against a potential lost-reference bug in flush() - -0.4.4 ------- - + - expressions used in filter(), filter_by() and others, + when they make usage of a clause generated from a + relation using the identity of a child object + (e.g. filter(Parent.child==)), evaluate + the actual primary key value of at + execution time so that the autoflush step of the + Query can complete, thereby populating the PK value + of in the case that was + pending. 0.4.3 ------ diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 5cc9cb3091..298a7f5119 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -246,7 +246,10 @@ class AttributeImpl(object): """return the unchanged value of this attribute""" if self.key in state.committed_state: - return state.committed_state.get(self.key) + if state.committed_state[self.key] is NO_VALUE: + return None + else: + return state.committed_state.get(self.key) else: return self.get(state) diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index bdc8ab9a99..81a5895f49 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -294,7 +294,9 @@ class LazyLoader(AbstractRelationLoader): mapper = reverse_direction and self.parent_property.mapper or self.parent_property.parent if bindparam.key in bind_to_col: # use the "committed" (database) version to get query column values - bindparam.value = mapper._get_committed_attr_by_column(instance, bind_to_col[bindparam.key]) + # also its a deferred value; so that when used by Query, the committed value is used + # after an autoflush occurs + bindparam.value = lambda: mapper._get_committed_attr_by_column(instance, bind_to_col[bindparam.key]) return visitors.traverse(criterion, clone=True, visit_bindparam=visit_bindparam) def _lazy_none_clause(self, reverse_direction=False): diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py index e60b220439..48b6ea4cb3 100644 --- a/lib/sqlalchemy/orm/unitofwork.py +++ b/lib/sqlalchemy/orm/unitofwork.py @@ -136,8 +136,8 @@ class UnitOfWork(object): delattr(state, 'insert_order') o = state.obj() - # prevent against last minute dereferences of "dirty" - # objects TODO: identify a code path where state.obj() is None + # prevent against last minute dereferences of the object + # TODO: identify a code path where state.obj() is None if o is not None: self.identity_map[state.dict['_instance_key']] = o state.commit_all() diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 8d8cfa38fa..c5ff974e52 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -213,10 +213,19 @@ class DefaultCompiler(engine.Compiled): pd[name] = params[paramname] break else: - pd[name] = bindparam.value + if callable(bindparam.value): + pd[name] = bindparam.value() + else: + pd[name] = bindparam.value return pd else: - return dict([(self.bind_names[bindparam], bindparam.value) for bindparam in self.bind_names]) + pd = {} + for bindparam in self.bind_names: + if callable(bindparam.value): + pd[self.bind_names[bindparam]] = bindparam.value() + else: + pd[self.bind_names[bindparam]] = bindparam.value + return pd params = property(construct_params) diff --git a/test/orm/attributes.py b/test/orm/attributes.py index 27860517c1..bfb4d0d770 100644 --- a/test/orm/attributes.py +++ b/test/orm/attributes.py @@ -597,6 +597,26 @@ class DeferredBackrefTest(TestBase): lazy_load = (p1, p2, p3) = [Post("post 1"), Post("post 2"), Post("post 3")] class HistoryTest(TestBase): + def test_get_committed_value(self): + class Foo(fixtures.Base): + pass + + attributes.register_class(Foo) + attributes.register_attribute(Foo, 'someattr', uselist=False, useobject=False) + + f = Foo() + self.assertEquals(Foo.someattr.impl.get_committed_value(f._state), None) + + f.someattr = 3 + self.assertEquals(Foo.someattr.impl.get_committed_value(f._state), None) + + f = Foo() + f.someattr = 3 + self.assertEquals(Foo.someattr.impl.get_committed_value(f._state), None) + + f._state.commit(['someattr']) + self.assertEquals(Foo.someattr.impl.get_committed_value(f._state), 3) + def test_scalar(self): class Foo(fixtures.Base): pass diff --git a/test/orm/session.py b/test/orm/session.py index a1c850073e..1d6742d8ad 100644 --- a/test/orm/session.py +++ b/test/orm/session.py @@ -140,6 +140,21 @@ class SessionTest(TestBase, AssertsExecutionResults): assert testing.db.connect().execute("select count(1) from users").scalar() == 1 sess.close() + def test_autoflush_expressions(self): + class User(fixtures.Base): + pass + class Address(fixtures.Base): + pass + mapper(User, users, properties={ + 'addresses':relation(Address, backref="user") + }) + mapper(Address, addresses) + + sess = create_session(autoflush=True, transactional=True) + u = User(user_name='ed', addresses=[Address(email_address='foo')]) + sess.save(u) + self.assertEquals(sess.query(Address).filter(Address.user==u).one(), Address(email_address='foo')) + @testing.unsupported('sqlite', 'mssql') # TEMP: test causes mssql to hang @engines.close_open_connections def test_autoflush_unbound(self): -- 2.47.3