diff -r 7df3494ae657 -r 04034421b072 server/schemahooks.py --- a/server/schemahooks.py Fri Aug 14 09:20:33 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1057 +0,0 @@ -"""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 - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -__docformat__ = "restructuredtext en" - -from yams.schema import BASE_TYPES -from yams.buildobjs import EntityType, RelationType, RelationDefinition -from yams.schema2sql import eschema2sql, rschema2sql, type_from_constraints - - -from cubicweb import ValidationError, RepositoryError -from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS -from cubicweb.server import schemaserial as ss -from cubicweb.server.sqlutils import SQL_PREFIX -from cubicweb.server.pool import Operation, SingleLastOperation, PreCommitOperation -from cubicweb.server.hookhelper import (entity_attr, entity_name, - check_internal_entity) - - -TYPE_CONVERTER = { # XXX - 'Boolean': bool, - 'Int': int, - 'Float': float, - 'Password': str, - 'String': unicode, - 'Date' : unicode, - 'Datetime' : unicode, - 'Time' : unicode, - } - -# core entity and relation types which can't be removed -CORE_ETYPES = list(BASE_TYPES) + ['CWEType', 'CWRType', 'CWUser', 'CWGroup', - 'CWConstraint', 'CWAttribute', 'CWRelation'] -CORE_RTYPES = ['eid', 'creation_date', 'modification_date', 'cwuri', - 'login', 'upassword', 'name', - 'is', 'instanceof', 'owned_by', 'created_by', 'in_group', - 'relation_type', 'from_entity', 'to_entity', - 'constrainted_by', - 'read_permission', 'add_permission', - 'delete_permission', 'updated_permission', - ] - -def get_constraints(session, entity): - constraints = [] - for cstreid in session.transaction_data.get(entity.eid, ()): - cstrent = session.entity_from_eid(cstreid) - cstr = CONSTRAINTS[cstrent.type].deserialize(cstrent.value) - cstr.eid = cstreid - constraints.append(cstr) - return constraints - -def add_inline_relation_column(session, etype, rtype): - """add necessary column and index for an inlined relation""" - table = SQL_PREFIX + etype - column = SQL_PREFIX + rtype - try: - session.system_sql(str('ALTER TABLE %s ADD COLUMN %s integer' - % (table, column)), rollback_on_failure=False) - session.info('added column %s to table %s', column, table) - except: - # silent exception here, if this error has not been raised because the - # column already exists, index creation will fail anyway - session.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) - session.pool.source('system').create_index(session, table, column) - session.info('added index on %s(%s)', table, column) - session.transaction_data.setdefault('createdattrs', []).append( - '%s.%s' % (etype, rtype)) - - -# operations for low-level database alteration ################################ - -class DropTable(PreCommitOperation): - """actually remove a database from the instance's schema""" - table = None # make pylint happy - def precommit_event(self): - dropped = self.session.transaction_data.setdefault('droppedtables', - set()) - if self.table in dropped: - return # already processed - dropped.add(self.table) - self.session.system_sql('DROP TABLE %s' % self.table) - self.info('dropped table %s', self.table) - - -class DropRelationTable(DropTable): - def __init__(self, session, rtype): - super(DropRelationTable, self).__init__( - session, table='%s_relation' % rtype) - session.transaction_data.setdefault('pendingrtypes', set()).add(rtype) - - -class DropColumn(PreCommitOperation): - """actually remove the attribut's column from entity table in the system - database - """ - table = column = None # make pylint happy - def precommit_event(self): - session, table, column = self.session, self.table, self.column - # drop index if any - session.pool.source('system').drop_index(session, table, column) - try: - session.system_sql('ALTER TABLE %s DROP COLUMN %s' - % (table, column), rollback_on_failure=False) - self.info('dropped column %s from table %s', column, table) - except Exception, ex: - # not supported by sqlite for instance - self.error('error while altering table %s: %s', table, ex) - - -# base operations for in-memory schema synchronization ######################## - -class MemSchemaNotifyChanges(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, session): - self.repo = session.repo - SingleLastOperation.__init__(self, session) - - def commit_event(self): - self.repo.set_schema(self.repo.schema) - - -class MemSchemaOperation(Operation): - """base class for schema operations""" - def __init__(self, session, kobj=None, **kwargs): - self.schema = session.schema - self.kobj = kobj - # once Operation.__init__ has been called, event may be triggered, so - # do this last ! - Operation.__init__(self, session, **kwargs) - # every schema operation is triggering a schema update - MemSchemaNotifyChanges(session) - - def prepare_constraints(self, subjtype, rtype, objtype): - constraints = rtype.rproperty(subjtype, objtype, 'constraints') - self.constraints = list(constraints) - rtype.set_rproperty(subjtype, objtype, 'constraints', self.constraints) - - -class MemSchemaEarlyOperation(MemSchemaOperation): - def insert_index(self): - """schema operation which are inserted at the begining of the queue - (typically to add/remove entity or relation types) - """ - i = -1 - for i, op in enumerate(self.session.pending_operations): - if not isinstance(op, MemSchemaEarlyOperation): - return i - return i + 1 - - -class MemSchemaPermissionOperation(MemSchemaOperation): - """base class to synchronize schema permission definitions""" - def __init__(self, session, perm, etype_eid): - self.perm = perm - try: - self.name = entity_name(session, etype_eid) - except IndexError: - self.error('changing permission of a no more existant type #%s', - etype_eid) - else: - Operation.__init__(self, session) - - -# operations for high-level source database alteration ######################## - -class SourceDbCWETypeRename(PreCommitOperation): - """this operation updates physical storage accordingly""" - oldname = newname = None # make pylint happy - - def precommit_event(self): - # we need sql to operate physical changes on the system database - sqlexec = self.session.system_sql - sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (SQL_PREFIX, self.oldname, - SQL_PREFIX, self.newname)) - self.info('renamed table %s to %s', self.oldname, self.newname) - sqlexec('UPDATE entities SET type=%s WHERE type=%s', - (self.newname, self.oldname)) - sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s', - (self.newname, self.oldname)) - - -class SourceDbCWRTypeUpdate(PreCommitOperation): - """actually update some properties of a relation definition""" - rschema = values = entity = None # make pylint happy - - def precommit_event(self): - session = self.session - rschema = self.rschema - if rschema.is_final() or not 'inlined' in self.values: - return # nothing to do - inlined = self.values['inlined'] - entity = self.entity - # check in-lining is necessary / possible - if not entity.inlined_changed(inlined): - return # nothing to do - # inlined changed, make necessary physical changes! - sqlexec = self.session.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 session.transaction_data.get('createdtables', ()): - tablesql = rschema2sql(rschema) - # create the necessary table - for sql in tablesql.split(';'): - if sql.strip(): - sqlexec(sql) - session.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 - for etype in rschema.subjects(): - DropColumn(session, table=SQL_PREFIX + str(etype), - column=SQL_PREFIX + rtype) - else: - for etype in rschema.subjects(): - try: - add_inline_relation_column(session, str(etype), rtype) - except Exception, 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', 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(session, rtype) - - -class SourceDbCWAttributeAdd(PreCommitOperation): - """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 - self.session.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}) - subj = str(fromentity.name) - rtype = entity.rtype.name - obj = str(entity.otype.name) - constraints = get_constraints(self.session, entity) - rdef = RelationDefinition(subj, rtype, obj, - description=entity.description, - cardinality=entity.cardinality, - constraints=constraints, - order=entity.ordernum, - eid=entity.eid, - **kwargs) - MemSchemaRDefAdd(self.session, rdef) - return rdef - - def precommit_event(self): - session = self.session - entity = self.entity - # entity.defaultval is a string or None, but we need a correctly typed - # value - default = entity.defaultval - if default is not None: - default = TYPE_CONVERTER[entity.otype.name](default) - rdef = self.init_rdef(default=default, - indexed=entity.indexed, - fulltextindexed=entity.fulltextindexed, - internationalizable=entity.internationalizable) - sysource = session.pool.source('system') - attrtype = type_from_constraints(sysource.dbhelper, rdef.object, - rdef.constraints) - # XXX should be moved somehow into lgc.adbh: sqlite doesn't support to - # add a new column with UNIQUE, it should be added after the ALTER TABLE - # using ADD INDEX - if sysource.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 + rdef.subject - column = SQL_PREFIX + rdef.name - try: - session.system_sql(str('ALTER TABLE %s ADD COLUMN %s %s' - % (table, column, attrtype)), - rollback_on_failure=False) - self.info('added column %s to table %s', table, column) - except Exception, 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: - sysource.create_index(session, table, column, - unique=extra_unique_index) - except Exception, ex: - self.error('error while creating index for %s.%s: %s', - table, column, ex) - - -class SourceDbCWRelationAdd(SourceDbCWAttributeAdd): - """an actual relation has been added: - * 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 - * 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 precommit_event(self): - session = self.session - entity = self.entity - rdef = self.init_rdef(composite=entity.composite) - schema = session.schema - rtype = rdef.name - rschema = session.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 - # and if it has not been added during other event of the same - # transaction - key = '%s.%s' % (rdef.subject, rtype) - try: - alreadythere = bool(rschema.objects(rdef.subject)) - except KeyError: - alreadythere = False - if not (alreadythere or - key in session.transaction_data.get('createdattrs', ())): - add_inline_relation_column(session, rdef.subject, rtype) - else: - # 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 (rschema.subjects() or - rtype in session.transaction_data.get('createdtables', ())): - try: - rschema = session.schema.rschema(rtype) - tablesql = rschema2sql(rschema) - except KeyError: - # fake we add it to the schema now to get a correctly - # initialized schema but remove it before doing anything - # more dangerous... - rschema = session.schema.add_relation_type(rdef) - tablesql = rschema2sql(rschema) - session.schema.del_relation_type(rtype) - # create the necessary table - for sql in tablesql.split(';'): - if sql.strip(): - session.system_sql(sql) - session.transaction_data.setdefault('createdtables', []).append( - rtype) - - -class SourceDbRDefUpdate(PreCommitOperation): - """actually update some properties of a relation definition""" - rschema = values = None # make pylint happy - - def precommit_event(self): - etype = self.kobj[0] - table = SQL_PREFIX + etype - column = SQL_PREFIX + self.rschema.type - if 'indexed' in self.values: - sysource = self.session.pool.source('system') - if self.values['indexed']: - sysource.create_index(self.session, table, column) - else: - sysource.drop_index(self.session, table, column) - if 'cardinality' in self.values and self.rschema.is_final(): - adbh = self.session.pool.source('system').dbhelper - if not adbh.alter_column_support: - # not supported (and NOT NULL not set by yams in that case, so - # no worry) - return - atype = self.rschema.objects(etype)[0] - constraints = self.rschema.rproperty(etype, atype, 'constraints') - coltype = type_from_constraints(adbh, atype, constraints, - creating=False) - # XXX check self.values['cardinality'][0] actually changed? - sql = adbh.sql_set_null_allowed(table, column, coltype, - self.values['cardinality'][0] != '1') - self.session.system_sql(sql) - - -class SourceDbCWConstraintAdd(PreCommitOperation): - """actually update constraint of a relation definition""" - entity = None # make pylint happy - cancelled = False - - def precommit_event(self): - rdef = self.entity.reverse_constrained_by[0] - session = self.session - # 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 rdef.eid in session.transaction_data.get('neweids', ()): - return - subjtype, rtype, objtype = session.schema.schema_by_eid(rdef.eid) - cstrtype = self.entity.type - oldcstr = rtype.constraint_by_type(subjtype, objtype, cstrtype) - newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value) - table = SQL_PREFIX + str(subjtype) - column = SQL_PREFIX + str(rtype) - # alter the physical schema on size constraint changes - if newcstr.type() == 'SizeConstraint' and ( - oldcstr is None or oldcstr.max != newcstr.max): - adbh = self.session.pool.source('system').dbhelper - card = rtype.rproperty(subjtype, objtype, 'cardinality') - coltype = type_from_constraints(adbh, objtype, [newcstr], - creating=False) - sql = adbh.sql_change_col_type(table, column, coltype, card != '1') - try: - session.system_sql(sql, rollback_on_failure=False) - self.info('altered column %s of table %s: now VARCHAR(%s)', - column, table, newcstr.max) - except Exception, ex: - # not supported by sqlite for instance - self.error('error while altering table %s: %s', table, ex) - elif cstrtype == 'UniqueConstraint' and oldcstr is None: - session.pool.source('system').create_index( - self.session, table, column, unique=True) - - -class SourceDbCWConstraintDel(PreCommitOperation): - """actually remove a constraint of a relation definition""" - rtype = subjtype = objtype = None # make pylint happy - - def precommit_event(self): - cstrtype = self.cstr.type() - table = SQL_PREFIX + str(self.subjtype) - column = SQL_PREFIX + str(self.rtype) - # alter the physical schema on size/unique constraint changes - if cstrtype == 'SizeConstraint': - try: - self.session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE TEXT' - % (table, column), - rollback_on_failure=False) - self.info('altered column %s of table %s: now TEXT', - column, table) - except Exception, ex: - # not supported by sqlite for instance - self.error('error while altering table %s: %s', table, ex) - elif cstrtype == 'UniqueConstraint': - self.session.pool.source('system').drop_index( - self.session, table, column, unique=True) - - -# operations for in-memory schema synchronization ############################# - -class MemSchemaCWETypeAdd(MemSchemaEarlyOperation): - """actually add the entity type to the instance's schema""" - eid = None # make pylint happy - def commit_event(self): - self.schema.add_entity_type(self.kobj) - - -class MemSchemaCWETypeRename(MemSchemaOperation): - """this operation updates physical storage accordingly""" - oldname = newname = None # make pylint happy - - def commit_event(self): - self.session.schema.rename_entity_type(self.oldname, self.newname) - - -class MemSchemaCWETypeDel(MemSchemaOperation): - """actually remove the entity type from the instance's schema""" - def commit_event(self): - try: - # del_entity_type also removes entity's relations - self.schema.del_entity_type(self.kobj) - except KeyError: - # s/o entity type have already been deleted - pass - - -class MemSchemaCWRTypeAdd(MemSchemaEarlyOperation): - """actually add the relation type to the instance's schema""" - eid = None # make pylint happy - def commit_event(self): - rschema = self.schema.add_relation_type(self.kobj) - rschema.set_default_groups() - - -class MemSchemaCWRTypeUpdate(MemSchemaOperation): - """actually update some properties of a relation definition""" - rschema = values = None # make pylint happy - - def commit_event(self): - # structure should be clean, not need to remove entity's relations - # at this point - self.rschema.__dict__.update(self.values) - - -class MemSchemaCWRTypeDel(MemSchemaOperation): - """actually remove the relation type from the instance's schema""" - def commit_event(self): - try: - self.schema.del_relation_type(self.kobj) - except KeyError: - # s/o entity type have already been deleted - pass - - -class MemSchemaRDefAdd(MemSchemaEarlyOperation): - """actually add the attribute relation definition to the instance's - schema - """ - def commit_event(self): - self.schema.add_relation_def(self.kobj) - - -class MemSchemaRDefUpdate(MemSchemaOperation): - """actually update some properties of a relation definition""" - rschema = values = None # make pylint happy - - def commit_event(self): - # structure should be clean, not need to remove entity's relations - # at this point - self.rschema._rproperties[self.kobj].update(self.values) - - -class MemSchemaRDefDel(MemSchemaOperation): - """actually remove the relation definition from the instance's schema""" - def commit_event(self): - subjtype, rtype, objtype = self.kobj - try: - self.schema.del_relation_def(subjtype, rtype, objtype) - except KeyError: - # relation type may have been already deleted - pass - - -class MemSchemaCWConstraintAdd(MemSchemaOperation): - """actually update constraint of a relation definition - - has to be called before SourceDbCWConstraintAdd - """ - cancelled = False - - def precommit_event(self): - rdef = 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 rdef.eid in self.session.transaction_data.get('neweids', ()): - self.cancelled = True - return - subjtype, rtype, objtype = self.session.schema.schema_by_eid(rdef.eid) - self.prepare_constraints(subjtype, rtype, objtype) - cstrtype = self.entity.type - self.cstr = rtype.constraint_by_type(subjtype, objtype, cstrtype) - self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value) - self.newcstr.eid = self.entity.eid - - def commit_event(self): - if self.cancelled: - return - # in-place modification - if not self.cstr is None: - self.constraints.remove(self.cstr) - self.constraints.append(self.newcstr) - - -class MemSchemaCWConstraintDel(MemSchemaOperation): - """actually remove a constraint of a relation definition - - has to be called before SourceDbCWConstraintDel - """ - rtype = subjtype = objtype = None # make pylint happy - def precommit_event(self): - self.prepare_constraints(self.subjtype, self.rtype, self.objtype) - - def commit_event(self): - self.constraints.remove(self.cstr) - - -class MemSchemaPermissionCWGroupAdd(MemSchemaPermissionOperation): - """synchronize schema when a *_permission relation has been added on a group - """ - def __init__(self, session, perm, etype_eid, group_eid): - self.group = entity_name(session, group_eid) - super(MemSchemaPermissionCWGroupAdd, self).__init__( - session, perm, etype_eid) - - def commit_event(self): - """the observed connections pool has been commited""" - try: - erschema = self.schema[self.name] - except KeyError: - # duh, schema not found, log error and skip operation - self.error('no schema for %s', self.name) - return - groups = list(erschema.get_groups(self.perm)) - try: - groups.index(self.group) - self.warning('group %s already have permission %s on %s', - self.group, self.perm, erschema.type) - except ValueError: - groups.append(self.group) - erschema.set_groups(self.perm, groups) - - -class MemSchemaPermissionCWGroupDel(MemSchemaPermissionCWGroupAdd): - """synchronize schema when a *_permission relation has been deleted from a - group - """ - - def commit_event(self): - """the observed connections pool has been commited""" - try: - erschema = self.schema[self.name] - except KeyError: - # duh, schema not found, log error and skip operation - self.error('no schema for %s', self.name) - return - groups = list(erschema.get_groups(self.perm)) - try: - groups.remove(self.group) - erschema.set_groups(self.perm, groups) - except ValueError: - self.error('can\'t remove permission %s on %s to group %s', - self.perm, erschema.type, self.group) - - -class MemSchemaPermissionRQLExpressionAdd(MemSchemaPermissionOperation): - """synchronize schema when a *_permission relation has been added on a rql - expression - """ - def __init__(self, session, perm, etype_eid, expression): - self.expr = expression - super(MemSchemaPermissionRQLExpressionAdd, self).__init__( - session, perm, etype_eid) - - def commit_event(self): - """the observed connections pool has been commited""" - try: - erschema = self.schema[self.name] - except KeyError: - # duh, schema not found, log error and skip operation - self.error('no schema for %s', self.name) - return - exprs = list(erschema.get_rqlexprs(self.perm)) - exprs.append(erschema.rql_expression(self.expr)) - erschema.set_rqlexprs(self.perm, exprs) - - -class MemSchemaPermissionRQLExpressionDel(MemSchemaPermissionRQLExpressionAdd): - """synchronize schema when a *_permission relation has been deleted from an - rql expression - """ - - def commit_event(self): - """the observed connections pool has been commited""" - try: - erschema = self.schema[self.name] - except KeyError: - # duh, schema not found, log error and skip operation - self.error('no schema for %s', self.name) - return - rqlexprs = list(erschema.get_rqlexprs(self.perm)) - for i, rqlexpr in enumerate(rqlexprs): - if rqlexpr.expression == self.expr: - rqlexprs.pop(i) - break - else: - self.error('can\'t remove permission %s on %s for expression %s', - self.perm, erschema.type, self.expr) - return - erschema.set_rqlexprs(self.perm, rqlexprs) - - -# deletion hooks ############################################################### - -def before_del_eetype(session, eid): - """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 - """ - # final entities can't be deleted, don't care about that - name = check_internal_entity(session, eid, CORE_ETYPES) - # delete every entities of this type - session.unsafe_execute('DELETE %s X' % name) - DropTable(session, table=SQL_PREFIX + name) - MemSchemaCWETypeDel(session, name) - - -def after_del_eetype(session, eid): - # workflow cleanup - session.execute('DELETE State X WHERE NOT X state_of Y') - session.execute('DELETE Transition X WHERE NOT X transition_of Y') - - -def before_del_ertype(session, eid): - """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 - """ - name = check_internal_entity(session, eid, CORE_RTYPES) - # delete relation definitions using this relation type - session.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s', - {'x': eid}) - session.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s', - {'x': eid}) - MemSchemaCWRTypeDel(session, name) - - -def after_del_relation_type(session, rdefeid, rtype, rteid): - """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 - """ - subjschema, rschema, objschema = session.schema.schema_by_eid(rdefeid) - pendings = session.transaction_data.get('pendingeids', ()) - # first delete existing relation if necessary - if rschema.is_final(): - rdeftype = 'CWAttribute' - else: - rdeftype = 'CWRelation' - if not (subjschema.eid in pendings or objschema.eid in pendings): - pending = session.transaction_data.setdefault('pendingrdefs', set()) - pending.add((subjschema, rschema, objschema)) - session.execute('DELETE X %s Y WHERE X is %s, Y is %s' - % (rschema, subjschema, objschema)) - execute = session.unsafe_execute - rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,' - 'R eid %%(x)s' % rdeftype, {'x': rteid}) - 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.is_final() or rschema.inlined): - rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, ' - 'R eid %%(x)s, X from_entity E, E name %%(name)s' - % rdeftype, {'x': rteid, 'name': str(subjschema)}) - if rset[0][0] == 0 and not subjschema.eid in pendings: - ptypes = session.transaction_data.setdefault('pendingrtypes', set()) - ptypes.add(rschema.type) - DropColumn(session, table=SQL_PREFIX + subjschema.type, - column=SQL_PREFIX + rschema.type) - elif lastrel: - DropRelationTable(session, rschema.type) - # if this is the last instance, drop associated relation type - if lastrel and not rteid in pendings: - execute('DELETE CWRType X WHERE X eid %(x)s', {'x': rteid}, 'x') - MemSchemaRDefDel(session, (subjschema, rschema, objschema)) - - -# addition hooks ############################################################### - -def before_add_eetype(session, entity): - """before adding a CWEType entity: - * check that we are not using an existing entity type, - """ - name = entity['name'] - schema = session.schema - if name in schema and schema[name].eid is not None: - raise RepositoryError('an entity type %s already exists' % name) - -def after_add_eetype(session, entity): - """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 - """ - if entity.get('final'): - return - schema = session.schema - name = entity['name'] - etype = EntityType(name=name, description=entity.get('description'), - meta=entity.get('meta')) # don't care about final - # fake we add it to the schema now to get a correctly initialized schema - # but remove it before doing anything more dangerous... - schema = session.schema - eschema = schema.add_entity_type(etype) - eschema.set_default_groups() - # generate table sql and rql to add metadata - tablesql = eschema2sql(session.pool.source('system').dbhelper, eschema, - prefix=SQL_PREFIX) - relrqls = [] - for rtype in (META_RTYPES - VIRTUAL_RTYPES): - rschema = schema[rtype] - sampletype = rschema.subjects()[0] - desttype = rschema.objects()[0] - props = rschema.rproperties(sampletype, desttype) - relrqls += list(ss.rdef2rql(rschema, name, desttype, props)) - # now remove it ! - schema.del_entity_type(name) - # create the necessary table - for sql in tablesql.split(';'): - if sql.strip(): - session.system_sql(sql) - # register operation to modify the schema on commit - # this have to be done before adding other relations definitions - # or permission settings - etype.eid = entity.eid - MemSchemaCWETypeAdd(session, etype) - # add meta relations - for rql, kwargs in relrqls: - session.execute(rql, kwargs) - - -def before_add_ertype(session, entity): - """before adding a CWRType entity: - * check that we are not using an existing relation type, - * register an operation to add the relation type to the instance's - schema on commit - - We don't know yeat this point if a table is necessary - """ - name = entity['name'] - if name in session.schema.relations(): - raise RepositoryError('a relation type %s already exists' % name) - - -def after_add_ertype(session, entity): - """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 yeat this point if a table is necessary - """ - rtype = RelationType(name=entity['name'], - description=entity.get('description'), - meta=entity.get('meta', False), - inlined=entity.get('inlined', False), - symetric=entity.get('symetric', False)) - rtype.eid = entity.eid - MemSchemaCWRTypeAdd(session, rtype) - - -def after_add_efrdef(session, entity): - SourceDbCWAttributeAdd(session, entity=entity) - -def after_add_enfrdef(session, entity): - SourceDbCWRelationAdd(session, entity=entity) - - -# update hooks ################################################################# - -def check_valid_changes(session, entity, ro_attrs=('name', 'final')): - errors = {} - # don't use getattr(entity, attr), we would get the modified value if any - for attr in ro_attrs: - origval = entity_attr(session, entity.eid, attr) - if entity.get(attr, origval) != origval: - errors[attr] = session._("can't change the %s attribute") % \ - display_name(session, attr) - if errors: - raise ValidationError(entity.eid, errors) - -def before_update_eetype(session, entity): - """check name change, handle final""" - check_valid_changes(session, entity, ro_attrs=('final',)) - # don't use getattr(entity, attr), we would get the modified value if any - oldname = entity_attr(session, entity.eid, 'name') - newname = entity.get('name', oldname) - if newname.lower() != oldname.lower(): - SourceDbCWETypeRename(session, oldname=oldname, newname=newname) - MemSchemaCWETypeRename(session, oldname=oldname, newname=newname) - -def before_update_ertype(session, entity): - """check name change, handle final""" - check_valid_changes(session, entity) - - -def after_update_erdef(session, entity): - if entity.eid in session.transaction_data.get('pendingeids', ()): - return - desttype = entity.otype.name - rschema = session.schema[entity.rtype.name] - newvalues = {} - for prop in rschema.rproperty_defs(desttype): - if prop == 'constraints': - continue - if prop == 'order': - prop = 'ordernum' - if prop in entity.edited_attributes: - newvalues[prop] = entity[prop] - if newvalues: - subjtype = entity.stype.name - MemSchemaRDefUpdate(session, kobj=(subjtype, desttype), - rschema=rschema, values=newvalues) - SourceDbRDefUpdate(session, kobj=(subjtype, desttype), - rschema=rschema, values=newvalues) - -def after_update_ertype(session, entity): - rschema = session.schema.rschema(entity.name) - newvalues = {} - for prop in ('meta', 'symetric', 'inlined'): - if prop in entity: - newvalues[prop] = entity[prop] - if newvalues: - MemSchemaCWRTypeUpdate(session, rschema=rschema, values=newvalues) - SourceDbCWRTypeUpdate(session, rschema=rschema, values=newvalues, - entity=entity) - -# constraints synchronization hooks ############################################ - -def after_add_econstraint(session, entity): - MemSchemaCWConstraintAdd(session, entity=entity) - SourceDbCWConstraintAdd(session, entity=entity) - - -def after_update_econstraint(session, entity): - MemSchemaCWConstraintAdd(session, entity=entity) - SourceDbCWConstraintAdd(session, entity=entity) - - -def before_delete_constrained_by(session, fromeid, rtype, toeid): - if not fromeid in session.transaction_data.get('pendingeids', ()): - schema = session.schema - entity = session.entity_from_eid(toeid) - subjtype, rtype, objtype = schema.schema_by_eid(fromeid) - try: - cstr = rtype.constraint_by_type(subjtype, objtype, - entity.cstrtype[0].name) - except IndexError: - session.critical('constraint type no more accessible') - else: - SourceDbCWConstraintDel(session, subjtype=subjtype, rtype=rtype, - objtype=objtype, cstr=cstr) - MemSchemaCWConstraintDel(session, subjtype=subjtype, rtype=rtype, - objtype=objtype, cstr=cstr) - - -def after_add_constrained_by(session, fromeid, rtype, toeid): - if fromeid in session.transaction_data.get('neweids', ()): - session.transaction_data.setdefault(fromeid, []).append(toeid) - - -# permissions synchronization hooks ############################################ - -def after_add_permission(session, subject, rtype, object): - """added entity/relation *_permission, need to update schema""" - perm = rtype.split('_', 1)[0] - if session.describe(object)[0] == 'CWGroup': - MemSchemaPermissionCWGroupAdd(session, perm, subject, object) - else: # RQLExpression - expr = session.execute('Any EXPR WHERE X eid %(x)s, X expression EXPR', - {'x': object}, 'x')[0][0] - MemSchemaPermissionRQLExpressionAdd(session, perm, subject, expr) - - -def before_del_permission(session, subject, rtype, object): - """delete entity/relation *_permission, need to update schema - - skip the operation if the related type is being deleted - """ - if subject in session.transaction_data.get('pendingeids', ()): - return - perm = rtype.split('_', 1)[0] - if session.describe(object)[0] == 'CWGroup': - MemSchemaPermissionCWGroupDel(session, perm, subject, object) - else: # RQLExpression - expr = session.execute('Any EXPR WHERE X eid %(x)s, X expression EXPR', - {'x': object}, 'x')[0][0] - MemSchemaPermissionRQLExpressionDel(session, perm, subject, expr) - - -def rebuild_infered_relations(session, subject, rtype, object): - # registering a schema operation will trigger a call to - # repo.set_schema() on commit which will in turn rebuild - # infered relation definitions - MemSchemaNotifyChanges(session) - - -def _register_schema_hooks(hm): - """register schema related hooks on the hooks manager""" - # schema synchronisation ##################### - # before/after add - hm.register_hook(before_add_eetype, 'before_add_entity', 'CWEType') - hm.register_hook(before_add_ertype, 'before_add_entity', 'CWRType') - hm.register_hook(after_add_eetype, 'after_add_entity', 'CWEType') - hm.register_hook(after_add_ertype, 'after_add_entity', 'CWRType') - hm.register_hook(after_add_efrdef, 'after_add_entity', 'CWAttribute') - hm.register_hook(after_add_enfrdef, 'after_add_entity', 'CWRelation') - # before/after update - hm.register_hook(before_update_eetype, 'before_update_entity', 'CWEType') - hm.register_hook(before_update_ertype, 'before_update_entity', 'CWRType') - hm.register_hook(after_update_ertype, 'after_update_entity', 'CWRType') - hm.register_hook(after_update_erdef, 'after_update_entity', 'CWAttribute') - hm.register_hook(after_update_erdef, 'after_update_entity', 'CWRelation') - # before/after delete - hm.register_hook(before_del_eetype, 'before_delete_entity', 'CWEType') - hm.register_hook(after_del_eetype, 'after_delete_entity', 'CWEType') - hm.register_hook(before_del_ertype, 'before_delete_entity', 'CWRType') - hm.register_hook(after_del_relation_type, 'after_delete_relation', 'relation_type') - hm.register_hook(rebuild_infered_relations, 'after_add_relation', 'specializes') - hm.register_hook(rebuild_infered_relations, 'after_delete_relation', 'specializes') - # constraints synchronization hooks - hm.register_hook(after_add_econstraint, 'after_add_entity', 'CWConstraint') - hm.register_hook(after_update_econstraint, 'after_update_entity', 'CWConstraint') - hm.register_hook(before_delete_constrained_by, 'before_delete_relation', 'constrained_by') - hm.register_hook(after_add_constrained_by, 'after_add_relation', 'constrained_by') - # permissions synchronisation ################ - for perm in ('read_permission', 'add_permission', - 'delete_permission', 'update_permission'): - hm.register_hook(after_add_permission, 'after_add_relation', perm) - hm.register_hook(before_del_permission, 'before_delete_relation', perm)