hooks/syncschema.py
brancholdstable
changeset 7074 e4580e5f0703
parent 7021 fc7ac3409b0c
child 7181 e54ad6984e01
equal deleted inserted replaced
6749:48f468f33704 7074:e4580e5f0703
     1 # copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     3 #
     3 #
     4 # This file is part of CubicWeb.
     4 # This file is part of CubicWeb.
     5 #
     5 #
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
    28 from copy import copy
    28 from copy import copy
    29 from yams.schema import BASE_TYPES, RelationSchema, RelationDefinitionSchema
    29 from yams.schema import BASE_TYPES, RelationSchema, RelationDefinitionSchema
    30 from yams import buildobjs as ybo, schema2sql as y2sql
    30 from yams import buildobjs as ybo, schema2sql as y2sql
    31 
    31 
    32 from logilab.common.decorators import clear_cache
    32 from logilab.common.decorators import clear_cache
    33 from logilab.common.testlib import mock_object
       
    34 
    33 
    35 from cubicweb import ValidationError
    34 from cubicweb import ValidationError
    36 from cubicweb.selectors import is_instance
    35 from cubicweb.selectors import is_instance
    37 from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES,
    36 from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES,
    38                              CONSTRAINTS, ETYPE_NAME_MAP, display_name)
    37                              CONSTRAINTS, ETYPE_NAME_MAP, display_name)
   119 
   118 
   120 
   119 
   121 def check_valid_changes(session, entity, ro_attrs=('name', 'final')):
   120 def check_valid_changes(session, entity, ro_attrs=('name', 'final')):
   122     errors = {}
   121     errors = {}
   123     # don't use getattr(entity, attr), we would get the modified value if any
   122     # don't use getattr(entity, attr), we would get the modified value if any
   124     for attr in entity.edited_attributes:
   123     for attr in entity.cw_edited:
   125         if attr in ro_attrs:
   124         if attr in ro_attrs:
   126             newval = entity.pop(attr)
   125             origval, newval = entity.cw_edited.oldnewvalue(attr)
   127             origval = getattr(entity, attr)
       
   128             if newval != origval:
   126             if newval != origval:
   129                 errors[attr] = session._("can't change the %s attribute") % \
   127                 errors[attr] = session._("can't change the %s attribute") % \
   130                                display_name(session, attr)
   128                                display_name(session, attr)
   131             entity[attr] = newval
       
   132     if errors:
   129     if errors:
   133         raise ValidationError(entity.eid, errors)
   130         raise ValidationError(entity.eid, errors)
       
   131 
       
   132 
       
   133 class _MockEntity(object): # XXX use a named tuple with python 2.6
       
   134     def __init__(self, eid):
       
   135         self.eid = eid
   134 
   136 
   135 
   137 
   136 class SyncSchemaHook(hook.Hook):
   138 class SyncSchemaHook(hook.Hook):
   137     """abstract class for schema synchronization hooks (in the `syncschema`
   139     """abstract class for schema synchronization hooks (in the `syncschema`
   138     category)
   140     category)
   257                 session.system_sql(sql)
   259                 session.system_sql(sql)
   258         # add meta relations
   260         # add meta relations
   259         gmap = group_mapping(session)
   261         gmap = group_mapping(session)
   260         cmap = ss.cstrtype_mapping(session)
   262         cmap = ss.cstrtype_mapping(session)
   261         for rtype in (META_RTYPES - VIRTUAL_RTYPES):
   263         for rtype in (META_RTYPES - VIRTUAL_RTYPES):
   262             rschema = schema[rtype]
   264             try:
       
   265                 rschema = schema[rtype]
       
   266             except:
       
   267                 if rtype == 'cw_source':
       
   268                     continue # XXX 3.10 migration
       
   269                 raise
   263             sampletype = rschema.subjects()[0]
   270             sampletype = rschema.subjects()[0]
   264             desttype = rschema.objects()[0]
   271             desttype = rschema.objects()[0]
   265             rdef = copy(rschema.rdef(sampletype, desttype))
   272             rdef = copy(rschema.rdef(sampletype, desttype))
   266             rdef.subject = mock_object(eid=entity.eid)
   273             rdef.subject = _MockEntity(eid=entity.eid)
   267             mock = mock_object(eid=None)
   274             mock = _MockEntity(eid=None)
   268             ss.execschemarql(session.execute, mock, ss.rdef2rql(rdef, cmap, gmap))
   275             ss.execschemarql(session.execute, mock, ss.rdef2rql(rdef, cmap, gmap))
   269 
   276 
   270     def revertprecommit_event(self):
   277     def revertprecommit_event(self):
   271         # revert changes on in memory schema
   278         # revert changes on in memory schema
   272         self.session.vreg.schema.del_entity_type(self.entity.name)
   279         self.session.vreg.schema.del_entity_type(self.entity.name)
   309         rschema = self.rschema
   316         rschema = self.rschema
   310         if rschema.final:
   317         if rschema.final:
   311             return # watched changes to final relation type are unexpected
   318             return # watched changes to final relation type are unexpected
   312         session = self.session
   319         session = self.session
   313         if 'fulltext_container' in self.values:
   320         if 'fulltext_container' in self.values:
       
   321             op = UpdateFTIndexOp.get_instance(session)
   314             for subjtype, objtype in rschema.rdefs:
   322             for subjtype, objtype in rschema.rdefs:
   315                 hook.set_operation(session, 'fti_update_etypes', subjtype,
   323                 op.add_data(subjtype)
   316                                    UpdateFTIndexOp)
   324                 op.add_data(objtype)
   317                 hook.set_operation(session, 'fti_update_etypes', objtype,
       
   318                                    UpdateFTIndexOp)
       
   319         # update the in-memory schema first
   325         # update the in-memory schema first
   320         self.oldvalues = dict( (attr, getattr(rschema, attr)) for attr in self.values)
   326         self.oldvalues = dict( (attr, getattr(rschema, attr)) for attr in self.values)
   321         self.rschema.__dict__.update(self.values)
   327         self.rschema.__dict__.update(self.values)
   322         # then make necessary changes to the system source database
   328         # then make necessary changes to the system source database
   323         if not 'inlined' in self.values:
   329         if not 'inlined' in self.values:
   605                                              rdef.rtype.inlined) \
   611                                              rdef.rtype.inlined) \
   606               and self.values['cardinality'][0] != self.oldvalues['cardinality'][0]:
   612               and self.values['cardinality'][0] != self.oldvalues['cardinality'][0]:
   607             syssource.update_rdef_null_allowed(self.session, rdef)
   613             syssource.update_rdef_null_allowed(self.session, rdef)
   608             self.null_allowed_changed = True
   614             self.null_allowed_changed = True
   609         if 'fulltextindexed' in self.values:
   615         if 'fulltextindexed' in self.values:
   610             hook.set_operation(session, 'fti_update_etypes', rdef.subject,
   616             UpdateFTIndexOp.get_instance(session).add_data(rdef.subject)
   611                                UpdateFTIndexOp)
       
   612 
   617 
   613     def revertprecommit_event(self):
   618     def revertprecommit_event(self):
   614         if self.rdef is None:
   619         if self.rdef is None:
   615             return
   620             return
   616         # revert changes on in memory schema
   621         # revert changes on in memory schema
   698             self.size_cstr_changed = True
   703             self.size_cstr_changed = True
   699         elif cstrtype == 'UniqueConstraint' and oldcstr is None:
   704         elif cstrtype == 'UniqueConstraint' and oldcstr is None:
   700             syssource.update_rdef_unique(session, rdef)
   705             syssource.update_rdef_unique(session, rdef)
   701             self.unique_changed = True
   706             self.unique_changed = True
   702 
   707 
       
   708 
   703 class CWUniqueTogetherConstraintAddOp(MemSchemaOperation):
   709 class CWUniqueTogetherConstraintAddOp(MemSchemaOperation):
   704     entity = None # make pylint happy
   710     entity = None # make pylint happy
   705     def precommit_event(self):
   711     def precommit_event(self):
   706         session = self.session
   712         session = self.session
   707         prefix = SQL_PREFIX
   713         prefix = SQL_PREFIX
   708         table = '%s%s' % (prefix, self.entity.constraint_of[0].name)
   714         table = '%s%s' % (prefix, self.entity.constraint_of[0].name)
   709         cols = ['%s%s' % (prefix, r.rtype.name)
   715         cols = ['%s%s' % (prefix, r.name) for r in self.entity.relations]
   710                 for r in self.entity.relations]
       
   711         dbhelper= session.pool.source('system').dbhelper
   716         dbhelper= session.pool.source('system').dbhelper
   712         sqls = dbhelper.sqls_create_multicol_unique_index(table, cols)
   717         sqls = dbhelper.sqls_create_multicol_unique_index(table, cols)
   713         for sql in sqls:
   718         for sql in sqls:
   714             session.system_sql(sql)
   719             session.system_sql(sql)
   715 
   720 
   716     # XXX revertprecommit_event
   721     # XXX revertprecommit_event
   717 
   722 
   718     def postcommit_event(self):
   723     def postcommit_event(self):
   719         eschema = self.session.vreg.schema.schema_by_eid(self.entity.constraint_of[0].eid)
   724         eschema = self.session.vreg.schema.schema_by_eid(self.entity.constraint_of[0].eid)
   720         attrs = [r.rtype.name for r in self.entity.relations]
   725         attrs = [r.name for r in self.entity.relations]
   721         eschema._unique_together.append(attrs)
   726         eschema._unique_together.append(attrs)
       
   727 
   722 
   728 
   723 class CWUniqueTogetherConstraintDelOp(MemSchemaOperation):
   729 class CWUniqueTogetherConstraintDelOp(MemSchemaOperation):
   724     entity = oldcstr = None # for pylint
   730     entity = oldcstr = None # for pylint
   725     cols = [] # for pylint
   731     cols = [] # for pylint
   726     def precommit_event(self):
   732     def precommit_event(self):
   739         eschema = self.session.vreg.schema.schema_by_eid(self.entity.eid)
   745         eschema = self.session.vreg.schema.schema_by_eid(self.entity.eid)
   740         cols = set(self.cols)
   746         cols = set(self.cols)
   741         unique_together = [ut for ut in eschema._unique_together
   747         unique_together = [ut for ut in eschema._unique_together
   742                            if set(ut) != cols]
   748                            if set(ut) != cols]
   743         eschema._unique_together = unique_together
   749         eschema._unique_together = unique_together
       
   750 
   744 
   751 
   745 # operations for in-memory schema synchronization  #############################
   752 # operations for in-memory schema synchronization  #############################
   746 
   753 
   747 class MemSchemaCWETypeDel(MemSchemaOperation):
   754 class MemSchemaCWETypeDel(MemSchemaOperation):
   748     """actually remove the entity type from the instance's schema"""
   755     """actually remove the entity type from the instance's schema"""
   902     __regid__ = 'syncaddcwetype'
   909     __regid__ = 'syncaddcwetype'
   903     events = ('after_add_entity',)
   910     events = ('after_add_entity',)
   904 
   911 
   905     def __call__(self):
   912     def __call__(self):
   906         entity = self.entity
   913         entity = self.entity
   907         if entity.get('final'):
   914         if entity.cw_edited.get('final'):
   908             return
   915             return
   909         CWETypeAddOp(self._cw, entity=entity)
   916         CWETypeAddOp(self._cw, entity=entity)
   910 
   917 
   911 
   918 
   912 class BeforeUpdateCWETypeHook(DelCWETypeHook):
   919 class BeforeUpdateCWETypeHook(DelCWETypeHook):
   916 
   923 
   917     def __call__(self):
   924     def __call__(self):
   918         entity = self.entity
   925         entity = self.entity
   919         check_valid_changes(self._cw, entity, ro_attrs=('final',))
   926         check_valid_changes(self._cw, entity, ro_attrs=('final',))
   920         # don't use getattr(entity, attr), we would get the modified value if any
   927         # don't use getattr(entity, attr), we would get the modified value if any
   921         if 'name' in entity.edited_attributes:
   928         if 'name' in entity.cw_edited:
   922             oldname, newname = hook.entity_oldnewvalue(entity, 'name')
   929             oldname, newname = entity.cw_edited.oldnewvalue('name')
   923             if newname.lower() != oldname.lower():
   930             if newname.lower() != oldname.lower():
   924                 CWETypeRenameOp(self._cw, oldname=oldname, newname=newname)
   931                 CWETypeRenameOp(self._cw, oldname=oldname, newname=newname)
   925 
   932 
   926 
   933 
   927 # CWRType hooks ################################################################
   934 # CWRType hooks ################################################################
   960 
   967 
   961     def __call__(self):
   968     def __call__(self):
   962         entity = self.entity
   969         entity = self.entity
   963         rtypedef = ybo.RelationType(name=entity.name,
   970         rtypedef = ybo.RelationType(name=entity.name,
   964                                     description=entity.description,
   971                                     description=entity.description,
   965                                     inlined=entity.get('inlined', False),
   972                                     inlined=entity.cw_edited.get('inlined', False),
   966                                     symmetric=entity.get('symmetric', False),
   973                                     symmetric=entity.cw_edited.get('symmetric', False),
   967                                     eid=entity.eid)
   974                                     eid=entity.eid)
   968         MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef)
   975         MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef)
   969 
   976 
   970 
   977 
   971 class BeforeUpdateCWRTypeHook(DelCWRTypeHook):
   978 class BeforeUpdateCWRTypeHook(DelCWRTypeHook):
   976     def __call__(self):
   983     def __call__(self):
   977         entity = self.entity
   984         entity = self.entity
   978         check_valid_changes(self._cw, entity)
   985         check_valid_changes(self._cw, entity)
   979         newvalues = {}
   986         newvalues = {}
   980         for prop in ('symmetric', 'inlined', 'fulltext_container'):
   987         for prop in ('symmetric', 'inlined', 'fulltext_container'):
   981             if prop in entity.edited_attributes:
   988             if prop in entity.cw_edited:
   982                 old, new = hook.entity_oldnewvalue(entity, prop)
   989                 old, new = entity.cw_edited.oldnewvalue(prop)
   983                 if old != new:
   990                 if old != new:
   984                     newvalues[prop] = entity[prop]
   991                     newvalues[prop] = new
   985         if newvalues:
   992         if newvalues:
   986             rschema = self._cw.vreg.schema.rschema(entity.name)
   993             rschema = self._cw.vreg.schema.rschema(entity.name)
   987             CWRTypeUpdateOp(self._cw, rschema=rschema, entity=entity,
   994             CWRTypeUpdateOp(self._cw, rschema=rschema, entity=entity,
   988                             values=newvalues)
   995                             values=newvalues)
   989 
   996 
  1064                 continue
  1071                 continue
  1065             if prop == 'order':
  1072             if prop == 'order':
  1066                 attr = 'ordernum'
  1073                 attr = 'ordernum'
  1067             else:
  1074             else:
  1068                 attr = prop
  1075                 attr = prop
  1069             if attr in entity.edited_attributes:
  1076             if attr in entity.cw_edited:
  1070                 old, new = hook.entity_oldnewvalue(entity, attr)
  1077                 old, new = entity.cw_edited.oldnewvalue(attr)
  1071                 if old != new:
  1078                 if old != new:
  1072                     newvalues[prop] = new
  1079                     newvalues[prop] = new
  1073         if newvalues:
  1080         if newvalues:
  1074             RDefUpdateOp(self._cw, rschema=rschema, rdefkey=(subjtype, objtype),
  1081             RDefUpdateOp(self._cw, rschema=rschema, rdefkey=(subjtype, objtype),
  1075                          values=newvalues)
  1082                          values=newvalues)
  1118 # unique_together constraints
  1125 # unique_together constraints
  1119 # XXX: use setoperations and before_add_relation here (on constraint_of and relations)
  1126 # XXX: use setoperations and before_add_relation here (on constraint_of and relations)
  1120 class AfterAddCWUniqueTogetherConstraintHook(SyncSchemaHook):
  1127 class AfterAddCWUniqueTogetherConstraintHook(SyncSchemaHook):
  1121     __regid__ = 'syncadd_cwuniquetogether_constraint'
  1128     __regid__ = 'syncadd_cwuniquetogether_constraint'
  1122     __select__ = SyncSchemaHook.__select__ & is_instance('CWUniqueTogetherConstraint')
  1129     __select__ = SyncSchemaHook.__select__ & is_instance('CWUniqueTogetherConstraint')
  1123     events = ('after_add_entity', 'after_update_entity')
  1130     events = ('after_add_entity',)
  1124 
  1131 
  1125     def __call__(self):
  1132     def __call__(self):
  1126         CWUniqueTogetherConstraintAddOp(self._cw, entity=self.entity)
  1133         CWUniqueTogetherConstraintAddOp(self._cw, entity=self.entity)
  1127 
  1134 
  1128 
  1135 
  1135         if self._cw.deleted_in_transaction(self.eidto):
  1142         if self._cw.deleted_in_transaction(self.eidto):
  1136             return
  1143             return
  1137         schema = self._cw.vreg.schema
  1144         schema = self._cw.vreg.schema
  1138         cstr = self._cw.entity_from_eid(self.eidfrom)
  1145         cstr = self._cw.entity_from_eid(self.eidfrom)
  1139         entity = schema.schema_by_eid(self.eidto)
  1146         entity = schema.schema_by_eid(self.eidto)
  1140         cols = [r.rtype.name
  1147         cols = [r.name for r in cstr.relations]
  1141                 for r in cstr.relations]
  1148         CWUniqueTogetherConstraintDelOp(self._cw, entity=entity,
  1142         CWUniqueTogetherConstraintDelOp(self._cw, entity=entity, oldcstr=cstr, cols=cols)
  1149                                         oldcstr=cstr, cols=cols)
  1143 
  1150 
  1144 
  1151 
  1145 # permissions synchronization hooks ############################################
  1152 # permissions synchronization hooks ############################################
  1146 
  1153 
  1147 class AfterAddPermissionHook(SyncSchemaHook):
  1154 class AfterAddPermissionHook(SyncSchemaHook):
  1183             MemSchemaPermissionDel(self._cw, action=action, eid=self.eidfrom,
  1190             MemSchemaPermissionDel(self._cw, action=action, eid=self.eidfrom,
  1184                                    expr=expr)
  1191                                    expr=expr)
  1185 
  1192 
  1186 
  1193 
  1187 
  1194 
  1188 class UpdateFTIndexOp(hook.SingleLastOperation):
  1195 class UpdateFTIndexOp(hook.DataOperationMixIn, hook.SingleLastOperation):
  1189     """operation to update full text indexation of entity whose schema change
  1196     """operation to update full text indexation of entity whose schema change
  1190 
  1197 
  1191     We wait after the commit to as the schema in memory is only updated after the commit.
  1198     We wait after the commit to as the schema in memory is only updated after
       
  1199     the commit.
  1192     """
  1200     """
  1193 
  1201 
  1194     def postcommit_event(self):
  1202     def postcommit_event(self):
  1195         session = self.session
  1203         session = self.session
  1196         source = session.repo.system_source
  1204         source = session.repo.system_source
  1197         to_reindex = session.transaction_data.pop('fti_update_etypes', ())
  1205         schema = session.repo.vreg.schema
       
  1206         to_reindex = self.get_data()
  1198         self.info('%i etypes need full text indexed reindexation',
  1207         self.info('%i etypes need full text indexed reindexation',
  1199                   len(to_reindex))
  1208                   len(to_reindex))
  1200         schema = self.session.repo.vreg.schema
       
  1201         for etype in to_reindex:
  1209         for etype in to_reindex:
  1202             rset = session.execute('Any X WHERE X is %s' % etype)
  1210             rset = session.execute('Any X WHERE X is %s' % etype)
  1203             self.info('Reindexing full text index for %i entity of type %s',
  1211             self.info('Reindexing full text index for %i entity of type %s',
  1204                       len(rset), etype)
  1212                       len(rset), etype)
  1205             still_fti = list(schema[etype].indexable_attributes())
  1213             still_fti = list(schema[etype].indexable_attributes())
  1206             for entity in rset.entities():
  1214             for entity in rset.entities():
  1207                 source.fti_unindex_entity(session, entity.eid)
  1215                 source.fti_unindex_entities(session, [entity])
  1208                 for container in entity.cw_adapt_to('IFTIndexable').fti_containers():
  1216                 for container in entity.cw_adapt_to('IFTIndexable').fti_containers():
  1209                     if still_fti or container is not entity:
  1217                     if still_fti or container is not entity:
  1210                         source.fti_unindex_entity(session, container.eid)
  1218                         source.fti_unindex_entities(session, [container])
  1211                         source.fti_index_entity(session, container)
  1219                         source.fti_index_entities(session, [container])
  1212         if len(to_reindex):
  1220         if to_reindex:
  1213             # Transaction have already been committed
  1221             # Transaction has already been committed
  1214             session.pool.commit()
  1222             session.pool.commit()
  1215 
  1223 
  1216 
  1224 
  1217 
  1225 
  1218 
  1226