]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- select().as_scalar() will raise an exception if the select does not have
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 18 Dec 2007 00:24:03 +0000 (00:24 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 18 Dec 2007 00:24:03 +0000 (00:24 +0000)
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

CHANGES
doc/build/content/ormtutorial.txt
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/sql/expression.py
test/orm/attributes.py
test/sql/select.py

diff --git a/CHANGES b/CHANGES
index f76a85901078e7cabca91aee25864014a3902ffd..7f48b7e0892f7439949e4b387972905f4c614baa 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -28,6 +28,9 @@ CHANGES
       names.  Generated bind params now have the form "<paramname>_<num>",
       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"
index fbbd3c5525ec31e80c42fb62d959085fc9a61cb9..a0382708ef481ac757d98436b0a08967b6bb7b9e 100644 (file)
@@ -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}[]
 
index 677167c1b9edd42860a426651a00be2e14fd4e97..6d5fae50771db9800f66ff54f78084bdc7d5209d 100644 (file)
@@ -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
index ddeaaf8addad5c0c589022663cd65ae9d0eda8d7..55001dc700857bfbdc3f20047cf244a2c6c0e88e 100644 (file)
@@ -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.  
         
index 088e33623716cf1fc6321aa6c5d6789c38d95e67..a756566d5f087f49249455b4830685a50aceb047 100644 (file)
@@ -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()
index 10e92a0833408b0ce6cf52eaefce7f6679fdc3ae..3f613596e2fb884c1b7de8beb231746e86fedd0e 100644 (file)
@@ -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")