--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/hooks/syncschema.py Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,1417 @@
+# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+"""schema hooks:
+
+- synchronize the living schema object with the persistent schema
+- perform physical update on the source when necessary
+
+checking for schema consistency is done in hooks.py
+"""
+
+__docformat__ = "restructuredtext en"
+from cubicweb import _
+
+import json
+from copy import copy
+from hashlib import md5
+
+from yams.schema import (BASE_TYPES, BadSchemaDefinition,
+ RelationSchema, RelationDefinitionSchema)
+from yams import buildobjs as ybo, convert_default_value
+
+from logilab.common.decorators import clear_cache
+
+from cubicweb import validation_error
+from cubicweb.predicates import is_instance
+from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES,
+ CONSTRAINTS, UNIQUE_CONSTRAINTS, ETYPE_NAME_MAP)
+from cubicweb.server import hook, schemaserial as ss, schema2sql as y2sql
+from cubicweb.server.sqlutils import SQL_PREFIX
+from cubicweb.hooks.synccomputed import RecomputeAttributeOperation
+
+# core entity and relation types which can't be removed
+CORE_TYPES = BASE_TYPES | SCHEMA_TYPES | META_RTYPES | set(
+ ('CWUser', 'CWGroup','login', 'upassword', 'name', 'in_group'))
+
+
+def get_constraints(cnx, entity):
+ constraints = []
+ for cstreid in cnx.transaction_data.get(entity.eid, ()):
+ cstrent = cnx.entity_from_eid(cstreid)
+ cstr = CONSTRAINTS[cstrent.type].deserialize(cstrent.value)
+ cstr.eid = cstreid
+ constraints.append(cstr)
+ return constraints
+
+def group_mapping(cw):
+ try:
+ return cw.transaction_data['groupmap']
+ except KeyError:
+ cw.transaction_data['groupmap'] = gmap = ss.group_mapping(cw)
+ return gmap
+
+def add_inline_relation_column(cnx, etype, rtype):
+ """add necessary column and index for an inlined relation"""
+ attrkey = '%s.%s' % (etype, rtype)
+ createdattrs = cnx.transaction_data.setdefault('createdattrs', set())
+ if attrkey in createdattrs:
+ return
+ createdattrs.add(attrkey)
+ table = SQL_PREFIX + etype
+ column = SQL_PREFIX + rtype
+ try:
+ cnx.system_sql(str('ALTER TABLE %s ADD %s integer REFERENCES entities (eid)' % (table, column)),
+ rollback_on_failure=False)
+ cnx.info('added column %s to table %s', column, table)
+ except Exception:
+ # silent exception here, if this error has not been raised because the
+ # column already exists, index creation will fail anyway
+ cnx.exception('error while adding column %s to table %s',
+ table, column)
+ # create index before alter table which may expectingly fail during test
+ # (sqlite) while index creation should never fail (test for index existence
+ # is done by the dbhelper)
+ cnx.repo.system_source.create_index(cnx, table, column)
+ cnx.info('added index on %s(%s)', table, column)
+
+
+def insert_rdef_on_subclasses(cnx, eschema, rschema, rdefdef, props):
+ # XXX 'infered': True/False, not clear actually
+ props.update({'constraints': rdefdef.constraints,
+ 'description': rdefdef.description,
+ 'cardinality': rdefdef.cardinality,
+ 'permissions': rdefdef.get_permissions(),
+ 'order': rdefdef.order,
+ 'infered': False, 'eid': None
+ })
+ cstrtypemap = ss.cstrtype_mapping(cnx)
+ groupmap = group_mapping(cnx)
+ object = rschema.schema.eschema(rdefdef.object)
+ for specialization in eschema.specialized_by(False):
+ if (specialization, rdefdef.object) in rschema.rdefs:
+ continue
+ sperdef = RelationDefinitionSchema(specialization, rschema,
+ object, None, values=props)
+ ss.execschemarql(cnx.execute, sperdef,
+ ss.rdef2rql(sperdef, cstrtypemap, groupmap))
+
+
+def check_valid_changes(cnx, entity, ro_attrs=('name', 'final')):
+ errors = {}
+ # don't use getattr(entity, attr), we would get the modified value if any
+ for attr in entity.cw_edited:
+ if attr in ro_attrs:
+ origval, newval = entity.cw_edited.oldnewvalue(attr)
+ if newval != origval:
+ errors[attr] = _("can't change this attribute")
+ if errors:
+ raise validation_error(entity, errors)
+
+
+class _MockEntity(object): # XXX use a named tuple with python 2.6
+ def __init__(self, eid):
+ self.eid = eid
+
+
+class SyncSchemaHook(hook.Hook):
+ """abstract class for schema synchronization hooks (in the `syncschema`
+ category)
+ """
+ __abstract__ = True
+ category = 'syncschema'
+
+
+# operations for low-level database alteration ################################
+
+class DropTable(hook.Operation):
+ """actually remove a database from the instance's schema"""
+ table = None # make pylint happy
+ def precommit_event(self):
+ dropped = self.cnx.transaction_data.setdefault('droppedtables',
+ set())
+ if self.table in dropped:
+ return # already processed
+ dropped.add(self.table)
+ self.cnx.system_sql('DROP TABLE %s' % self.table)
+ self.info('dropped table %s', self.table)
+
+ # XXX revertprecommit_event
+
+
+class DropRelationTable(DropTable):
+ def __init__(self, cnx, rtype):
+ super(DropRelationTable, self).__init__(
+ cnx, table='%s_relation' % rtype)
+ cnx.transaction_data.setdefault('pendingrtypes', set()).add(rtype)
+
+
+class DropColumn(hook.DataOperationMixIn, hook.Operation):
+ """actually remove the attribut's column from entity table in the system
+ database
+ """
+ def precommit_event(self):
+ cnx = self.cnx
+ for etype, attr in self.get_data():
+ table = SQL_PREFIX + etype
+ column = SQL_PREFIX + attr
+ source = cnx.repo.system_source
+ # drop index if any
+ source.drop_index(cnx, table, column)
+ if source.dbhelper.alter_column_support:
+ cnx.system_sql('ALTER TABLE %s DROP COLUMN %s' % (table, column),
+ rollback_on_failure=False)
+ self.info('dropped column %s from table %s', column, table)
+ else:
+ # not supported by sqlite for instance
+ self.error('dropping column not supported by the backend, handle '
+ 'it yourself (%s.%s)', table, column)
+
+ # XXX revertprecommit_event
+
+
+# base operations for in-memory schema synchronization ########################
+
+class MemSchemaNotifyChanges(hook.SingleLastOperation):
+ """the update schema operation:
+
+ special operation which should be called once and after all other schema
+ operations. It will trigger internal structures rebuilding to consider
+ schema changes.
+ """
+
+ def __init__(self, cnx):
+ hook.SingleLastOperation.__init__(self, cnx)
+
+ def precommit_event(self):
+ for eschema in self.cnx.repo.schema.entities():
+ if not eschema.final:
+ clear_cache(eschema, 'ordered_relations')
+
+ def postcommit_event(self):
+ repo = self.cnx.repo
+ # commit event should not raise error, while set_schema has chances to
+ # do so because it triggers full vreg reloading
+ try:
+ repo.schema.rebuild_infered_relations()
+ # trigger vreg reload
+ repo.set_schema(repo.schema)
+ # CWUser class might have changed, update current session users
+ cwuser_cls = self.cnx.vreg['etypes'].etype_class('CWUser')
+ for session in repo._sessions.values():
+ session.user.__class__ = cwuser_cls
+ except Exception:
+ self.critical('error while setting schema', exc_info=True)
+
+ def rollback_event(self):
+ self.precommit_event()
+
+
+class MemSchemaOperation(hook.Operation):
+ """base class for schema operations"""
+ def __init__(self, cnx, **kwargs):
+ hook.Operation.__init__(self, cnx, **kwargs)
+ # every schema operation is triggering a schema update
+ MemSchemaNotifyChanges(cnx)
+
+
+# operations for high-level source database alteration ########################
+
+class CWETypeAddOp(MemSchemaOperation):
+ """after adding a CWEType entity:
+ * add it to the instance's schema
+ * create the necessary table
+ * set creation_date and modification_date by creating the necessary
+ CWAttribute entities
+ * add <meta rtype> relation by creating the necessary CWRelation entity
+ """
+ entity = None # make pylint happy
+
+ def precommit_event(self):
+ cnx = self.cnx
+ entity = self.entity
+ schema = cnx.vreg.schema
+ etype = ybo.EntityType(eid=entity.eid, name=entity.name,
+ description=entity.description)
+ eschema = schema.add_entity_type(etype)
+ # create the necessary table
+ tablesql = y2sql.eschema2sql(cnx.repo.system_source.dbhelper,
+ eschema, prefix=SQL_PREFIX)
+ for sql in tablesql.split(';'):
+ if sql.strip():
+ cnx.system_sql(sql)
+ # add meta relations
+ gmap = group_mapping(cnx)
+ cmap = ss.cstrtype_mapping(cnx)
+ for rtype in (META_RTYPES - VIRTUAL_RTYPES):
+ try:
+ rschema = schema[rtype]
+ except KeyError:
+ self.critical('rtype %s was not handled at cwetype creation time', rtype)
+ continue
+ if not rschema.rdefs:
+ self.warning('rtype %s has no relation definition yet', rtype)
+ continue
+ sampletype = rschema.subjects()[0]
+ desttype = rschema.objects()[0]
+ try:
+ rdef = copy(rschema.rdef(sampletype, desttype))
+ except KeyError:
+ # this combo does not exist because this is not a universal META_RTYPE
+ continue
+ rdef.subject = _MockEntity(eid=entity.eid)
+ mock = _MockEntity(eid=None)
+ ss.execschemarql(cnx.execute, mock, ss.rdef2rql(rdef, cmap, gmap))
+
+ def revertprecommit_event(self):
+ # revert changes on in memory schema
+ self.cnx.vreg.schema.del_entity_type(self.entity.name)
+ # revert changes on database
+ self.cnx.system_sql('DROP TABLE %s%s' % (SQL_PREFIX, self.entity.name))
+
+
+class CWETypeRenameOp(MemSchemaOperation):
+ """this operation updates physical storage accordingly"""
+ oldname = newname = None # make pylint happy
+
+ def rename(self, oldname, newname):
+ self.cnx.vreg.schema.rename_entity_type(oldname, newname)
+ # we need sql to operate physical changes on the system database
+ sqlexec = self.cnx.system_sql
+ dbhelper = self.cnx.repo.system_source.dbhelper
+ sql = dbhelper.sql_rename_table(SQL_PREFIX+oldname,
+ SQL_PREFIX+newname)
+ sqlexec(sql)
+ self.info('renamed table %s to %s', oldname, newname)
+ sqlexec('UPDATE entities SET type=%(newname)s WHERE type=%(oldname)s',
+ {'newname': newname, 'oldname': oldname})
+ for eid, (etype, extid, auri) in self.cnx.repo._type_source_cache.items():
+ if etype == oldname:
+ self.cnx.repo._type_source_cache[eid] = (newname, extid, auri)
+ # XXX transaction records
+
+ def precommit_event(self):
+ self.rename(self.oldname, self.newname)
+
+ def revertprecommit_event(self):
+ self.rename(self.newname, self.oldname)
+
+
+class CWRTypeUpdateOp(MemSchemaOperation):
+ """actually update some properties of a relation definition"""
+ rschema = entity = values = None # make pylint happy
+ oldvalues = None
+
+ def precommit_event(self):
+ rschema = self.rschema
+ if rschema.final:
+ return # watched changes to final relation type are unexpected
+ cnx = self.cnx
+ if 'fulltext_container' in self.values:
+ op = UpdateFTIndexOp.get_instance(cnx)
+ for subjtype, objtype in rschema.rdefs:
+ if self.values['fulltext_container'] == 'subject':
+ op.add_data(subjtype)
+ op.add_data(objtype)
+ else:
+ op.add_data(objtype)
+ op.add_data(subjtype)
+ # update the in-memory schema first
+ self.oldvalues = dict( (attr, getattr(rschema, attr)) for attr in self.values)
+ self.rschema.__dict__.update(self.values)
+ # then make necessary changes to the system source database
+ if 'inlined' not in self.values:
+ return # nothing to do
+ inlined = self.values['inlined']
+ # check in-lining is possible when inlined
+ if inlined:
+ self.entity.check_inlined_allowed()
+ # inlined changed, make necessary physical changes!
+ sqlexec = self.cnx.system_sql
+ rtype = rschema.type
+ eidcolumn = SQL_PREFIX + 'eid'
+ if not inlined:
+ # need to create the relation if it has not been already done by
+ # another event of the same transaction
+ if not rschema.type in cnx.transaction_data.get('createdtables', ()):
+ tablesql = y2sql.rschema2sql(rschema)
+ # create the necessary table
+ for sql in tablesql.split(';'):
+ if sql.strip():
+ sqlexec(sql)
+ cnx.transaction_data.setdefault('createdtables', []).append(
+ rschema.type)
+ # copy existant data
+ column = SQL_PREFIX + rtype
+ for etype in rschema.subjects():
+ table = SQL_PREFIX + str(etype)
+ sqlexec('INSERT INTO %s_relation SELECT %s, %s FROM %s WHERE NOT %s IS NULL'
+ % (rtype, eidcolumn, column, table, column))
+ # drop existant columns
+ #if cnx.repo.system_source.dbhelper.alter_column_support:
+ for etype in rschema.subjects():
+ DropColumn.get_instance(cnx).add_data((str(etype), rtype))
+ else:
+ for etype in rschema.subjects():
+ try:
+ add_inline_relation_column(cnx, str(etype), rtype)
+ except Exception as ex:
+ # the column probably already exists. this occurs when the
+ # entity's type has just been added or if the column has not
+ # been previously dropped (eg sqlite)
+ self.error('error while altering table %s: %s', etype, ex)
+ # copy existant data.
+ # XXX don't use, it's not supported by sqlite (at least at when i tried it)
+ #sqlexec('UPDATE %(etype)s SET %(rtype)s=eid_to '
+ # 'FROM %(rtype)s_relation '
+ # 'WHERE %(etype)s.eid=%(rtype)s_relation.eid_from'
+ # % locals())
+ table = SQL_PREFIX + str(etype)
+ cursor = sqlexec('SELECT eid_from, eid_to FROM %(table)s, '
+ '%(rtype)s_relation WHERE %(table)s.%(eidcolumn)s='
+ '%(rtype)s_relation.eid_from' % locals())
+ args = [{'val': eid_to, 'x': eid} for eid, eid_to in cursor.fetchall()]
+ if args:
+ column = SQL_PREFIX + rtype
+ cursor.executemany('UPDATE %s SET %s=%%(val)s WHERE %s=%%(x)s'
+ % (table, column, eidcolumn), args)
+ # drop existant table
+ DropRelationTable(cnx, rtype)
+
+ def revertprecommit_event(self):
+ # revert changes on in memory schema
+ self.rschema.__dict__.update(self.oldvalues)
+ # XXX revert changes on database
+
+
+class CWComputedRTypeUpdateOp(MemSchemaOperation):
+ """actually update some properties of a computed relation definition"""
+ rschema = entity = rule = None # make pylint happy
+ old_rule = None
+
+ def precommit_event(self):
+ # update the in-memory schema first
+ self.old_rule = self.rschema.rule
+ self.rschema.rule = self.rule
+
+ def revertprecommit_event(self):
+ # revert changes on in memory schema
+ self.rschema.rule = self.old_rule
+
+
+class CWAttributeAddOp(MemSchemaOperation):
+ """an attribute relation (CWAttribute) has been added:
+ * add the necessary column
+ * set default on this column if any and possible
+ * register an operation to add the relation definition to the
+ instance's schema on commit
+
+ constraints are handled by specific hooks
+ """
+ entity = None # make pylint happy
+
+ def init_rdef(self, **kwargs):
+ entity = self.entity
+ fromentity = entity.stype
+ rdefdef = self.rdefdef = ybo.RelationDefinition(
+ str(fromentity.name), entity.rtype.name, str(entity.otype.name),
+ description=entity.description, cardinality=entity.cardinality,
+ constraints=get_constraints(self.cnx, entity),
+ order=entity.ordernum, eid=entity.eid, **kwargs)
+ try:
+ self.cnx.vreg.schema.add_relation_def(rdefdef)
+ except BadSchemaDefinition:
+ # rdef has been infered then explicitly added (current consensus is
+ # not clear at all versus infered relation handling (and much
+ # probably buggy)
+ rdef = self.cnx.vreg.schema.rschema(rdefdef.name).rdefs[rdefdef.subject, rdefdef.object]
+ assert rdef.infered
+ else:
+ rdef = self.cnx.vreg.schema.rschema(rdefdef.name).rdefs[rdefdef.subject, rdefdef.object]
+
+ self.cnx.execute('SET X ordernum Y+1 '
+ 'WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, '
+ 'X ordernum >= %(order)s, NOT X eid %(x)s',
+ {'x': entity.eid, 'se': fromentity.eid,
+ 'order': entity.ordernum or 0})
+ return rdefdef, rdef
+
+ def precommit_event(self):
+ cnx = self.cnx
+ entity = self.entity
+ # entity.defaultval is a Binary or None, but we need a correctly typed
+ # value
+ default = entity.defaultval
+ if default is not None:
+ default = default.unzpickle()
+ props = {'default': default,
+ 'indexed': entity.indexed,
+ 'fulltextindexed': entity.fulltextindexed,
+ 'internationalizable': entity.internationalizable}
+ if entity.extra_props:
+ props.update(json.loads(entity.extra_props.getvalue().decode('ascii')))
+ # entity.formula may not exist yet if we're migrating to 3.20
+ if hasattr(entity, 'formula'):
+ props['formula'] = entity.formula
+ # update the in-memory schema first
+ rdefdef, rdef = self.init_rdef(**props)
+ # then make necessary changes to the system source database
+ syssource = cnx.repo.system_source
+ attrtype = y2sql.type_from_rdef(syssource.dbhelper, rdef)
+ # XXX should be moved somehow into lgdb: sqlite doesn't support to
+ # add a new column with UNIQUE, it should be added after the ALTER TABLE
+ # using ADD INDEX
+ if syssource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
+ extra_unique_index = True
+ attrtype = attrtype.replace(' UNIQUE', '')
+ else:
+ extra_unique_index = False
+ # added some str() wrapping query since some backend (eg psycopg) don't
+ # allow unicode queries
+ table = SQL_PREFIX + rdefdef.subject
+ column = SQL_PREFIX + rdefdef.name
+ try:
+ cnx.system_sql(str('ALTER TABLE %s ADD %s %s'
+ % (table, column, attrtype)),
+ rollback_on_failure=False)
+ self.info('added column %s to table %s', column, table)
+ except Exception as ex:
+ # the column probably already exists. this occurs when
+ # the entity's type has just been added or if the column
+ # has not been previously dropped
+ self.error('error while altering table %s: %s', table, ex)
+ if extra_unique_index or entity.indexed:
+ try:
+ syssource.create_index(cnx, table, column,
+ unique=extra_unique_index)
+ except Exception as ex:
+ self.error('error while creating index for %s.%s: %s',
+ table, column, ex)
+ # final relations are not infered, propagate
+ schema = cnx.vreg.schema
+ try:
+ eschema = schema.eschema(rdefdef.subject)
+ except KeyError:
+ return # entity type currently being added
+ # propagate attribute to children classes
+ rschema = schema.rschema(rdefdef.name)
+ # if relation type has been inserted in the same transaction, its final
+ # attribute is still set to False, so we've to ensure it's False
+ rschema.final = True
+ insert_rdef_on_subclasses(cnx, eschema, rschema, rdefdef, props)
+ # update existing entities with the default value of newly added attribute
+ if default is not None:
+ default = convert_default_value(self.rdefdef, default)
+ cnx.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column),
+ {'default': default})
+ # if attribute is computed, compute it
+ if getattr(entity, 'formula', None):
+ # add rtype attribute for RelationDefinitionSchema api compat, this
+ # is what RecomputeAttributeOperation expect
+ rdefdef.rtype = rdefdef.name
+ RecomputeAttributeOperation.get_instance(cnx).add_data(rdefdef)
+
+ def revertprecommit_event(self):
+ # revert changes on in memory schema
+ if getattr(self, 'rdefdef', None) is None:
+ return
+ self.cnx.vreg.schema.del_relation_def(
+ self.rdefdef.subject, self.rdefdef.name, self.rdefdef.object)
+ # XXX revert changes on database
+
+
+class CWRelationAddOp(CWAttributeAddOp):
+ """an actual relation has been added:
+
+ * add the relation definition to the instance's schema
+
+ * if this is an inlined relation, add the necessary column else if it's the
+ first instance of this relation type, add the necessary table and set
+ default permissions
+
+ constraints are handled by specific hooks
+ """
+ entity = None # make pylint happy
+
+ def precommit_event(self):
+ cnx = self.cnx
+ entity = self.entity
+ # update the in-memory schema first
+ rdefdef, rdef = self.init_rdef(composite=entity.composite)
+ # then make necessary changes to the system source database
+ schema = cnx.vreg.schema
+ rtype = rdefdef.name
+ rschema = schema.rschema(rtype)
+ # this have to be done before permissions setting
+ if rschema.inlined:
+ # need to add a column if the relation is inlined and if this is the
+ # first occurence of "Subject relation Something" whatever Something
+ if len(rschema.objects(rdefdef.subject)) == 1:
+ add_inline_relation_column(cnx, rdefdef.subject, rtype)
+ eschema = schema[rdefdef.subject]
+ insert_rdef_on_subclasses(cnx, eschema, rschema, rdefdef,
+ {'composite': entity.composite})
+ else:
+ if rschema.symmetric:
+ # for symmetric relations, rdefs will store relation definitions
+ # in both ways (i.e. (subj -> obj) and (obj -> subj))
+ relation_already_defined = len(rschema.rdefs) > 2
+ else:
+ relation_already_defined = len(rschema.rdefs) > 1
+ # need to create the relation if no relation definition in the
+ # schema and if it has not been added during other event of the same
+ # transaction
+ if not (relation_already_defined or
+ rtype in cnx.transaction_data.get('createdtables', ())):
+ rschema = schema.rschema(rtype)
+ # create the necessary table
+ for sql in y2sql.rschema2sql(rschema).split(';'):
+ if sql.strip():
+ cnx.system_sql(sql)
+ cnx.transaction_data.setdefault('createdtables', []).append(
+ rtype)
+
+ # XXX revertprecommit_event
+
+
+class RDefDelOp(MemSchemaOperation):
+ """an actual relation has been removed"""
+ rdef = None # make pylint happy
+
+ def precommit_event(self):
+ cnx = self.cnx
+ rdef = self.rdef
+ rschema = rdef.rtype
+ # make necessary changes to the system source database first
+ rdeftype = rschema.final and 'CWAttribute' or 'CWRelation'
+ execute = cnx.execute
+ rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
+ 'R eid %%(x)s' % rdeftype, {'x': rschema.eid})
+ lastrel = rset[0][0] == 0
+ # we have to update physical schema systematically for final and inlined
+ # relations, but only if it's the last instance for this relation type
+ # for other relations
+ if (rschema.final or rschema.inlined):
+ rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
+ 'R eid %%(r)s, X from_entity E, E eid %%(e)s'
+ % rdeftype,
+ {'r': rschema.eid, 'e': rdef.subject.eid})
+ if rset[0][0] == 0 and not cnx.deleted_in_transaction(rdef.subject.eid):
+ ptypes = cnx.transaction_data.setdefault('pendingrtypes', set())
+ ptypes.add(rschema.type)
+ DropColumn.get_instance(cnx).add_data((str(rdef.subject), str(rschema)))
+ elif rschema.inlined:
+ cnx.system_sql('UPDATE %s%s SET %s%s=NULL WHERE '
+ 'EXISTS(SELECT 1 FROM entities '
+ ' WHERE eid=%s%s AND type=%%(to_etype)s)'
+ % (SQL_PREFIX, rdef.subject, SQL_PREFIX, rdef.rtype,
+ SQL_PREFIX, rdef.rtype),
+ {'to_etype': rdef.object.type})
+ elif lastrel:
+ DropRelationTable(cnx, str(rschema))
+ else:
+ cnx.system_sql('DELETE FROM %s_relation WHERE '
+ 'EXISTS(SELECT 1 FROM entities '
+ ' WHERE eid=eid_from AND type=%%(from_etype)s)'
+ ' AND EXISTS(SELECT 1 FROM entities '
+ ' WHERE eid=eid_to AND type=%%(to_etype)s)'
+ % rschema,
+ {'from_etype': rdef.subject.type, 'to_etype': rdef.object.type})
+ # then update the in-memory schema
+ if rdef.subject not in ETYPE_NAME_MAP and rdef.object not in ETYPE_NAME_MAP:
+ rschema.del_relation_def(rdef.subject, rdef.object)
+ # if this is the last relation definition of this type, drop associated
+ # relation type
+ if lastrel and not cnx.deleted_in_transaction(rschema.eid):
+ execute('DELETE CWRType X WHERE X eid %(x)s', {'x': rschema.eid})
+
+ def revertprecommit_event(self):
+ # revert changes on in memory schema
+ #
+ # Note: add_relation_def takes a RelationDefinition, not a
+ # RelationDefinitionSchema, needs to fake it
+ rdef = self.rdef
+ rdef.name = str(rdef.rtype)
+ if rdef.subject not in ETYPE_NAME_MAP and rdef.object not in ETYPE_NAME_MAP:
+ self.cnx.vreg.schema.add_relation_def(rdef)
+
+
+
+class RDefUpdateOp(MemSchemaOperation):
+ """actually update some properties of a relation definition"""
+ rschema = rdefkey = values = None # make pylint happy
+ rdef = oldvalues = None
+ indexed_changed = null_allowed_changed = False
+
+ def precommit_event(self):
+ cnx = self.cnx
+ rdef = self.rdef = self.rschema.rdefs[self.rdefkey]
+ # update the in-memory schema first
+ self.oldvalues = dict( (attr, getattr(rdef, attr)) for attr in self.values)
+ rdef.update(self.values)
+ # then make necessary changes to the system source database
+ syssource = cnx.repo.system_source
+ if 'indexed' in self.values:
+ syssource.update_rdef_indexed(cnx, rdef)
+ self.indexed_changed = True
+ if 'cardinality' in self.values and rdef.rtype.final \
+ and self.values['cardinality'][0] != self.oldvalues['cardinality'][0]:
+ syssource.update_rdef_null_allowed(self.cnx, rdef)
+ self.null_allowed_changed = True
+ if 'fulltextindexed' in self.values:
+ UpdateFTIndexOp.get_instance(cnx).add_data(rdef.subject)
+ if 'formula' in self.values:
+ RecomputeAttributeOperation.get_instance(cnx).add_data(rdef)
+
+ def revertprecommit_event(self):
+ if self.rdef is None:
+ return
+ # revert changes on in memory schema
+ self.rdef.update(self.oldvalues)
+ # revert changes on database
+ syssource = self.cnx.repo.system_source
+ if self.indexed_changed:
+ syssource.update_rdef_indexed(self.cnx, self.rdef)
+ if self.null_allowed_changed:
+ syssource.update_rdef_null_allowed(self.cnx, self.rdef)
+
+
+def _set_modifiable_constraints(rdef):
+ # for proper in-place modification of in-memory schema: if rdef.constraints
+ # is already a list, reuse it (we're updating multiple constraints of the
+ # same rdef in the same transaction)
+ if not isinstance(rdef.constraints, list):
+ rdef.constraints = list(rdef.constraints)
+
+
+class CWConstraintDelOp(MemSchemaOperation):
+ """actually remove a constraint of a relation definition"""
+ rdef = oldcstr = newcstr = None # make pylint happy
+ size_cstr_changed = unique_changed = False
+
+ def precommit_event(self):
+ cnx = self.cnx
+ rdef = self.rdef
+ # in-place modification of in-memory schema first
+ _set_modifiable_constraints(rdef)
+ if self.oldcstr in rdef.constraints:
+ rdef.constraints.remove(self.oldcstr)
+ else:
+ self.critical('constraint %s for rdef %s was missing or already removed',
+ self.oldcstr, rdef)
+ if cnx.deleted_in_transaction(rdef.eid):
+ # don't try to alter a table that's going away (or is already gone)
+ return
+ # then update database: alter the physical schema on size/unique
+ # constraint changes
+ syssource = cnx.repo.system_source
+ cstrtype = self.oldcstr.type()
+ if cstrtype == 'SizeConstraint':
+ # if the size constraint is being replaced with a new max size, we'll
+ # call update_rdef_column in CWConstraintAddOp, skip it here
+ for cstr in cnx.transaction_data.get('newsizecstr', ()):
+ rdefentity = cstr.reverse_constrained_by[0]
+ cstrrdef = cnx.vreg.schema.schema_by_eid(rdefentity.eid)
+ if cstrrdef == rdef:
+ return
+
+ # we found that the size constraint for this rdef is really gone,
+ # not just replaced by another
+ syssource.update_rdef_column(cnx, rdef)
+ self.size_cstr_changed = True
+ elif cstrtype == 'UniqueConstraint':
+ syssource.update_rdef_unique(cnx, rdef)
+ self.unique_changed = True
+ if cstrtype in ('BoundaryConstraint', 'IntervalBoundConstraint', 'StaticVocabularyConstraint'):
+ cstrname = 'cstr' + md5((rdef.subject.type + rdef.rtype.type + cstrtype +
+ (self.oldcstr.serialize() or '')).encode('utf-8')).hexdigest()
+ cnx.system_sql('ALTER TABLE %s%s DROP CONSTRAINT %s' % (SQL_PREFIX, rdef.subject.type, cstrname))
+
+ def revertprecommit_event(self):
+ # revert changes on in memory schema
+ if self.newcstr is not None:
+ self.rdef.constraints.remove(self.newcstr)
+ if self.oldcstr is not None:
+ self.rdef.constraints.append(self.oldcstr)
+ # revert changes on database
+ syssource = self.cnx.repo.system_source
+ if self.size_cstr_changed:
+ syssource.update_rdef_column(self.cnx, self.rdef)
+ if self.unique_changed:
+ syssource.update_rdef_unique(self.cnx, self.rdef)
+
+
+class CWConstraintAddOp(CWConstraintDelOp):
+ """actually update constraint of a relation definition"""
+ entity = None # make pylint happy
+
+ def precommit_event(self):
+ cnx = self.cnx
+ rdefentity = self.entity.reverse_constrained_by[0]
+ # when the relation is added in the same transaction, the constraint
+ # object is created by the operation adding the attribute or relation,
+ # so there is nothing to do here
+ if cnx.added_in_transaction(rdefentity.eid):
+ return
+ rdef = self.rdef = cnx.vreg.schema.schema_by_eid(rdefentity.eid)
+ cstrtype = self.entity.type
+ if cstrtype in UNIQUE_CONSTRAINTS:
+ oldcstr = self.oldcstr = rdef.constraint_by_type(cstrtype)
+ else:
+ oldcstr = None
+ newcstr = self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
+ # in-place modification of in-memory schema first
+ _set_modifiable_constraints(rdef)
+ newcstr.eid = self.entity.eid
+ if oldcstr is not None:
+ rdef.constraints.remove(oldcstr)
+ rdef.constraints.append(newcstr)
+ # then update database: alter the physical schema on size/unique
+ # constraint changes
+ syssource = cnx.repo.system_source
+ if cstrtype == 'SizeConstraint' and (oldcstr is None or
+ oldcstr.max != newcstr.max):
+ syssource.update_rdef_column(cnx, rdef)
+ self.size_cstr_changed = True
+ elif cstrtype == 'UniqueConstraint' and oldcstr is None:
+ syssource.update_rdef_unique(cnx, rdef)
+ self.unique_changed = True
+ if cstrtype in ('BoundaryConstraint', 'IntervalBoundConstraint', 'StaticVocabularyConstraint'):
+ if oldcstr is not None:
+ oldcstrname = 'cstr' + md5((rdef.subject.type + rdef.rtype.type + cstrtype +
+ (self.oldcstr.serialize() or '')).encode('ascii')).hexdigest()
+ cnx.system_sql('ALTER TABLE %s%s DROP CONSTRAINT %s' %
+ (SQL_PREFIX, rdef.subject.type, oldcstrname))
+ cstrname, check = y2sql.check_constraint(rdef.subject, rdef.object, rdef.rtype.type,
+ newcstr, syssource.dbhelper, prefix=SQL_PREFIX)
+ cnx.system_sql('ALTER TABLE %s%s ADD CONSTRAINT %s CHECK(%s)' %
+ (SQL_PREFIX, rdef.subject.type, cstrname, check))
+
+
+class CWUniqueTogetherConstraintAddOp(MemSchemaOperation):
+ entity = None # make pylint happy
+
+ def precommit_event(self):
+ cnx = self.cnx
+ prefix = SQL_PREFIX
+ entity = self.entity
+ table = '%s%s' % (prefix, entity.constraint_of[0].name)
+ cols = ['%s%s' % (prefix, r.name) for r in entity.relations]
+ dbhelper = cnx.repo.system_source.dbhelper
+ sqls = dbhelper.sqls_create_multicol_unique_index(table, cols, entity.name)
+ for sql in sqls:
+ cnx.system_sql(sql)
+
+ def postcommit_event(self):
+ entity = self.entity
+ eschema = self.cnx.vreg.schema.schema_by_eid(entity.constraint_of[0].eid)
+ attrs = [r.name for r in entity.relations]
+ eschema._unique_together.append(attrs)
+
+
+class CWUniqueTogetherConstraintDelOp(MemSchemaOperation):
+ entity = cstrname = None # for pylint
+ cols = () # for pylint
+
+ def insert_index(self):
+ # We need to run before CWConstraintDelOp: if a size constraint is
+ # removed and the column is part of a unique_together constraint, we
+ # remove the unique_together index before changing the column's type.
+ # SQL Server does not support unique indices on unlimited text columns.
+ return 0
+
+ def precommit_event(self):
+ cnx = self.cnx
+ prefix = SQL_PREFIX
+ table = '%s%s' % (prefix, self.entity.type)
+ dbhelper = cnx.repo.system_source.dbhelper
+ cols = ['%s%s' % (prefix, c) for c in self.cols]
+ sqls = dbhelper.sqls_drop_multicol_unique_index(table, cols, self.cstrname)
+ for sql in sqls:
+ cnx.system_sql(sql)
+
+ def postcommit_event(self):
+ eschema = self.cnx.vreg.schema.schema_by_eid(self.entity.eid)
+ cols = set(self.cols)
+ unique_together = [ut for ut in eschema._unique_together
+ if set(ut) != cols]
+ eschema._unique_together = unique_together
+
+
+# operations for in-memory schema synchronization #############################
+
+class MemSchemaCWETypeDel(MemSchemaOperation):
+ """actually remove the entity type from the instance's schema"""
+ etype = None # make pylint happy
+
+ def postcommit_event(self):
+ # del_entity_type also removes entity's relations
+ self.cnx.vreg.schema.del_entity_type(self.etype)
+
+
+class MemSchemaCWRTypeAdd(MemSchemaOperation):
+ """actually add the relation type to the instance's schema"""
+ rtypedef = None # make pylint happy
+
+ def precommit_event(self):
+ self.cnx.vreg.schema.add_relation_type(self.rtypedef)
+
+ def revertprecommit_event(self):
+ self.cnx.vreg.schema.del_relation_type(self.rtypedef.name)
+
+
+class MemSchemaCWRTypeDel(MemSchemaOperation):
+ """actually remove the relation type from the instance's schema"""
+ rtype = None # make pylint happy
+
+ def postcommit_event(self):
+ try:
+ self.cnx.vreg.schema.del_relation_type(self.rtype)
+ except KeyError:
+ # s/o entity type have already been deleted
+ pass
+
+
+class MemSchemaPermissionAdd(MemSchemaOperation):
+ """synchronize schema when a *_permission relation has been added on a group
+ """
+ eid = action = group_eid = expr = None # make pylint happy
+
+ def precommit_event(self):
+ """the observed connections.cnxset has been commited"""
+ try:
+ erschema = self.cnx.vreg.schema.schema_by_eid(self.eid)
+ except KeyError:
+ # duh, schema not found, log error and skip operation
+ self.warning('no schema for %s', self.eid)
+ return
+ perms = list(erschema.action_permissions(self.action))
+ if self.group_eid is not None:
+ perm = self.cnx.entity_from_eid(self.group_eid).name
+ else:
+ perm = erschema.rql_expression(self.expr)
+ try:
+ perms.index(perm)
+ self.warning('%s already in permissions for %s on %s',
+ perm, self.action, erschema)
+ except ValueError:
+ perms.append(perm)
+ erschema.set_action_permissions(self.action, perms)
+
+ # XXX revertprecommit_event
+
+
+class MemSchemaPermissionDel(MemSchemaPermissionAdd):
+ """synchronize schema when a *_permission relation has been deleted from a
+ group
+ """
+
+ def precommit_event(self):
+ """the observed connections set has been commited"""
+ try:
+ erschema = self.cnx.vreg.schema.schema_by_eid(self.eid)
+ except KeyError:
+ # duh, schema not found, log error and skip operation
+ self.warning('no schema for %s', self.eid)
+ return
+ perms = list(erschema.action_permissions(self.action))
+ if self.group_eid is not None:
+ perm = self.cnx.entity_from_eid(self.group_eid).name
+ else:
+ perm = erschema.rql_expression(self.expr)
+ try:
+ perms.remove(perm)
+ erschema.set_action_permissions(self.action, perms)
+ except ValueError:
+ self.error('can\'t remove permission %s for %s on %s',
+ perm, self.action, erschema)
+
+ # XXX revertprecommit_event
+
+
+class MemSchemaSpecializesAdd(MemSchemaOperation):
+ etypeeid = parentetypeeid = None # make pylint happy
+
+ def precommit_event(self):
+ eschema = self.cnx.vreg.schema.schema_by_eid(self.etypeeid)
+ parenteschema = self.cnx.vreg.schema.schema_by_eid(self.parentetypeeid)
+ eschema._specialized_type = parenteschema.type
+ parenteschema._specialized_by.append(eschema.type)
+
+ # XXX revertprecommit_event
+
+
+class MemSchemaSpecializesDel(MemSchemaOperation):
+ etypeeid = parentetypeeid = None # make pylint happy
+
+ def precommit_event(self):
+ try:
+ eschema = self.cnx.vreg.schema.schema_by_eid(self.etypeeid)
+ parenteschema = self.cnx.vreg.schema.schema_by_eid(self.parentetypeeid)
+ except KeyError:
+ # etype removed, nothing to do
+ return
+ eschema._specialized_type = None
+ parenteschema._specialized_by.remove(eschema.type)
+
+ # XXX revertprecommit_event
+
+
+# CWEType hooks ################################################################
+
+class DelCWETypeHook(SyncSchemaHook):
+ """before deleting a CWEType entity:
+ * check that we don't remove a core entity type
+ * cascade to delete related CWAttribute and CWRelation entities
+ * instantiate an operation to delete the entity type on commit
+ """
+ __regid__ = 'syncdelcwetype'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWEType')
+ events = ('before_delete_entity',)
+
+ def __call__(self):
+ # final entities can't be deleted, don't care about that
+ name = self.entity.name
+ if name in CORE_TYPES:
+ raise validation_error(self.entity, {None: _("can't be deleted")})
+ # delete every entities of this type
+ if name not in ETYPE_NAME_MAP:
+ MemSchemaCWETypeDel(self._cw, etype=name)
+ DropTable(self._cw, table=SQL_PREFIX + name)
+
+
+class AfterDelCWETypeHook(DelCWETypeHook):
+ __regid__ = 'wfcleanup'
+ events = ('after_delete_entity',)
+
+ def __call__(self):
+ # workflow cleanup
+ self._cw.execute('DELETE Workflow X WHERE NOT X workflow_of Y')
+
+
+class AfterAddCWETypeHook(DelCWETypeHook):
+ """after adding a CWEType entity:
+ * create the necessary table
+ * set creation_date and modification_date by creating the necessary
+ CWAttribute entities
+ * add owned_by relation by creating the necessary CWRelation entity
+ * register an operation to add the entity type to the instance's
+ schema on commit
+ """
+ __regid__ = 'syncaddcwetype'
+ events = ('after_add_entity',)
+
+ def __call__(self):
+ entity = self.entity
+ if entity.cw_edited.get('final'):
+ # final entity types don't need a table in the database and are
+ # systematically added by yams at schema initialization time so
+ # there is no need to do further processing. Simply assign its eid.
+ self._cw.vreg.schema[entity.name].eid = entity.eid
+ return
+ CWETypeAddOp(self._cw, entity=entity)
+
+
+class BeforeUpdateCWETypeHook(DelCWETypeHook):
+ """check name change, handle final"""
+ __regid__ = 'syncupdatecwetype'
+ events = ('before_update_entity',)
+
+ def __call__(self):
+ entity = self.entity
+ check_valid_changes(self._cw, entity, ro_attrs=('final',))
+ # don't use getattr(entity, attr), we would get the modified value if any
+ if 'name' in entity.cw_edited:
+ oldname, newname = entity.cw_edited.oldnewvalue('name')
+ if newname.lower() != oldname.lower():
+ CWETypeRenameOp(self._cw, oldname=oldname, newname=newname)
+
+
+# CWRType hooks ################################################################
+
+class DelCWRTypeHook(SyncSchemaHook):
+ """before deleting a CWRType entity:
+ * check that we don't remove a core relation type
+ * cascade to delete related CWAttribute and CWRelation entities
+ * instantiate an operation to delete the relation type on commit
+ """
+ __regid__ = 'syncdelcwrtype'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWRType')
+ events = ('before_delete_entity',)
+
+ def __call__(self):
+ name = self.entity.name
+ if name in CORE_TYPES:
+ raise validation_error(self.entity, {None: _("can't be deleted")})
+ # delete relation definitions using this relation type
+ self._cw.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
+ {'x': self.entity.eid})
+ self._cw.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s',
+ {'x': self.entity.eid})
+ MemSchemaCWRTypeDel(self._cw, rtype=name)
+
+
+class AfterAddCWComputedRTypeHook(SyncSchemaHook):
+ """after a CWComputedRType entity has been added:
+ * register an operation to add the relation type to the instance's
+ schema on commit
+
+ We don't know yet this point if a table is necessary
+ """
+ __regid__ = 'syncaddcwcomputedrtype'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWComputedRType')
+ events = ('after_add_entity',)
+
+ def __call__(self):
+ entity = self.entity
+ rtypedef = ybo.ComputedRelation(name=entity.name,
+ eid=entity.eid,
+ rule=entity.rule)
+ MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef)
+
+
+class AfterAddCWRTypeHook(SyncSchemaHook):
+ """after a CWRType entity has been added:
+ * register an operation to add the relation type to the instance's
+ schema on commit
+
+ We don't know yet this point if a table is necessary
+ """
+ __regid__ = 'syncaddcwrtype'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWRType')
+ events = ('after_add_entity',)
+
+ def __call__(self):
+ entity = self.entity
+ rtypedef = ybo.RelationType(name=entity.name,
+ description=entity.description,
+ inlined=entity.cw_edited.get('inlined', False),
+ symmetric=entity.cw_edited.get('symmetric', False),
+ eid=entity.eid)
+ MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef)
+
+
+class BeforeUpdateCWRTypeHook(SyncSchemaHook):
+ """check name change, handle final"""
+ __regid__ = 'syncupdatecwrtype'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWRType')
+ events = ('before_update_entity',)
+
+ def __call__(self):
+ entity = self.entity
+ check_valid_changes(self._cw, entity)
+ newvalues = {}
+ for prop in ('symmetric', 'inlined', 'fulltext_container'):
+ if prop in entity.cw_edited:
+ old, new = entity.cw_edited.oldnewvalue(prop)
+ if old != new:
+ newvalues[prop] = new
+ if newvalues:
+ rschema = self._cw.vreg.schema.rschema(entity.name)
+ CWRTypeUpdateOp(self._cw, rschema=rschema, entity=entity,
+ values=newvalues)
+
+
+class BeforeUpdateCWComputedRTypeHook(SyncSchemaHook):
+ """check name change, handle final"""
+ __regid__ = 'syncupdatecwcomputedrtype'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWComputedRType')
+ events = ('before_update_entity',)
+
+ def __call__(self):
+ entity = self.entity
+ check_valid_changes(self._cw, entity)
+ if 'rule' in entity.cw_edited:
+ old, new = entity.cw_edited.oldnewvalue('rule')
+ if old != new:
+ rschema = self._cw.vreg.schema.rschema(entity.name)
+ CWComputedRTypeUpdateOp(self._cw, rschema=rschema,
+ entity=entity, rule=new)
+
+
+class AfterDelRelationTypeHook(SyncSchemaHook):
+ """before deleting a CWAttribute or CWRelation entity:
+ * if this is a final or inlined relation definition, instantiate an
+ operation to drop necessary column, else if this is the last instance
+ of a non final relation, instantiate an operation to drop necessary
+ table
+ * instantiate an operation to delete the relation definition on commit
+ * delete the associated relation type when necessary
+ """
+ __regid__ = 'syncdelrelationtype'
+ __select__ = SyncSchemaHook.__select__ & hook.match_rtype('relation_type')
+ events = ('after_delete_relation',)
+
+ def __call__(self):
+ cnx = self._cw
+ try:
+ rdef = cnx.vreg.schema.schema_by_eid(self.eidfrom)
+ except KeyError:
+ self.critical('cant get schema rdef associated to %s', self.eidfrom)
+ return
+ subjschema, rschema, objschema = rdef.as_triple()
+ pendingrdefs = cnx.transaction_data.setdefault('pendingrdefs', set())
+ # first delete existing relation if necessary
+ if rschema.final:
+ rdeftype = 'CWAttribute'
+ pendingrdefs.add((subjschema, rschema))
+ else:
+ rdeftype = 'CWRelation'
+ pendingrdefs.add((subjschema, rschema, objschema))
+ RDefDelOp(cnx, rdef=rdef)
+
+
+# CWComputedRType hooks #######################################################
+
+class DelCWComputedRTypeHook(SyncSchemaHook):
+ """before deleting a CWComputedRType entity:
+ * check that we don't remove a core relation type
+ * instantiate an operation to delete the relation type on commit
+ """
+ __regid__ = 'syncdelcwcomputedrtype'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWComputedRType')
+ events = ('before_delete_entity',)
+
+ def __call__(self):
+ name = self.entity.name
+ if name in CORE_TYPES:
+ raise validation_error(self.entity, {None: _("can't be deleted")})
+ MemSchemaCWRTypeDel(self._cw, rtype=name)
+
+
+# CWAttribute / CWRelation hooks ###############################################
+
+class AfterAddCWAttributeHook(SyncSchemaHook):
+ __regid__ = 'syncaddcwattribute'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWAttribute')
+ events = ('after_add_entity',)
+
+ def __call__(self):
+ CWAttributeAddOp(self._cw, entity=self.entity)
+
+
+class AfterAddCWRelationHook(AfterAddCWAttributeHook):
+ __regid__ = 'syncaddcwrelation'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWRelation')
+
+ def __call__(self):
+ CWRelationAddOp(self._cw, entity=self.entity)
+
+
+class AfterUpdateCWRDefHook(SyncSchemaHook):
+ __regid__ = 'syncaddcwattribute'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWAttribute',
+ 'CWRelation')
+ events = ('before_update_entity',)
+
+ def __call__(self):
+ entity = self.entity
+ if self._cw.deleted_in_transaction(entity.eid):
+ return
+ subjtype = entity.stype.name
+ objtype = entity.otype.name
+ if subjtype in ETYPE_NAME_MAP or objtype in ETYPE_NAME_MAP:
+ return
+ rschema = self._cw.vreg.schema[entity.rtype.name]
+ # note: do not access schema rdef here, it may be added later by an
+ # operation
+ newvalues = {}
+ for prop in RelationDefinitionSchema.rproperty_defs(objtype):
+ if prop == 'constraints':
+ continue
+ if prop == 'order':
+ attr = 'ordernum'
+ else:
+ attr = prop
+ if attr in entity.cw_edited:
+ old, new = entity.cw_edited.oldnewvalue(attr)
+ if old != new:
+ newvalues[prop] = new
+ if newvalues:
+ RDefUpdateOp(self._cw, rschema=rschema, rdefkey=(subjtype, objtype),
+ values=newvalues)
+
+
+# constraints synchronization hooks ############################################
+
+class AfterAddCWConstraintHook(SyncSchemaHook):
+ __regid__ = 'syncaddcwconstraint'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWConstraint')
+ events = ('after_add_entity', 'after_update_entity')
+
+ def __call__(self):
+ if self.entity.cstrtype[0].name == 'SizeConstraint':
+ txdata = self._cw.transaction_data
+ if 'newsizecstr' not in txdata:
+ txdata['newsizecstr'] = set()
+ txdata['newsizecstr'].add(self.entity)
+ CWConstraintAddOp(self._cw, entity=self.entity)
+
+
+class AfterAddConstrainedByHook(SyncSchemaHook):
+ __regid__ = 'syncaddconstrainedby'
+ __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constrained_by')
+ events = ('after_add_relation',)
+
+ def __call__(self):
+ if self._cw.added_in_transaction(self.eidfrom):
+ # used by get_constraints() which is called in CWAttributeAddOp
+ self._cw.transaction_data.setdefault(self.eidfrom, []).append(self.eidto)
+
+
+class BeforeDeleteCWConstraintHook(SyncSchemaHook):
+ __regid__ = 'syncdelcwconstraint'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWConstraint')
+ events = ('before_delete_entity',)
+
+ def __call__(self):
+ entity = self.entity
+ schema = self._cw.vreg.schema
+ try:
+ # KeyError, e.g. composite chain deletion
+ rdef = schema.schema_by_eid(entity.reverse_constrained_by[0].eid)
+ # IndexError
+ cstr = rdef.constraint_by_eid(entity.eid)
+ except (KeyError, IndexError):
+ self._cw.critical('constraint type no more accessible')
+ else:
+ CWConstraintDelOp(self._cw, rdef=rdef, oldcstr=cstr)
+
+# unique_together constraints
+# XXX: use setoperations and before_add_relation here (on constraint_of and relations)
+class AfterAddCWUniqueTogetherConstraintHook(SyncSchemaHook):
+ __regid__ = 'syncadd_cwuniquetogether_constraint'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWUniqueTogetherConstraint')
+ events = ('after_add_entity',)
+
+ def __call__(self):
+ CWUniqueTogetherConstraintAddOp(self._cw, entity=self.entity)
+
+
+class BeforeDeleteConstraintOfHook(SyncSchemaHook):
+ __regid__ = 'syncdelconstraintof'
+ __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constraint_of')
+ events = ('before_delete_relation',)
+
+ def __call__(self):
+ if self._cw.deleted_in_transaction(self.eidto):
+ return
+ schema = self._cw.vreg.schema
+ cstr = self._cw.entity_from_eid(self.eidfrom)
+ entity = schema.schema_by_eid(self.eidto)
+ cols = tuple(r.name for r in cstr.relations)
+ CWUniqueTogetherConstraintDelOp(self._cw, entity=entity,
+ cstrname=cstr.name, cols=cols)
+
+
+# permissions synchronization hooks ############################################
+
+class AfterAddPermissionHook(SyncSchemaHook):
+ """added entity/relation *_permission, need to update schema"""
+ __regid__ = 'syncaddperm'
+ __select__ = SyncSchemaHook.__select__ & hook.match_rtype(
+ 'read_permission', 'add_permission', 'delete_permission',
+ 'update_permission')
+ events = ('after_add_relation',)
+
+ def __call__(self):
+ action = self.rtype.split('_', 1)[0]
+ if self._cw.entity_metas(self.eidto)['type'] == 'CWGroup':
+ MemSchemaPermissionAdd(self._cw, action=action, eid=self.eidfrom,
+ group_eid=self.eidto)
+ else: # RQLExpression
+ expr = self._cw.entity_from_eid(self.eidto).expression
+ MemSchemaPermissionAdd(self._cw, action=action, eid=self.eidfrom,
+ expr=expr)
+
+
+class BeforeDelPermissionHook(AfterAddPermissionHook):
+ """delete entity/relation *_permission, need to update schema
+
+ skip the operation if the related type is being deleted
+ """
+ __regid__ = 'syncdelperm'
+ events = ('before_delete_relation',)
+
+ def __call__(self):
+ if self._cw.deleted_in_transaction(self.eidfrom):
+ return
+ action = self.rtype.split('_', 1)[0]
+ if self._cw.entity_metas(self.eidto)['type'] == 'CWGroup':
+ MemSchemaPermissionDel(self._cw, action=action, eid=self.eidfrom,
+ group_eid=self.eidto)
+ else: # RQLExpression
+ expr = self._cw.entity_from_eid(self.eidto).expression
+ MemSchemaPermissionDel(self._cw, action=action, eid=self.eidfrom,
+ expr=expr)
+
+
+
+class UpdateFTIndexOp(hook.DataOperationMixIn, hook.SingleLastOperation):
+ """operation to update full text indexation of entity whose schema change
+
+ We wait after the commit to as the schema in memory is only updated after
+ the commit.
+ """
+ containercls = list
+
+ def postcommit_event(self):
+ cnx = self.cnx
+ source = cnx.repo.system_source
+ schema = cnx.repo.vreg.schema
+ to_reindex = self.get_data()
+ self.info('%i etypes need full text indexed reindexation',
+ len(to_reindex))
+ for etype in to_reindex:
+ rset = cnx.execute('Any X WHERE X is %s' % etype)
+ self.info('Reindexing full text index for %i entity of type %s',
+ len(rset), etype)
+ still_fti = list(schema[etype].indexable_attributes())
+ for entity in rset.entities():
+ source.fti_unindex_entities(cnx, [entity])
+ for container in entity.cw_adapt_to('IFTIndexable').fti_containers():
+ if still_fti or container is not entity:
+ source.fti_unindex_entities(cnx, [container])
+ source.fti_index_entities(cnx, [container])
+ if to_reindex:
+ # Transaction has already been committed
+ cnx.cnxset.commit()
+
+
+
+
+# specializes synchronization hooks ############################################
+
+
+class AfterAddSpecializesHook(SyncSchemaHook):
+ __regid__ = 'syncaddspecializes'
+ __select__ = SyncSchemaHook.__select__ & hook.match_rtype('specializes')
+ events = ('after_add_relation',)
+
+ def __call__(self):
+ MemSchemaSpecializesAdd(self._cw, etypeeid=self.eidfrom,
+ parentetypeeid=self.eidto)
+
+
+class AfterDelSpecializesHook(SyncSchemaHook):
+ __regid__ = 'syncdelspecializes'
+ __select__ = SyncSchemaHook.__select__ & hook.match_rtype('specializes')
+ events = ('after_delete_relation',)
+
+ def __call__(self):
+ MemSchemaSpecializesDel(self._cw, etypeeid=self.eidfrom,
+ parentetypeeid=self.eidto)