[schemaserial] serialize additional yams parameter for customs type
authorVincent Michel <vincent.michel@logilab.fr>
Fri, 26 Apr 2013 17:46:56 +0200
changeset 8945 ba9e3fbfa5a5
parent 8944 b167f039b6cb
child 8946 fae2f561b0f5
[schemaserial] serialize additional yams parameter for customs type Yams have a new custom type feature. We add support for extra parameter on CWAttribute in schema serialization handling. Closes #2847151
misc/migration/3.17.0_Any.py
schema.py
schemas/bootstrap.py
server/schemaserial.py
server/test/data-schemaserial/bootstrap_cubes
server/test/data-schemaserial/schema.py
server/test/data-schemaserial/site_cubicweb.py
server/test/unittest_repository.py
server/test/unittest_schemaserial.py
test/unittest_schema.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.17.0_Any.py	Fri Apr 26 17:46:56 2013 +0200
@@ -0,0 +1,1 @@
+add_attribute('CWAttribute', 'extra_props')
--- a/schema.py	Tue Apr 23 14:13:58 2013 +0200
+++ b/schema.py	Fri Apr 26 17:46:56 2013 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -90,11 +90,9 @@
 
 _LOGGER = getLogger('cubicweb.schemaloader')
 
-# schema entities created from serialized schema have an eid rproperty
+# entity and relation schema created from serialized schema have an eid
 ybo.ETYPE_PROPERTIES += ('eid',)
 ybo.RTYPE_PROPERTIES += ('eid',)
