4 - perform physical update on the source when necessary |
4 - perform physical update on the source when necessary |
5 |
5 |
6 checking for schema consistency is done in hooks.py |
6 checking for schema consistency is done in hooks.py |
7 |
7 |
8 :organization: Logilab |
8 :organization: Logilab |
9 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
9 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
10 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
10 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
11 """ |
11 """ |
12 __docformat__ = "restructuredtext en" |
12 __docformat__ = "restructuredtext en" |
13 |
13 |
14 from yams.schema import BASE_TYPES |
14 from yams.schema import BASE_TYPES |
15 from yams.buildobjs import EntityType, RelationType, RelationDefinition |
15 from yams.buildobjs import EntityType, RelationType, RelationDefinition |
16 from yams.schema2sql import eschema2sql, rschema2sql, _type_from_constraints |
16 from yams.schema2sql import eschema2sql, rschema2sql, _type_from_constraints |
17 |
17 |
18 from cubicweb import ValidationError, RepositoryError |
18 from cubicweb import ValidationError, RepositoryError |
19 from cubicweb.server import schemaserial as ss |
19 from cubicweb.server import schemaserial as ss |
|
20 from cubicweb.server.sqlutils import SQL_PREFIX |
20 from cubicweb.server.pool import Operation, SingleLastOperation, PreCommitOperation |
21 from cubicweb.server.pool import Operation, SingleLastOperation, PreCommitOperation |
21 from cubicweb.server.hookhelper import (entity_attr, entity_name, |
22 from cubicweb.server.hookhelper import (entity_attr, entity_name, |
22 check_internal_entity) |
23 check_internal_entity) |
23 |
24 |
24 # core entity and relation types which can't be removed |
25 # core entity and relation types which can't be removed |
42 constraints.append(cstr) |
43 constraints.append(cstr) |
43 return constraints |
44 return constraints |
44 |
45 |
45 def add_inline_relation_column(session, etype, rtype): |
46 def add_inline_relation_column(session, etype, rtype): |
46 """add necessary column and index for an inlined relation""" |
47 """add necessary column and index for an inlined relation""" |
|
48 table = SQL_PREFIX + etype |
|
49 column = SQL_PREFIX + rtype |
47 try: |
50 try: |
48 session.system_sql(str('ALTER TABLE %s ADD COLUMN %s integer' |
51 session.system_sql(str('ALTER TABLE %s ADD COLUMN %s integer' |
49 % (etype, rtype))) |
52 % (table, column))) |
50 session.info('added column %s to table %s', rtype, etype) |
53 session.info('added column %s to table %s', column, table) |
51 except: |
54 except: |
52 # silent exception here, if this error has not been raised because the |
55 # silent exception here, if this error has not been raised because the |
53 # column already exists, index creation will fail anyway |
56 # column already exists, index creation will fail anyway |
54 session.exception('error while adding column %s to table %s', etype, rtype) |
57 session.exception('error while adding column %s to table %s', |
|
58 table, column) |
55 # create index before alter table which may expectingly fail during test |
59 # create index before alter table which may expectingly fail during test |
56 # (sqlite) while index creation should never fail (test for index existence |
60 # (sqlite) while index creation should never fail (test for index existence |
57 # is done by the dbhelper) |
61 # is done by the dbhelper) |
58 session.pool.source('system').create_index(session, etype, rtype) |
62 session.pool.source('system').create_index(session, table, column) |
59 session.info('added index on %s(%s)', etype, rtype) |
63 session.info('added index on %s(%s)', table, column) |
60 session.add_query_data('createdattrs', '%s.%s' % (etype, rtype)) |
64 session.add_query_data('createdattrs', '%s.%s' % (etype, rtype)) |
61 |
65 |
62 |
66 |
63 class SchemaOperation(Operation): |
67 class SchemaOperation(Operation): |
64 """base class for schema operations""" |
68 """base class for schema operations""" |
223 if (rschema.is_final() or rschema.inlined): |
227 if (rschema.is_final() or rschema.inlined): |
224 rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, ' |
228 rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, ' |
225 'R eid %%(x)s, X from_entity E, E name %%(name)s' |
229 'R eid %%(x)s, X from_entity E, E name %%(name)s' |
226 % rdeftype, {'x': rteid, 'name': str(subjschema)}) |
230 % rdeftype, {'x': rteid, 'name': str(subjschema)}) |
227 if rset[0][0] == 0 and not subjschema.eid in pendings: |
231 if rset[0][0] == 0 and not subjschema.eid in pendings: |
228 DropColumnOp(session, table=subjschema.type, column=rschema.type) |
232 DropColumnOp(session, table=SQL_PREFIX + subjschema.type, |
|
233 column=SQL_PREFIX + rschema.type) |
229 elif lastrel: |
234 elif lastrel: |
230 DropTableOp(session, table='%s_relation' % rschema.type) |
235 DropTableOp(session, table='%s_relation' % rschema.type) |
231 # if this is the last instance, drop associated relation type |
236 # if this is the last instance, drop associated relation type |
232 if lastrel and not rteid in pendings: |
237 if lastrel and not rteid in pendings: |
233 execute('DELETE ERType X WHERE X eid %(x)s', {'x': rteid}, 'x') |
238 execute('DELETE ERType X WHERE X eid %(x)s', {'x': rteid}, 'x') |
271 # but remove it before doing anything more dangerous... |
276 # but remove it before doing anything more dangerous... |
272 schema = session.repo.schema |
277 schema = session.repo.schema |
273 eschema = schema.add_entity_type(etype) |
278 eschema = schema.add_entity_type(etype) |
274 eschema.set_default_groups() |
279 eschema.set_default_groups() |
275 # generate table sql and rql to add metadata |
280 # generate table sql and rql to add metadata |
276 tablesql = eschema2sql(session.pool.source('system').dbhelper, eschema) |
281 tablesql = eschema2sql(session.pool.source('system').dbhelper, eschema, |
|
282 prefix=SQL_PREFIX) |
277 relrqls = [] |
283 relrqls = [] |
278 for rtype in ('is', 'is_instance_of', 'creation_date', 'modification_date', |
284 for rtype in ('is', 'is_instance_of', 'creation_date', 'modification_date', |
279 'created_by', 'owned_by'): |
285 'created_by', 'owned_by'): |
280 rschema = schema[rtype] |
286 rschema = schema[rtype] |
281 sampletype = rschema.subjects()[0] |
287 sampletype = rschema.subjects()[0] |
396 attrtype = attrtype.replace(' UNIQUE', '') |
402 attrtype = attrtype.replace(' UNIQUE', '') |
397 else: |
403 else: |
398 extra_unique_index = False |
404 extra_unique_index = False |
399 # added some str() wrapping query since some backend (eg psycopg) don't |
405 # added some str() wrapping query since some backend (eg psycopg) don't |
400 # allow unicode queries |
406 # allow unicode queries |
|
407 table = SQL_PREFIX + subj |
|
408 column = SQL_PREFIX + rtype |
401 try: |
409 try: |
402 session.system_sql(str('ALTER TABLE %s ADD COLUMN %s %s' |
410 session.system_sql(str('ALTER TABLE %s ADD COLUMN %s %s' |
403 % (subj, rtype, attrtype))) |
411 % (table, column, attrtype))) |
404 self.info('added column %s to table %s', rtype, subj) |
412 self.info('added column %s to table %s', table, column) |
405 except Exception, ex: |
413 except Exception, ex: |
406 # the column probably already exists. this occurs when |
414 # the column probably already exists. this occurs when |
407 # the entity's type has just been added or if the column |
415 # the entity's type has just been added or if the column |
408 # has not been previously dropped |
416 # has not been previously dropped |
409 self.error('error while altering table %s: %s', subj, ex) |
417 self.error('error while altering table %s: %s', table, ex) |
410 if extra_unique_index or entity.indexed: |
418 if extra_unique_index or entity.indexed: |
411 try: |
419 try: |
412 sysource.create_index(session, subj, rtype, |
420 sysource.create_index(session, table, column, |
413 unique=extra_unique_index) |
421 unique=extra_unique_index) |
414 except Exception, ex: |
422 except Exception, ex: |
415 self.error('error while creating index for %s.%s: %s', |
423 self.error('error while creating index for %s.%s: %s', |
416 subj, rtype, ex) |
424 table, column, ex) |
417 # postgres doesn't implement, so do it in two times |
425 # postgres doesn't implement, so do it in two times |
418 # ALTER TABLE %s ADD COLUMN %s %s SET DEFAULT %s |
426 # ALTER TABLE %s ADD COLUMN %s %s SET DEFAULT %s |
419 if default is not None: |
427 if default is not None: |
420 if isinstance(default, unicode): |
428 if isinstance(default, unicode): |
421 default = default.encode(sysource.encoding) |
429 default = default.encode(sysource.encoding) |
422 try: |
430 try: |
423 session.system_sql('ALTER TABLE %s ALTER COLUMN %s SET DEFAULT ' |
431 session.system_sql('ALTER TABLE %s ALTER COLUMN %s SET DEFAULT ' |
424 '%%(default)s' % (subj, rtype), |
432 '%%(default)s' % (table, column), |
425 {'default': default}) |
433 {'default': default}) |
426 except Exception, ex: |
434 except Exception, ex: |
427 # not supported by sqlite for instance |
435 # not supported by sqlite for instance |
428 self.error('error while altering table %s: %s', subj, ex) |
436 self.error('error while altering table %s: %s', table, ex) |
429 session.system_sql('UPDATE %s SET %s=%%(default)s' % (subj, rtype), |
437 session.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column), |
430 {'default': default}) |
438 {'default': default}) |
431 AddErdefOp(session, rdef) |
439 AddErdefOp(session, rdef) |
432 |
440 |
433 def after_add_efrdef(session, entity): |
441 def after_add_efrdef(session, entity): |
434 AddEFRDefPreCommitOp(session, entity=entity) |
442 AddEFRDefPreCommitOp(session, entity=entity) |
539 oldname = newname = None # make pylint happy |
547 oldname = newname = None # make pylint happy |
540 |
548 |
541 def precommit_event(self): |
549 def precommit_event(self): |
542 # we need sql to operate physical changes on the system database |
550 # we need sql to operate physical changes on the system database |
543 sqlexec = self.session.system_sql |
551 sqlexec = self.session.system_sql |
544 sqlexec('ALTER TABLE %s RENAME TO %s' % (self.oldname, self.newname)) |
552 sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (SQL_PREFIX, self.oldname, |
|
553 SQL_PREFIX, self.newname)) |
545 self.info('renamed table %s to %s', self.oldname, self.newname) |
554 self.info('renamed table %s to %s', self.oldname, self.newname) |
546 sqlexec('UPDATE entities SET type=%s WHERE type=%s', |
555 sqlexec('UPDATE entities SET type=%s WHERE type=%s', |
547 (self.newname, self.oldname)) |
556 (self.newname, self.oldname)) |
548 sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s', |
557 sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s', |
549 (self.newname, self.oldname)) |
558 (self.newname, self.oldname)) |
557 rschema = values = None # make pylint happy |
566 rschema = values = None # make pylint happy |
558 |
567 |
559 def precommit_event(self): |
568 def precommit_event(self): |
560 if 'indexed' in self.values: |
569 if 'indexed' in self.values: |
561 sysource = self.session.pool.source('system') |
570 sysource = self.session.pool.source('system') |
562 table, column = self.kobj[0], self.rschema.type |
571 etype, rtype = self.kobj[0], self.rschema.type |
|
572 table = SQL_PREFIX + etype |
|
573 column = SQL_PREFIX + rtype |
563 if self.values['indexed']: |
574 if self.values['indexed']: |
564 sysource.create_index(self.session, table, column) |
575 sysource.create_index(self.session, table, column) |
565 else: |
576 else: |
566 sysource.drop_index(self.session, table, column) |
577 sysource.drop_index(self.session, table, column) |
567 |
578 |
568 def commit_event(self): |
579 def commit_event(self): |
569 # structure should be clean, not need to remove entity's relations |
580 # structure should be clean, not need to remove entity's relations |
570 # at this point |
581 # at this point |
571 self.rschema._rproperties[self.kobj].update(self.values) |
582 self.rschema._rproperties[self.kobj].update(self.values) |
|
583 |
572 |
584 |
573 def after_update_erdef(session, entity): |
585 def after_update_erdef(session, entity): |
574 desttype = entity.to_entity[0].name |
586 desttype = entity.to_entity[0].name |
575 rschema = session.repo.schema[entity.relation_type[0].name] |
587 rschema = session.repo.schema[entity.relation_type[0].name] |
576 newvalues = {} |
588 newvalues = {} |
612 for sql in tablesql.split(';'): |
625 for sql in tablesql.split(';'): |
613 if sql.strip(): |
626 if sql.strip(): |
614 sqlexec(sql) |
627 sqlexec(sql) |
615 session.add_query_data('createdtables', rschema.type) |
628 session.add_query_data('createdtables', rschema.type) |
616 # copy existant data |
629 # copy existant data |
|
630 column = SQL_PREFIX + rtype |
617 for etype in rschema.subjects(): |
631 for etype in rschema.subjects(): |
618 sqlexec('INSERT INTO %s_relation SELECT eid, %s FROM %s WHERE NOT %s IS NULL' |
632 table = SQL_PREFIX + str(etype) |
619 % (rtype, rtype, etype, rtype)) |
633 sqlexec('INSERT INTO %s_relation SELECT %s, %s FROM %s WHERE NOT %s IS NULL' |
|
634 % (rtype, eidcolumn, column, table, column)) |
620 # drop existant columns |
635 # drop existant columns |
621 for etype in rschema.subjects(): |
636 for etype in rschema.subjects(): |
622 DropColumnOp(session, table=str(etype), column=rtype) |
637 DropColumnOp(session, table=SQL_PREFIX + str(etype), |
|
638 column=SQL_PREFIX + rtype) |
623 else: |
639 else: |
624 for etype in rschema.subjects(): |
640 for etype in rschema.subjects(): |
625 try: |
641 try: |
626 add_inline_relation_column(session, str(etype), rtype) |
642 add_inline_relation_column(session, str(etype), rtype) |
627 except Exception, ex: |
643 except Exception, ex: |
633 # XXX don't use, it's not supported by sqlite (at least at when i tried it) |
649 # XXX don't use, it's not supported by sqlite (at least at when i tried it) |
634 #sqlexec('UPDATE %(etype)s SET %(rtype)s=eid_to ' |
650 #sqlexec('UPDATE %(etype)s SET %(rtype)s=eid_to ' |
635 # 'FROM %(rtype)s_relation ' |
651 # 'FROM %(rtype)s_relation ' |
636 # 'WHERE %(etype)s.eid=%(rtype)s_relation.eid_from' |
652 # 'WHERE %(etype)s.eid=%(rtype)s_relation.eid_from' |
637 # % locals()) |
653 # % locals()) |
638 cursor = sqlexec('SELECT eid_from, eid_to FROM %(etype)s, ' |
654 table = SQL_PREFIX + str(etype) |
639 '%(rtype)s_relation WHERE %(etype)s.eid=' |
655 cursor = sqlexec('SELECT eid_from, eid_to FROM %(table)s, ' |
|
656 '%(rtype)s_relation WHERE %(table)s.%(eidcolumn)s=' |
640 '%(rtype)s_relation.eid_from' % locals()) |
657 '%(rtype)s_relation.eid_from' % locals()) |
641 args = [{'val': eid_to, 'x': eid} for eid, eid_to in cursor.fetchall()] |
658 args = [{'val': eid_to, 'x': eid} for eid, eid_to in cursor.fetchall()] |
642 if args: |
659 if args: |
643 cursor.executemany('UPDATE %s SET %s=%%(val)s WHERE eid=%%(x)s' |
660 column = SQL_PREFIX + rtype |
644 % (etype, rtype), args) |
661 cursor.executemany('UPDATE %s SET %s=%%(val)s WHERE %s=%%(x)s' |
|
662 % (table, column, eidcolumn), args) |
645 # drop existant table |
663 # drop existant table |
646 DropTableOp(session, table='%s_relation' % rtype) |
664 DropTableOp(session, table='%s_relation' % rtype) |
647 |
665 |
648 def commit_event(self): |
666 def commit_event(self): |
649 # structure should be clean, not need to remove entity's relations |
667 # structure should be clean, not need to remove entity's relations |
687 self.prepare_constraints(rtype, subjtype, objtype) |
705 self.prepare_constraints(rtype, subjtype, objtype) |
688 cstrtype = self.entity.type |
706 cstrtype = self.entity.type |
689 self.cstr = rtype.constraint_by_type(subjtype, objtype, cstrtype) |
707 self.cstr = rtype.constraint_by_type(subjtype, objtype, cstrtype) |
690 self._cstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value) |
708 self._cstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value) |
691 self._cstr.eid = self.entity.eid |
709 self._cstr.eid = self.entity.eid |
|
710 table = SQL_PREFIX + str(subjtype) |
|
711 column = SQL_PREFIX + str(rtype) |
692 # alter the physical schema on size constraint changes |
712 # alter the physical schema on size constraint changes |
693 if self._cstr.type() == 'SizeConstraint' and ( |
713 if self._cstr.type() == 'SizeConstraint' and ( |
694 self.cstr is None or self.cstr.max != self._cstr.max): |
714 self.cstr is None or self.cstr.max != self._cstr.max): |
695 try: |
715 try: |
696 session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE VARCHAR(%s)' |
716 session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE VARCHAR(%s)' |
697 % (subjtype, rtype, self._cstr.max)) |
717 % (table, column, self._cstr.max)) |
698 self.info('altered column %s of table %s: now VARCHAR(%s)', |
718 self.info('altered column %s of table %s: now VARCHAR(%s)', |
699 rtype, subjtype, self._cstr.max) |
719 column, table, self._cstr.max) |
700 except Exception, ex: |
720 except Exception, ex: |
701 # not supported by sqlite for instance |
721 # not supported by sqlite for instance |
702 self.error('error while altering table %s: %s', subjtype, ex) |
722 self.error('error while altering table %s: %s', table, ex) |
703 elif cstrtype == 'UniqueConstraint': |
723 elif cstrtype == 'UniqueConstraint': |
704 session.pool.source('system').create_index( |
724 session.pool.source('system').create_index( |
705 self.session, str(subjtype), str(rtype), unique=True) |
725 self.session, table, column, unique=True) |
706 |
726 |
707 def commit_event(self): |
727 def commit_event(self): |
708 if self.cancelled: |
728 if self.cancelled: |
709 return |
729 return |
710 # in-place removing |
730 # in-place removing |
711 if not self.cstr is None: |
731 if not self.cstr is None: |
712 self.constraints.remove(self.cstr) |
732 self.constraints.remove(self.cstr) |
713 self.constraints.append(self._cstr) |
733 self.constraints.append(self._cstr) |
714 |
734 |
|
735 |
715 def after_add_econstraint(session, entity): |
736 def after_add_econstraint(session, entity): |
716 ConstraintOp(session, entity=entity) |
737 ConstraintOp(session, entity=entity) |
717 |
738 |
718 def after_update_econstraint(session, entity): |
739 def after_update_econstraint(session, entity): |
719 ConstraintOp(session, entity=entity) |
740 ConstraintOp(session, entity=entity) |
|
741 |
720 |
742 |
721 class DelConstraintOp(ConstraintOp): |
743 class DelConstraintOp(ConstraintOp): |
722 """actually remove a constraint of a relation definition""" |
744 """actually remove a constraint of a relation definition""" |
723 rtype = subjtype = objtype = None # make pylint happy |
745 rtype = subjtype = objtype = None # make pylint happy |
724 |
746 |
725 def precommit_event(self): |
747 def precommit_event(self): |
726 self.prepare_constraints(self.rtype, self.subjtype, self.objtype) |
748 self.prepare_constraints(self.rtype, self.subjtype, self.objtype) |
727 cstrtype = self.cstr.type() |
749 cstrtype = self.cstr.type() |
|
750 table = SQL_PREFIX + str(self.subjtype) |
|
751 column = SQL_PREFIX + str(self.rtype) |
728 # alter the physical schema on size/unique constraint changes |
752 # alter the physical schema on size/unique constraint changes |
729 if cstrtype == 'SizeConstraint': |
753 if cstrtype == 'SizeConstraint': |
730 try: |
754 try: |
731 self.session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE TEXT' |
755 self.session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE TEXT' |
732 % (self.subjtype, self.rtype)) |
756 % (table, column)) |
733 self.info('altered column %s of table %s: now TEXT', |
757 self.info('altered column %s of table %s: now TEXT', |
734 self.rtype, self.subjtype) |
758 column, table) |
735 except Exception, ex: |
759 except Exception, ex: |
736 # not supported by sqlite for instance |
760 # not supported by sqlite for instance |
737 self.error('error while altering table %s: %s', |
761 self.error('error while altering table %s: %s', table, ex) |
738 self.subjtype, ex) |
|
739 elif cstrtype == 'UniqueConstraint': |
762 elif cstrtype == 'UniqueConstraint': |
740 self.session.pool.source('system').drop_index( |
763 self.session.pool.source('system').drop_index( |
741 self.session, str(self.subjtype), str(self.rtype), unique=True) |
764 self.session, table, column, unique=True) |
742 |
765 |
743 def commit_event(self): |
766 def commit_event(self): |
744 self.constraints.remove(self.cstr) |
767 self.constraints.remove(self.cstr) |
745 |
768 |
746 |
769 |