backport stable 3.5
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 21 Aug 2009 15:07:47 +0200
branch3.5
changeset 2967 e7d348134006
parent 2966 0aeb81eb179c (current diff)
parent 2963 12ad88615a12 (diff)
child 2968 0e3460341023
child 2970 d83f5d96e1d7
backport stable
schema.py
server/migractions.py
server/repository.py
server/schemahooks.py
server/sources/native.py
server/test/data/migratedapp/schema.py
server/test/unittest_migractions.py
--- a/schema.py	Fri Aug 21 15:07:13 2009 +0200
+++ b/schema.py	Fri Aug 21 15:07:47 2009 +0200
@@ -447,6 +447,7 @@
     reading_from_database = False
     entity_class = CubicWebEntitySchema
     relation_class = CubicWebRelationSchema
+    no_specialization_inference = ('identity',)
 
     def __init__(self, *args, **kwargs):
         self._eid_index = {}
--- a/server/migractions.py	Fri Aug 21 15:07:13 2009 +0200
+++ b/server/migractions.py	Fri Aug 21 15:07:47 2009 +0200
@@ -61,6 +61,7 @@
             assert repo
             self._cnx = cnx
             self.repo = repo
+            self.session.data['rebuild-infered'] = False
         elif connect:
             self.repo_connect()
         if not schema:
@@ -133,33 +134,36 @@
         os.chmod(backupfile, 0600)
         # backup
         tmpdir = tempfile.mkdtemp(dir=instbkdir)
-        for source in repo.sources:
-            try:
-                source.backup(osp.join(tmpdir,source.uri))
-            except Exception, exc:
-                print '-> error trying to backup [%s]' % exc
-                if not self.confirm('Continue anyway?', default='n'):
-                    raise SystemExit(1)
-        bkup = tarfile.open(backupfile, 'w|gz')
-        for filename in os.listdir(tmpdir):
-            bkup.add(osp.join(tmpdir,filename), filename)
-        bkup.close()
-        shutil.rmtree(tmpdir)
-        # call hooks
-        repo.hm.call_hooks('server_backup', repo=repo, timestamp=timestamp)
-        # done
-        print '-> backup file',  backupfile
+        try:
+            for source in repo.sources:
+                try:
+                    source.backup(osp.join(tmpdir, source.uri))
+                except Exception, exc:
+                    print '-> error trying to backup [%s]' % exc
+                    if not self.confirm('Continue anyway?', default='n'):
+                        raise SystemExit(1)
+                    else:
+                        break
+            else:
+                bkup = tarfile.open(backupfile, 'w|gz')
+                for filename in os.listdir(tmpdir):
+                    bkup.add(osp.join(tmpdir,filename), filename)
+                bkup.close()
+                # call hooks
+                repo.hm.call_hooks('server_backup', repo=repo, timestamp=timestamp)
+                # done
+                print '-> backup file',  backupfile
+        finally:
+            shutil.rmtree(tmpdir)
 
     def restore_database(self, backupfile, drop=True, systemonly=True,
                          askconfirm=True):
-        config = self.config
-        repo = self.repo_connect()
         # check
         if not osp.exists(backupfile):
             raise Exception("Backup file %s doesn't exist" % backupfile)
             return
         if askconfirm and not self.confirm('Restore %s database from %s ?'
-                                           % (config.appid, backupfile)):
+                                           % (self.config.appid, backupfile)):
             return
         # unpack backup
         bkup = tarfile.open(backupfile, 'r|gz')
@@ -170,6 +174,9 @@
         bkup = tarfile.open(backupfile, 'r|gz')
         tmpdir = tempfile.mkdtemp()
         bkup.extractall(path=tmpdir)
+
+        self.config.open_connections_pools = False
+        repo = self.repo_connect()
         for source in repo.sources:
             if systemonly and source.uri != 'system':
                 continue
@@ -182,6 +189,7 @@
         bkup.close()
         shutil.rmtree(tmpdir)
         # call hooks
+        repo.open_connections_pools()
         repo.hm.call_hooks('server_restore', repo=repo, timestamp=backupfile)
         print '-> database restored.'
 