-ybo.RDEF_PROPERTIES += ('eid',)
-
 
 PUB_SYSTEM_ENTITY_PERMS = {
     'read':   ('managers', 'users', 'guests',),
@@ -296,6 +294,9 @@
 
 
 RelationDefinitionSchema._RPROPERTIES['eid'] = None
+# remember rproperties defined at this point. Others will have to be serialized in
+# CWAttribute.extra_props
+KNOWN_RPROPERTIES = RelationDefinitionSchema.ALL_PROPERTIES()
 
 def rql_expression(self, expression, mainvars=None, eid=None):
     """rql expression factory"""
--- a/schemas/bootstrap.py	Tue Apr 23 14:13:58 2013 +0200
+++ b/schemas/bootstrap.py	Fri Apr 26 17:46:56 2013 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -21,7 +21,7 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
-from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
+from yams.buildobjs import (EntityType, RelationType, RelationDefinition, Bytes,
                             SubjectRelation, RichString, String, Boolean, Int)
 from cubicweb.schema import (
     RQLConstraint,
@@ -84,6 +84,7 @@
     fulltextindexed = Boolean(description=_('index this attribute\'s value in the plain text index'))
     internationalizable = Boolean(description=_('is this attribute\'s value translatable'))
     defaultval = String(maxsize=256)
+    extra_props = Bytes(description=_('additional type specific properties'))
 
     description = RichString(internationalizable=True,
                              description=_('semantic description of this attribute'))
--- a/server/schemaserial.py	Tue Apr 23 14:13:58 2013 +0200
+++ b/server/schemaserial.py	Fri Apr 26 17:46:56 2013 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -21,16 +21,18 @@
 
 import os
 from itertools import chain
+import json
 
 from logilab.common.shellutils import ProgressBar
 
 from yams import BadSchemaDefinition, schema as schemamod, buildobjs as ybo
 
-from cubicweb import CW_SOFTWARE_ROOT
-from cubicweb.schema import (CONSTRAINTS, ETYPE_NAME_MAP,
+from cubicweb import CW_SOFTWARE_ROOT, Binary
+from cubicweb.schema import (KNOWN_RPROPERTIES, CONSTRAINTS, ETYPE_NAME_MAP,
                              VIRTUAL_RTYPES, PURE_VIRTUAL_RTYPES)
 from cubicweb.server import sqlutils
 
+
 def group_mapping(cursor, interactive=True):
     """create a group mapping from an rql cursor
 
@@ -195,7 +197,12 @@
             if rdefs is not None:
                 ertidx[rdefeid] = rdefs
                 set_perms(rdefs, permsidx)
-
+    # Get the type parameters for additional base types.
+    try:
+        extra_props = dict(session.execute('Any X, XTP WHERE X is CWAttribute, '
+                                           'X extra_props XTP'))
+    except Exception:
+        extra_props = {} # not yet in the schema (introduced by 3.17 migration)
     for values in session.execute(
         'Any X,SE,RT,OE,CARD,ORD,DESC,IDX,FTIDX,I18N,DFLT WHERE X is CWAttribute,'
         'X relation_type RT, X cardinality CARD, X ordernum ORD, X indexed IDX,'
@@ -203,10 +210,12 @@
         'X fulltextindexed FTIDX, X from_entity SE, X to_entity OE',
         build_descr=False):
         rdefeid, seid, reid, oeid, card, ord, desc, idx, ftidx, i18n, default = values
+        typeparams = extra_props.get(rdefeid)
+        typeparams = json.load(typeparams) if typeparams else {}
         _add_rdef(rdefeid, seid, reid, oeid,
                   cardinality=card, description=desc, order=ord,
                   indexed=idx, fulltextindexed=ftidx, internationalizable=i18n,
-                  default=default)
+                  default=default, **typeparams)
     for values in session.execute(
         'Any X,SE,RT,OE,CARD,ORD,DESC,C WHERE X is CWRelation, X relation_type RT,'
         'X cardinality CARD, X ordernum ORD, X description DESC, '
@@ -509,10 +518,14 @@
 def _rdef_values(rdef):
     amap = {'order': 'ordernum', 'default': 'defaultval'}
     values = {}
-    for prop, default in rdef.rproperty_defs(rdef.object).iteritems():
+    extra = {}
+    for prop in rdef.rproperty_defs(rdef.object):
         if prop in ('eid', 'constraints', 'uid', 'infered', 'permissions'):
             continue
         value = getattr(rdef, prop)
+        if prop not in KNOWN_RPROPERTIES:
+            extra[prop] = value
+            continue
         # XXX type cast really necessary?
         if prop in ('indexed', 'fulltextindexed', 'internationalizable'):
             value = bool(value)
@@ -526,6 +539,8 @@
             if not isinstance(value, unicode):
                 value = unicode(value)
         values[amap.get(prop, prop)] = value
+    if extra:
+        values['extra_props'] = Binary(json.dumps(extra))
     relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
     return relations, values
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-schemaserial/bootstrap_cubes	Fri Apr 26 17:46:56 2013 +0200
@@ -0,0 +1,1 @@
+card,comment,folder,tag,basket,email,file,localperms
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-schemaserial/schema.py	Fri Apr 26 17:46:56 2013 +0200
@@ -0,0 +1,256 @@
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+
+from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
+                            SubjectRelation, RichString, String, Int, Float,
+                            Boolean, Datetime, TZDatetime, Bytes)
+from yams.constraints import SizeConstraint
+from cubicweb.schema import (WorkflowableEntityType,
+                             RQLConstraint, RQLUniqueConstraint,
+                             ERQLExpression, RRQLExpression)
+
+from yams.buildobjs import make_type
+BabarTestType = make_type('BabarTestType')
+
+
+class Affaire(WorkflowableEntityType):
+    __permissions__ = {
+        'read':   ('managers',
+                   ERQLExpression('X owned_by U'), ERQLExpression('X concerne S?, S owned_by U')),
+        'add':    ('managers', ERQLExpression('X concerne S, S owned_by U')),
+        'update': ('managers', 'owners', ERQLExpression('X in_state S, S name in ("pitetre", "en cours")')),
+        'delete': ('managers', 'owners', ERQLExpression('X concerne S, S owned_by U')),
+        }
+
+    ref = String(fulltextindexed=True, indexed=True,
+                 constraints=[SizeConstraint(16)])
+    sujet = String(fulltextindexed=True,
+                   constraints=[SizeConstraint(256)])
+    descr = RichString(fulltextindexed=True,
+                       description=_('more detailed description'))
+
+    duration = Int()
+    invoiced = Float()
+    opt_attr = Bytes()
+
+    depends_on = SubjectRelation('Affaire')
+    require_permission = SubjectRelation('CWPermission')
+    concerne = SubjectRelation(('Societe', 'Note'))
+    todo_by = SubjectRelation('Personne', cardinality='?*')
+    documented_by = SubjectRelation('Card')
+
+
+class Societe(EntityType):
+    __unique_together__ = [('nom', 'type', 'cp')]
+    __permissions__ = {
+        'read': ('managers', 'users', 'guests'),
+        'update': ('managers', 'owners', ERQLExpression('U login L, X nom L')),
+        'delete': ('managers', 'owners', ERQLExpression('U login L, X nom L')),
+        'add': ('managers', 'users',)
+        }
+
+    nom  = String(maxsize=64, fulltextindexed=True)
+    web  = String(maxsize=128)
+    type  = String(maxsize=128) # attribute in common with Note
+    tel  = Int()
+    fax  = Int()
+    rncs = String(maxsize=128)
+    ad1  = String(maxsize=128)
+    ad2  = String(maxsize=128)
+    ad3  = String(maxsize=128)
+    cp   = String(maxsize=12)
+    ville= String(maxsize=32)
+
+
+class Division(Societe):
+    __specializes_schema__ = True
+
+class SubDivision(Division):
+    __specializes_schema__ = True
+
+class travaille_subdivision(RelationDefinition):
+    subject = 'Personne'
+    object = 'SubDivision'
+
+from cubicweb.schemas.base import CWUser
+CWUser.get_relations('login').next().fulltextindexed = True
+
+class Note(WorkflowableEntityType):
+    date = String(maxsize=10)
+    type = String(maxsize=6)
+    para = String(maxsize=512,
+                  __permissions__ = {
+                      'read':   ('managers', 'users', 'guests'),
+                      'update': ('managers', ERQLExpression('X in_state S, S name "todo"')),
+                      })
+
+    migrated_from = SubjectRelation('Note')
+    attachment = SubjectRelation('File')
+    inline1 = SubjectRelation('Affaire', inlined=True, cardinality='?*',
+                              constraints=[RQLUniqueConstraint('S type T, S inline1 A1, A1 todo_by C, '
+                                                              'Y type T, Y inline1 A2, A2 todo_by C',
+                                                               'S,Y')])
+    todo_by = SubjectRelation('CWUser')
+
+class Personne(EntityType):
+    __unique_together__ = [('nom', 'prenom', 'inline2')]
+    nom    = String(fulltextindexed=True, required=True, maxsize=64)
+    prenom = String(fulltextindexed=True, maxsize=64)
+    sexe   = String(maxsize=1, default='M', fulltextindexed=True)
+    promo  = String(vocabulary=('bon','pasbon'))
+    titre  = String(fulltextindexed=True, maxsize=128)
+    adel   = String(maxsize=128)
+    ass    = String(maxsize=128)
+    web    = String(maxsize=128)
+    tel    = Int()
+    fax    = Int()
+    datenaiss = Datetime()
+    tzdatenaiss = TZDatetime()
+    test   = Boolean(__permissions__={
+        'read': ('managers', 'users', 'guests'),
+        'update': ('managers',),
+        })
+    description = String()
+    firstname = String(fulltextindexed=True, maxsize=64)
+
+    concerne = SubjectRelation('Affaire')
+    connait = SubjectRelation('Personne')
+    inline2 = SubjectRelation('Affaire', inlined=True, cardinality='?*')
+
+    custom_field_of_jungle = BabarTestType(jungle_speed=42)
+
+
+class Old(EntityType):
+    name = String()
+
+
+class connait(RelationType):
+    symmetric = True
+
+class concerne(RelationType):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', RRQLExpression('U has_update_permission S')),
+        'delete': ('managers', RRQLExpression('O owned_by U')),
+        }
+
+class travaille(RelationDefinition):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', RRQLExpression('U has_update_permission S')),
+        'delete': ('managers', RRQLExpression('O owned_by U')),
+        }
+    subject = 'Personne'
+    object = 'Societe'
+
+class comments(RelationDefinition):
+    subject = 'Comment'
+    object = 'Personne'
+
+class fiche(RelationDefinition):
+    inlined = True
+    subject = 'Personne'
+    object = 'Card'
+    cardinality = '??'
+
+class multisource_inlined_rel(RelationDefinition):
+    inlined = True
+    cardinality = '?*'
+    subject = ('Card', 'Note')
+    object = ('Affaire', 'Note')
+
+class multisource_rel(RelationDefinition):
+    subject = ('Card', 'Note')
+    object = 'Note'
+
+class multisource_crossed_rel(RelationDefinition):
+    subject = ('Card', 'Note')
+    object = 'Note'
+
+
+class see_also_1(RelationDefinition):
+    name = 'see_also'
+    subject = object = 'Folder'
+
+class see_also_2(RelationDefinition):
+    name = 'see_also'
+    subject = ('Bookmark', 'Note')
+    object = ('Bookmark', 'Note')
+
+class evaluee(RelationDefinition):
+    subject = ('Personne', 'CWUser', 'Societe')
+    object = ('Note')
+
+class ecrit_par(RelationType):
+    inlined = True
+
+class ecrit_par_1(RelationDefinition):
+    name = 'ecrit_par'
+    subject = 'Note'
+    object ='Personne'
+    constraints = [RQLConstraint('E concerns P, S version_of P')]
+    cardinality = '?*'
+
+class ecrit_par_2(RelationDefinition):
+    name = 'ecrit_par'
+    subject = 'Note'
+    object ='CWUser'
+    cardinality='?*'
+
+
+class copain(RelationDefinition):
+    subject = object = 'CWUser'
+
+class tags(RelationDefinition):
+    subject = 'Tag'
+    object = ('CWUser', 'CWGroup', 'State', 'Note', 'Card', 'Affaire')
+
+class filed_under(RelationDefinition):
+    subject = ('Note', 'Affaire')
+    object = 'Folder'
+
+class require_permission(RelationDefinition):
+    subject = ('Card', 'Note', 'Personne')
+    object = 'CWPermission'
+
+class require_state(RelationDefinition):
+    subject = 'CWPermission'
+    object = 'State'
+
+class personne_composite(RelationDefinition):
+    subject='Personne'
+    object='Personne'
+    composite='subject'
+
+class personne_inlined(RelationDefinition):
+    subject='Personne'
+    object='Personne'
+    cardinality='?*'
+    inlined=True
+
+
+class login_user(RelationDefinition):
+    subject = 'Personne'
+    object = 'CWUser'
+    cardinality = '??'
+
+class ambiguous_inlined(RelationDefinition):
+    subject = ('Affaire', 'Note')
+    object = 'CWUser'
+    inlined = True
+    cardinality = '?*'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-schemaserial/site_cubicweb.py	Fri Apr 26 17:46:56 2013 +0200
@@ -0,0 +1,30 @@
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+
+from logilab.database import FunctionDescr
+from logilab.database.sqlite import register_sqlite_pyfunc
+from rql.utils import register_function
+
+class DUMB_SORT(FunctionDescr):
+    pass
+
+register_function(DUMB_SORT)
+def dumb_sort(something):
+    return something
+register_sqlite_pyfunc(dumb_sort)
+
--- a/server/test/unittest_repository.py	Tue Apr 23 14:13:58 2013 +0200
+++ b/server/test/unittest_repository.py	Fri Apr 26 17:46:56 2013 +0200
@@ -1,5 +1,5 @@
 # -*- coding: iso-8859-1 -*-
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -29,6 +29,9 @@
 from logilab.common.testlib import TestCase, unittest_main
 
 from yams.constraints import UniqueConstraint
+from yams import register_base_type, unregister_base_type
+
+from logilab.database import get_db_helper
 
 from cubicweb import (BadConnectionId, RepositoryError, ValidationError,
                       UnknownEid, AuthenticationError, Unauthorized, QueryError)
@@ -57,44 +60,6 @@
                           'type': u'violates unique_together constraints (cp, nom, type)'},
                      wraperr.exception.args[1])
 
-    def test_fill_schema(self):
-        origshema = self.repo.schema
-        try:
-            self.repo.schema = CubicWebSchema(self.repo.config.appid)
-            self.repo.config._cubes = None # avoid assertion error
-            self.repo.config.repairing = True # avoid versions checking
-            self.repo.fill_schema()
-            table = SQL_PREFIX + 'CWEType'
-            namecol = SQL_PREFIX + 'name'
-            finalcol = SQL_PREFIX + 'final'
-            self.session.set_cnxset()
-            cu = self.session.system_sql('SELECT %s FROM %s WHERE %s is NULL' % (
-                namecol, table, finalcol))
-            self.assertEqual(cu.fetchall(), [])
-            cu = self.session.system_sql('SELECT %s FROM %s WHERE %s=%%(final)s ORDER BY %s'
-                                         % (namecol, table, finalcol, namecol), {'final': True})
-            self.assertEqual(cu.fetchall(), [(u'BigInt',), (u'Boolean',), (u'Bytes',),
-                                             (u'Date',), (u'Datetime',),
-                                             (u'Decimal',),(u'Float',),
-                                             (u'Int',),
-                                             (u'Interval',), (u'Password',),
-                                             (u'String',),
-                                             (u'TZDatetime',), (u'TZTime',), (u'Time',)])
-            sql = ("SELECT etype.cw_eid, etype.cw_name, cstr.cw_eid, rel.eid_to "
-                   "FROM cw_CWUniqueTogetherConstraint as cstr, "
-                   "     relations_relation as rel, "
-                   "     cw_CWEType as etype "
-                   "WHERE cstr.cw_eid = rel.eid_from "
-                   "  AND cstr.cw_constraint_of = etype.cw_eid "
-                   "  AND etype.cw_name = 'Personne' "
-                   ";")
-            cu = self.session.system_sql(sql)
-            rows = cu.fetchall()
-            self.assertEqual(len(rows), 3)
-            self.test_unique_together()
-        finally:
-            self.repo.set_schema(origshema)
-
     def test_unique_together(self):
         person = self.repo.schema.eschema('Personne')
         self.assertEqual(len(person._unique_together), 1)
@@ -317,7 +282,8 @@
                                'constrained_by',
                                'cardinality', 'ordernum',
                                'indexed', 'fulltextindexed', 'internationalizable',
-                               'defaultval', 'description', 'description_format'])
+                               'defaultval', 'extra_props',
+                               'description', 'description_format'])
 
         self.assertEqual(schema.eschema('CWEType').main_attribute(), 'name')
         self.assertEqual(schema.eschema('State').main_attribute(), 'name')
@@ -584,6 +550,85 @@
             req.create_entity('Affaire', ref=u'AFF02')
             req.execute('SET A duration 10 WHERE A is Affaire')
 
+class SchemaDeserialTC(CubicWebTC):
+
+    appid = 'data-schemaserial'
+
+    @classmethod
+    def setUpClass(cls):
+        register_base_type('BabarTestType', ('jungle_speed',))
+        helper = get_db_helper('sqlite')
+        helper.TYPE_MAPPING['BabarTestType'] = 'TEXT'
+        helper.TYPE_CONVERTERS['BabarTestType'] = lambda x: '"%s"' % x
+        super(SchemaDeserialTC, cls).setUpClass()
+
+
+    @classmethod
+    def tearDownClass(cls):
+        unregister_base_type('BabarTestType')
+        helper = get_db_helper('sqlite')
+        helper.TYPE_MAPPING.pop('BabarTestType', None)
+        helper.TYPE_CONVERTERS.pop('BabarTestType', None)
+        super(SchemaDeserialTC, cls).tearDownClass()
+
+    def test_fill_schema(self):
+        origshema = self.repo.schema
+        try:
+            self.repo.schema = CubicWebSchema(self.repo.config.appid)
+            self.repo.config._cubes = None # avoid assertion error
+            self.repo.config.repairing = True # avoid versions checking
+            self.repo.fill_schema()
+            table = SQL_PREFIX + 'CWEType'
+            namecol = SQL_PREFIX + 'name'
+            finalcol = SQL_PREFIX + 'final'
+            self.session.set_cnxset()
+            cu = self.session.system_sql('SELECT %s FROM %s WHERE %s is NULL' % (
+                namecol, table, finalcol))
+            self.assertEqual(cu.fetchall(), [])
+            cu = self.session.system_sql('SELECT %s FROM %s WHERE %s=%%(final)s ORDER BY %s'
+                                         % (namecol, table, finalcol, namecol), {'final': True})
+            self.assertEqual(cu.fetchall(), [(u'BabarTestType',),
+                                             (u'BigInt',), (u'Boolean',), (u'Bytes',),
+                                             (u'Date',), (u'Datetime',),
+                                             (u'Decimal',),(u'Float',),
+                                             (u'Int',),
+                                             (u'Interval',), (u'Password',),
+                                             (u'String',),
+                                             (u'TZDatetime',), (u'TZTime',), (u'Time',)])
+            sql = ("SELECT etype.cw_eid, etype.cw_name, cstr.cw_eid, rel.eid_to "
+                   "FROM cw_CWUniqueTogetherConstraint as cstr, "
+                   "     relations_relation as rel, "
+                   "     cw_CWEType as etype "
+                   "WHERE cstr.cw_eid = rel.eid_from "
+                   "  AND cstr.cw_constraint_of = etype.cw_eid "
+                   "  AND etype.cw_name = 'Personne' "
+                   ";")
+            cu = self.session.system_sql(sql)
+            rows = cu.fetchall()
+            self.assertEqual(len(rows), 3)
+            person = self.repo.schema.eschema('Personne')
+            self.assertEqual(len(person._unique_together), 1)
+            self.assertItemsEqual(person._unique_together[0],
+                                  ('nom', 'prenom', 'inline2'))
+
+        finally:
+            self.repo.set_schema(origshema)
+
+    def test_custom_attribute_param(self):
+        origshema = self.repo.schema
+        try:
+            self.repo.schema = CubicWebSchema(self.repo.config.appid)
+            self.repo.config._cubes = None # avoid assertion error
+            self.repo.config.repairing = True # avoid versions checking
+            self.repo.fill_schema()
+            pes = self.repo.schema['Personne']
+            attr = pes.rdef('custom_field_of_jungle')
+            self.assertIn('jungle_speed', vars(attr))
+            self.assertEqual(42, attr.jungle_speed)
+        finally:
+            self.repo.set_schema(origshema)
+
+
 
 class DataHelpersTC(CubicWebTC):
 
--- a/server/test/unittest_schemaserial.py	Tue Apr 23 14:13:58 2013 +0200
+++ b/server/test/unittest_schemaserial.py	Fri Apr 26 17:46:56 2013 +0200
@@ -25,17 +25,32 @@
 from cubicweb.schema import CubicWebSchemaLoader
 from cubicweb.devtools import TestServerConfiguration
 
+from logilab.database import get_db_helper
+from yams import register_base_type, unregister_base_type
+
 def setUpModule(*args):
+    register_base_type('BabarTestType', ('jungle_speed',))
+    helper = get_db_helper('sqlite')
+    helper.TYPE_MAPPING['BabarTestType'] = 'TEXT'
+    helper.TYPE_CONVERTERS['BabarTestType'] = lambda x: '"%s"' % x
+
     global schema, config
     loader = CubicWebSchemaLoader()
-    config = TestServerConfiguration('data', apphome=Schema2RQLTC.datadir)
+    apphome = Schema2RQLTC.datadir + '-schemaserial'
+    config = TestServerConfiguration('data', apphome=apphome)
     config.bootstrap_cubes()
     schema = loader.load(config)
 
+
 def tearDownModule(*args):
     global schema, config
     del schema, config
 
+    unregister_base_type('BabarTestType')
+    helper = get_db_helper('sqlite')
+    helper.TYPE_MAPPING.pop('BabarTestType', None)
+    helper.TYPE_CONVERTERS.pop('BabarTestType', None)
+
 from cubicweb.server.schemaserial import *
 from cubicweb.server.schemaserial import _erperms2rql as erperms2rql
 
@@ -72,6 +87,13 @@
                                ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
                                 {'et': None, 'x': None})])
 
+    def test_esche2rql_custom_type(self):
+        expected = [('INSERT CWEType X: X description %(description)s,X final %(final)s,X name %(name)s',
+                     {'description': u'',
+                     'name': u'BabarTestType', 'final': True},)]
+        got = list(eschema2rql(schema.eschema('BabarTestType')))
+        self.assertListEqual(expected, got)
+
     def test_rschema2rql1(self):
         self.assertListEqual(list(rschema2rql(schema.rschema('relation_type'), cstrtypemap)),
                              [
@@ -136,6 +158,42 @@
              {'x': None, 'ct': u'StaticVocabularyConstraint_eid', 'value': u"u'?*', u'1*', u'+*', u'**', u'?+', u'1+', u'++', u'*+', u'?1', u'11', u'+1', u'*1', u'??', u'1?', u'+?', u'*?'"}),
             ])
 
+    def test_rschema2rql_custom_type(self):
+        expected = [('INSERT CWRType X: X description %(description)s,X final %(final)s,'
+                     'X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,'
+                     'X name %(name)s,X symmetric %(symmetric)s',
+                     {'description': u'',
+                      'final': True,
+                      'fulltext_container': None,
+                      'inlined': False,
+                      'name': u'custom_field_of_jungle',
+                      'symmetric': False}),
+                     ('INSERT CWAttribute X: X cardinality %(cardinality)s,'
+                      'X defaultval %(defaultval)s,X description %(description)s,'
+                      'X extra_props %(extra_props)s,X indexed %(indexed)s,'
+                      'X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,'
+                      'X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
+                      {'cardinality': u'?1',
+                       'defaultval': None,
+                       'description': u'',
+                       'extra_props': '{"jungle_speed": 42}',
+                       'indexed': False,
+                       'oe': None,
+                       'ordernum': 19,
+                       'rt': None,
+                       'se': None})]
+
+        got = list(rschema2rql(schema.rschema('custom_field_of_jungle'), cstrtypemap))
+        self.assertEqual(2, len(got))
+        # this is a custom type attribute with an extra parameter
+        self.assertIn('extra_props', got[1][1])
+        # this extr
+        extra_props = got[1][1]['extra_props']
+        from cubicweb import Binary
+        self.assertIsInstance(extra_props, Binary)
+        got[1][1]['extra_props'] = got[1][1]['extra_props'].getvalue()
+        self.assertListEqual(expected, got)
+
     def test_rdef2rql(self):
         self.assertListEqual(list(rdef2rql(schema['description_format'].rdefs[('CWRType', 'String')], cstrtypemap)),
                               [
--- a/test/unittest_schema.py	Tue Apr 23 14:13:58 2013 +0200
+++ b/test/unittest_schema.py	Fri Apr 26 17:46:56 2013 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -186,7 +186,7 @@
                               'data', 'data_encoding', 'data_format', 'data_name', 'default_workflow', 'defaultval', 'delete_permission',
                               'description', 'description_format', 'destination_state', 'dirige',
 
-                              'ecrit_par', 'eid', 'end_timestamp', 'evaluee', 'expression', 'exprtype',
+                              'ecrit_par', 'eid', 'end_timestamp', 'evaluee', 'expression', 'exprtype', 'extra_props',
 
                               'fabrique_par', 'final', 'firstname', 'for_user', 'fournit',
                               'from_entity', 'from_state', 'fulltext_container', 'fulltextindexed',