Avoid to update inlined relation column to NULL when deleting entities
When deleting entities, cubicweb run a rql DELETE on all relations to trigger hooks.
For an inlined relation this also mean set the column to NULL. This operation
may fail if there's additional constraints on the column.
Also this is a weird and useless behavior since deleting the entity row will by
definition delete the relation.
We still doesn't handle the case where both subject and object are going to be
deleted because rows need to be deleted in a particular order that cubicweb
doesn't handle.
Add a test checking UPDATE does not occur but hooks are correctly called.
Closes #17236690
--- a/cubicweb/server/sources/native.py Wed Sep 11 11:37:05 2019 +0200
+++ b/cubicweb/server/sources/native.py Wed Sep 11 11:56:43 2019 +0200
@@ -653,6 +653,18 @@
def delete_relation(self, cnx, subject, rtype, object):
"""delete a relation from the source"""
rschema = self.schema.rschema(rtype)
+ # we should not issue UPDATE on inlined relations when subject is going
+ # to be deleted.
+ # Unless object is also going to be deleted and is not equal to the
+ # subject, in this case DELETE should occur in a order which cubicweb
+ # doesn't handle (yet). For example
+ # cubicweb/server/test/unittest_migractions.py::MigrationCommandsTC::test_add_drop_entity_type
+ # trigger such case.
+ if (
+ rschema.inlined and cnx.deleted_in_transaction(subject)
+ and (subject == object or not cnx.deleted_in_transaction(object))
+ ):
+ return
self._delete_relation(cnx, subject, rtype, object, rschema.inlined)
if cnx.ertype_supports_undo(rtype):
self._record_tx_action(cnx, 'tx_relation_actions', u'R',
--- a/cubicweb/server/test/unittest_repository.py Wed Sep 11 11:37:05 2019 +0200
+++ b/cubicweb/server/test/unittest_repository.py Wed Sep 11 11:56:43 2019 +0200
@@ -322,6 +322,58 @@
cnx.commit()
self.assertEqual(bk.title, 'root')
+ def test_delete_entity_with_inlined_relation(self):
+ """Test deletion of entity with inlined relations.
+
+ In this case, inlined relation column should not be deleted (= e.g. not
+ updated to NULL), but hooks before_delete_relation and
+ afer_delete_relation must be called.
+ """
+ class OnDeleteInlined(Hook):
+ __regid__ = 'on_delete_inlined'
+ __select__ = Hook.__select__ & hook.match_rtype('personne_inlined')
+ events = ('before_delete_relation', 'after_delete_relation')
+ CALLED = []
+
+ def __call__(self):
+ OnDeleteInlined.CALLED.append(self.event)
+
+ with self.admin_access.cnx() as cnx:
+ # allow only one null on cw_personne_inlined column
+ cnx.system_sql(
+ 'CREATE UNIQUE INDEX test_composite_idx ON cw_personne(true) '
+ 'WHERE cw_personne_inlined is NULL')
+ cnx.commit()
+
+ # deletion of p1 should not set personne_inlined to NULL (otherwise
+ # the unique index will raise)
+ p0 = cnx.create_entity('Personne', nom=u'p0')
+ p1 = cnx.create_entity('Personne', nom=u'p1', personne_inlined=p0)
+ cnx.commit()
+ with self.temporary_appobjects(OnDeleteInlined):
+ cnx.entity_from_eid(p1.eid).cw_delete()
+ assert OnDeleteInlined.CALLED == [
+ 'before_delete_relation', 'after_delete_relation']
+ cnx.commit()
+
+ # XXX: This case is not handled because entities need to be deleted
+ # in a specific order
+ # p1 = cnx.create_entity('Personne', nom=u'p1', personne_inlined=p0)
+ # cnx.commit()
+ # cnx.execute('DELETE Personne X WHERE X eid IN ({}, {})'.format(p1.eid, p0.eid))
+ # cnx.commit()
+ cnx.entity_from_eid(p0.eid).cw_delete()
+ cnx.commit()
+
+ # deletion of p1 should not set personne_inlined to NULL
+ p1 = cnx.create_entity('Personne', nom=u'p1')
+ p1.cw_set(personne_inlined=p1)
+ p0 = cnx.create_entity('Personne', nom=u'p0')
+ cnx.commit()
+ cnx.entity_from_eid(p1.eid).cw_delete()
+ cnx.commit()
+
+
class SchemaDeserialTC(CubicWebTC):
appid = 'data-schemaserial'