--- a/hooks/syncschema.py Mon Jan 04 18:40:30 2016 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1417 +0,0 @@
-# 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)