Commit aa8461f8 authored by Adrien Di Mascio's avatar Adrien Di Mascio
Browse files

[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.
parent 6b0832bbd1da
"""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)
......@@ -19,6 +19,7 @@ from pickle import loads, dumps
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 @@ class NativeSQLSource(SQLAdapterMixIn, AbstractSource):
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 @@ class NativeSQLSource(SQLAdapterMixIn, AbstractSource):
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 @@ class NativeSQLSource(SQLAdapterMixIn, AbstractSource):
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"""
......
......@@ -4,16 +4,11 @@ from os import unlink, path as osp
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"""
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment