# HG changeset patch # User Adrien Di Mascio # Date 1269022891 -3600 # Node ID d9e8af8a7a42f53b9835b486ff5d0bc14a9315c6 # Parent 6b0832bbd1daf27c2ce445af5b5222e1e522fb90 [source] implement storages right in the source rather than in hooks The problem is that Storage objects will most probably change entity's dictionary so that values are correctly set before the source's corresponding method (e.g. entity_added()) is called. For instance, the BFSFileStorage will change the original binary data and replace it with the destination file path in order to store the file path in the database. This change must be local to the source in order not to impact other hooks or attribute access during the transaction, the whole idea being that the same application code should work exactly the same whether or not a BFSStorage is used or not. diff -r 6b0832bbd1da -r d9e8af8a7a42 hooks/storages.py --- a/hooks/storages.py Fri Mar 19 15:27:45 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,41 +0,0 @@ -"""hooks to handle attributes mapped to a custom storage -""" -from cubicweb.server.hook import Hook -from cubicweb.server.sources.storages import ETYPE_ATTR_STORAGE - - -class BFSSHook(Hook): - """abstract class for bytes file-system storage hooks""" - __abstract__ = True - category = 'bfss' - - -class PreAddEntityHook(BFSSHook): - """""" - __regid__ = 'bfss_add_entity' - events = ('before_add_entity', ) - - def __call__(self): - etype = self.entity.__regid__ - for attr in ETYPE_ATTR_STORAGE.get(etype, ()): - ETYPE_ATTR_STORAGE[etype][attr].entity_added(self.entity, attr) - -class PreUpdateEntityHook(BFSSHook): - """""" - __regid__ = 'bfss_update_entity' - events = ('before_update_entity', ) - - def __call__(self): - etype = self.entity.__regid__ - for attr in ETYPE_ATTR_STORAGE.get(etype, ()): - ETYPE_ATTR_STORAGE[etype][attr].entity_updated(self.entity, attr) - -class PreDeleteEntityHook(BFSSHook): - """""" - __regid__ = 'bfss_delete_entity' - events = ('before_delete_entity', ) - - def __call__(self): - etype = self.entity.__regid__ - for attr in ETYPE_ATTR_STORAGE.get(etype, ()): - ETYPE_ATTR_STORAGE[etype][attr].entity_deleted(self.entity, attr) diff -r 6b0832bbd1da -r d9e8af8a7a42 server/sources/native.py --- a/server/sources/native.py Fri Mar 19 15:27:45 2010 +0100 +++ b/server/sources/native.py Fri Mar 19 19:21:31 2010 +0100 @@ -19,6 +19,7 @@ from threading import Lock from datetime import datetime from base64 import b64decode, b64encode +from contextlib import contextmanager from logilab.common.compat import any from logilab.common.cache import Cache @@ -191,6 +192,8 @@ self._cache = Cache(repo.config['rql-cache-size']) self._temp_table_data = {} self._eid_creation_lock = Lock() + # (etype, attr) / storage mapping + self._storages = {} # XXX no_sqlite_wrap trick since we've a sqlite locking pb when # running unittest_multisources with the wrapping below if self.dbdriver == 'sqlite' and \ @@ -267,6 +270,18 @@ def unmap_attribute(self, etype, attr): self._rql_sqlgen.attr_map.pop('%s.%s' % (etype, attr), None) + def set_storage(self, etype, attr, storage): + storage_dict = self._storages.setdefault(etype, {}) + storage_dict[attr] = storage + self.map_attribute(etype, attr, storage.sqlgen_callback) + + def unset_storage(self, etype, attr): + self._storages[etype].pop(attr) + # if etype has no storage left, remove the entry + if not self._storages[etype]: + del self._storages[etype] + self.unmap_attribute(etype, attr) + # ISource interface ####################################################### def compile_rql(self, rql, sols): @@ -402,40 +417,63 @@ except KeyError: continue + @contextmanager + def _storage_handler(self, entity, event): + # 1/ memorize values as they are before the storage is called. + # For instance, the BFSStorage will replace the `data` + # binary value with a Binary containing the destination path + # on the filesystem. To make the entity.data usage absolutely + # transparent, we'll have to reset entity.data to its binary + # value once the SQL query will be executed + orig_values = {} + etype = entity.__regid__ + for attr, storage in self._storages.get(etype, {}).items(): + if attr in entity.edited_attributes: + orig_values[attr] = entity[attr] + handler = getattr(storage, 'entity_%s' % event) + handler(entity, attr) + yield # 2/ execute the source's instructions + # 3/ restore original values + for attr, value in orig_values.items(): + entity[attr] = value + def add_entity(self, session, entity): """add a new entity to the source""" - attrs = self.preprocess_entity(entity) - sql = self.sqlgen.insert(SQL_PREFIX + entity.__regid__, attrs) - self.doexec(session, sql, attrs) - if session.undoable_action('C', entity.__regid__): - self._record_tx_action(session, 'tx_entity_actions', 'C', - etype=entity.__regid__, eid=entity.eid) + with self._storage_handler(entity, 'added'): + attrs = self.preprocess_entity(entity) + sql = self.sqlgen.insert(SQL_PREFIX + entity.__regid__, attrs) + self.doexec(session, sql, attrs) + if session.undoable_action('C', entity.__regid__): + self._record_tx_action(session, 'tx_entity_actions', 'C', + etype=entity.__regid__, eid=entity.eid) def update_entity(self, session, entity): """replace an entity in the source""" - attrs = self.preprocess_entity(entity) - if session.undoable_action('U', entity.__regid__): - changes = self._save_attrs(session, entity, attrs) - self._record_tx_action(session, 'tx_entity_actions', 'U', - etype=entity.__regid__, eid=entity.eid, - changes=self._binary(dumps(changes))) - sql = self.sqlgen.update(SQL_PREFIX + entity.__regid__, attrs, - ['cw_eid']) - self.doexec(session, sql, attrs) + with self._storage_handler(entity, 'updated'): + attrs = self.preprocess_entity(entity) + if session.undoable_action('U', entity.__regid__): + changes = self._save_attrs(session, entity, attrs) + self._record_tx_action(session, 'tx_entity_actions', 'U', + etype=entity.__regid__, eid=entity.eid, + changes=self._binary(dumps(changes))) + sql = self.sqlgen.update(SQL_PREFIX + entity.__regid__, attrs, + ['cw_eid']) + self.doexec(session, sql, attrs) def delete_entity(self, session, entity): """delete an entity from the source""" - if session.undoable_action('D', entity.__regid__): - attrs = [SQL_PREFIX + r.type - for r in entity.e_schema.subject_relations() - if (r.final or r.inlined) and not r in VIRTUAL_RTYPES] - changes = self._save_attrs(session, entity, attrs) - self._record_tx_action(session, 'tx_entity_actions', 'D', - etype=entity.__regid__, eid=entity.eid, - changes=self._binary(dumps(changes))) - attrs = {'cw_eid': entity.eid} - sql = self.sqlgen.delete(SQL_PREFIX + entity.__regid__, attrs) - self.doexec(session, sql, attrs) + with self._storage_handler(entity, 'deleted'): + if session.undoable_action('D', entity.__regid__): + attrs = [SQL_PREFIX + r.type + for r in entity.e_schema.subject_relations() + if (r.final or r.inlined) and not r in VIRTUAL_RTYPES] + changes = self._save_attrs(session, entity, attrs) + self._record_tx_action(session, 'tx_entity_actions', 'D', + etype=entity.__regid__, eid=entity.eid, + changes=self._binary(dumps(changes))) + attrs = {'cw_eid': entity.eid} + sql = self.sqlgen.delete(SQL_PREFIX + entity.__regid__, attrs) + self.doexec(session, sql, attrs) def _add_relation(self, session, subject, rtype, object, inlined=False): """add a relation to the source""" diff -r 6b0832bbd1da -r d9e8af8a7a42 server/sources/storages.py --- a/server/sources/storages.py Fri Mar 19 15:27:45 2010 +0100 +++ b/server/sources/storages.py Fri Mar 19 19:21:31 2010 +0100 @@ -4,16 +4,11 @@ from cubicweb import Binary from cubicweb.server.hook import Operation - -ETYPE_ATTR_STORAGE = {} def set_attribute_storage(repo, etype, attr, storage): - ETYPE_ATTR_STORAGE.setdefault(etype, {})[attr] = storage - repo.system_source.map_attribute(etype, attr, storage.sqlgen_callback) + repo.system_source.set_storage(etype, attr, storage) def unset_attribute_storage(repo, etype, attr): - ETYPE_ATTR_STORAGE.setdefault(etype, {}).pop(attr, None) - repo.system_source.unmap_attribute(etype, attr) - + repo.system_source.unset_storage(etype, attr) class Storage(object): """abstract storage"""