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) |
|