]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
merge -r6064:6082 of 0.5 trunk
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 3 Jul 2009 16:05:55 +0000 (16:05 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 3 Jul 2009 16:05:55 +0000 (16:05 +0000)
CHANGES
doc/build/mappers.rst
lib/sqlalchemy/dialects/firebird/base.py
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/scoping.py
test/ext/test_declarative.py
test/orm/test_attributes.py
test/orm/test_scoping.py

diff --git a/CHANGES b/CHANGES
index 1d986020a2a687ea5c98f8cf656c75c9864a4cab..faee0d3dd281d62d8315284a16ca6970fc493f85 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -12,6 +12,13 @@ CHANGES
       the tests.  [ticket:970]
       
 - orm
+    - Session.mapper is now *deprecated*.   
+      Call session.add() if you'd like a free-standing object to be 
+      part of your session.  Otherwise, a DIY version of
+      Session.mapper is now documented at 
+      http://www.sqlalchemy.org/trac/wiki/UsageRecipes/SessionAwareMapper
+      The method will remain deprecated throughout 0.6.
+    
     - Fixed bug introduced in 0.5.4 whereby Composite types
       fail when default-holding columns are flushed.
       
@@ -45,6 +52,8 @@ CHANGES
       columns, has been enhanced such that the fk->itself aspect of the 
       relation won't be used to determine relation direction.
      
+    - repaired non-working attributes.set_committed_value function.
+
     - Trimmed the pickle format for InstanceState which should further
       reduce the memory footprint of pickled instances.  The format
       should be backwards compatible with that of 0.5.4 and previous.
index ff9b1f95bf327c19dbabe648d498bb5daabf20df..2dcd52fb3bef61ae7600c4781bd063425d24ed3f 100644 (file)
@@ -1205,8 +1205,24 @@ Eager loading of relations occurs using joins or outerjoins from parent to child
 Specifying Alternate Join Conditions to relation() 
 ---------------------------------------------------
 
-
-The ``relation()`` function uses the foreign key relationship between the parent and child tables to formulate the **primary join condition** between parent and child; in the case of a many-to-many relationship it also formulates the **secondary join condition**.  If you are working with a ``Table`` which has no ``ForeignKey`` objects on it (which can be the case when using reflected tables with MySQL), or if the join condition cannot be expressed by a simple foreign key relationship, use the ``primaryjoin`` and possibly ``secondaryjoin`` conditions to create the appropriate relationship.
+The ``relation()`` function uses the foreign key relationship between the parent and child tables to formulate the **primary join condition** between parent and child; in the case of a many-to-many relationship it also formulates the **secondary join condition**::
+
+      one to many/many to one:
+      ------------------------
+      
+      parent_table -->  parent_table.c.id == child_table.c.parent_id -->  child_table
+                                     primaryjoin
+      
+      many to many:
+      -------------
+                                
+      parent_table -->  parent_table.c.id == secondary_table.c.parent_id -->  
+                                     primaryjoin
+                                 
+                        secondary_table.c.child_id == child_table.c.id --> child_table
+                                    secondaryjoin
+
+If you are working with a ``Table`` which has no ``ForeignKey`` objects on it (which can be the case when using reflected tables with MySQL), or if the join condition cannot be expressed by a simple foreign key relationship, use the ``primaryjoin`` and possibly ``secondaryjoin`` conditions to create the appropriate relationship.
 
 In this example we create a relation ``boston_addresses`` which will only load the user addresses with a city of "Boston":
 
@@ -1287,10 +1303,45 @@ Theres no restriction on how many times you can relate from parent to child.  SQ
 
 .. _alternate_collection_implementations:
 
+Rows that point to themselves / Mutually Dependent Rows
+-------------------------------------------------------
+
+This is a very specific case where relation() must perform an INSERT and a second UPDATE in order to properly populate a row (and vice versa an UPDATE and DELETE in order to delete without violating foreign key constraints).   The two use cases are:
+
+ * A table contains a foreign key to itself, and a single row will have a foreign key value pointing to its own primary key.
+ * Two tables each contain a foreign key referencing the other table, with a row in each table referencing the other.
+
+For example::
+
+              user
+    ---------------------------------
+    user_id    name   related_user_id
+       1       'ed'          1
+
+Or::
+
+                 widget                                                  entry
+    -------------------------------------------             ---------------------------------
+    widget_id     name        favorite_entry_id             entry_id      name      widget_id
+       1       'somewidget'          5                         5       'someentry'     1
+
+In the first case, a row points to itself.  Technically, a database that uses sequences such as Postgres or Oracle can INSERT the row at once using a previously generated value, but databases which rely upon autoincrement-style primary key identifiers cannot.  The ``relation()`` always assumes a "parent/child" model of row population during flush, so unless you are populating the primary key/foreign key columns directly, ``relation()`` needs to use two statements.
+
+In the second case, the "widget" row must be inserted before any referring "entry" rows, but then the "favorite_entry_id" column of that "widget" row cannot be set until the "entry" rows have been generated.  In this case, it's typically impossible to insert the "widget" and "entry" rows using just two INSERT statements; an UPDATE must be performed in order to keep foreign key constraints fulfilled.   The exception is if the foreign keys are configured as "deferred until commit" (a feature some databases support) and if the identifiers were populated manually (again essentially bypassing ``relation()``).
+
+To enable the UPDATE after INSERT / UPDATE before DELETE behavior on ``relation()``, use the ``post_update`` flag on *one* of the relations, preferably the many-to-one side::
+
+    mapper(Widget, widget, properties={
+        'entries':relation(Entry, primaryjoin=widget.c.widget_id==entry.c.widget_id),
+        'favorite_entry':relation(Entry, primaryjoin=widget.c.favorite_entry_id==entry.c.entry_id, post_update=True)
+    })
+
+When a structure using the above mapping is flushed, the "widget" row will be INSERTed minus the "favorite_entry_id" value, then all the "entry" rows will be INSERTed referencing the parent "widget" row, and then an UPDATE statement will populate the "favorite_entry_id" column of the "widget" table (it's one row at a time for the time being).
+
+
 Alternate Collection Implementations 
 -------------------------------------
 
-
 Mapping a one-to-many or many-to-many relationship results in a collection of values accessible through an attribute on the parent instance.  By default, this collection is a ``list``:
 
 .. sourcecode:: python+sql
index a77801fcfc2eede8bab2c7e740c2dbee8027c47e..a616ee0f692d5c3f9115d8d5392d7332f1e27af3 100644 (file)
@@ -254,7 +254,7 @@ ischema_names = {
        'DATE': lambda r: FBDate(),
        'TIME': lambda r: FBTime(),
        'TEXT': lambda r: FBString(r['flen']),
-      'INT64': lambda r: FBNumeric(precision=r['fprec'], length=r['fscale'] * -1), # This generically handles NUMERIC()
+      'INT64': lambda r: FBNumeric(precision=r['fprec'], scale=r['fscale'] * -1), # This generically handles NUMERIC()
      'DOUBLE': lambda r: FBFloat(),
   'TIMESTAMP': lambda r: FBDateTime(),
     'VARYING': lambda r: FBString(r['flen']),
index f187877c49ea41417fe4c1e02cbd82ab15847174..97d0e42e9196994b9eb554909124e4dbc41b78fb 100644 (file)
@@ -1415,7 +1415,7 @@ def set_committed_value(instance, key, value):
     
     """
     state, dict_ = instance_state(instance), instance_dict(instance)
-    state.get_impl(key).set_committed_value(state, dict_, key, value)
+    state.get_impl(key).set_committed_value(state, dict_, value)
     
 def set_attribute(instance, key, value):
     """Set the value of an attribute, firing history events.
index c8d90a5c1b223f6c0fdf156ae225ab8bd5b01048..28eb63819ec4ba5a52cb754c869f844383116415 100644 (file)
@@ -5,7 +5,7 @@
 # the MIT License: http://www.opensource.org/licenses/mit-license.php
 
 import sqlalchemy.exceptions as sa_exc
-from sqlalchemy.util import ScopedRegistry, to_list, get_cls_kwargs
+from sqlalchemy.util import ScopedRegistry, to_list, get_cls_kwargs, deprecated
 from sqlalchemy.orm import (
     EXT_CONTINUE, MapperExtension, class_mapper, object_session
     )
@@ -23,12 +23,7 @@ class ScopedSession(object):
 
       Session = scoped_session(sessionmaker(autoflush=True))
 
-      To map classes so that new instances are saved in the current
-      Session automatically, as well as to provide session-aware
-      class attributes such as "query":
-
-      mapper = Session.mapper
-      mapper(Class, table, ...)
+      ... use session normally.
 
     """
 
@@ -57,8 +52,15 @@ class ScopedSession(object):
             self.registry().close()
         self.registry.clear()
 
+    @deprecated("Session.mapper is deprecated.  "
+        "Please see http://www.sqlalchemy.org/trac/wiki/UsageRecipes/SessionAwareMapper "
+        "for information on how to replicate its behavior.")
     def mapper(self, *args, **kwargs):
-        """return a mapper() function which associates this ScopedSession with the Mapper."""
+        """return a mapper() function which associates this ScopedSession with the Mapper.
+        
+        DEPRECATED.
+        
+        """
 
         from sqlalchemy.orm import mapper
 
index c49c00cec0d267c8aca0c70016c1b02042a56cf9..1e2fc9b60dc73eb2710e8efae87848a3a2a08397 100644 (file)
@@ -398,6 +398,7 @@ class DeclarativeTest(DeclarativeTestBase):
             Address(email='two'),
         ])])
         
+    @testing.uses_deprecated()
     def test_custom_mapper(self):
         class MyExt(sa.orm.MapperExtension):
             def create_instance(self):
index a94fdbe41460e1787656a789ff0a733360dc8ede..0295e42b22ac9654df5e330f5caa3c3e00a59716 100644 (file)
@@ -513,6 +513,32 @@ class AttributesTest(_base.ORMTest):
         except sa_exc.ArgumentError, e:
             assert False
 
+class UtilTest(_base.ORMTest):
+    def test_helpers(self):
+        class Foo(object):
+            pass
+
+        class Bar(object):
+            pass
+        
+        attributes.register_class(Foo)
+        attributes.register_class(Bar)
+        attributes.register_attribute(Foo, "coll", uselist=True, useobject=True)
+    
+        f1 = Foo()
+        b1 = Bar()
+        b2 = Bar()
+        coll = attributes.init_collection(f1, "coll")
+        assert coll.data is f1.coll
+        assert attributes.get_attribute(f1, "coll") is f1.coll
+        attributes.set_attribute(f1, "coll", [b1])
+        assert f1.coll == [b1]
+        eq_(attributes.get_history(f1, "coll"), ([b1], [], []))
+        attributes.set_committed_value(f1, "coll", [b2])
+        eq_(attributes.get_history(f1, "coll"), ((), [b2], ()))
+        
+        attributes.del_attribute(f1, "coll")
+        assert "coll" not in f1.__dict__
 
 class BackrefTest(_base.ORMTest):
 
index 2117e8dccbf47cc2935d65598601b96c544c7745..9f2f59e19b42db526f5d5920b489b73159b02fb1 100644 (file)
@@ -96,6 +96,7 @@ class ScopedMapperTest(_ScopedTest):
             pass
 
     @classmethod
+    @testing.uses_deprecated()
     @testing.resolve_artifact_names
     def setup_mappers(cls):
         Session = scoped_session(sa.orm.create_session)
@@ -122,6 +123,7 @@ class ScopedMapperTest(_ScopedTest):
         sso = SomeOtherObject.query().first()
         assert SomeObject.query.filter_by(id=1).one().options[0].id == sso.id
 
+    @testing.uses_deprecated()
     @testing.resolve_artifact_names
     def test_query_compiles(self):
         class Foo(object):
@@ -141,6 +143,7 @@ class ScopedMapperTest(_ScopedTest):
         Session.mapper(Baz, table2, extension=ext)
         assert hasattr(Baz, 'query')
 
+    @testing.uses_deprecated()
     @testing.resolve_artifact_names
     def test_default_constructor_state_not_shared(self):
         scope = scoped_session(sa.orm.sessionmaker())
@@ -171,6 +174,7 @@ class ScopedMapperTest(_ScopedTest):
         assert_raises(TypeError, C, foo='bar')
         D(foo='bar')
 
+    @testing.uses_deprecated()
     @testing.resolve_artifact_names
     def test_validating_constructor(self):
         s2 = SomeObject(someid=12)
@@ -183,6 +187,7 @@ class ScopedMapperTest(_ScopedTest):
         assert_raises(sa.exc.ArgumentError, ValidatedOtherObject,
                           someid=12, bogus=345)
 
+    @testing.uses_deprecated()
     @testing.resolve_artifact_names
     def test_dont_clobber_methods(self):
         class MyClass(object):
@@ -215,6 +220,7 @@ class ScopedMapperTest2(_ScopedTest):
             pass
 
     @classmethod
+    @testing.uses_deprecated()
     @testing.resolve_artifact_names
     def setup_mappers(cls):
         Session = scoped_session(sa.orm.sessionmaker())