hooks/syncschema.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """schema hooks:
       
    19 
       
    20 - synchronize the living schema object with the persistent schema
       
    21 - perform physical update on the source when necessary
       
    22 
       
    23 checking for schema consistency is done in hooks.py
       
    24 """
       
    25 
       
    26 __docformat__ = "restructuredtext en"
       
    27 from cubicweb import _
       
    28 
       
    29 import json
       
    30 from copy import copy
       
    31 from hashlib import md5
       
    32 
       
    33 from yams.schema import (BASE_TYPES, BadSchemaDefinition,
       
    34                          RelationSchema, RelationDefinitionSchema)
       
    35 from yams import buildobjs as ybo, convert_default_value
       
    36 
       
    37 from logilab.common.decorators import clear_cache
       
    38 
       
    39 from cubicweb import validation_error
       
    40 from cubicweb.predicates import is_instance
       
    41 from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES,
       
    42                              CONSTRAINTS, UNIQUE_CONSTRAINTS, ETYPE_NAME_MAP)
       
    43 from cubicweb.server import hook, schemaserial as ss, schema2sql as y2sql
       
    44 from cubicweb.server.sqlutils import SQL_PREFIX
       
    45 from cubicweb.hooks.synccomputed import RecomputeAttributeOperation
       
    46 
       
    47 # core entity and relation types which can't be removed
       
    48 CORE_TYPES = BASE_TYPES | SCHEMA_TYPES | META_RTYPES | set(
       
    49     ('CWUser', 'CWGroup','login', 'upassword', 'name', 'in_group'))
       
    50 
       
    51 
       
    52 def get_constraints(cnx, entity):
       
    53     constraints = []
       
    54     for cstreid in cnx.transaction_data.get(entity.eid, ()):
       
    55         cstrent = cnx.entity_from_eid(cstreid)
       
    56         cstr = CONSTRAINTS[cstrent.type].deserialize(cstrent.value)
       
    57         cstr.eid = cstreid
       
    58         constraints.append(cstr)
       
    59     return constraints
       
    60 
       
    61 def group_mapping(cw):
       
    62     try:
       
    63         return cw.transaction_data['groupmap']
       
    64     except KeyError:
       
    65         cw.transaction_data['groupmap'] = gmap = ss.group_mapping(cw)
       
    66         return gmap
       
    67 
       
    68 def add_inline_relation_column(cnx, etype, rtype):
       
    69     """add necessary column and index for an inlined relation"""
       
    70     attrkey = '%s.%s' % (etype, rtype)
       
    71     createdattrs = cnx.transaction_data.setdefault('createdattrs', set())
       
    72     if attrkey in createdattrs:
       
    73         return
       
    74     createdattrs.add(attrkey)
       
    75     table = SQL_PREFIX + etype
       
    76     column = SQL_PREFIX + rtype
       
    77     try:
       
    78         cnx.system_sql(str('ALTER TABLE %s ADD %s integer REFERENCES entities (eid)' % (table, column)),
       
    79                        rollback_on_failure=False)
       
    80         cnx.info('added column %s to table %s', column, table)
       
    81     except Exception:
       
    82         # silent exception here, if this error has not been raised because the
       
    83         # column already exists, index creation will fail anyway
       
    84         cnx.exception('error while adding column %s to table %s',
       
    85                       table, column)
       
    86     # create index before alter table which may expectingly fail during test
       
    87     # (sqlite) while index creation should never fail (test for index existence
       
    88     # is done by the dbhelper)
       
    89     cnx.repo.system_source.create_index(cnx, table, column)
       
    90     cnx.info('added index on %s(%s)', table, column)
       
    91 
       
    92 
       
    93 def insert_rdef_on_subclasses(cnx, eschema, rschema, rdefdef, props):
       
    94     # XXX 'infered': True/False, not clear actually
       
    95     props.update({'constraints': rdefdef.constraints,
       
    96                   'description': rdefdef.description,
       
    97                   'cardinality': rdefdef.cardinality,
       
    98                   'permissions': rdefdef.get_permissions(),
       
    99                   'order': rdefdef.order,
       
   100                   'infered': False, 'eid': None
       
   101                   })
       
   102     cstrtypemap = ss.cstrtype_mapping(cnx)
       
   103     groupmap = group_mapping(cnx)
       
   104     object = rschema.schema.eschema(rdefdef.object)
       
   105     for specialization in eschema.specialized_by(False):
       
   106         if (specialization, rdefdef.object) in rschema.rdefs:
       
   107             continue
       
   108         sperdef = RelationDefinitionSchema(specialization, rschema,
       
   109                                            object, None, values=props)
       
   110         ss.execschemarql(cnx.execute, sperdef,
       
   111                          ss.rdef2rql(sperdef, cstrtypemap, groupmap))
       
   112 
       
   113 
       
   114 def check_valid_changes(cnx, entity, ro_attrs=('name', 'final')):
       
   115     errors = {}
       
   116     # don't use getattr(entity, attr), we would get the modified value if any
       
   117     for attr in entity.cw_edited:
       
   118         if attr in ro_attrs:
       
   119             origval, newval = entity.cw_edited.oldnewvalue(attr)
       
   120             if newval != origval:
       
   121                 errors[attr] = _("can't change this attribute")
       
   122     if errors:
       
   123         raise validation_error(entity, errors)
       
   124 
       
   125 
       
   126 class _MockEntity(object): # XXX use a named tuple with python 2.6
       
   127     def __init__(self, eid):
       
   128         self.eid = eid
       
   129 
       
   130 
       
   131 class SyncSchemaHook(hook.Hook):
       
   132     """abstract class for schema synchronization hooks (in the `syncschema`
       
   133     category)
       
   134     """
       
   135     __abstract__ = True
       
   136     category = 'syncschema'
       
   137 
       
   138 
       
   139 # operations for low-level database alteration  ################################
       
   140 
       
   141 class DropTable(hook.Operation):
       
   142     """actually remove a database from the instance's schema"""
       
   143     table = None # make pylint happy
       
   144     def precommit_event(self):
       
   145         dropped = self.cnx.transaction_data.setdefault('droppedtables',
       
   146                                                            set())
       
   147         if self.table in dropped:
       
   148             return # already processed
       
   149         dropped.add(self.table)
       
   150         self.cnx.system_sql('DROP TABLE %s' % self.table)
       
   151         self.info('dropped table %s', self.table)
       
   152 
       
   153     # XXX revertprecommit_event
       
   154 
       
   155 
       
   156 class DropRelationTable(DropTable):
       
   157     def __init__(self, cnx, rtype):
       
   158         super(DropRelationTable, self).__init__(
       
   159             cnx, table='%s_relation' % rtype)
       
   160         cnx.transaction_data.setdefault('pendingrtypes', set()).add(rtype)
       
   161 
       
   162 
       
   163 class DropColumn(hook.DataOperationMixIn, hook.Operation):
       
   164     """actually remove the attribut's column from entity table in the system
       
   165     database
       
   166     """
       
   167     def precommit_event(self):
       
   168         cnx = self.cnx
       
   169         for etype, attr in self.get_data():
       
   170             table = SQL_PREFIX + etype
       
   171             column = SQL_PREFIX + attr
       
   172             source = cnx.repo.system_source
       
   173             # drop index if any
       
   174             source.drop_index(cnx, table, column)
       
   175             if source.dbhelper.alter_column_support:
       
   176                 cnx.system_sql('ALTER TABLE %s DROP COLUMN %s' % (table, column),
       
   177                                rollback_on_failure=False)
       
   178                 self.info('dropped column %s from table %s', column, table)
       
   179             else:
       
   180                 # not supported by sqlite for instance
       
   181                 self.error('dropping column not supported by the backend, handle '
       
   182                            'it yourself (%s.%s)', table, column)
       
   183 
       
   184     # XXX revertprecommit_event
       
   185 
       
   186 
       
   187 # base operations for in-memory schema synchronization  ########################
       
   188 
       
   189 class MemSchemaNotifyChanges(hook.SingleLastOperation):
       
   190     """the update schema operation:
       
   191 
       
   192     special operation which should be called once and after all other schema
       
   193     operations. It will trigger internal structures rebuilding to consider
       
   194     schema changes.
       
   195     """
       
   196 
       
   197     def __init__(self, cnx):
       
   198         hook.SingleLastOperation.__init__(self, cnx)
       
   199 
       
   200     def precommit_event(self):
       
   201         for eschema in self.cnx.repo.schema.entities():
       
   202             if not eschema.final:
       
   203                 clear_cache(eschema, 'ordered_relations')
       
   204 
       
   205     def postcommit_event(self):
       
   206         repo = self.cnx.repo
       
   207         # commit event should not raise error, while set_schema has chances to
       
   208         # do so because it triggers full vreg reloading
       
   209         try:
       
   210             repo.schema.rebuild_infered_relations()
       
   211             # trigger vreg reload
       
   212             repo.set_schema(repo.schema)
       
   213             # CWUser class might have changed, update current session users
       
   214             cwuser_cls = self.cnx.vreg['etypes'].etype_class('CWUser')
       
   215             for session in repo._sessions.values():
       
   216                 session.user.__class__ = cwuser_cls
       
   217         except Exception:
       
   218             self.critical('error while setting schema', exc_info=True)
       
   219 
       
   220     def rollback_event(self):
       
   221         self.precommit_event()
       
   222 
       
   223 
       
   224 class MemSchemaOperation(hook.Operation):
       
   225     """base class for schema operations"""
       
   226     def __init__(self, cnx, **kwargs):
       
   227         hook.Operation.__init__(self, cnx, **kwargs)
       
   228         # every schema operation is triggering a schema update
       
   229         MemSchemaNotifyChanges(cnx)
       
   230 
       
   231 
       
   232 # operations for high-level source database alteration  ########################
       
   233 
       
   234 class CWETypeAddOp(MemSchemaOperation):
       
   235     """after adding a CWEType entity:
       
   236     * add it to the instance's schema
       
   237     * create the necessary table
       
   238     * set creation_date and modification_date by creating the necessary
       
   239       CWAttribute entities
       
   240     * add <meta rtype> relation by creating the necessary CWRelation entity
       
   241     """
       
   242     entity = None # make pylint happy
       
   243 
       
   244     def precommit_event(self):
       
   245         cnx = self.cnx
       
   246         entity = self.entity
       
   247         schema = cnx.vreg.schema
       
   248         etype = ybo.EntityType(eid=entity.eid, name=entity.name,
       
   249                                description=entity.description)
       
   250         eschema = schema.add_entity_type(etype)
       
   251         # create the necessary table
       
   252         tablesql = y2sql.eschema2sql(cnx.repo.system_source.dbhelper,
       
   253                                      eschema, prefix=SQL_PREFIX)
       
   254         for sql in tablesql.split(';'):
       
   255             if sql.strip():
       
   256                 cnx.system_sql(sql)
       
   257         # add meta relations
       
   258         gmap = group_mapping(cnx)
       
   259         cmap = ss.cstrtype_mapping(cnx)
       
   260         for rtype in (META_RTYPES - VIRTUAL_RTYPES):
       
   261             try:
       
   262                 rschema = schema[rtype]
       
   263             except KeyError:
       
   264                 self.critical('rtype %s was not handled at cwetype creation time', rtype)
       
   265                 continue
       
   266             if not rschema.rdefs:
       
   267                 self.warning('rtype %s has no relation definition yet', rtype)
       
   268                 continue
       
   269             sampletype = rschema.subjects()[0]
       
   270             desttype = rschema.objects()[0]
       
   271             try:
       
   272                 rdef = copy(rschema.rdef(sampletype, desttype))
       
   273             except KeyError:
       
   274                 # this combo does not exist because this is not a universal META_RTYPE
       
   275                 continue
       
   276             rdef.subject = _MockEntity(eid=entity.eid)
       
   277             mock = _MockEntity(eid=None)
       
   278             ss.execschemarql(cnx.execute, mock, ss.rdef2rql(rdef, cmap, gmap))
       
   279 
       
   280     def revertprecommit_event(self):
       
   281         # revert changes on in memory schema
       
   282         self.cnx.vreg.schema.del_entity_type(self.entity.name)
       
   283         # revert changes on database
       
   284         self.cnx.system_sql('DROP TABLE %s%s' % (SQL_PREFIX, self.entity.name))
       
   285 
       
   286 
       
   287 class CWETypeRenameOp(MemSchemaOperation):
       
   288     """this operation updates physical storage accordingly"""
       
   289     oldname = newname = None # make pylint happy
       
   290 
       
   291     def rename(self, oldname, newname):
       
   292         self.cnx.vreg.schema.rename_entity_type(oldname, newname)
       
   293         # we need sql to operate physical changes on the system database
       
   294         sqlexec = self.cnx.system_sql
       
   295         dbhelper = self.cnx.repo.system_source.dbhelper
       
   296         sql = dbhelper.sql_rename_table(SQL_PREFIX+oldname,
       
   297                                         SQL_PREFIX+newname)
       
   298         sqlexec(sql)
       
   299         self.info('renamed table %s to %s', oldname, newname)
       
   300         sqlexec('UPDATE entities SET type=%(newname)s WHERE type=%(oldname)s',
       
   301                 {'newname': newname, 'oldname': oldname})
       
   302         for eid, (etype, extid, auri) in self.cnx.repo._type_source_cache.items():
       
   303             if etype == oldname:
       
   304                 self.cnx.repo._type_source_cache[eid] = (newname, extid, auri)
       
   305         # XXX transaction records
       
   306 
       
   307     def precommit_event(self):
       
   308         self.rename(self.oldname, self.newname)
       
   309 
       
   310     def revertprecommit_event(self):
       
   311         self.rename(self.newname, self.oldname)
       
   312 
       
   313 
       
   314 class CWRTypeUpdateOp(MemSchemaOperation):
       
   315     """actually update some properties of a relation definition"""
       
   316     rschema = entity = values = None # make pylint happy
       
   317     oldvalues = None
       
   318 
       
   319     def precommit_event(self):
       
   320         rschema = self.rschema
       
   321         if rschema.final:
       
   322             return # watched changes to final relation type are unexpected
       
   323         cnx = self.cnx
       
   324         if 'fulltext_container' in self.values:
       
   325             op = UpdateFTIndexOp.get_instance(cnx)
       
   326             for subjtype, objtype in rschema.rdefs:
       
   327                 if self.values['fulltext_container'] == 'subject':
       
   328                     op.add_data(subjtype)
       
   329                     op.add_data(objtype)
       
   330                 else:
       
   331                     op.add_data(objtype)
       
   332                     op.add_data(subjtype)
       
   333         # update the in-memory schema first
       
   334         self.oldvalues = dict( (attr, getattr(rschema, attr)) for attr in self.values)
       
   335         self.rschema.__dict__.update(self.values)
       
   336         # then make necessary changes to the system source database
       
   337         if 'inlined' not in self.values:
       
   338             return # nothing to do
       
   339         inlined = self.values['inlined']
       
   340         # check in-lining is possible when inlined
       
   341         if inlined:
       
   342             self.entity.check_inlined_allowed()
       
   343         # inlined changed, make necessary physical changes!
       
   344         sqlexec = self.cnx.system_sql
       
   345         rtype = rschema.type
       
   346         eidcolumn = SQL_PREFIX + 'eid'
       
   347         if not inlined:
       
   348             # need to create the relation if it has not been already done by
       
   349             # another event of the same transaction
       
   350             if not rschema.type in cnx.transaction_data.get('createdtables', ()):
       
   351                 tablesql = y2sql.rschema2sql(rschema)
       
   352                 # create the necessary table
       
   353                 for sql in tablesql.split(';'):
       
   354                     if sql.strip():
       
   355                         sqlexec(sql)
       
   356                 cnx.transaction_data.setdefault('createdtables', []).append(
       
   357                     rschema.type)
       
   358             # copy existant data
       
   359             column = SQL_PREFIX + rtype
       
   360             for etype in rschema.subjects():
       
   361                 table = SQL_PREFIX + str(etype)
       
   362                 sqlexec('INSERT INTO %s_relation SELECT %s, %s FROM %s WHERE NOT %s IS NULL'
       
   363                         % (rtype, eidcolumn, column, table, column))
       
   364             # drop existant columns
       
   365             #if cnx.repo.system_source.dbhelper.alter_column_support:
       
   366             for etype in rschema.subjects():
       
   367                 DropColumn.get_instance(cnx).add_data((str(etype), rtype))
       
   368         else:
       
   369             for etype in rschema.subjects():
       
   370                 try:
       
   371                     add_inline_relation_column(cnx, str(etype), rtype)
       
   372                 except Exception as ex:
       
   373                     # the column probably already exists. this occurs when the
       
   374                     # entity's type has just been added or if the column has not
       
   375                     # been previously dropped (eg sqlite)
       
   376                     self.error('error while altering table %s: %s', etype, ex)
       
   377                 # copy existant data.
       
   378                 # XXX don't use, it's not supported by sqlite (at least at when i tried it)
       
   379                 #sqlexec('UPDATE %(etype)s SET %(rtype)s=eid_to '
       
   380                 #        'FROM %(rtype)s_relation '
       
   381                 #        'WHERE %(etype)s.eid=%(rtype)s_relation.eid_from'
       
   382                 #        % locals())
       
   383                 table = SQL_PREFIX + str(etype)
       
   384                 cursor = sqlexec('SELECT eid_from, eid_to FROM %(table)s, '
       
   385                                  '%(rtype)s_relation WHERE %(table)s.%(eidcolumn)s='
       
   386                                  '%(rtype)s_relation.eid_from' % locals())
       
   387                 args = [{'val': eid_to, 'x': eid} for eid, eid_to in cursor.fetchall()]
       
   388                 if args:
       
   389                     column = SQL_PREFIX + rtype
       
   390                     cursor.executemany('UPDATE %s SET %s=%%(val)s WHERE %s=%%(x)s'
       
   391                                        % (table, column, eidcolumn), args)
       
   392                 # drop existant table
       
   393                 DropRelationTable(cnx, rtype)
       
   394 
       
   395     def revertprecommit_event(self):
       
   396         # revert changes on in memory schema
       
   397         self.rschema.__dict__.update(self.oldvalues)
       
   398         # XXX revert changes on database
       
   399 
       
   400 
       
   401 class CWComputedRTypeUpdateOp(MemSchemaOperation):
       
   402     """actually update some properties of a computed relation definition"""
       
   403     rschema = entity = rule = None # make pylint happy
       
   404     old_rule = None
       
   405 
       
   406     def precommit_event(self):
       
   407         # update the in-memory schema first
       
   408         self.old_rule = self.rschema.rule
       
   409         self.rschema.rule = self.rule
       
   410 
       
   411     def revertprecommit_event(self):
       
   412         # revert changes on in memory schema
       
   413         self.rschema.rule = self.old_rule
       
   414 
       
   415 
       
   416 class CWAttributeAddOp(MemSchemaOperation):
       
   417     """an attribute relation (CWAttribute) has been added:
       
   418     * add the necessary column
       
   419     * set default on this column if any and possible
       
   420     * register an operation to add the relation definition to the
       
   421       instance's schema on commit
       
   422 
       
   423     constraints are handled by specific hooks
       
   424     """
       
   425     entity = None # make pylint happy
       
   426 
       
   427     def init_rdef(self, **kwargs):
       
   428         entity = self.entity
       
   429         fromentity = entity.stype
       
   430         rdefdef = self.rdefdef = ybo.RelationDefinition(
       
   431             str(fromentity.name), entity.rtype.name, str(entity.otype.name),
       
   432             description=entity.description, cardinality=entity.cardinality,
       
   433             constraints=get_constraints(self.cnx, entity),
       
   434             order=entity.ordernum, eid=entity.eid, **kwargs)
       
   435         try:
       
   436             self.cnx.vreg.schema.add_relation_def(rdefdef)
       
   437         except BadSchemaDefinition:
       
   438             # rdef has been infered then explicitly added (current consensus is
       
   439             # not clear at all versus infered relation handling (and much
       
   440             # probably buggy)
       
   441             rdef = self.cnx.vreg.schema.rschema(rdefdef.name).rdefs[rdefdef.subject, rdefdef.object]
       
   442             assert rdef.infered
       
   443         else:
       
   444             rdef = self.cnx.vreg.schema.rschema(rdefdef.name).rdefs[rdefdef.subject, rdefdef.object]
       
   445 
       
   446         self.cnx.execute('SET X ordernum Y+1 '
       
   447                          'WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, '
       
   448                          'X ordernum >= %(order)s, NOT X eid %(x)s',
       
   449                          {'x': entity.eid, 'se': fromentity.eid,
       
   450                           'order': entity.ordernum or 0})
       
   451         return rdefdef, rdef
       
   452 
       
   453     def precommit_event(self):
       
   454         cnx = self.cnx
       
   455         entity = self.entity
       
   456         # entity.defaultval is a Binary or None, but we need a correctly typed
       
   457         # value
       
   458         default = entity.defaultval
       
   459         if default is not None:
       
   460             default = default.unzpickle()
       
   461         props = {'default': default,
       
   462                  'indexed': entity.indexed,
       
   463                  'fulltextindexed': entity.fulltextindexed,
       
   464                  'internationalizable': entity.internationalizable}
       
   465         if entity.extra_props:
       
   466             props.update(json.loads(entity.extra_props.getvalue().decode('ascii')))
       
   467         # entity.formula may not exist yet if we're migrating to 3.20
       
   468         if hasattr(entity, 'formula'):
       
   469             props['formula'] = entity.formula
       
   470         # update the in-memory schema first
       
   471         rdefdef, rdef = self.init_rdef(**props)
       
   472         # then make necessary changes to the system source database
       
   473         syssource = cnx.repo.system_source
       
   474         attrtype = y2sql.type_from_rdef(syssource.dbhelper, rdef)
       
   475         # XXX should be moved somehow into lgdb: sqlite doesn't support to
       
   476         # add a new column with UNIQUE, it should be added after the ALTER TABLE
       
   477         # using ADD INDEX
       
   478         if syssource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
       
   479             extra_unique_index = True
       
   480             attrtype = attrtype.replace(' UNIQUE', '')
       
   481         else:
       
   482             extra_unique_index = False
       
   483         # added some str() wrapping query since some backend (eg psycopg) don't
       
   484         # allow unicode queries
       
   485         table = SQL_PREFIX + rdefdef.subject
       
   486         column = SQL_PREFIX + rdefdef.name
       
   487         try:
       
   488             cnx.system_sql(str('ALTER TABLE %s ADD %s %s'
       
   489                                % (table, column, attrtype)),
       
   490                            rollback_on_failure=False)
       
   491             self.info('added column %s to table %s', column, table)
       
   492         except Exception as ex:
       
   493             # the column probably already exists. this occurs when
       
   494             # the entity's type has just been added or if the column
       
   495             # has not been previously dropped
       
   496             self.error('error while altering table %s: %s', table, ex)
       
   497         if extra_unique_index or entity.indexed:
       
   498             try:
       
   499                 syssource.create_index(cnx, table, column,
       
   500                                       unique=extra_unique_index)
       
   501             except Exception as ex:
       
   502                 self.error('error while creating index for %s.%s: %s',
       
   503                            table, column, ex)
       
   504         # final relations are not infered, propagate
       
   505         schema = cnx.vreg.schema
       
   506         try:
       
   507             eschema = schema.eschema(rdefdef.subject)
       
   508         except KeyError:
       
   509             return # entity type currently being added
       
   510         # propagate attribute to children classes
       
   511         rschema = schema.rschema(rdefdef.name)
       
   512         # if relation type has been inserted in the same transaction, its final
       
   513         # attribute is still set to False, so we've to ensure it's False
       
   514         rschema.final = True
       
   515         insert_rdef_on_subclasses(cnx, eschema, rschema, rdefdef, props)
       
   516         # update existing entities with the default value of newly added attribute
       
   517         if default is not None:
       
   518             default = convert_default_value(self.rdefdef, default)
       
   519             cnx.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column),
       
   520                                {'default': default})
       
   521         # if attribute is computed, compute it
       
   522         if getattr(entity, 'formula', None):
       
   523             # add rtype attribute for RelationDefinitionSchema api compat, this
       
   524             # is what RecomputeAttributeOperation expect
       
   525             rdefdef.rtype = rdefdef.name
       
   526             RecomputeAttributeOperation.get_instance(cnx).add_data(rdefdef)
       
   527 
       
   528     def revertprecommit_event(self):
       
   529         # revert changes on in memory schema
       
   530         if getattr(self, 'rdefdef', None) is None:
       
   531             return
       
   532         self.cnx.vreg.schema.del_relation_def(
       
   533             self.rdefdef.subject, self.rdefdef.name, self.rdefdef.object)
       
   534         # XXX revert changes on database
       
   535 
       
   536 
       
   537 class CWRelationAddOp(CWAttributeAddOp):
       
   538     """an actual relation has been added:
       
   539 
       
   540     * add the relation definition to the instance's schema
       
   541 
       
   542     * if this is an inlined relation, add the necessary column else if it's the
       
   543       first instance of this relation type, add the necessary table and set
       
   544       default permissions
       
   545 
       
   546     constraints are handled by specific hooks
       
   547     """
       
   548     entity = None # make pylint happy
       
   549 
       
   550     def precommit_event(self):
       
   551         cnx = self.cnx
       
   552         entity = self.entity
       
   553         # update the in-memory schema first
       
   554         rdefdef, rdef = self.init_rdef(composite=entity.composite)
       
   555         # then make necessary changes to the system source database
       
   556         schema = cnx.vreg.schema
       
   557         rtype = rdefdef.name
       
   558         rschema = schema.rschema(rtype)
       
   559         # this have to be done before permissions setting
       
   560         if rschema.inlined:
       
   561             # need to add a column if the relation is inlined and if this is the
       
   562             # first occurence of "Subject relation Something" whatever Something
       
   563             if len(rschema.objects(rdefdef.subject)) == 1:
       
   564                 add_inline_relation_column(cnx, rdefdef.subject, rtype)
       
   565             eschema = schema[rdefdef.subject]
       
   566             insert_rdef_on_subclasses(cnx, eschema, rschema, rdefdef,
       
   567                                       {'composite': entity.composite})
       
   568         else:
       
   569             if rschema.symmetric:
       
   570                 # for symmetric relations, rdefs will store relation definitions
       
   571                 # in both ways (i.e. (subj -> obj) and (obj -> subj))
       
   572                 relation_already_defined = len(rschema.rdefs) > 2
       
   573             else:
       
   574                 relation_already_defined = len(rschema.rdefs) > 1
       
   575             # need to create the relation if no relation definition in the
       
   576             # schema and if it has not been added during other event of the same
       
   577             # transaction
       
   578             if not (relation_already_defined or
       
   579                     rtype in cnx.transaction_data.get('createdtables', ())):
       
   580                 rschema = schema.rschema(rtype)
       
   581                 # create the necessary table
       
   582                 for sql in y2sql.rschema2sql(rschema).split(';'):
       
   583                     if sql.strip():
       
   584                         cnx.system_sql(sql)
       
   585                 cnx.transaction_data.setdefault('createdtables', []).append(
       
   586                     rtype)
       
   587 
       
   588     # XXX revertprecommit_event
       
   589 
       
   590 
       
   591 class RDefDelOp(MemSchemaOperation):
       
   592     """an actual relation has been removed"""
       
   593     rdef = None # make pylint happy
       
   594 
       
   595     def precommit_event(self):
       
   596         cnx = self.cnx
       
   597         rdef = self.rdef
       
   598         rschema = rdef.rtype
       
   599         # make necessary changes to the system source database first
       
   600         rdeftype = rschema.final and 'CWAttribute' or 'CWRelation'
       
   601         execute = cnx.execute
       
   602         rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
       
   603                        'R eid %%(x)s' % rdeftype, {'x': rschema.eid})
       
   604         lastrel = rset[0][0] == 0
       
   605         # we have to update physical schema systematically for final and inlined
       
   606         # relations, but only if it's the last instance for this relation type
       
   607         # for other relations
       
   608         if (rschema.final or rschema.inlined):
       
   609             rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
       
   610                            'R eid %%(r)s, X from_entity E, E eid %%(e)s'
       
   611                            % rdeftype,
       
   612                            {'r': rschema.eid, 'e': rdef.subject.eid})
       
   613             if rset[0][0] == 0 and not cnx.deleted_in_transaction(rdef.subject.eid):
       
   614                 ptypes = cnx.transaction_data.setdefault('pendingrtypes', set())
       
   615                 ptypes.add(rschema.type)
       
   616                 DropColumn.get_instance(cnx).add_data((str(rdef.subject), str(rschema)))
       
   617             elif rschema.inlined:
       
   618                 cnx.system_sql('UPDATE %s%s SET %s%s=NULL WHERE '
       
   619                                'EXISTS(SELECT 1 FROM entities '
       
   620                                '       WHERE eid=%s%s AND type=%%(to_etype)s)'
       
   621                                % (SQL_PREFIX, rdef.subject, SQL_PREFIX, rdef.rtype,
       
   622                                   SQL_PREFIX, rdef.rtype),
       
   623                                {'to_etype': rdef.object.type})
       
   624         elif lastrel:
       
   625             DropRelationTable(cnx, str(rschema))
       
   626         else:
       
   627             cnx.system_sql('DELETE FROM %s_relation WHERE '
       
   628                            'EXISTS(SELECT 1 FROM entities '
       
   629                            '       WHERE eid=eid_from AND type=%%(from_etype)s)'
       
   630                            ' AND EXISTS(SELECT 1 FROM entities '
       
   631                            '       WHERE eid=eid_to AND type=%%(to_etype)s)'
       
   632                            % rschema,
       
   633                            {'from_etype': rdef.subject.type, 'to_etype': rdef.object.type})
       
   634         # then update the in-memory schema
       
   635         if rdef.subject not in ETYPE_NAME_MAP and rdef.object not in ETYPE_NAME_MAP:
       
   636             rschema.del_relation_def(rdef.subject, rdef.object)
       
   637         # if this is the last relation definition of this type, drop associated
       
   638         # relation type
       
   639         if lastrel and not cnx.deleted_in_transaction(rschema.eid):
       
   640             execute('DELETE CWRType X WHERE X eid %(x)s', {'x': rschema.eid})
       
   641 
       
   642     def revertprecommit_event(self):
       
   643         # revert changes on in memory schema
       
   644         #
       
   645         # Note: add_relation_def takes a RelationDefinition, not a
       
   646         # RelationDefinitionSchema, needs to fake it
       
   647         rdef = self.rdef
       
   648         rdef.name = str(rdef.rtype)
       
   649         if rdef.subject not in ETYPE_NAME_MAP and rdef.object not in ETYPE_NAME_MAP:
       
   650             self.cnx.vreg.schema.add_relation_def(rdef)
       
   651 
       
   652 
       
   653 
       
   654 class RDefUpdateOp(MemSchemaOperation):
       
   655     """actually update some properties of a relation definition"""
       
   656     rschema = rdefkey = values = None # make pylint happy
       
   657     rdef = oldvalues = None
       
   658     indexed_changed = null_allowed_changed = False
       
   659 
       
   660     def precommit_event(self):
       
   661         cnx = self.cnx
       
   662         rdef = self.rdef = self.rschema.rdefs[self.rdefkey]
       
   663         # update the in-memory schema first
       
   664         self.oldvalues = dict( (attr, getattr(rdef, attr)) for attr in self.values)
       
   665         rdef.update(self.values)
       
   666         # then make necessary changes to the system source database
       
   667         syssource = cnx.repo.system_source
       
   668         if 'indexed' in self.values:
       
   669             syssource.update_rdef_indexed(cnx, rdef)
       
   670             self.indexed_changed = True
       
   671         if 'cardinality' in self.values and rdef.rtype.final \
       
   672               and self.values['cardinality'][0] != self.oldvalues['cardinality'][0]:
       
   673             syssource.update_rdef_null_allowed(self.cnx, rdef)
       
   674             self.null_allowed_changed = True
       
   675         if 'fulltextindexed' in self.values:
       
   676             UpdateFTIndexOp.get_instance(cnx).add_data(rdef.subject)
       
   677         if 'formula' in self.values:
       
   678             RecomputeAttributeOperation.get_instance(cnx).add_data(rdef)
       
   679 
       
   680     def revertprecommit_event(self):
       
   681         if self.rdef is None:
       
   682             return
       
   683         # revert changes on in memory schema
       
   684         self.rdef.update(self.oldvalues)
       
   685         # revert changes on database
       
   686         syssource = self.cnx.repo.system_source
       
   687         if self.indexed_changed:
       
   688             syssource.update_rdef_indexed(self.cnx, self.rdef)
       
   689         if self.null_allowed_changed:
       
   690             syssource.update_rdef_null_allowed(self.cnx, self.rdef)
       
   691 
       
   692 
       
   693 def _set_modifiable_constraints(rdef):
       
   694     # for proper in-place modification of in-memory schema: if rdef.constraints
       
   695     # is already a list, reuse it (we're updating multiple constraints of the
       
   696     # same rdef in the same transaction)
       
   697     if not isinstance(rdef.constraints, list):
       
   698         rdef.constraints = list(rdef.constraints)
       
   699 
       
   700 
       
   701 class CWConstraintDelOp(MemSchemaOperation):
       
   702     """actually remove a constraint of a relation definition"""
       
   703     rdef = oldcstr = newcstr = None # make pylint happy
       
   704     size_cstr_changed = unique_changed = False
       
   705 
       
   706     def precommit_event(self):
       
   707         cnx = self.cnx
       
   708         rdef = self.rdef
       
   709         # in-place modification of in-memory schema first
       
   710         _set_modifiable_constraints(rdef)
       
   711         if self.oldcstr in rdef.constraints:
       
   712             rdef.constraints.remove(self.oldcstr)
       
   713         else:
       
   714             self.critical('constraint %s for rdef %s was missing or already removed',
       
   715                           self.oldcstr, rdef)
       
   716         if cnx.deleted_in_transaction(rdef.eid):
       
   717             # don't try to alter a table that's going away (or is already gone)
       
   718             return
       
   719         # then update database: alter the physical schema on size/unique
       
   720         # constraint changes
       
   721         syssource = cnx.repo.system_source
       
   722         cstrtype = self.oldcstr.type()
       
   723         if cstrtype == 'SizeConstraint':
       
   724             # if the size constraint is being replaced with a new max size, we'll
       
   725             # call update_rdef_column in CWConstraintAddOp, skip it here
       
   726             for cstr in cnx.transaction_data.get('newsizecstr', ()):
       
   727                 rdefentity = cstr.reverse_constrained_by[0]
       
   728                 cstrrdef = cnx.vreg.schema.schema_by_eid(rdefentity.eid)
       
   729                 if cstrrdef == rdef:
       
   730                     return
       
   731 
       
   732             # we found that the size constraint for this rdef is really gone,
       
   733             # not just replaced by another
       
   734             syssource.update_rdef_column(cnx, rdef)
       
   735             self.size_cstr_changed = True
       
   736         elif cstrtype == 'UniqueConstraint':
       
   737             syssource.update_rdef_unique(cnx, rdef)
       
   738             self.unique_changed = True
       
   739         if cstrtype in ('BoundaryConstraint', 'IntervalBoundConstraint', 'StaticVocabularyConstraint'):
       
   740             cstrname = 'cstr' + md5((rdef.subject.type + rdef.rtype.type + cstrtype +
       
   741                                      (self.oldcstr.serialize() or '')).encode('utf-8')).hexdigest()
       
   742             cnx.system_sql('ALTER TABLE %s%s DROP CONSTRAINT %s' % (SQL_PREFIX, rdef.subject.type, cstrname))
       
   743 
       
   744     def revertprecommit_event(self):
       
   745         # revert changes on in memory schema
       
   746         if self.newcstr is not None:
       
   747             self.rdef.constraints.remove(self.newcstr)
       
   748         if self.oldcstr is not None:
       
   749             self.rdef.constraints.append(self.oldcstr)
       
   750         # revert changes on database
       
   751         syssource = self.cnx.repo.system_source
       
   752         if self.size_cstr_changed:
       
   753             syssource.update_rdef_column(self.cnx, self.rdef)
       
   754         if self.unique_changed:
       
   755             syssource.update_rdef_unique(self.cnx, self.rdef)
       
   756 
       
   757 
       
   758 class CWConstraintAddOp(CWConstraintDelOp):
       
   759     """actually update constraint of a relation definition"""
       
   760     entity = None # make pylint happy
       
   761 
       
   762     def precommit_event(self):
       
   763         cnx = self.cnx
       
   764         rdefentity = self.entity.reverse_constrained_by[0]
       
   765         # when the relation is added in the same transaction, the constraint
       
   766         # object is created by the operation adding the attribute or relation,
       
   767         # so there is nothing to do here
       
   768         if cnx.added_in_transaction(rdefentity.eid):
       
   769             return
       
   770         rdef = self.rdef = cnx.vreg.schema.schema_by_eid(rdefentity.eid)
       
   771         cstrtype = self.entity.type
       
   772         if cstrtype in UNIQUE_CONSTRAINTS:
       
   773             oldcstr = self.oldcstr = rdef.constraint_by_type(cstrtype)
       
   774         else:
       
   775             oldcstr = None
       
   776         newcstr = self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
       
   777         # in-place modification of in-memory schema first
       
   778         _set_modifiable_constraints(rdef)
       
   779         newcstr.eid = self.entity.eid
       
   780         if oldcstr is not None:
       
   781             rdef.constraints.remove(oldcstr)
       
   782         rdef.constraints.append(newcstr)
       
   783         # then update database: alter the physical schema on size/unique
       
   784         # constraint changes
       
   785         syssource = cnx.repo.system_source
       
   786         if cstrtype == 'SizeConstraint' and (oldcstr is None or
       
   787                                              oldcstr.max != newcstr.max):
       
   788             syssource.update_rdef_column(cnx, rdef)
       
   789             self.size_cstr_changed = True
       
   790         elif cstrtype == 'UniqueConstraint' and oldcstr is None:
       
   791             syssource.update_rdef_unique(cnx, rdef)
       
   792             self.unique_changed = True
       
   793         if cstrtype in ('BoundaryConstraint', 'IntervalBoundConstraint', 'StaticVocabularyConstraint'):
       
   794             if oldcstr is not None:
       
   795                 oldcstrname = 'cstr' + md5((rdef.subject.type + rdef.rtype.type + cstrtype +
       
   796                                             (self.oldcstr.serialize() or '')).encode('ascii')).hexdigest()
       
   797                 cnx.system_sql('ALTER TABLE %s%s DROP CONSTRAINT %s' %
       
   798                                (SQL_PREFIX, rdef.subject.type, oldcstrname))
       
   799             cstrname, check = y2sql.check_constraint(rdef.subject, rdef.object, rdef.rtype.type,
       
   800                     newcstr, syssource.dbhelper, prefix=SQL_PREFIX)
       
   801             cnx.system_sql('ALTER TABLE %s%s ADD CONSTRAINT %s CHECK(%s)' %
       
   802                            (SQL_PREFIX, rdef.subject.type, cstrname, check))
       
   803 
       
   804 
       
   805 class CWUniqueTogetherConstraintAddOp(MemSchemaOperation):
       
   806     entity = None # make pylint happy
       
   807 
       
   808     def precommit_event(self):
       
   809         cnx = self.cnx
       
   810         prefix = SQL_PREFIX
       
   811         entity = self.entity
       
   812         table = '%s%s' % (prefix, entity.constraint_of[0].name)
       
   813         cols = ['%s%s' % (prefix, r.name) for r in entity.relations]
       
   814         dbhelper = cnx.repo.system_source.dbhelper
       
   815         sqls = dbhelper.sqls_create_multicol_unique_index(table, cols, entity.name)
       
   816         for sql in sqls:
       
   817             cnx.system_sql(sql)
       
   818 
       
   819     def postcommit_event(self):
       
   820         entity = self.entity
       
   821         eschema = self.cnx.vreg.schema.schema_by_eid(entity.constraint_of[0].eid)
       
   822         attrs = [r.name for r in entity.relations]
       
   823         eschema._unique_together.append(attrs)
       
   824 
       
   825 
       
   826 class CWUniqueTogetherConstraintDelOp(MemSchemaOperation):
       
   827     entity = cstrname = None # for pylint
       
   828     cols = () # for pylint
       
   829 
       
   830     def insert_index(self):
       
   831         # We need to run before CWConstraintDelOp: if a size constraint is
       
   832         # removed and the column is part of a unique_together constraint, we
       
   833         # remove the unique_together index before changing the column's type.
       
   834         # SQL Server does not support unique indices on unlimited text columns.
       
   835         return 0
       
   836 
       
   837     def precommit_event(self):
       
   838         cnx = self.cnx
       
   839         prefix = SQL_PREFIX
       
   840         table = '%s%s' % (prefix, self.entity.type)
       
   841         dbhelper = cnx.repo.system_source.dbhelper
       
   842         cols = ['%s%s' % (prefix, c) for c in self.cols]
       
   843         sqls = dbhelper.sqls_drop_multicol_unique_index(table, cols, self.cstrname)
       
   844         for sql in sqls:
       
   845             cnx.system_sql(sql)
       
   846 
       
   847     def postcommit_event(self):
       
   848         eschema = self.cnx.vreg.schema.schema_by_eid(self.entity.eid)
       
   849         cols = set(self.cols)
       
   850         unique_together = [ut for ut in eschema._unique_together
       
   851                            if set(ut) != cols]
       
   852         eschema._unique_together = unique_together
       
   853 
       
   854 
       
   855 # operations for in-memory schema synchronization  #############################
       
   856 
       
   857 class MemSchemaCWETypeDel(MemSchemaOperation):
       
   858     """actually remove the entity type from the instance's schema"""
       
   859     etype = None # make pylint happy
       
   860 
       
   861     def postcommit_event(self):
       
   862         # del_entity_type also removes entity's relations
       
   863         self.cnx.vreg.schema.del_entity_type(self.etype)
       
   864 
       
   865 
       
   866 class MemSchemaCWRTypeAdd(MemSchemaOperation):
       
   867     """actually add the relation type to the instance's schema"""
       
   868     rtypedef = None # make pylint happy
       
   869 
       
   870     def precommit_event(self):
       
   871         self.cnx.vreg.schema.add_relation_type(self.rtypedef)
       
   872 
       
   873     def revertprecommit_event(self):
       
   874         self.cnx.vreg.schema.del_relation_type(self.rtypedef.name)
       
   875 
       
   876 
       
   877 class MemSchemaCWRTypeDel(MemSchemaOperation):
       
   878     """actually remove the relation type from the instance's schema"""
       
   879     rtype = None # make pylint happy
       
   880 
       
   881     def postcommit_event(self):
       
   882         try:
       
   883             self.cnx.vreg.schema.del_relation_type(self.rtype)
       
   884         except KeyError:
       
   885             # s/o entity type have already been deleted
       
   886             pass
       
   887 
       
   888 
       
   889 class MemSchemaPermissionAdd(MemSchemaOperation):
       
   890     """synchronize schema when a *_permission relation has been added on a group
       
   891     """
       
   892     eid = action = group_eid = expr = None # make pylint happy
       
   893 
       
   894     def precommit_event(self):
       
   895         """the observed connections.cnxset has been commited"""
       
   896         try:
       
   897             erschema = self.cnx.vreg.schema.schema_by_eid(self.eid)
       
   898         except KeyError:
       
   899             # duh, schema not found, log error and skip operation
       
   900             self.warning('no schema for %s', self.eid)
       
   901             return
       
   902         perms = list(erschema.action_permissions(self.action))
       
   903         if self.group_eid is not None:
       
   904             perm = self.cnx.entity_from_eid(self.group_eid).name
       
   905         else:
       
   906             perm = erschema.rql_expression(self.expr)
       
   907         try:
       
   908             perms.index(perm)
       
   909             self.warning('%s already in permissions for %s on %s',
       
   910                          perm, self.action, erschema)
       
   911         except ValueError:
       
   912             perms.append(perm)
       
   913             erschema.set_action_permissions(self.action, perms)
       
   914 
       
   915     # XXX revertprecommit_event
       
   916 
       
   917 
       
   918 class MemSchemaPermissionDel(MemSchemaPermissionAdd):
       
   919     """synchronize schema when a *_permission relation has been deleted from a
       
   920     group
       
   921     """
       
   922 
       
   923     def precommit_event(self):
       
   924         """the observed connections set has been commited"""
       
   925         try:
       
   926             erschema = self.cnx.vreg.schema.schema_by_eid(self.eid)
       
   927         except KeyError:
       
   928             # duh, schema not found, log error and skip operation
       
   929             self.warning('no schema for %s', self.eid)
       
   930             return
       
   931         perms = list(erschema.action_permissions(self.action))
       
   932         if self.group_eid is not None:
       
   933             perm = self.cnx.entity_from_eid(self.group_eid).name
       
   934         else:
       
   935             perm = erschema.rql_expression(self.expr)
       
   936         try:
       
   937             perms.remove(perm)
       
   938             erschema.set_action_permissions(self.action, perms)
       
   939         except ValueError:
       
   940             self.error('can\'t remove permission %s for %s on %s',
       
   941                        perm, self.action, erschema)
       
   942 
       
   943     # XXX revertprecommit_event
       
   944 
       
   945 
       
   946 class MemSchemaSpecializesAdd(MemSchemaOperation):
       
   947     etypeeid = parentetypeeid = None # make pylint happy
       
   948 
       
   949     def precommit_event(self):
       
   950         eschema = self.cnx.vreg.schema.schema_by_eid(self.etypeeid)
       
   951         parenteschema = self.cnx.vreg.schema.schema_by_eid(self.parentetypeeid)
       
   952         eschema._specialized_type = parenteschema.type
       
   953         parenteschema._specialized_by.append(eschema.type)
       
   954 
       
   955     # XXX revertprecommit_event
       
   956 
       
   957 
       
   958 class MemSchemaSpecializesDel(MemSchemaOperation):
       
   959     etypeeid = parentetypeeid = None # make pylint happy
       
   960 
       
   961     def precommit_event(self):
       
   962         try:
       
   963             eschema = self.cnx.vreg.schema.schema_by_eid(self.etypeeid)
       
   964             parenteschema = self.cnx.vreg.schema.schema_by_eid(self.parentetypeeid)
       
   965         except KeyError:
       
   966             # etype removed, nothing to do
       
   967             return
       
   968         eschema._specialized_type = None
       
   969         parenteschema._specialized_by.remove(eschema.type)
       
   970 
       
   971     # XXX revertprecommit_event
       
   972 
       
   973 
       
   974 # CWEType hooks ################################################################
       
   975 
       
   976 class DelCWETypeHook(SyncSchemaHook):
       
   977     """before deleting a CWEType entity:
       
   978     * check that we don't remove a core entity type
       
   979     * cascade to delete related CWAttribute and CWRelation entities
       
   980     * instantiate an operation to delete the entity type on commit
       
   981     """
       
   982     __regid__ = 'syncdelcwetype'
       
   983     __select__ = SyncSchemaHook.__select__ & is_instance('CWEType')
       
   984     events = ('before_delete_entity',)
       
   985 
       
   986     def __call__(self):
       
   987         # final entities can't be deleted, don't care about that
       
   988         name = self.entity.name
       
   989         if name in CORE_TYPES:
       
   990             raise validation_error(self.entity, {None: _("can't be deleted")})
       
   991         # delete every entities of this type
       
   992         if name not in ETYPE_NAME_MAP:
       
   993             MemSchemaCWETypeDel(self._cw, etype=name)
       
   994         DropTable(self._cw, table=SQL_PREFIX + name)
       
   995 
       
   996 
       
   997 class AfterDelCWETypeHook(DelCWETypeHook):
       
   998     __regid__ = 'wfcleanup'
       
   999     events = ('after_delete_entity',)
       
  1000 
       
  1001     def __call__(self):
       
  1002         # workflow cleanup
       
  1003         self._cw.execute('DELETE Workflow X WHERE NOT X workflow_of Y')
       
  1004 
       
  1005 
       
  1006 class AfterAddCWETypeHook(DelCWETypeHook):
       
  1007     """after adding a CWEType entity:
       
  1008     * create the necessary table
       
  1009     * set creation_date and modification_date by creating the necessary
       
  1010       CWAttribute entities
       
  1011     * add owned_by relation by creating the necessary CWRelation entity
       
  1012     * register an operation to add the entity type to the instance's
       
  1013       schema on commit
       
  1014     """
       
  1015     __regid__ = 'syncaddcwetype'
       
  1016     events = ('after_add_entity',)
       
  1017 
       
  1018     def __call__(self):
       
  1019         entity = self.entity
       
  1020         if entity.cw_edited.get('final'):
       
  1021             # final entity types don't need a table in the database and are
       
  1022             # systematically added by yams at schema initialization time so
       
  1023             # there is no need to do further processing. Simply assign its eid.
       
  1024             self._cw.vreg.schema[entity.name].eid = entity.eid
       
  1025             return
       
  1026         CWETypeAddOp(self._cw, entity=entity)
       
  1027 
       
  1028 
       
  1029 class BeforeUpdateCWETypeHook(DelCWETypeHook):
       
  1030     """check name change, handle final"""
       
  1031     __regid__ = 'syncupdatecwetype'
       
  1032     events = ('before_update_entity',)
       
  1033 
       
  1034     def __call__(self):
       
  1035         entity = self.entity
       
  1036         check_valid_changes(self._cw, entity, ro_attrs=('final',))
       
  1037         # don't use getattr(entity, attr), we would get the modified value if any
       
  1038         if 'name' in entity.cw_edited:
       
  1039             oldname, newname = entity.cw_edited.oldnewvalue('name')
       
  1040             if newname.lower() != oldname.lower():
       
  1041                 CWETypeRenameOp(self._cw, oldname=oldname, newname=newname)
       
  1042 
       
  1043 
       
  1044 # CWRType hooks ################################################################
       
  1045 
       
  1046 class DelCWRTypeHook(SyncSchemaHook):
       
  1047     """before deleting a CWRType entity:
       
  1048     * check that we don't remove a core relation type
       
  1049     * cascade to delete related CWAttribute and CWRelation entities
       
  1050     * instantiate an operation to delete the relation type on commit
       
  1051     """
       
  1052     __regid__ = 'syncdelcwrtype'
       
  1053     __select__ = SyncSchemaHook.__select__ & is_instance('CWRType')
       
  1054     events = ('before_delete_entity',)
       
  1055 
       
  1056     def __call__(self):
       
  1057         name = self.entity.name
       
  1058         if name in CORE_TYPES:
       
  1059             raise validation_error(self.entity, {None: _("can't be deleted")})
       
  1060         # delete relation definitions using this relation type
       
  1061         self._cw.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
       
  1062                         {'x': self.entity.eid})
       
  1063         self._cw.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s',
       
  1064                         {'x': self.entity.eid})
       
  1065         MemSchemaCWRTypeDel(self._cw, rtype=name)
       
  1066 
       
  1067 
       
  1068 class AfterAddCWComputedRTypeHook(SyncSchemaHook):
       
  1069     """after a CWComputedRType entity has been added:
       
  1070     * register an operation to add the relation type to the instance's
       
  1071       schema on commit
       
  1072 
       
  1073     We don't know yet this point if a table is necessary
       
  1074     """
       
  1075     __regid__ = 'syncaddcwcomputedrtype'
       
  1076     __select__ = SyncSchemaHook.__select__ & is_instance('CWComputedRType')
       
  1077     events = ('after_add_entity',)
       
  1078 
       
  1079     def __call__(self):
       
  1080         entity = self.entity
       
  1081         rtypedef = ybo.ComputedRelation(name=entity.name,
       
  1082                                         eid=entity.eid,
       
  1083                                         rule=entity.rule)
       
  1084         MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef)
       
  1085 
       
  1086 
       
  1087 class AfterAddCWRTypeHook(SyncSchemaHook):
       
  1088     """after a CWRType entity has been added:
       
  1089     * register an operation to add the relation type to the instance's
       
  1090       schema on commit
       
  1091 
       
  1092     We don't know yet this point if a table is necessary
       
  1093     """
       
  1094     __regid__ = 'syncaddcwrtype'
       
  1095     __select__ = SyncSchemaHook.__select__ & is_instance('CWRType')
       
  1096     events = ('after_add_entity',)
       
  1097 
       
  1098     def __call__(self):
       
  1099         entity = self.entity
       
  1100         rtypedef = ybo.RelationType(name=entity.name,
       
  1101                                     description=entity.description,
       
  1102                                     inlined=entity.cw_edited.get('inlined', False),
       
  1103                                     symmetric=entity.cw_edited.get('symmetric', False),
       
  1104                                     eid=entity.eid)
       
  1105         MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef)
       
  1106 
       
  1107 
       
  1108 class BeforeUpdateCWRTypeHook(SyncSchemaHook):
       
  1109     """check name change, handle final"""
       
  1110     __regid__ = 'syncupdatecwrtype'
       
  1111     __select__ = SyncSchemaHook.__select__ & is_instance('CWRType')
       
  1112     events = ('before_update_entity',)
       
  1113 
       
  1114     def __call__(self):
       
  1115         entity = self.entity
       
  1116         check_valid_changes(self._cw, entity)
       
  1117         newvalues = {}
       
  1118         for prop in ('symmetric', 'inlined', 'fulltext_container'):
       
  1119             if prop in entity.cw_edited:
       
  1120                 old, new = entity.cw_edited.oldnewvalue(prop)
       
  1121                 if old != new:
       
  1122                     newvalues[prop] = new
       
  1123         if newvalues:
       
  1124             rschema = self._cw.vreg.schema.rschema(entity.name)
       
  1125             CWRTypeUpdateOp(self._cw, rschema=rschema, entity=entity,
       
  1126                             values=newvalues)
       
  1127 
       
  1128 
       
  1129 class BeforeUpdateCWComputedRTypeHook(SyncSchemaHook):
       
  1130     """check name change, handle final"""
       
  1131     __regid__ = 'syncupdatecwcomputedrtype'
       
  1132     __select__ = SyncSchemaHook.__select__ & is_instance('CWComputedRType')
       
  1133     events = ('before_update_entity',)
       
  1134 
       
  1135     def __call__(self):
       
  1136         entity = self.entity
       
  1137         check_valid_changes(self._cw, entity)
       
  1138         if 'rule' in entity.cw_edited:
       
  1139             old, new = entity.cw_edited.oldnewvalue('rule')
       
  1140             if old != new:
       
  1141                 rschema = self._cw.vreg.schema.rschema(entity.name)
       
  1142                 CWComputedRTypeUpdateOp(self._cw, rschema=rschema,
       
  1143                                         entity=entity, rule=new)
       
  1144 
       
  1145 
       
  1146 class AfterDelRelationTypeHook(SyncSchemaHook):
       
  1147     """before deleting a CWAttribute or CWRelation entity:
       
  1148     * if this is a final or inlined relation definition, instantiate an
       
  1149       operation to drop necessary column, else if this is the last instance
       
  1150       of a non final relation, instantiate an operation to drop necessary
       
  1151       table
       
  1152     * instantiate an operation to delete the relation definition on commit
       
  1153     * delete the associated relation type when necessary
       
  1154     """
       
  1155     __regid__ = 'syncdelrelationtype'
       
  1156     __select__ = SyncSchemaHook.__select__ & hook.match_rtype('relation_type')
       
  1157     events = ('after_delete_relation',)
       
  1158 
       
  1159     def __call__(self):
       
  1160         cnx = self._cw
       
  1161         try:
       
  1162             rdef = cnx.vreg.schema.schema_by_eid(self.eidfrom)
       
  1163         except KeyError:
       
  1164             self.critical('cant get schema rdef associated to %s', self.eidfrom)
       
  1165             return
       
  1166         subjschema, rschema, objschema = rdef.as_triple()
       
  1167         pendingrdefs = cnx.transaction_data.setdefault('pendingrdefs', set())
       
  1168         # first delete existing relation if necessary
       
  1169         if rschema.final:
       
  1170             rdeftype = 'CWAttribute'
       
  1171             pendingrdefs.add((subjschema, rschema))
       
  1172         else:
       
  1173             rdeftype = 'CWRelation'
       
  1174             pendingrdefs.add((subjschema, rschema, objschema))
       
  1175         RDefDelOp(cnx, rdef=rdef)
       
  1176 
       
  1177 
       
  1178 # CWComputedRType hooks #######################################################
       
  1179 
       
  1180 class DelCWComputedRTypeHook(SyncSchemaHook):
       
  1181     """before deleting a CWComputedRType entity:
       
  1182     * check that we don't remove a core relation type
       
  1183     * instantiate an operation to delete the relation type on commit
       
  1184     """
       
  1185     __regid__ = 'syncdelcwcomputedrtype'
       
  1186     __select__ = SyncSchemaHook.__select__ & is_instance('CWComputedRType')
       
  1187     events = ('before_delete_entity',)
       
  1188 
       
  1189     def __call__(self):
       
  1190         name = self.entity.name
       
  1191         if name in CORE_TYPES:
       
  1192             raise validation_error(self.entity, {None: _("can't be deleted")})
       
  1193         MemSchemaCWRTypeDel(self._cw, rtype=name)
       
  1194 
       
  1195 
       
  1196 # CWAttribute / CWRelation hooks ###############################################
       
  1197 
       
  1198 class AfterAddCWAttributeHook(SyncSchemaHook):
       
  1199     __regid__ = 'syncaddcwattribute'
       
  1200     __select__ = SyncSchemaHook.__select__ & is_instance('CWAttribute')
       
  1201     events = ('after_add_entity',)
       
  1202 
       
  1203     def __call__(self):
       
  1204         CWAttributeAddOp(self._cw, entity=self.entity)
       
  1205 
       
  1206 
       
  1207 class AfterAddCWRelationHook(AfterAddCWAttributeHook):
       
  1208     __regid__ = 'syncaddcwrelation'
       
  1209     __select__ = SyncSchemaHook.__select__ & is_instance('CWRelation')
       
  1210 
       
  1211     def __call__(self):
       
  1212         CWRelationAddOp(self._cw, entity=self.entity)
       
  1213 
       
  1214 
       
  1215 class AfterUpdateCWRDefHook(SyncSchemaHook):
       
  1216     __regid__ = 'syncaddcwattribute'
       
  1217     __select__ = SyncSchemaHook.__select__ & is_instance('CWAttribute',
       
  1218                                                          'CWRelation')
       
  1219     events = ('before_update_entity',)
       
  1220 
       
  1221     def __call__(self):
       
  1222         entity = self.entity
       
  1223         if self._cw.deleted_in_transaction(entity.eid):
       
  1224             return
       
  1225         subjtype = entity.stype.name
       
  1226         objtype = entity.otype.name
       
  1227         if subjtype in ETYPE_NAME_MAP or objtype in ETYPE_NAME_MAP:
       
  1228             return
       
  1229         rschema = self._cw.vreg.schema[entity.rtype.name]
       
  1230         # note: do not access schema rdef here, it may be added later by an
       
  1231         # operation
       
  1232         newvalues = {}
       
  1233         for prop in RelationDefinitionSchema.rproperty_defs(objtype):
       
  1234             if prop == 'constraints':
       
  1235                 continue
       
  1236             if prop == 'order':
       
  1237                 attr = 'ordernum'
       
  1238             else:
       
  1239                 attr = prop
       
  1240             if attr in entity.cw_edited:
       
  1241                 old, new = entity.cw_edited.oldnewvalue(attr)
       
  1242                 if old != new:
       
  1243                     newvalues[prop] = new
       
  1244         if newvalues:
       
  1245             RDefUpdateOp(self._cw, rschema=rschema, rdefkey=(subjtype, objtype),
       
  1246                          values=newvalues)
       
  1247 
       
  1248 
       
  1249 # constraints synchronization hooks ############################################
       
  1250 
       
  1251 class AfterAddCWConstraintHook(SyncSchemaHook):
       
  1252     __regid__ = 'syncaddcwconstraint'
       
  1253     __select__ = SyncSchemaHook.__select__ & is_instance('CWConstraint')
       
  1254     events = ('after_add_entity', 'after_update_entity')
       
  1255 
       
  1256     def __call__(self):
       
  1257         if self.entity.cstrtype[0].name == 'SizeConstraint':
       
  1258             txdata = self._cw.transaction_data
       
  1259             if 'newsizecstr' not in txdata:
       
  1260                 txdata['newsizecstr'] = set()
       
  1261             txdata['newsizecstr'].add(self.entity)
       
  1262         CWConstraintAddOp(self._cw, entity=self.entity)
       
  1263 
       
  1264 
       
  1265 class AfterAddConstrainedByHook(SyncSchemaHook):
       
  1266     __regid__ = 'syncaddconstrainedby'
       
  1267     __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constrained_by')
       
  1268     events = ('after_add_relation',)
       
  1269 
       
  1270     def __call__(self):
       
  1271         if self._cw.added_in_transaction(self.eidfrom):
       
  1272             # used by get_constraints() which is called in CWAttributeAddOp
       
  1273             self._cw.transaction_data.setdefault(self.eidfrom, []).append(self.eidto)
       
  1274 
       
  1275 
       
  1276 class BeforeDeleteCWConstraintHook(SyncSchemaHook):
       
  1277     __regid__ = 'syncdelcwconstraint'
       
  1278     __select__ = SyncSchemaHook.__select__ & is_instance('CWConstraint')
       
  1279     events = ('before_delete_entity',)
       
  1280 
       
  1281     def __call__(self):
       
  1282         entity = self.entity
       
  1283         schema = self._cw.vreg.schema
       
  1284         try:
       
  1285             # KeyError, e.g. composite chain deletion
       
  1286             rdef = schema.schema_by_eid(entity.reverse_constrained_by[0].eid)
       
  1287             # IndexError
       
  1288             cstr = rdef.constraint_by_eid(entity.eid)
       
  1289         except (KeyError, IndexError):
       
  1290             self._cw.critical('constraint type no more accessible')
       
  1291         else:
       
  1292             CWConstraintDelOp(self._cw, rdef=rdef, oldcstr=cstr)
       
  1293 
       
  1294 # unique_together constraints
       
  1295 # XXX: use setoperations and before_add_relation here (on constraint_of and relations)
       
  1296 class AfterAddCWUniqueTogetherConstraintHook(SyncSchemaHook):
       
  1297     __regid__ = 'syncadd_cwuniquetogether_constraint'
       
  1298     __select__ = SyncSchemaHook.__select__ & is_instance('CWUniqueTogetherConstraint')
       
  1299     events = ('after_add_entity',)
       
  1300 
       
  1301     def __call__(self):
       
  1302         CWUniqueTogetherConstraintAddOp(self._cw, entity=self.entity)
       
  1303 
       
  1304 
       
  1305 class BeforeDeleteConstraintOfHook(SyncSchemaHook):
       
  1306     __regid__ = 'syncdelconstraintof'
       
  1307     __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constraint_of')
       
  1308     events = ('before_delete_relation',)
       
  1309 
       
  1310     def __call__(self):
       
  1311         if self._cw.deleted_in_transaction(self.eidto):
       
  1312             return
       
  1313         schema = self._cw.vreg.schema
       
  1314         cstr = self._cw.entity_from_eid(self.eidfrom)
       
  1315         entity = schema.schema_by_eid(self.eidto)
       
  1316         cols = tuple(r.name for r in cstr.relations)
       
  1317         CWUniqueTogetherConstraintDelOp(self._cw, entity=entity,
       
  1318                                         cstrname=cstr.name, cols=cols)
       
  1319 
       
  1320 
       
  1321 # permissions synchronization hooks ############################################
       
  1322 
       
  1323 class AfterAddPermissionHook(SyncSchemaHook):
       
  1324     """added entity/relation *_permission, need to update schema"""
       
  1325     __regid__ = 'syncaddperm'
       
  1326     __select__ = SyncSchemaHook.__select__ & hook.match_rtype(
       
  1327         'read_permission', 'add_permission', 'delete_permission',
       
  1328         'update_permission')
       
  1329     events = ('after_add_relation',)
       
  1330 
       
  1331     def __call__(self):
       
  1332         action = self.rtype.split('_', 1)[0]
       
  1333         if self._cw.entity_metas(self.eidto)['type'] == 'CWGroup':
       
  1334             MemSchemaPermissionAdd(self._cw, action=action, eid=self.eidfrom,
       
  1335                                    group_eid=self.eidto)
       
  1336         else: # RQLExpression
       
  1337             expr = self._cw.entity_from_eid(self.eidto).expression
       
  1338             MemSchemaPermissionAdd(self._cw, action=action, eid=self.eidfrom,
       
  1339                                    expr=expr)
       
  1340 
       
  1341 
       
  1342 class BeforeDelPermissionHook(AfterAddPermissionHook):
       
  1343     """delete entity/relation *_permission, need to update schema
       
  1344 
       
  1345     skip the operation if the related type is being deleted
       
  1346     """
       
  1347     __regid__ = 'syncdelperm'
       
  1348     events = ('before_delete_relation',)
       
  1349 
       
  1350     def __call__(self):
       
  1351         if self._cw.deleted_in_transaction(self.eidfrom):
       
  1352             return
       
  1353         action = self.rtype.split('_', 1)[0]
       
  1354         if self._cw.entity_metas(self.eidto)['type'] == 'CWGroup':
       
  1355             MemSchemaPermissionDel(self._cw, action=action, eid=self.eidfrom,
       
  1356                                    group_eid=self.eidto)
       
  1357         else: # RQLExpression
       
  1358             expr = self._cw.entity_from_eid(self.eidto).expression
       
  1359             MemSchemaPermissionDel(self._cw, action=action, eid=self.eidfrom,
       
  1360                                    expr=expr)
       
  1361 
       
  1362 
       
  1363 
       
  1364 class UpdateFTIndexOp(hook.DataOperationMixIn, hook.SingleLastOperation):
       
  1365     """operation to update full text indexation of entity whose schema change
       
  1366 
       
  1367     We wait after the commit to as the schema in memory is only updated after
       
  1368     the commit.
       
  1369     """
       
  1370     containercls = list
       
  1371 
       
  1372     def postcommit_event(self):
       
  1373         cnx = self.cnx
       
  1374         source = cnx.repo.system_source
       
  1375         schema = cnx.repo.vreg.schema
       
  1376         to_reindex = self.get_data()
       
  1377         self.info('%i etypes need full text indexed reindexation',
       
  1378                   len(to_reindex))
       
  1379         for etype in to_reindex:
       
  1380             rset = cnx.execute('Any X WHERE X is %s' % etype)
       
  1381             self.info('Reindexing full text index for %i entity of type %s',
       
  1382                       len(rset), etype)
       
  1383             still_fti = list(schema[etype].indexable_attributes())
       
  1384             for entity in rset.entities():
       
  1385                 source.fti_unindex_entities(cnx, [entity])
       
  1386                 for container in entity.cw_adapt_to('IFTIndexable').fti_containers():
       
  1387                     if still_fti or container is not entity:
       
  1388                         source.fti_unindex_entities(cnx, [container])
       
  1389                         source.fti_index_entities(cnx, [container])
       
  1390         if to_reindex:
       
  1391             # Transaction has already been committed
       
  1392             cnx.cnxset.commit()
       
  1393 
       
  1394 
       
  1395 
       
  1396 
       
  1397 # specializes synchronization hooks ############################################
       
  1398 
       
  1399 
       
  1400 class AfterAddSpecializesHook(SyncSchemaHook):
       
  1401     __regid__ = 'syncaddspecializes'
       
  1402     __select__ = SyncSchemaHook.__select__ & hook.match_rtype('specializes')
       
  1403     events = ('after_add_relation',)
       
  1404 
       
  1405     def __call__(self):
       
  1406         MemSchemaSpecializesAdd(self._cw, etypeeid=self.eidfrom,
       
  1407                                 parentetypeeid=self.eidto)
       
  1408 
       
  1409 
       
  1410 class AfterDelSpecializesHook(SyncSchemaHook):
       
  1411     __regid__ = 'syncdelspecializes'
       
  1412     __select__ = SyncSchemaHook.__select__ & hook.match_rtype('specializes')
       
  1413     events = ('after_delete_relation',)
       
  1414 
       
  1415     def __call__(self):
       
  1416         MemSchemaSpecializesDel(self._cw, etypeeid=self.eidfrom,
       
  1417                                 parentetypeeid=self.eidto)