|
1 """schema hooks: |
|
2 |
|
3 - synchronize the living schema object with the persistent schema |
|
4 - perform physical update on the source when necessary |
|
5 |
|
6 checking for schema consistency is done in hooks.py |
|
7 |
|
8 :organization: Logilab |
|
9 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
10 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
11 """ |
|
12 __docformat__ = "restructuredtext en" |
|
13 |
|
14 from yams.schema import BASE_TYPES |
|
15 from yams.buildobjs import EntityType, RelationType, RelationDefinition |
|
16 from yams.schema2sql import eschema2sql, rschema2sql, _type_from_constraints |
|
17 |
|
18 from cubicweb import ValidationError, RepositoryError |
|
19 from cubicweb.server import schemaserial as ss |
|
20 from cubicweb.server.pool import Operation, SingleLastOperation, PreCommitOperation |
|
21 from cubicweb.server.hookhelper import (entity_attr, entity_name, |
|
22 check_internal_entity) |
|
23 |
|
24 # core entity and relation types which can't be removed |
|
25 CORE_ETYPES = list(BASE_TYPES) + ['EEType', 'ERType', 'EUser', 'EGroup', |
|
26 'EConstraint', 'EFRDef', 'ENFRDef'] |
|
27 CORE_RTYPES = ['eid', 'creation_date', 'modification_date', |
|
28 'login', 'upassword', 'name', |
|
29 'is', 'instanceof', 'owned_by', 'created_by', 'in_group', |
|
30 'relation_type', 'from_entity', 'to_entity', |
|
31 'constrainted_by', |
|
32 'read_permission', 'add_permission', |
|
33 'delete_permission', 'updated_permission', |
|
34 ] |
|
35 |
|
36 def get_constraints(session, entity): |
|
37 constraints = [] |
|
38 for cstreid in session.query_data(entity.eid, ()): |
|
39 cstrent = session.entity(cstreid) |
|
40 cstr = CONSTRAINTS[cstrent.type].deserialize(cstrent.value) |
|
41 cstr.eid = cstreid |
|
42 constraints.append(cstr) |
|
43 return constraints |
|
44 |
|
45 def add_inline_relation_column(session, etype, rtype): |
|
46 """add necessary column and index for an inlined relation""" |
|
47 try: |
|
48 session.system_sql(str('ALTER TABLE %s ADD COLUMN %s integer' |
|
49 % (etype, rtype))) |
|
50 session.info('added column %s to table %s', rtype, etype) |
|
51 except: |
|
52 # silent exception here, if this error has not been raised because the |
|
53 # column already exists, index creation will fail anyway |
|
54 session.exception('error while adding column %s to table %s', etype, rtype) |
|
55 # create index before alter table which may expectingly fail during test |
|
56 # (sqlite) while index creation should never fail (test for index existence |
|
57 # is done by the dbhelper) |
|
58 session.pool.source('system').create_index(session, etype, rtype) |
|
59 session.info('added index on %s(%s)', etype, rtype) |
|
60 session.add_query_data('createdattrs', '%s.%s' % (etype, rtype)) |
|
61 |
|
62 |
|
63 class SchemaOperation(Operation): |
|
64 """base class for schema operations""" |
|
65 def __init__(self, session, kobj=None, **kwargs): |
|
66 self.schema = session.repo.schema |
|
67 self.kobj = kobj |
|
68 # once Operation.__init__ has been called, event may be triggered, so |
|
69 # do this last ! |
|
70 Operation.__init__(self, session, **kwargs) |
|
71 # every schema operation is triggering a schema update |
|
72 UpdateSchemaOp(session) |
|
73 |
|
74 class EarlySchemaOperation(SchemaOperation): |
|
75 def insert_index(self): |
|
76 """schema operation which are inserted at the begining of the queue |
|
77 (typically to add/remove entity or relation types) |
|
78 """ |
|
79 i = -1 |
|
80 for i, op in enumerate(self.session.pending_operations): |
|
81 if not isinstance(op, EarlySchemaOperation): |
|
82 return i |
|
83 return i + 1 |
|
84 |
|
85 class UpdateSchemaOp(SingleLastOperation): |
|
86 """the update schema operation: |
|
87 |
|
88 special operation which should be called once and after all other schema |
|
89 operations. It will trigger internal structures rebuilding to consider |
|
90 schema changes |
|
91 """ |
|
92 |
|
93 def __init__(self, session): |
|
94 self.repo = session.repo |
|
95 SingleLastOperation.__init__(self, session) |
|
96 |
|
97 def commit_event(self): |
|
98 self.repo.set_schema(self.repo.schema) |
|
99 |
|
100 |
|
101 class DropTableOp(PreCommitOperation): |
|
102 """actually remove a database from the application's schema""" |
|
103 def precommit_event(self): |
|
104 dropped = self.session.query_data('droppedtables', |
|
105 default=set(), setdefault=True) |
|
106 if self.table in dropped: |
|
107 return # already processed |
|
108 dropped.add(self.table) |
|
109 self.session.system_sql('DROP TABLE %s' % self.table) |
|
110 self.info('dropped table %s', self.table) |
|
111 |
|
112 class DropColumnOp(PreCommitOperation): |
|
113 """actually remove the attribut's column from entity table in the system |
|
114 database |
|
115 """ |
|
116 def precommit_event(self): |
|
117 session, table, column = self.session, self.table, self.column |
|
118 # drop index if any |
|
119 session.pool.source('system').drop_index(session, table, column) |
|
120 try: |
|
121 session.system_sql('ALTER TABLE %s DROP COLUMN %s' |
|
122 % (table, column)) |
|
123 self.info('dropped column %s from table %s', column, table) |
|
124 except Exception, ex: |
|
125 # not supported by sqlite for instance |
|
126 self.error('error while altering table %s: %s', table, ex) |
|
127 |
|
128 |
|
129 # deletion #################################################################### |
|
130 |
|
131 class DeleteEETypeOp(SchemaOperation): |
|
132 """actually remove the entity type from the application's schema""" |
|
133 def commit_event(self): |
|
134 try: |
|
135 # del_entity_type also removes entity's relations |
|
136 self.schema.del_entity_type(self.kobj) |
|
137 except KeyError: |
|
138 # s/o entity type have already been deleted |
|
139 pass |
|
140 |
|
141 def before_del_eetype(session, eid): |
|
142 """before deleting a EEType entity: |
|
143 * check that we don't remove a core entity type |
|
144 * cascade to delete related EFRDef and ENFRDef entities |
|
145 * instantiate an operation to delete the entity type on commit |
|
146 """ |
|
147 # final entities can't be deleted, don't care about that |
|
148 name = check_internal_entity(session, eid, CORE_ETYPES) |
|
149 # delete every entities of this type |
|
150 session.unsafe_execute('DELETE %s X' % name) |
|
151 DropTableOp(session, table=name) |
|
152 DeleteEETypeOp(session, name) |
|
153 |
|
154 def after_del_eetype(session, eid): |
|
155 # workflow cleanup |
|
156 session.execute('DELETE State X WHERE NOT X state_of Y') |
|
157 session.execute('DELETE Transition X WHERE NOT X transition_of Y') |
|
158 |
|
159 |
|
160 class DeleteERTypeOp(SchemaOperation): |
|
161 """actually remove the relation type from the application's schema""" |
|
162 def commit_event(self): |
|
163 try: |
|
164 self.schema.del_relation_type(self.kobj) |
|
165 except KeyError: |
|
166 # s/o entity type have already been deleted |
|
167 pass |
|
168 |
|
169 def before_del_ertype(session, eid): |
|
170 """before deleting a ERType entity: |
|
171 * check that we don't remove a core relation type |
|
172 * cascade to delete related EFRDef and ENFRDef entities |
|
173 * instantiate an operation to delete the relation type on commit |
|
174 """ |
|
175 name = check_internal_entity(session, eid, CORE_RTYPES) |
|
176 # delete relation definitions using this relation type |
|
177 session.execute('DELETE EFRDef X WHERE X relation_type Y, Y eid %(x)s', |
|
178 {'x': eid}) |
|
179 session.execute('DELETE ENFRDef X WHERE X relation_type Y, Y eid %(x)s', |
|
180 {'x': eid}) |
|
181 DeleteERTypeOp(session, name) |
|
182 |
|
183 |
|
184 class DelErdefOp(SchemaOperation): |
|
185 """actually remove the relation definition from the application's schema""" |
|
186 def commit_event(self): |
|
187 subjtype, rtype, objtype = self.kobj |
|
188 try: |
|
189 self.schema.del_relation_def(subjtype, rtype, objtype) |
|
190 except KeyError: |
|
191 # relation type may have been already deleted |
|
192 pass |
|
193 |
|
194 def after_del_relation_type(session, rdefeid, rtype, rteid): |
|
195 """before deleting a EFRDef or ENFRDef entity: |
|
196 * if this is a final or inlined relation definition, instantiate an |
|
197 operation to drop necessary column, else if this is the last instance |
|
198 of a non final relation, instantiate an operation to drop necessary |
|
199 table |
|
200 * instantiate an operation to delete the relation definition on commit |
|
201 * delete the associated relation type when necessary |
|
202 """ |
|
203 subjschema, rschema, objschema = session.repo.schema.schema_by_eid(rdefeid) |
|
204 pendings = session.query_data('pendingeids', ()) |
|
205 # first delete existing relation if necessary |
|
206 if rschema.is_final(): |
|
207 rdeftype = 'EFRDef' |
|
208 else: |
|
209 rdeftype = 'ENFRDef' |
|
210 if not (subjschema.eid in pendings or objschema.eid in pendings): |
|
211 session.execute('DELETE X %s Y WHERE X is %s, Y is %s' |
|
212 % (rschema, subjschema, objschema)) |
|
213 execute = session.unsafe_execute |
|
214 rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,' |
|
215 'R eid %%(x)s' % rdeftype, {'x': rteid}) |
|
216 lastrel = rset[0][0] == 0 |
|
217 # we have to update physical schema systematically for final and inlined |
|
218 # relations, but only if it's the last instance for this relation type |
|
219 # for other relations |
|
220 |
|
221 if (rschema.is_final() or rschema.inlined): |
|
222 rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, ' |
|
223 'R eid %%(x)s, X from_entity E, E name %%(name)s' |
|
224 % rdeftype, {'x': rteid, 'name': str(subjschema)}) |
|
225 if rset[0][0] == 0 and not subjschema.eid in pendings: |
|
226 DropColumnOp(session, table=subjschema.type, column=rschema.type) |
|
227 elif lastrel: |
|
228 DropTableOp(session, table='%s_relation' % rschema.type) |
|
229 # if this is the last instance, drop associated relation type |
|
230 if lastrel and not rteid in pendings: |
|
231 execute('DELETE ERType X WHERE X eid %(x)s', {'x': rteid}, 'x') |
|
232 DelErdefOp(session, (subjschema, rschema, objschema)) |
|
233 |
|
234 |
|
235 # addition #################################################################### |
|
236 |
|
237 class AddEETypeOp(EarlySchemaOperation): |
|
238 """actually add the entity type to the application's schema""" |
|
239 def commit_event(self): |
|
240 eschema = self.schema.add_entity_type(self.kobj) |
|
241 eschema.eid = self.eid |
|
242 |
|
243 def before_add_eetype(session, entity): |
|
244 """before adding a EEType entity: |
|
245 * check that we are not using an existing entity type, |
|
246 """ |
|
247 name = entity['name'] |
|
248 schema = session.repo.schema |
|
249 if name in schema and schema[name].eid is not None: |
|
250 raise RepositoryError('an entity type %s already exists' % name) |
|
251 |
|
252 def after_add_eetype(session, entity): |
|
253 """after adding a EEType entity: |
|
254 * create the necessary table |
|
255 * set creation_date and modification_date by creating the necessary |
|
256 EFRDef entities |
|
257 * add owned_by relation by creating the necessary ENFRDef entity |
|
258 * register an operation to add the entity type to the application's |
|
259 schema on commit |
|
260 """ |
|
261 if entity.get('final'): |
|
262 return |
|
263 schema = session.repo.schema |
|
264 name = entity['name'] |
|
265 etype = EntityType(name=name, description=entity.get('description'), |
|
266 meta=entity.get('meta')) # don't care about final |
|
267 # fake we add it to the schema now to get a correctly initialized schema |
|
268 # but remove it before doing anything more dangerous... |
|
269 schema = session.repo.schema |
|
270 eschema = schema.add_entity_type(etype) |
|
271 eschema.set_default_groups() |
|
272 # generate table sql and rql to add metadata |
|
273 tablesql = eschema2sql(session.pool.source('system').dbhelper, eschema) |
|
274 relrqls = [] |
|
275 for rtype in ('is', 'is_instance_of', 'creation_date', 'modification_date', |
|
276 'created_by', 'owned_by'): |
|
277 rschema = schema[rtype] |
|
278 sampletype = rschema.subjects()[0] |
|
279 desttype = rschema.objects()[0] |
|
280 props = rschema.rproperties(sampletype, desttype) |
|
281 relrqls += list(ss.rdef2rql(rschema, name, desttype, props)) |
|
282 # now remove it ! |
|
283 schema.del_entity_type(name) |
|
284 # create the necessary table |
|
285 for sql in tablesql.split(';'): |
|
286 if sql.strip(): |
|
287 session.system_sql(sql) |
|
288 # register operation to modify the schema on commit |
|
289 # this have to be done before adding other relations definitions |
|
290 # or permission settings |
|
291 AddEETypeOp(session, etype, eid=entity.eid) |
|
292 # add meta creation_date, modification_date and owned_by relations |
|
293 for rql, kwargs in relrqls: |
|
294 session.execute(rql, kwargs) |
|
295 |
|
296 |
|
297 class AddERTypeOp(EarlySchemaOperation): |
|
298 """actually add the relation type to the application's schema""" |
|
299 def commit_event(self): |
|
300 rschema = self.schema.add_relation_type(self.kobj) |
|
301 rschema.set_default_groups() |
|
302 rschema.eid = self.eid |
|
303 |
|
304 def before_add_ertype(session, entity): |
|
305 """before adding a ERType entity: |
|
306 * check that we are not using an existing relation type, |
|
307 * register an operation to add the relation type to the application's |
|
308 schema on commit |
|
309 |
|
310 We don't know yeat this point if a table is necessary |
|
311 """ |
|
312 name = entity['name'] |
|
313 if name in session.repo.schema.relations(): |
|
314 raise RepositoryError('a relation type %s already exists' % name) |
|
315 |
|
316 def after_add_ertype(session, entity): |
|
317 """after a ERType entity has been added: |
|
318 * register an operation to add the relation type to the application's |
|
319 schema on commit |
|
320 We don't know yeat this point if a table is necessary |
|
321 """ |
|
322 AddERTypeOp(session, RelationType(name=entity['name'], |
|
323 description=entity.get('description'), |
|
324 meta=entity.get('meta', False), |
|
325 inlined=entity.get('inlined', False), |
|
326 symetric=entity.get('symetric', False)), |
|
327 eid=entity.eid) |
|
328 |
|
329 |
|
330 class AddErdefOp(EarlySchemaOperation): |
|
331 """actually add the attribute relation definition to the application's |
|
332 schema |
|
333 """ |
|
334 def commit_event(self): |
|
335 self.schema.add_relation_def(self.kobj) |
|
336 |
|
337 TYPE_CONVERTER = { |
|
338 'Boolean': bool, |
|
339 'Int': int, |
|
340 'Float': float, |
|
341 'Password': str, |
|
342 'String': unicode, |
|
343 'Date' : unicode, |
|
344 'Datetime' : unicode, |
|
345 'Time' : unicode, |
|
346 } |
|
347 |
|
348 |
|
349 class AddEFRDefPreCommitOp(PreCommitOperation): |
|
350 """an attribute relation (EFRDef) has been added: |
|
351 * add the necessary column |
|
352 * set default on this column if any and possible |
|
353 * register an operation to add the relation definition to the |
|
354 application's schema on commit |
|
355 |
|
356 constraints are handled by specific hooks |
|
357 """ |
|
358 def precommit_event(self): |
|
359 session = self.session |
|
360 entity = self.entity |
|
361 fromentity = entity.from_entity[0] |
|
362 relationtype = entity.relation_type[0] |
|
363 session.execute('SET X ordernum Y+1 WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, X ordernum >= %(order)s, NOT X eid %(x)s', |
|
364 {'x': entity.eid, 'se': fromentity.eid, 'order': entity.ordernum or 0}) |
|
365 subj, rtype = str(fromentity.name), str(relationtype.name) |
|
366 obj = str(entity.to_entity[0].name) |
|
367 # at this point default is a string or None, but we need a correctly |
|
368 # typed value |
|
369 default = entity.defaultval |
|
370 if default is not None: |
|
371 default = TYPE_CONVERTER[obj](default) |
|
372 constraints = get_constraints(session, entity) |
|
373 rdef = RelationDefinition(subj, rtype, obj, |
|
374 cardinality=entity.cardinality, |
|
375 order=entity.ordernum, |
|
376 description=entity.description, |
|
377 default=default, |
|
378 indexed=entity.indexed, |
|
379 fulltextindexed=entity.fulltextindexed, |
|
380 internationalizable=entity.internationalizable, |
|
381 constraints=constraints, |
|
382 eid=entity.eid) |
|
383 sysource = session.pool.source('system') |
|
384 attrtype = _type_from_constraints(sysource.dbhelper, rdef.object, |
|
385 constraints) |
|
386 # XXX should be moved somehow into lgc.adbh: sqlite doesn't support to |
|
387 # add a new column with UNIQUE, it should be added after the ALTER TABLE |
|
388 # using ADD INDEX |
|
389 if sysource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype: |
|
390 extra_unique_index = True |
|
391 attrtype = attrtype.replace(' UNIQUE', '') |
|
392 else: |
|
393 extra_unique_index = False |
|
394 # added some str() wrapping query since some backend (eg psycopg) don't |
|
395 # allow unicode queries |
|
396 try: |
|
397 session.system_sql(str('ALTER TABLE %s ADD COLUMN %s %s' |
|
398 % (subj, rtype, attrtype))) |
|
399 self.info('added column %s to table %s', rtype, subj) |
|
400 except Exception, ex: |
|
401 # the column probably already exists. this occurs when |
|
402 # the entity's type has just been added or if the column |
|
403 # has not been previously dropped |
|
404 self.error('error while altering table %s: %s', subj, ex) |
|
405 if extra_unique_index or entity.indexed: |
|
406 try: |
|
407 sysource.create_index(session, subj, rtype, |
|
408 unique=extra_unique_index) |
|
409 except Exception, ex: |
|
410 self.error('error while creating index for %s.%s: %s', |
|
411 subj, rtype, ex) |
|
412 # postgres doesn't implement, so do it in two times |
|
413 # ALTER TABLE %s ADD COLUMN %s %s SET DEFAULT %s |
|
414 if default is not None: |
|
415 if isinstance(default, unicode): |
|
416 default = default.encode(sysource.encoding) |
|
417 try: |
|
418 session.system_sql('ALTER TABLE %s ALTER COLUMN %s SET DEFAULT ' |
|
419 '%%(default)s' % (subj, rtype), |
|
420 {'default': default}) |
|
421 except Exception, ex: |
|
422 # not supported by sqlite for instance |
|
423 self.error('error while altering table %s: %s', subj, ex) |
|
424 session.system_sql('UPDATE %s SET %s=%%(default)s' % (subj, rtype), |
|
425 {'default': default}) |
|
426 AddErdefOp(session, rdef) |
|
427 |
|
428 def after_add_efrdef(session, entity): |
|
429 AddEFRDefPreCommitOp(session, entity=entity) |
|
430 |
|
431 |
|
432 class AddENFRDefPreCommitOp(PreCommitOperation): |
|
433 """an actual relation has been added: |
|
434 * if this is an inlined relation, add the necessary column |
|
435 else if it's the first instance of this relation type, add the |
|
436 necessary table and set default permissions |
|
437 * register an operation to add the relation definition to the |
|
438 application's schema on commit |
|
439 |
|
440 constraints are handled by specific hooks |
|
441 """ |
|
442 def precommit_event(self): |
|
443 session = self.session |
|
444 entity = self.entity |
|
445 fromentity = entity.from_entity[0] |
|
446 relationtype = entity.relation_type[0] |
|
447 session.execute('SET X ordernum Y+1 WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, X ordernum >= %(order)s, NOT X eid %(x)s', |
|
448 {'x': entity.eid, 'se': fromentity.eid, 'order': entity.ordernum or 0}) |
|
449 subj, rtype = str(fromentity.name), str(relationtype.name) |
|
450 obj = str(entity.to_entity[0].name) |
|
451 card = entity.get('cardinality') |
|
452 rdef = RelationDefinition(subj, rtype, obj, |
|
453 cardinality=card, |
|
454 order=entity.ordernum, |
|
455 composite=entity.composite, |
|
456 description=entity.description, |
|
457 constraints=get_constraints(session, entity), |
|
458 eid=entity.eid) |
|
459 schema = session.repo.schema |
|
460 rschema = schema.rschema(rtype) |
|
461 # this have to be done before permissions setting |
|
462 AddErdefOp(session, rdef) |
|
463 if rschema.inlined: |
|
464 # need to add a column if the relation is inlined and if this is the |
|
465 # first occurence of "Subject relation Something" whatever Something |
|
466 # and if it has not been added during other event of the same |
|
467 # transaction |
|
468 key = '%s.%s' % (subj, rtype) |
|
469 try: |
|
470 alreadythere = bool(rschema.objects(subj)) |
|
471 except KeyError: |
|
472 alreadythere = False |
|
473 if not (alreadythere or |
|
474 key in session.query_data('createdattrs', ())): |
|
475 add_inline_relation_column(session, subj, rtype) |
|
476 else: |
|
477 # need to create the relation if no relation definition in the |
|
478 # schema and if it has not been added during other event of the same |
|
479 # transaction |
|
480 if not (rschema.subjects() or |
|
481 rtype in session.query_data('createdtables', ())): |
|
482 try: |
|
483 rschema = schema[rtype] |
|
484 tablesql = rschema2sql(rschema) |
|
485 except KeyError: |
|
486 # fake we add it to the schema now to get a correctly |
|
487 # initialized schema but remove it before doing anything |
|
488 # more dangerous... |
|
489 rschema = schema.add_relation_type(rdef) |
|
490 tablesql = rschema2sql(rschema) |
|
491 schema.del_relation_type(rtype) |
|
492 # create the necessary table |
|
493 for sql in tablesql.split(';'): |
|
494 if sql.strip(): |
|
495 self.session.system_sql(sql) |
|
496 session.add_query_data('createdtables', rtype) |
|
497 |
|
498 def after_add_enfrdef(session, entity): |
|
499 AddENFRDefPreCommitOp(session, entity=entity) |
|
500 |
|
501 |
|
502 # update ###################################################################### |
|
503 |
|
504 def check_valid_changes(session, entity, ro_attrs=('name', 'final')): |
|
505 errors = {} |
|
506 # don't use getattr(entity, attr), we would get the modified value if any |
|
507 for attr in ro_attrs: |
|
508 origval = entity_attr(session, entity.eid, attr) |
|
509 if entity.get(attr, origval) != origval: |
|
510 errors[attr] = session._("can't change the %s attribute") % \ |
|
511 display_name(session, attr) |
|
512 if errors: |
|
513 raise ValidationError(entity.eid, errors) |
|
514 |
|
515 def before_update_eetype(session, entity): |
|
516 """check name change, handle final""" |
|
517 check_valid_changes(session, entity, ro_attrs=('final',)) |
|
518 # don't use getattr(entity, attr), we would get the modified value if any |
|
519 oldname = entity_attr(session, entity.eid, 'name') |
|
520 newname = entity.get('name', oldname) |
|
521 if newname.lower() != oldname.lower(): |
|
522 eschema = session.repo.schema[oldname] |
|
523 UpdateEntityTypeName(session, eschema=eschema, |
|
524 oldname=oldname, newname=newname) |
|
525 |
|
526 def before_update_ertype(session, entity): |
|
527 """check name change, handle final""" |
|
528 check_valid_changes(session, entity) |
|
529 |
|
530 |
|
531 class UpdateEntityTypeName(SchemaOperation): |
|
532 """this operation updates physical storage accordingly""" |
|
533 |
|
534 def precommit_event(self): |
|
535 # we need sql to operate physical changes on the system database |
|
536 sqlexec = self.session.system_sql |
|
537 sqlexec('ALTER TABLE %s RENAME TO %s' % (self.oldname, self.newname)) |
|
538 self.info('renamed table %s to %s', self.oldname, self.newname) |
|
539 sqlexec('UPDATE entities SET type=%s WHERE type=%s', |
|
540 (self.newname, self.oldname)) |
|
541 sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s', |
|
542 (self.newname, self.oldname)) |
|
543 |
|
544 def commit_event(self): |
|
545 self.session.repo.schema.rename_entity_type(self.oldname, self.newname) |
|
546 |
|
547 |
|
548 class UpdateRdefOp(SchemaOperation): |
|
549 """actually update some properties of a relation definition""" |
|
550 |
|
551 def precommit_event(self): |
|
552 if 'indexed' in self.values: |
|
553 sysource = self.session.pool.source('system') |
|
554 table, column = self.kobj[0], self.rschema.type |
|
555 if self.values['indexed']: |
|
556 sysource.create_index(self.session, table, column) |
|
557 else: |
|
558 sysource.drop_index(self.session, table, column) |
|
559 |
|
560 def commit_event(self): |
|
561 # structure should be clean, not need to remove entity's relations |
|
562 # at this point |
|
563 self.rschema._rproperties[self.kobj].update(self.values) |
|
564 |
|
565 def after_update_erdef(session, entity): |
|
566 desttype = entity.to_entity[0].name |
|
567 rschema = session.repo.schema[entity.relation_type[0].name] |
|
568 newvalues = {} |
|
569 for prop in rschema.rproperty_defs(desttype): |
|
570 if prop == 'constraints': |
|
571 continue |
|
572 if prop == 'order': |
|
573 prop = 'ordernum' |
|
574 if prop in entity: |
|
575 newvalues[prop] = entity[prop] |
|
576 if newvalues: |
|
577 subjtype = entity.from_entity[0].name |
|
578 UpdateRdefOp(session, (subjtype, desttype), rschema=rschema, |
|
579 values=newvalues) |
|
580 |
|
581 |
|
582 class UpdateRtypeOp(SchemaOperation): |
|
583 """actually update some properties of a relation definition""" |
|
584 def precommit_event(self): |
|
585 session = self.session |
|
586 rschema = self.rschema |
|
587 if rschema.is_final() or not 'inlined' in self.values: |
|
588 return # nothing to do |
|
589 inlined = self.values['inlined'] |
|
590 entity = self.entity |
|
591 if not entity.inlined_changed(inlined): # check in-lining is necessary/possible |
|
592 return # nothing to do |
|
593 # inlined changed, make necessary physical changes! |
|
594 sqlexec = self.session.system_sql |
|
595 rtype = rschema.type |
|
596 if not inlined: |
|
597 # need to create the relation if it has not been already done by another |
|
598 # event of the same transaction |
|
599 if not rschema.type in session.query_data('createdtables', ()): |
|
600 tablesql = rschema2sql(rschema) |
|
601 # create the necessary table |
|
602 for sql in tablesql.split(';'): |
|
603 if sql.strip(): |
|
604 sqlexec(sql) |
|
605 session.add_query_data('createdtables', rschema.type) |
|
606 # copy existant data |
|
607 for etype in rschema.subjects(): |
|
608 sqlexec('INSERT INTO %s_relation SELECT eid, %s FROM %s WHERE NOT %s IS NULL' |
|
609 % (rtype, rtype, etype, rtype)) |
|
610 # drop existant columns |
|
611 for etype in rschema.subjects(): |
|
612 DropColumnOp(session, table=str(etype), column=rtype) |
|
613 else: |
|
614 for etype in rschema.subjects(): |
|
615 try: |
|
616 add_inline_relation_column(session, str(etype), rtype) |
|
617 except Exception, ex: |
|
618 # the column probably already exists. this occurs when |
|
619 # the entity's type has just been added or if the column |
|
620 # has not been previously dropped |
|
621 self.error('error while altering table %s: %s', etype, ex) |
|
622 # copy existant data. |
|
623 # XXX don't use, it's not supported by sqlite (at least at when i tried it) |
|
624 #sqlexec('UPDATE %(etype)s SET %(rtype)s=eid_to ' |
|
625 # 'FROM %(rtype)s_relation ' |
|
626 # 'WHERE %(etype)s.eid=%(rtype)s_relation.eid_from' |
|
627 # % locals()) |
|
628 cursor = sqlexec('SELECT eid_from, eid_to FROM %(etype)s, ' |
|
629 '%(rtype)s_relation WHERE %(etype)s.eid=' |
|
630 '%(rtype)s_relation.eid_from' % locals()) |
|
631 args = [{'val': eid_to, 'x': eid} for eid, eid_to in cursor.fetchall()] |
|
632 if args: |
|
633 cursor.executemany('UPDATE %s SET %s=%%(val)s WHERE eid=%%(x)s' |
|
634 % (etype, rtype), args) |
|
635 # drop existant table |
|
636 DropTableOp(session, table='%s_relation' % rtype) |
|
637 |
|
638 def commit_event(self): |
|
639 # structure should be clean, not need to remove entity's relations |
|
640 # at this point |
|
641 self.rschema.__dict__.update(self.values) |
|
642 |
|
643 def after_update_ertype(session, entity): |
|
644 rschema = session.repo.schema.rschema(entity.name) |
|
645 newvalues = {} |
|
646 for prop in ('meta', 'symetric', 'inlined'): |
|
647 if prop in entity: |
|
648 newvalues[prop] = entity[prop] |
|
649 if newvalues: |
|
650 UpdateRtypeOp(session, entity=entity, rschema=rschema, values=newvalues) |
|
651 |
|
652 # constraints synchronization ################################################# |
|
653 |
|
654 from cubicweb.schema import CONSTRAINTS |
|
655 |
|
656 class ConstraintOp(SchemaOperation): |
|
657 """actually update constraint of a relation definition""" |
|
658 def prepare_constraints(self, rtype, subjtype, objtype): |
|
659 constraints = rtype.rproperty(subjtype, objtype, 'constraints') |
|
660 self.constraints = list(constraints) |
|
661 rtype.set_rproperty(subjtype, objtype, 'constraints', self.constraints) |
|
662 return self.constraints |
|
663 |
|
664 def precommit_event(self): |
|
665 rdef = self.entity.reverse_constrained_by[0] |
|
666 session = self.session |
|
667 # when the relation is added in the same transaction, the constraint object |
|
668 # is created by AddEN?FRDefPreCommitOp, there is nothing to do here |
|
669 if rdef.eid in session.query_data('neweids', ()): |
|
670 self.cancelled = True |
|
671 return |
|
672 self.cancelled = False |
|
673 schema = session.repo.schema |
|
674 subjtype, rtype, objtype = schema.schema_by_eid(rdef.eid) |
|
675 self.prepare_constraints(rtype, subjtype, objtype) |
|
676 cstrtype = self.entity.type |
|
677 self.cstr = rtype.constraint_by_type(subjtype, objtype, cstrtype) |
|
678 self._cstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value) |
|
679 self._cstr.eid = self.entity.eid |
|
680 # alter the physical schema on size constraint changes |
|
681 if self._cstr.type() == 'SizeConstraint' and ( |
|
682 self.cstr is None or self.cstr.max != self._cstr.max): |
|
683 try: |
|
684 session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE VARCHAR(%s)' |
|
685 % (subjtype, rtype, self._cstr.max)) |
|
686 self.info('altered column %s of table %s: now VARCHAR(%s)', |
|
687 rtype, subjtype, self._cstr.max) |
|
688 except Exception, ex: |
|
689 # not supported by sqlite for instance |
|
690 self.error('error while altering table %s: %s', subjtype, ex) |
|
691 elif cstrtype == 'UniqueConstraint': |
|
692 session.pool.source('system').create_index( |
|
693 self.session, str(subjtype), str(rtype), unique=True) |
|
694 |
|
695 def commit_event(self): |
|
696 if self.cancelled: |
|
697 return |
|
698 # in-place removing |
|
699 if not self.cstr is None: |
|
700 self.constraints.remove(self.cstr) |
|
701 self.constraints.append(self._cstr) |
|
702 |
|
703 def after_add_econstraint(session, entity): |
|
704 ConstraintOp(session, entity=entity) |
|
705 |
|
706 def after_update_econstraint(session, entity): |
|
707 ConstraintOp(session, entity=entity) |
|
708 |
|
709 class DelConstraintOp(ConstraintOp): |
|
710 """actually remove a constraint of a relation definition""" |
|
711 |
|
712 def precommit_event(self): |
|
713 self.prepare_constraints(self.rtype, self.subjtype, self.objtype) |
|
714 cstrtype = self.cstr.type() |
|
715 # alter the physical schema on size/unique constraint changes |
|
716 if cstrtype == 'SizeConstraint': |
|
717 try: |
|
718 self.session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE TEXT' |
|
719 % (self.subjtype, self.rtype)) |
|
720 self.info('altered column %s of table %s: now TEXT', |
|
721 self.rtype, self.subjtype) |
|
722 except Exception, ex: |
|
723 # not supported by sqlite for instance |
|
724 self.error('error while altering table %s: %s', |
|
725 self.subjtype, ex) |
|
726 elif cstrtype == 'UniqueConstraint': |
|
727 self.session.pool.source('system').drop_index( |
|
728 self.session, str(self.subjtype), str(self.rtype), unique=True) |
|
729 |
|
730 def commit_event(self): |
|
731 self.constraints.remove(self.cstr) |
|
732 |
|
733 |
|
734 def before_delete_constrained_by(session, fromeid, rtype, toeid): |
|
735 if not fromeid in session.query_data('pendingeids', ()): |
|
736 schema = session.repo.schema |
|
737 entity = session.eid_rset(toeid).get_entity(0, 0) |
|
738 subjtype, rtype, objtype = schema.schema_by_eid(fromeid) |
|
739 try: |
|
740 cstr = rtype.constraint_by_type(subjtype, objtype, entity.cstrtype[0].name) |
|
741 DelConstraintOp(session, subjtype=subjtype, rtype=rtype, objtype=objtype, |
|
742 cstr=cstr) |
|
743 except IndexError: |
|
744 session.critical('constraint type no more accessible') |
|
745 |
|
746 |
|
747 def after_add_constrained_by(session, fromeid, rtype, toeid): |
|
748 if fromeid in session.query_data('neweids', ()): |
|
749 session.add_query_data(fromeid, toeid) |
|
750 |
|
751 |
|
752 # schema permissions synchronization ########################################## |
|
753 |
|
754 class PermissionOp(Operation): |
|
755 """base class to synchronize schema permission definitions""" |
|
756 def __init__(self, session, perm, etype_eid): |
|
757 self.perm = perm |
|
758 try: |
|
759 self.name = entity_name(session, etype_eid) |
|
760 except IndexError: |
|
761 self.error('changing permission of a no more existant type #%s', |
|
762 etype_eid) |
|
763 else: |
|
764 Operation.__init__(self, session) |
|
765 |
|
766 class AddGroupPermissionOp(PermissionOp): |
|
767 """synchronize schema when a *_permission relation has been added on a group |
|
768 """ |
|
769 def __init__(self, session, perm, etype_eid, group_eid): |
|
770 self.group = entity_name(session, group_eid) |
|
771 PermissionOp.__init__(self, session, perm, etype_eid) |
|
772 |
|
773 def commit_event(self): |
|
774 """the observed connections pool has been commited""" |
|
775 try: |
|
776 erschema = self.schema[self.name] |
|
777 except KeyError: |
|
778 # duh, schema not found, log error and skip operation |
|
779 self.error('no schema for %s', self.name) |
|
780 return |
|
781 groups = list(erschema.get_groups(self.perm)) |
|
782 try: |
|
783 groups.index(self.group) |
|
784 self.warning('group %s already have permission %s on %s', |
|
785 self.group, self.perm, erschema.type) |
|
786 except ValueError: |
|
787 groups.append(self.group) |
|
788 erschema.set_groups(self.perm, groups) |
|
789 |
|
790 class AddRQLExpressionPermissionOp(PermissionOp): |
|
791 """synchronize schema when a *_permission relation has been added on a rql |
|
792 expression |
|
793 """ |
|
794 def __init__(self, session, perm, etype_eid, expression): |
|
795 self.expr = expression |
|
796 PermissionOp.__init__(self, session, perm, etype_eid) |
|
797 |
|
798 def commit_event(self): |
|
799 """the observed connections pool has been commited""" |
|
800 try: |
|
801 erschema = self.schema[self.name] |
|
802 except KeyError: |
|
803 # duh, schema not found, log error and skip operation |
|
804 self.error('no schema for %s', self.name) |
|
805 return |
|
806 exprs = list(erschema.get_rqlexprs(self.perm)) |
|
807 exprs.append(erschema.rql_expression(self.expr)) |
|
808 erschema.set_rqlexprs(self.perm, exprs) |
|
809 |
|
810 def after_add_permission(session, subject, rtype, object): |
|
811 """added entity/relation *_permission, need to update schema""" |
|
812 perm = rtype.split('_', 1)[0] |
|
813 if session.describe(object)[0] == 'EGroup': |
|
814 AddGroupPermissionOp(session, perm, subject, object) |
|
815 else: # RQLExpression |
|
816 expr = session.execute('Any EXPR WHERE X eid %(x)s, X expression EXPR', |
|
817 {'x': object}, 'x')[0][0] |
|
818 AddRQLExpressionPermissionOp(session, perm, subject, expr) |
|
819 |
|
820 |
|
821 |
|
822 class DelGroupPermissionOp(AddGroupPermissionOp): |
|
823 """synchronize schema when a *_permission relation has been deleted from a group""" |
|
824 |
|
825 def commit_event(self): |
|
826 """the observed connections pool has been commited""" |
|
827 try: |
|
828 erschema = self.schema[self.name] |
|
829 except KeyError: |
|
830 # duh, schema not found, log error and skip operation |
|
831 self.error('no schema for %s', self.name) |
|
832 return |
|
833 groups = list(erschema.get_groups(self.perm)) |
|
834 try: |
|
835 groups.remove(self.group) |
|
836 erschema.set_groups(self.perm, groups) |
|
837 except ValueError: |
|
838 self.error('can\'t remove permission %s on %s to group %s', |
|
839 self.perm, erschema.type, self.group) |
|
840 |
|
841 |
|
842 class DelRQLExpressionPermissionOp(AddRQLExpressionPermissionOp): |
|
843 """synchronize schema when a *_permission relation has been deleted from an rql expression""" |
|
844 |
|
845 def commit_event(self): |
|
846 """the observed connections pool has been commited""" |
|
847 try: |
|
848 erschema = self.schema[self.name] |
|
849 except KeyError: |
|
850 # duh, schema not found, log error and skip operation |
|
851 self.error('no schema for %s', self.name) |
|
852 return |
|
853 rqlexprs = list(erschema.get_rqlexprs(self.perm)) |
|
854 for i, rqlexpr in enumerate(rqlexprs): |
|
855 if rqlexpr.expression == self.expr: |
|
856 rqlexprs.pop(i) |
|
857 break |
|
858 else: |
|
859 self.error('can\'t remove permission %s on %s for expression %s', |
|
860 self.perm, erschema.type, self.expr) |
|
861 return |
|
862 erschema.set_rqlexprs(self.perm, rqlexprs) |
|
863 |
|
864 |
|
865 def before_del_permission(session, subject, rtype, object): |
|
866 """delete entity/relation *_permission, need to update schema |
|
867 |
|
868 skip the operation if the related type is being deleted |
|
869 """ |
|
870 if subject in session.query_data('pendingeids', ()): |
|
871 return |
|
872 perm = rtype.split('_', 1)[0] |
|
873 if session.describe(object)[0] == 'EGroup': |
|
874 DelGroupPermissionOp(session, perm, subject, object) |
|
875 else: # RQLExpression |
|
876 expr = session.execute('Any EXPR WHERE X eid %(x)s, X expression EXPR', |
|
877 {'x': object}, 'x')[0][0] |
|
878 DelRQLExpressionPermissionOp(session, perm, subject, expr) |
|
879 |
|
880 |
|
881 def rebuild_infered_relations(session, subject, rtype, object): |
|
882 # registering a schema operation will trigger a call to |
|
883 # repo.set_schema() on commit which will in turn rebuild |
|
884 # infered relation definitions |
|
885 UpdateSchemaOp(session) |
|
886 |
|
887 |
|
888 def _register_schema_hooks(hm): |
|
889 """register schema related hooks on the hooks manager""" |
|
890 # schema synchronisation ##################### |
|
891 # before/after add |
|
892 hm.register_hook(before_add_eetype, 'before_add_entity', 'EEType') |
|
893 hm.register_hook(before_add_ertype, 'before_add_entity', 'ERType') |
|
894 hm.register_hook(after_add_eetype, 'after_add_entity', 'EEType') |
|
895 hm.register_hook(after_add_ertype, 'after_add_entity', 'ERType') |
|
896 hm.register_hook(after_add_efrdef, 'after_add_entity', 'EFRDef') |
|
897 hm.register_hook(after_add_enfrdef, 'after_add_entity', 'ENFRDef') |
|
898 # before/after update |
|
899 hm.register_hook(before_update_eetype, 'before_update_entity', 'EEType') |
|
900 hm.register_hook(before_update_ertype, 'before_update_entity', 'ERType') |
|
901 hm.register_hook(after_update_ertype, 'after_update_entity', 'ERType') |
|
902 hm.register_hook(after_update_erdef, 'after_update_entity', 'EFRDef') |
|
903 hm.register_hook(after_update_erdef, 'after_update_entity', 'ENFRDef') |
|
904 # before/after delete |
|
905 hm.register_hook(before_del_eetype, 'before_delete_entity', 'EEType') |
|
906 hm.register_hook(after_del_eetype, 'after_delete_entity', 'EEType') |
|
907 hm.register_hook(before_del_ertype, 'before_delete_entity', 'ERType') |
|
908 hm.register_hook(after_del_relation_type, 'after_delete_relation', 'relation_type') |
|
909 hm.register_hook(rebuild_infered_relations, 'after_add_relation', 'specializes') |
|
910 hm.register_hook(rebuild_infered_relations, 'after_delete_relation', 'specializes') |
|
911 # constraints synchronization hooks |
|
912 hm.register_hook(after_add_econstraint, 'after_add_entity', 'EConstraint') |
|
913 hm.register_hook(after_update_econstraint, 'after_update_entity', 'EConstraint') |
|
914 hm.register_hook(before_delete_constrained_by, 'before_delete_relation', 'constrained_by') |
|
915 hm.register_hook(after_add_constrained_by, 'after_add_relation', 'constrained_by') |
|
916 # permissions synchronisation ################ |
|
917 for perm in ('read_permission', 'add_permission', |
|
918 'delete_permission', 'update_permission'): |
|
919 hm.register_hook(after_add_permission, 'after_add_relation', perm) |
|
920 hm.register_hook(before_del_permission, 'before_delete_relation', perm) |