@@ -215,6 +223,7 @@
                     print 'aborting...'
                     sys.exit(0)
             self.session.keep_pool_mode('transaction')
+            self.session.data['rebuild-infered'] = False
             return self._cnx
 
     @property
@@ -408,12 +417,12 @@
                          {'x': str(repoeschema), 'y': str(espschema)})
         self.rqlexecall(ss.updateeschema2rql(eschema),
                         ask_confirm=self.verbosity >= 2)
-        for rschema, targettypes, x in eschema.relation_definitions(True):
-            if x == 'subject':
+        for rschema, targettypes, role in eschema.relation_definitions(True):
+            if role == 'subject':
                 if not rschema in repoeschema.subject_relations():
                     continue
                 subjtypes, objtypes = [etype], targettypes
-            else: # x == 'object'
+            else: # role == 'object'
                 if not rschema in repoeschema.object_relations():
                     continue
                 subjtypes, objtypes = targettypes, [etype]
@@ -619,11 +628,13 @@
         in auto mode, automatically register entity's relation where the
         targeted type is known
         """
-        applschema = self.repo.schema
-        if etype in applschema:
-            eschema = applschema[etype]
+        instschema = self.repo.schema
+        if etype in instschema:
+            # XXX (syt) plz explain: if we're adding an entity type, it should
+            # not be there...
+            eschema = instschema[etype]
             if eschema.is_final():
-                applschema.del_entity_type(etype)
+                instschema.del_entity_type(etype)
         else:
             eschema = self.fs_schema.eschema(etype)
         confirm = self.verbosity >= 2
@@ -639,13 +650,46 @@
             # ignore those meta relations, they will be automatically added
             if rschema.type in META_RTYPES:
                 continue
-            if not rschema.type in applschema:
+            if not rschema.type in instschema:
                 # need to add the relation type and to commit to get it
                 # actually in the schema
                 self.cmd_add_relation_type(rschema.type, False, commit=True)
             # register relation definition
             self.rqlexecall(ss.rdef2rql(rschema, etype, attrschema.type),
                             ask_confirm=confirm)
+        # take care to newly introduced base class
+        # XXX some part of this should probably be under the "if auto" block
+        for spschema in eschema.specialized_by(recursive=False):
+            try:
+                instspschema = instschema[spschema]
+            except KeyError:
+                # specialized entity type not in schema, ignore
+                continue
+            if instspschema.specializes() != eschema:
+                self.rqlexec('SET D specializes P WHERE D eid %(d)s, P name %(pn)s',
+                              {'d': instspschema.eid,
+                               'pn': eschema.type}, ask_confirm=confirm)
+                for rschema, tschemas, role in spschema.relation_definitions(True):
+                    for tschema in tschemas:
+                        if not tschema in instschema:
+                            continue
+                        if role == 'subject':
+                            subjschema = spschema
+                            objschema = tschema
+                            if rschema.final and instspschema.has_subject_relation(rschema):
+                                # attribute already set, has_rdef would check if
+                                # it's of the same type, we don't want this so
+                                # simply skip here
+                                continue
+                        elif role == 'object':
+                            subjschema = tschema
+                            objschema = spschema
+                        if (rschema.rproperty(subjschema, objschema, 'infered')
+                            or (instschema.has_relation(rschema) and
+                                instschema[rschema].has_rdef(subjschema, objschema))):
+                                continue
+                        self.cmd_add_relation_definition(
+                            subjschema.type, rschema.type, objschema.type)
         if auto:
             # we have commit here to get relation types actually in the schema
             self.commit()
@@ -655,12 +699,12 @@
                 # 'owned_by'/'created_by' will be automatically added
                 if rschema.final or rschema.type in META_RTYPES:
                     continue
-                rtypeadded = rschema.type in applschema
+                rtypeadded = rschema.type in instschema
                 for targetschema in rschema.objects(etype):
                     # ignore relations where the targeted type is not in the
                     # current instance schema
                     targettype = targetschema.type
-                    if not targettype in applschema and targettype != etype:
+                    if not targettype in instschema and targettype != etype:
                         continue
                     if not rtypeadded:
                         # need to add the relation type and to commit to get it
@@ -675,14 +719,14 @@
                     self.rqlexecall(ss.rdef2rql(rschema, etype, targettype),
                                     ask_confirm=confirm)
             for rschema in eschema.object_relations():
-                rtypeadded = rschema.type in applschema or rschema.type in added
+                rtypeadded = rschema.type in instschema or rschema.type in added
                 for targetschema in rschema.subjects(etype):
                     # ignore relations where the targeted type is not in the
                     # current instance schema
                     targettype = targetschema.type
                     # don't check targettype != etype since in this case the
                     # relation has already been added as a subject relation
-                    if not targettype in applschema:
+                    if not targettype in instschema:
                         continue
                     if not rtypeadded:
                         # need to add the relation type and to commit to get it
--- a/server/repository.py	Fri Aug 21 15:07:13 2009 +0200
+++ b/server/repository.py	Fri Aug 21 15:07:47 2009 +0200
@@ -176,6 +176,11 @@
         # create the hooks manager
         self.hm = HooksManager(self.schema)
         # open some connections pools
+        if config.open_connections_pools:
+            self.open_connections_pools()
+
+    def open_connections_pools(self):
+        config = self.config
         self._available_pools = Queue.Queue()
         self._available_pools.put_nowait(ConnectionsPool(self.sources))
         if config.read_instance_schema:
@@ -185,7 +190,7 @@
             # usually during repository creation
             self.warning("set fs instance'schema as bootstrap schema")
             config.bootstrap_cubes()
-            self.set_bootstrap_schema(self.config.load_schema())
+            self.set_bootstrap_schema(config.load_schema())
             # need to load the Any and CWUser entity types
             self.vreg.schema = self.schema
             etdirectory = join(CW_SOFTWARE_ROOT, 'entities')
@@ -200,7 +205,7 @@
             # test start: use the file system schema (quicker)
             self.warning("set fs instance'schema")
             config.bootstrap_cubes()
-            self.set_schema(self.config.load_schema())
+            self.set_schema(config.load_schema())
         if not config.creating:
             if 'CWProperty' in self.schema:
                 self.vreg.init_properties(self.properties())
@@ -228,8 +233,7 @@
             # call instance level initialisation hooks
             self.hm.call_hooks('server_startup', repo=self)
             # register a task to cleanup expired session
-            self.looping_task(self.config['session-time']/3.,
-                              self.clean_sessions)
+            self.looping_task(config['session-time']/3., self.clean_sessions)
         CW_EVENT_MANAGER.bind('after-registry-reload', self.reset_hooks)
 
     # internals ###############################################################
@@ -238,8 +242,9 @@
         source_config['uri'] = uri
         return get_source(source_config, self.schema, self)
 
-    def set_schema(self, schema, resetvreg=True):
-        schema.rebuild_infered_relations()
+    def set_schema(self, schema, resetvreg=True, rebuildinfered=True):
+        if rebuildinfered:
+            schema.rebuild_infered_relations()
         self.info('set schema %s %#x', schema.name, id(schema))
         self.debug(', '.join(sorted(str(e) for e in schema.entities())))
         self.querier.set_schema(schema)
--- a/server/schemahooks.py	Fri Aug 21 15:07:13 2009 +0200
+++ b/server/schemahooks.py	Fri Aug 21 15:07:47 2009 +0200
@@ -135,7 +135,8 @@
         SingleLastOperation.__init__(self, session)
 
     def commit_event(self):
-        self.repo.set_schema(self.repo.schema)
+        rebuildinfered = self.session.data.get('rebuild-infered', True)
+        self.repo.set_schema(self.repo.schema, rebuildinfered=rebuildinfered)
 
 
 class MemSchemaOperation(Operation):
@@ -718,6 +719,28 @@
         erschema.set_rqlexprs(self.perm, rqlexprs)
 
 
+class MemSchemaSpecializesAdd(MemSchemaOperation):
+
+    def commit_event(self):
+        eschema = self.session.schema.schema_by_eid(self.etypeeid)
+        parenteschema = self.session.schema.schema_by_eid(self.parentetypeeid)
+        eschema._specialized_type = parenteschema.type
+        parenteschema._specialized_by.append(eschema.type)
+
+
+class MemSchemaSpecializesDel(MemSchemaOperation):
+
+    def commit_event(self):
+        try:
+            eschema = self.session.schema.schema_by_eid(self.etypeeid)
+            parenteschema = self.session.schema.schema_by_eid(self.parentetypeeid)
+        except KeyError:
+            # etype removed, nothing to do
+            return
+        eschema._specialized_type = None
+        parenteschema._specialized_by.remove(eschema.type)
+
+
 # deletion hooks ###############################################################
 
 def before_del_eetype(session, eid):
@@ -1014,11 +1037,11 @@
         MemSchemaPermissionRQLExpressionDel(session, perm, subject, expr)
 
 
-def rebuild_infered_relations(session, subject, rtype, object):
-    # registering a schema operation will trigger a call to
-    # repo.set_schema() on commit which will in turn rebuild
-    # infered relation definitions
-    MemSchemaNotifyChanges(session)
+def after_add_specializes(session, subject, rtype, object):
+    MemSchemaSpecializesAdd(session, etypeeid=subject, parentetypeeid=object)
+
+def after_del_specializes(session, subject, rtype, object):
+    MemSchemaSpecializesDel(session, etypeeid=subject, parentetypeeid=object)
 
 
 def _register_schema_hooks(hm):
@@ -1042,8 +1065,8 @@
     hm.register_hook(after_del_eetype, 'after_delete_entity', 'CWEType')
     hm.register_hook(before_del_ertype, 'before_delete_entity', 'CWRType')
     hm.register_hook(after_del_relation_type, 'after_delete_relation', 'relation_type')
-    hm.register_hook(rebuild_infered_relations, 'after_add_relation', 'specializes')
-    hm.register_hook(rebuild_infered_relations, 'after_delete_relation', 'specializes')
+    hm.register_hook(after_add_specializes, 'after_add_relation', 'specializes')
+    hm.register_hook(after_del_specializes, 'after_delete_relation', 'specializes')
     # constraints synchronization hooks
     hm.register_hook(after_add_econstraint, 'after_add_entity', 'CWConstraint')
     hm.register_hook(after_update_econstraint, 'after_update_entity', 'CWConstraint')
--- a/server/serverconfig.py	Fri Aug 21 15:07:13 2009 +0200
+++ b/server/serverconfig.py	Fri Aug 21 15:07:47 2009 +0200
@@ -178,6 +178,10 @@
           }),
         ) + CubicWebConfiguration.options)
 
+    # should we open connections pools (eg connect to sources). This is usually
+    # necessary...
+    open_connections_pools = True
+
     # read the schema from the database
     read_instance_schema = True
     bootstrap_schema = True
--- a/server/sources/native.py	Fri Aug 21 15:07:13 2009 +0200
+++ b/server/sources/native.py	Fri Aug 21 15:07:47 2009 +0200
@@ -214,11 +214,13 @@
 
     def restore(self, backupfile, drop):
         """method called to restore a backup of source's data"""
