Hi! I applied the patch with an additional test - that the connection is not
a transaction and is in autocommit mode. A transaction or non-autocommit
connection must be rolled back after a exception but the place for the
rollback is application-dependent so I didn't include a rollback in the
code. Inside a transaction it's also usually impossible to remove the
parent row(s) after an error - the only thing that's allowed is a rollback.
   Committed in the revisions 4101-4103 (branches 0.11, 0.12 ad the trunk).
Thank you!

On Mon, Dec 07, 2009 at 06:41:54PM +0200, Tom Coetser wrote:
> I am doing some tests with InheritableSQLobject and have run into a problem 
> which I hope to get some feedback on.
> 
> The problem is that when creating a new object based on an 
> InheritableSQLObject class, and the creation of the child object fails after 
> a new parent was created, the parent record will remain in the database 
> without the relevant child.
> 
> Here is a short example:
> 
> ~~~~~~~~~~ file: inheritTest3.py ~~~~~~~~~~~~
> from sqlobject import *
> from sqlobject.inheritance import InheritableSQLObject
> 
> __connection__ = "sqlite:/:memory:"
> 
> class Vehicle(InheritableSQLObject):
>     vid = StringCol(notNone=True, default=None, unique=True)
>     wheels = IntCol()
> 
> class Bicycle(Vehicle):
>     seats = IntCol(notNone=True, default=None)
> 
> class Car(Vehicle):
>     doors = IntCol(notNone=True, default=None)
> 
> def createTables():
>     Vehicle.createTable()
>     Bicycle.createTable()
>     Car.createTable()
> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
> 
> Here is a sample session showing the problem when trying to create a bicycle 
> but only supplying the args required for the Vehicle part of the object:
> 
> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
> In [1]: from inheritTest3 import *
> 
> In [2]: createTables(True)
> 
> In [3]: b = Bicycle(vid="id1", wheels=4)
> ERROR: An unexpected error occurred while tokenizing input
> The following traceback may be corrupted or invalid
> The error message is: ('EOF in multi-line statement', (22, 0))
> 
> ---------------------------------------------------------------------------
> IntegrityError                            Traceback (most recent call last)
> 
> /home/tomc/mobilereporter.repo/main/app/<ipython console> in <module>()
> 
> /usr/lib/python2.5/site-packages/sqlobject/main.pyc in __init__(self, **kw)
>    1221                 id = None
>    1222
> -> 1223             self._create(id, **kw)
>    1224
> 
> .... snipping lots of traceback parts ............
> 
> /usr/lib/python2.5/site-packages/sqlobject/sqlite/sqliteconnection.pyc in 
> _executeRetry(self, conn, cursor, query)
>     187                 raise DuplicateEntryError(msg)
>     188             else:
> --> 189                 raise IntegrityError(msg)
>     190         except self.module.InternalError, e:
>     191             raise InternalError(ErrorMessage(e))
> 
> IntegrityError: bicycle.seats may not be NULL
> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
> 
> If you now supply all required args for the bicycle, using the same field 
> values for the vid and wheels as before, a duplicate entry error is raised:
> 
> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
> 
> In [4]: b = Bicycle(vid="id1", wheels=4, seats=2)
> ERROR: An unexpected error occurred while tokenizing input
> The following traceback may be corrupted or invalid
> The error message is: ('EOF in multi-line statement', (22, 0))
> 
> ---------------------------------------------------------------------------
> DuplicateEntryError                       Traceback (most recent call last)
> 
> /home/tomc/mobilereporter.repo/main/app/<ipython console> in <module>()
> 
> /usr/lib/python2.5/site-packages/sqlobject/main.pyc in __init__(self, **kw)
>    1221                 id = None
>    1222
> -> 1223             self._create(id, **kw)
>    1224
>    1225             for func in post_funcs:
> 
> .... snipping lots of traceback parts ............
> 
> /usr/lib/python2.5/site-packages/sqlobject/sqlite/sqliteconnection.pyc in 
> _executeRetry(self, conn, cursor, query)
>     185             msg = ErrorMessage(e)
>     186             if msg.startswith('column') and msg.endswith('not 
> unique'):
> --> 187                 raise DuplicateEntryError(msg)
>     188             else:
>     189                 raise IntegrityError(msg)
> 
> DuplicateEntryError: column vid is not unique
> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
> 
> The duplicate entry is because of the parent vehicle record was created when 
> the first bicycle creation failed: 
> 
> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
> In [5]: q = "SELECT * from vehicle"
> 
> In [6]: Vehicle._connection.queryAll(q)
> Out[6]: [(1, 'id1', 4, 'Bicycle')]
> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
> 
> So, the problem is that if a new child object is created, but fails after the 
> parent has been created, the parent is left in tact in the database.
> 
> I'm attaching a patch as a possible fix for this, but I am not sure under 
> which other circumstances this patch will cause other problems. Maybe a flag 
> is needed inside the block that creates the child (parentCreatedHere=True), 
> and only if this flag is set, should the parent be deleted in exception if 
> the child creation failed?
> 
> I would appreciate if someone with more insight into this can look at the 
> patch an modify where neccessary.
> 
> BTW: This patch is against SQLObject 0.11.0.
> 
> 
> 
> Cheers,
>  Tom

> --- inheritance/__init__.py.orig      2009-12-07 14:47:31.000000000 +0000
> +++ inheritance/__init__.py   2009-12-07 16:35:39.000000000 +0000
> @@ -384,7 +384,19 @@
>  
>              id = self._parent.id
>  
> -        super(InheritableSQLObject, self)._create(id, **kw)
> +        # TC: Create this record and catch all exceptions in order to destroy
> +        # TC: the parent if the child can not be created.
> +        try:
> +            super(InheritableSQLObject, self)._create(id, **kw)
> +        except:
> +            # TC: If we are the child, destroy the parent
> +            if self.sqlmeta.parentClass:
> +                self._parent.destroySelf()
> +                #TC: Do we need to do this??
> +                self._parent = None
> +            # TC: Reraise the original exception
> +            raise
> +
>  
>      def _findAlternateID(cls, name, dbName, value, connection=None):
>          result = list(cls.selectBy(connection, **{name: value}))

Oleg.
-- 
     Oleg Broytman            http://phd.pp.ru/            p...@phd.pp.ru
           Programmers don't die, they just GOSUB without RETURN.

------------------------------------------------------------------------------
The Planet: dedicated and managed hosting, cloud storage, colocation
Stay online with enterprise data centers and the best network in the business
Choose flexible plans and management services without long-term contracts
Personal 24x7 support from experience hosting pros just a phone call away.
http://p.sf.net/sfu/theplanet-com
_______________________________________________
sqlobject-discuss mailing list
sqlobject-discuss@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/sqlobject-discuss

Reply via email to