From: Mike Bayer Date: Tue, 18 Dec 2007 00:24:03 +0000 (+0000) Subject: - select().as_scalar() will raise an exception if the select does not have X-Git-Tag: rel_0_4_2~35 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=f6068a3522bb92fac18c930eb26798fc4cb1889f;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - select().as_scalar() will raise an exception if the select does not have exactly one expression in its columns clause. - added "helper exception" to select.type access, generic functions raise the chance of this happening - a slight behavioral change to attributes is, del'ing an attribute does *not* cause the lazyloader of that attribute to fire off again; the "del" makes the effective value of the attribute "None". To re-trigger the "loader" for an attribute, use session.expire(instance, [attrname]). - fix ormtutorial for IS NULL --- diff --git a/CHANGES b/CHANGES index f76a859010..7f48b7e089 100644 --- a/CHANGES +++ b/CHANGES @@ -28,6 +28,9 @@ CHANGES names. Generated bind params now have the form "_", whereas before only the second bind of the same name would have this form. + - select().as_scalar() will raise an exception if the select does not have + exactly one expression in its columns clause. + - bindparam() objects themselves can be used as keys for execute(), i.e. statement.execute({bind1:'foo', bind2:'bar'}) @@ -88,6 +91,12 @@ CHANGES (i.e. a pickled object or other mutable item), the reason for the copy-on-load change in the first place, retain the old behavior. + + - a slight behavioral change to attributes is, del'ing an attribute + does *not* cause the lazyloader of that attribute to fire off again; + the "del" makes the effective value of the attribute "None". To + re-trigger the "loader" for an attribute, use + session.expire(instance, [attrname]). - query.filter(SomeClass.somechild == None), when comparing a many-to-one property to None, properly generates "id IS NULL" diff --git a/doc/build/content/ormtutorial.txt b/doc/build/content/ormtutorial.txt index fbbd3c5525..a0382708ef 100644 --- a/doc/build/content/ormtutorial.txt +++ b/doc/build/content/ormtutorial.txt @@ -807,15 +807,13 @@ A summary of all operators usable on relations: [5] {stop}[] - a comparison to `None` also generates a negated EXISTS clause: + a comparison to `None` also generates an IS NULL clause for a many-to-one relation: {python} {sql}>>> session.query(Address).filter(Address.user==None).all() SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id FROM addresses - WHERE NOT (EXISTS (SELECT 1 - FROM users - WHERE users.id = addresses.user_id)) ORDER BY addresses.oid + WHERE addresses.user_id IS NULL ORDER BY addresses.oid [] {stop}[] diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 677167c1b9..6d5fae5077 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -208,20 +208,22 @@ class AttributeImpl(object): try: return state.dict[self.key] except KeyError: - callable_ = self._get_callable(state) - if callable_ is not None: - if passive: - return PASSIVE_NORESULT - value = callable_() - if value is not ATTR_WAS_SET: - return self.set_committed_value(state, value) - else: - if self.key not in state.dict: - return self.get(state, passive=passive) - return state.dict[self.key] - else: - # Return a new, empty value - return self.initialize(state) + # if no history, check for lazy callables, etc. + if self.key not in state.committed_state: + callable_ = self._get_callable(state) + if callable_ is not None: + if passive: + return PASSIVE_NORESULT + value = callable_() + if value is not ATTR_WAS_SET: + return self.set_committed_value(state, value) + else: + if self.key not in state.dict: + return self.get(state, passive=passive) + return state.dict[self.key] + + # Return a new, empty value + return self.initialize(state) def append(self, state, value, initiator, passive=False): self.set(state, value, initiator) @@ -767,9 +769,11 @@ class InstanceState(object): self.dict.pop(attr.impl.key, None) self.callables[attr.impl.key] = self.__fire_trigger self.expired_attributes.add(attr.impl.key) + self.committed_state = {} else: for key in attribute_names: self.dict.pop(key, None) + self.committed_state.pop(key, None) if not getattr(self.class_, key).impl.accepts_global_callable: continue diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index ddeaaf8add..55001dc700 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -1398,9 +1398,8 @@ class ColumnElement(ClauseElement, _CompareMixin): docstring for more details. """ - def __init__(self): - self.primary_key = False - self.foreign_keys = [] + primary_key = False + foreign_keys = [] def base_columns(self): if hasattr(self, '_base_columns'): @@ -1557,7 +1556,6 @@ class FromClause(Selectable): self.oid_column = None def _get_from_objects(self, **modifiers): - # this could also be [self], at the moment it doesnt matter to the Select object return [] def default_order_by(self): @@ -2486,7 +2484,6 @@ class _ColumnElementAdapter(ColumnElement): """ def __init__(self, elem): - ColumnElement.__init__(self) self.elem = elem self.type = getattr(elem, 'type', None) @@ -2900,8 +2897,11 @@ class _ScalarSelect(_Grouping): __visit_name__ = 'grouping' def __init__(self, elem): - super(_ScalarSelect, self).__init__(elem) - self.type = list(elem.inner_columns)[0].type + self.elem = elem + cols = list(elem.inner_columns) + if len(cols) != 1: + raise exceptions.InvalidRequestError("Scalar select can only be created from a Select object that has exactly one column expression.") + self.type = cols[0].type def _no_cols(self): raise exceptions.InvalidRequestError("Scalar Select expression has no columns; use this object directly within a column-level expression.") @@ -3086,6 +3086,10 @@ class Select(_SelectBaseMixin, FromClause): froms = property(_get_display_froms, doc="""Return a list of all FromClause elements which will be applied to the FROM clause of the resulting statement.""") + def type(self): + raise exceptions.InvalidRequestError("Select objects don't have a type. Call as_scalar() on this Select object to return a 'scalar' version of this Select.") + type = property(type) + def locate_all_froms(self): """return a Set of all FromClause elements referenced by this Select. diff --git a/test/orm/attributes.py b/test/orm/attributes.py index 088e336237..a756566d5f 100644 --- a/test/orm/attributes.py +++ b/test/orm/attributes.py @@ -905,8 +905,46 @@ class HistoryTest(PersistTest): f = Foo() f.bars.append(bar2) self.assertEquals(attributes.get_history(f._state, 'bars'), ([bar2], [], [])) - + def test_scalar_via_lazyload(self): + class Foo(fixtures.Base): + pass + + lazy_load = None + def lazyload(instance): + def load(): + return lazy_load + return load + + attributes.register_class(Foo) + attributes.register_attribute(Foo, 'bar', uselist=False, callable_=lazyload, useobject=False) + lazy_load = "hi" + + # with scalar non-object, the lazy callable is only executed on gets, not history + # operations + + f = Foo() + self.assertEquals(f.bar, "hi") + self.assertEquals(attributes.get_history(f._state, 'bar'), ([], ["hi"], [])) + + f = Foo() + f.bar = None + self.assertEquals(attributes.get_history(f._state, 'bar'), ([None], [], [])) + + f = Foo() + f.bar = "there" + self.assertEquals(attributes.get_history(f._state, 'bar'), (["there"], [], [])) + f.bar = "hi" + self.assertEquals(attributes.get_history(f._state, 'bar'), (["hi"], [], [])) + + f = Foo() + self.assertEquals(f.bar, "hi") + del f.bar + self.assertEquals(attributes.get_history(f._state, 'bar'), ([], [], ["hi"])) + assert f.bar is None + self.assertEquals(attributes.get_history(f._state, 'bar'), ([None], [], ["hi"])) + + def test_scalar_object_via_lazyload(self): class Foo(fixtures.Base): pass class Bar(fixtures.Base): @@ -924,6 +962,9 @@ class HistoryTest(PersistTest): bar1, bar2 = [Bar(id=1), Bar(id=2)] lazy_load = bar1 + # with scalar object, the lazy callable is only executed on gets and history + # operations + f = Foo() self.assertEquals(attributes.get_history(f._state, 'bar'), ([], [bar1], [])) @@ -937,5 +978,12 @@ class HistoryTest(PersistTest): f.bar = bar1 self.assertEquals(attributes.get_history(f._state, 'bar'), ([], [bar1], [])) + f = Foo() + self.assertEquals(f.bar, bar1) + del f.bar + self.assertEquals(attributes.get_history(f._state, 'bar'), ([None], [], [bar1])) + assert f.bar is None + self.assertEquals(attributes.get_history(f._state, 'bar'), ([None], [], [bar1])) + if __name__ == "__main__": testbase.main() diff --git a/test/sql/select.py b/test/sql/select.py index 10e92a0833..3f613596e2 100644 --- a/test/sql/select.py +++ b/test/sql/select.py @@ -60,7 +60,7 @@ class SelectTest(SQLCompileTest): assert hasattr(table1.select(), 'c') assert not hasattr(table1.c.myid.self_group(), 'columns') assert hasattr(table1.select().self_group(), 'columns') - assert not hasattr(table1.select().as_scalar().self_group(), 'columns') + assert not hasattr(select([table1.c.myid]).as_scalar().self_group(), 'columns') assert not hasattr(table1.c.myid, 'columns') assert not hasattr(table1.c.myid, 'c') assert not hasattr(table1.select().c.myid, 'c') @@ -245,6 +245,19 @@ sq.myothertable_othername AS sq_myothertable_othername FROM (" + sqstring + ") A def test_scalar_select(self): + try: + s = select([table1.c.myid, table1.c.name]).as_scalar() + assert False + except exceptions.InvalidRequestError, err: + assert str(err) == "Scalar select can only be created from a Select object that has exactly one column expression.", str(err) + + try: + # generic function which will look at the type of expression + func.coalesce(select([table1.c.myid])) + assert False + except exceptions.InvalidRequestError, err: + assert str(err) == "Select objects don't have a type. Call as_scalar() on this Select object to return a 'scalar' version of this Select.", str(err) + s = select([table1.c.myid], scalar=True, correlate=False) self.assert_compile(select([table1, s]), "SELECT mytable.myid, mytable.name, mytable.description, (SELECT mytable.myid FROM mytable) AS anon_1 FROM mytable")