-        self.close_pool_connections()
+        if self.repo.config.open_connections_pools:
+            self.close_pool_connections()
         try:
             self.restore_from_file(backupfile, drop)
         finally:
-            self.open_pool_connections()
+            if self.repo.config.open_connections_pools:
+                self.open_pool_connections()
 
     def init(self):
         self.init_creating()
--- a/server/test/data/migratedapp/schema.py	Fri Aug 21 15:07:13 2009 +0200
+++ b/server/test/data/migratedapp/schema.py	Fri Aug 21 15:07:47 2009 +0200
@@ -33,7 +33,15 @@
         'delete': ('managers', RRQLExpression('O owned_by U')),
         }
 
-class Note(EntityType):
+class Para(EntityType):
+    para = String(maxsize=512)
+    newattr = String()
+    newinlined = SubjectRelation('Affaire', cardinality='?*', inlined=True)
+    newnotinlined = SubjectRelation('Affaire', cardinality='?*')
+
+class Note(Para):
+    __specializes_schema__ = True
+
     permissions = {'read':   ('managers', 'users', 'guests',),
                    'update': ('managers', 'owners',),
                    'delete': ('managers', ),
@@ -46,11 +54,14 @@
     type = String(maxsize=1)
     whatever = Int()
     mydate = Date(default='TODAY')
-    para = String(maxsize=512)
     shortpara = String(maxsize=64)
     ecrit_par = SubjectRelation('Personne', constraints=[RQLConstraint('S concerne A, O concerne A')])
     attachment = SubjectRelation(('File', 'Image'))
 
+class Text(Para):
+    __specializes_schema__ = True
+    summary = String(maxsize=512)
+
 class ecrit_par(RelationType):
     permissions = {'read':   ('managers', 'users', 'guests',),
                    'delete': ('managers', ),
--- a/server/test/unittest_migractions.py	Fri Aug 21 15:07:13 2009 +0200
+++ b/server/test/unittest_migractions.py	Fri Aug 21 15:07:47 2009 +0200
@@ -449,5 +449,47 @@
         user.clear_related_cache('in_state', 'subject')
         self.assertEquals(user.state, 'deactivated')
 
+    def test_introduce_base_class(self):
+        self.mh.cmd_add_entity_type('Para')
+        self.mh.repo.schema.rebuild_infered_relations()
+        self.assertEquals(sorted(et.type for et in self.schema['Para'].specialized_by()),
+                          ['Note'])
+        self.assertEquals(self.schema['Note'].specializes().type, 'Para')
+        self.mh.cmd_add_entity_type('Text')
+        self.mh.repo.schema.rebuild_infered_relations()
+        self.assertEquals(sorted(et.type for et in self.schema['Para'].specialized_by()),
+                          ['Note', 'Text'])
+        self.assertEquals(self.schema['Text'].specializes().type, 'Para')
+        # test columns have been actually added
+        text = self.execute('INSERT Text X: X para "hip", X summary "hop", X newattr "momo"').get_entity(0, 0)
+        note = self.execute('INSERT Note X: X para "hip", X shortpara "hop", X newattr "momo"').get_entity(0, 0)
+        aff = self.execute('INSERT Affaire X').get_entity(0, 0)
+        self.failUnless(self.execute('SET X newnotinlined Y WHERE X eid %(x)s, Y eid %(y)s',
+                                     {'x': text.eid, 'y': aff.eid}, 'x'))
+        self.failUnless(self.execute('SET X newnotinlined Y WHERE X eid %(x)s, Y eid %(y)s',
+                                     {'x': note.eid, 'y': aff.eid}, 'x'))
+        self.failUnless(self.execute('SET X newinlined Y WHERE X eid %(x)s, Y eid %(y)s',
+                                     {'x': text.eid, 'y': aff.eid}, 'x'))
+        self.failUnless(self.execute('SET X newinlined Y WHERE X eid %(x)s, Y eid %(y)s',
+                                     {'x': note.eid, 'y': aff.eid}, 'x'))
+        # XXX remove specializes by ourselves, else tearDown fails when removing
+        # Para because of Note inheritance. This could be fixed by putting the
+        # MemSchemaCWETypeDel(session, name) operation in the
+        # after_delete_entity(CWEType) hook, since in that case the MemSchemaSpecializesDel
+        # operation would be removed before, but I'm not sure this is a desired behaviour.
+        #
+        # also we need more tests about introducing/removing base classes or
+        # specialization relationship...
+        self.session.data['rebuild-infered'] = True
+        try:
+            self.execute('DELETE X specializes Y WHERE Y name "Para"')
+            self.commit()
+        finally:
+            self.session.data['rebuild-infered'] = False
+        self.assertEquals(sorted(et.type for et in self.schema['Para'].specialized_by()),
+                          [])
+        self.assertEquals(self.schema['Note'].specializes(), None)
+        self.assertEquals(self.schema['Text'].specializes(), None)
+
 if __name__ == '__main__':
     unittest_main()