cubicweb/server/schemaserial.py
changeset 11057 0b59724cb3f2
parent 10789 d5671a61f281
child 11151 4259c55df3e7
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/schemaserial.py	Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,656 @@
+# 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.
+#
+# 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/>.
+"""functions for schema / permissions (de)serialization using RQL"""
+from __future__ import print_function
+
+__docformat__ = "restructuredtext en"
+
+import os
+import json
+import sys
+
+from six import PY2, text_type, string_types
+
+from logilab.common.shellutils import ProgressBar, DummyProgressBar
+
+from yams import BadSchemaDefinition, schema as schemamod, buildobjs as ybo
+
+from cubicweb import Binary
+from cubicweb.schema import (KNOWN_RPROPERTIES, CONSTRAINTS, ETYPE_NAME_MAP,
+                             VIRTUAL_RTYPES)
+from cubicweb.server import sqlutils, schema2sql as y2sql
+
+
+def group_mapping(cnx, interactive=True):
+    """create a group mapping from an rql cursor
+
+    A group mapping has standard group names as key (managers, owners at least)
+    and the actual CWGroup entity's eid as associated value.
+    In interactive mode (the default), missing groups'eid will be prompted
+    from the user.
+    """
+    res = {}
+    for eid, name in cnx.execute('Any G, N WHERE G is CWGroup, G name N',
+                                    build_descr=False):
+        res[name] = eid
+    if not interactive:
+        return res
+    missing = [g for g in ('owners', 'managers', 'users', 'guests') if not g in res]
+    if missing:
+        print('some native groups are missing but the following groups have been found:')
+        print('\n'.join('* %s (%s)' % (n, eid) for n, eid in res.items()))
+        print()
+        print('enter the eid of a to group to map to each missing native group')
+        print('or just type enter to skip permissions granted to a group')
+        for group in missing:
+            while True:
+                value = raw_input('eid for group %s: ' % group).strip()
+                if not value:
+                    continue
+                try:
+                    eid = int(value)
+                except ValueError:
+                    print('eid should be an integer')
+                    continue
+                for eid_ in res.values():
+                    if eid == eid_:
+                        break
+                else:
+                    print('eid is not a group eid')
+                    continue
+                res[name] = eid
+                break
+    return res
+
+def cstrtype_mapping(cnx):
+    """cached constraint types mapping"""
+    map = dict(cnx.execute('Any T, X WHERE X is CWConstraintType, X name T'))
+    return map
+
+# schema / perms deserialization ##############################################
+
+def deserialize_schema(schema, cnx):
+    """return a schema according to information stored in an rql database
+    as CWRType and CWEType entities
+    """
+    repo = cnx.repo
+    dbhelper = repo.system_source.dbhelper
+
+    # Computed Rtype
+    with cnx.ensure_cnx_set:
+        tables = set(t.lower() for t in dbhelper.list_tables(cnx.cnxset.cu))
+        has_computed_relations = 'cw_cwcomputedrtype' in tables
+    # computed attribute
+    try:
+        cnx.system_sql("SELECT cw_formula FROM cw_CWAttribute")
+        has_computed_attributes = True
+    except Exception:
+        cnx.rollback()
+        has_computed_attributes = False
+
+    # XXX bw compat (3.6 migration)
+    sqlcu = cnx.system_sql("SELECT * FROM cw_CWRType WHERE cw_name='symetric'")
+    if sqlcu.fetchall():
+        sql = dbhelper.sql_rename_col('cw_CWRType', 'cw_symetric', 'cw_symmetric',
+                                      dbhelper.TYPE_MAPPING['Boolean'], True)
+        sqlcu.execute(sql)
+        sqlcu.execute("UPDATE cw_CWRType SET cw_name='symmetric' WHERE cw_name='symetric'")
+        cnx.commit()
+    ertidx = {}
+    copiedeids = set()
+    permsidx = deserialize_ertype_permissions(cnx)
+    schema.reading_from_database = True
+    # load every entity types
+    for eid, etype, desc in cnx.execute(
+        'Any X, N, D WHERE X is CWEType, X name N, X description D',
+        build_descr=False):
+        # base types are already in the schema, skip them
+        if etype in schemamod.BASE_TYPES:
+            # just set the eid
+            eschema = schema.eschema(etype)
+            eschema.eid = eid
+            ertidx[eid] = etype
+            continue
+        if etype in ETYPE_NAME_MAP:
+            needcopy = False
+            netype = ETYPE_NAME_MAP[etype]
+            # can't use write rql queries at this point, use raw sql
+            sqlexec = cnx.system_sql
+            if sqlexec('SELECT 1 FROM %(p)sCWEType WHERE %(p)sname=%%(n)s'
+                       % {'p': sqlutils.SQL_PREFIX}, {'n': netype}).fetchone():
+                # the new type already exists, we should copy (eg make existing
+                # instances of the old type instances of the new type)
+                assert etype.lower() != netype.lower()
+                needcopy = True
+            else:
+                # the new type doesn't exist, we should rename
+                sqlexec('UPDATE %(p)sCWEType SET %(p)sname=%%(n)s WHERE %(p)seid=%%(x)s'
+                        % {'p': sqlutils.SQL_PREFIX}, {'x': eid, 'n': netype})
+                if etype.lower() != netype.lower():
+                    alter_table_sql = dbhelper.sql_rename_table(sqlutils.SQL_PREFIX+etype,
+                                                                sqlutils.SQL_PREFIX+netype)
+                    sqlexec(alter_table_sql)
+            sqlexec('UPDATE entities SET type=%(n)s WHERE type=%(x)s',
+                    {'x': etype, 'n': netype})
+            cnx.commit(False)
+            tocleanup = [eid]
+            tocleanup += (eid for eid, cached in repo._type_source_cache.items()
+                          if etype == cached[0])
+            repo.clear_caches(tocleanup)
+            cnx.commit(False)
+            if needcopy:
+                ertidx[eid] = netype
+                copiedeids.add(eid)
+                # copy / CWEType entity removal expected to be done through
+                # rename_entity_type in a migration script
+                continue
+            etype = netype
+        ertidx[eid] = etype
+        eschema = schema.add_entity_type(
+            ybo.EntityType(name=etype, description=desc, eid=eid))
+        set_perms(eschema, permsidx)
+    # load inheritance relations
+    for etype, stype in cnx.execute(
+        'Any XN, ETN WHERE X is CWEType, X name XN, X specializes ET, ET name ETN',
+        build_descr=False):
+        etype = ETYPE_NAME_MAP.get(etype, etype)
+        stype = ETYPE_NAME_MAP.get(stype, stype)
+        schema.eschema(etype)._specialized_type = stype
+        schema.eschema(stype)._specialized_by.append(etype)
+    if has_computed_relations:
+        rset = cnx.execute(
+            'Any X, N, R, D WHERE X is CWComputedRType, X name N, '
+            'X rule R, X description D')
+        for eid, rule_name, rule, description in rset.rows:
+            rtype = ybo.ComputedRelation(name=rule_name, rule=rule, eid=eid,
+                                         description=description)
+            rschema = schema.add_relation_type(rtype)
+            set_perms(rschema, permsidx)
+    # load every relation types
+    for eid, rtype, desc, sym, il, ftc in cnx.execute(
+        'Any X,N,D,S,I,FTC WHERE X is CWRType, X name N, X description D, '
+        'X symmetric S, X inlined I, X fulltext_container FTC', build_descr=False):
+        ertidx[eid] = rtype
+        rschema = schema.add_relation_type(
+            ybo.RelationType(name=rtype, description=desc,
+                             symmetric=bool(sym), inlined=bool(il),
+                             fulltext_container=ftc, eid=eid))
+    # remains to load every relation definitions (ie relations and attributes)
+    cstrsidx = deserialize_rdef_constraints(cnx)
+    pendingrdefs = []
+    # closure to factorize common code of attribute/relation rdef addition
+    def _add_rdef(rdefeid, seid, reid, oeid, **kwargs):
+        rdef = ybo.RelationDefinition(ertidx[seid], ertidx[reid], ertidx[oeid],
+                                      constraints=cstrsidx.get(rdefeid, ()),
+                                      eid=rdefeid, **kwargs)
+        if seid in copiedeids or oeid in copiedeids:
+            # delay addition of this rdef. We'll insert them later if needed. We
+            # have to do this because:
+            #
+            # * on etype renaming, we want relation of the old entity type being
+            #   redirected to the new type during migration
+            #
+            # * in the case of a copy, we've to take care that rdef already
+            #   existing in the schema are not overwritten by a redirected one,
+            #   since we want correct eid on them (redirected rdef will be
+            #   removed in rename_entity_type)
+            pendingrdefs.append(rdef)
+        else:
+            # add_relation_def return a RelationDefinitionSchema if it has been
+            # actually added (can be None on duplicated relation definitions,
+            # e.g. if the relation type is marked as beeing symmetric)
+            rdefs = schema.add_relation_def(rdef)
+            if rdefs is not None:
+                ertidx[rdefeid] = rdefs
+                set_perms(rdefs, permsidx)
+    # Get the type parameters for additional base types.
+    try:
+        extra_props = dict(cnx.execute('Any X, XTP WHERE X is CWAttribute, '
+                                       'X extra_props XTP'))
+    except Exception:
+        cnx.critical('Previous CRITICAL notification about extra_props is not '
+                     'a problem if you are migrating to cubicweb 3.17')
+        extra_props = {} # not yet in the schema (introduced by 3.17 migration)
+
+    # load attributes
+    rql = ('Any X,SE,RT,OE,CARD,ORD,DESC,IDX,FTIDX,I18N,DFLT%(fm)s '
+           'WHERE X is CWAttribute, X relation_type RT, X cardinality CARD,'
+           '      X ordernum ORD, X indexed IDX, X description DESC, '
+           '      X internationalizable I18N, X defaultval DFLT,%(fmsnip)s'
+           '      X fulltextindexed FTIDX, X from_entity SE, X to_entity OE')
+    if has_computed_attributes:
+        rql = rql % {'fm': ',FM', 'fmsnip': 'X formula FM,'}
+    else:
+        rql = rql % {'fm': '', 'fmsnip': ''}
+    for values in cnx.execute(rql, build_descr=False):
+        attrs = dict(zip(
+            ('rdefeid', 'seid', 'reid', 'oeid', 'cardinality',
+             'order', 'description', 'indexed', 'fulltextindexed',
+             'internationalizable', 'default', 'formula'), values))
+        typeparams = extra_props.get(attrs['rdefeid'])
+        attrs.update(json.loads(typeparams.getvalue().decode('ascii')) if typeparams else {})
+        default = attrs['default']
+        if default is not None:
+            if isinstance(default, Binary):
+                # while migrating from 3.17 to 3.18, we still have to
+                # handle String defaults
+                attrs['default'] = default.unzpickle()
+        _add_rdef(**attrs)
+    # load relations
+    for values in cnx.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, '
+        'X from_entity SE, X to_entity OE, X composite C', build_descr=False):
+        rdefeid, seid, reid, oeid, card, ord, desc, comp = values
+        _add_rdef(rdefeid, seid, reid, oeid,
+                  cardinality=card, description=desc, order=ord,
+                  composite=comp)
+    for rdef in pendingrdefs:
+        try:
+            rdefs = schema.add_relation_def(rdef)
+        except BadSchemaDefinition:
+            continue
+        if rdefs is not None:
+            set_perms(rdefs, permsidx)
+    unique_togethers = {}
+    rset = cnx.execute(
+    'Any X,E,R WHERE '
+    'X is CWUniqueTogetherConstraint, '
+    'X constraint_of E, X relations R', build_descr=False)
+    for values in rset:
+        uniquecstreid, eeid, releid = values
+        eschema = schema.schema_by_eid(eeid)
+        relations = unique_togethers.setdefault(uniquecstreid, (eschema, []))
+        rel = ertidx[releid]
+        if isinstance(rel, schemamod.RelationDefinitionSchema):
+            # not yet migrated 3.9 database ('relations' target type changed
+            # to CWRType in 3.10)
+            rtype = rel.rtype.type
+        else:
+            rtype = str(rel)
+        relations[1].append(rtype)
+    for eschema, unique_together in unique_togethers.values():
+        eschema._unique_together.append(tuple(sorted(unique_together)))
+    schema.infer_specialization_rules()
+    cnx.commit()
+    schema.finalize()
+    schema.reading_from_database = False
+
+
+def deserialize_ertype_permissions(cnx):
+    """return sect action:groups associations for the given
+    entity or relation schema with its eid, according to schema's
+    permissions stored in the database as [read|add|delete|update]_permission
+    relations between CWEType/CWRType and CWGroup entities
+    """
+    res = {}
+    for action in ('read', 'add', 'update', 'delete'):
+        rql = 'Any E,N WHERE G is CWGroup, G name N, E %s_permission G' % action
+        for eid, gname in cnx.execute(rql, build_descr=False):
+            res.setdefault(eid, {}).setdefault(action, []).append(gname)
+        rql = ('Any E,X,EXPR,V WHERE X is RQLExpression, X expression EXPR, '
+               'E %s_permission X, X mainvars V' % action)
+        for eid, expreid, expr, mainvars in cnx.execute(rql, build_descr=False):
+            # we don't know yet if it's a rql expr for an entity or a relation,
+            # so append a tuple to differentiate from groups and so we'll be
+            # able to instantiate it later
+            res.setdefault(eid, {}).setdefault(action, []).append( (expr, mainvars, expreid) )
+    return res
+
+def deserialize_rdef_constraints(cnx):
+    """return the list of relation definition's constraints as instances"""
+    res = {}
+    for rdefeid, ceid, ct, val in cnx.execute(
+        'Any E, X,TN,V WHERE E constrained_by X, X is CWConstraint, '
+        'X cstrtype T, T name TN, X value V', build_descr=False):
+        cstr = CONSTRAINTS[ct].deserialize(val)
+        cstr.eid = ceid
+        res.setdefault(rdefeid, []).append(cstr)
+    return res
+
+def set_perms(erschema, permsidx):
+    """set permissions on the given erschema according to the permission
+    definition dictionary as built by deserialize_ertype_permissions for a
+    given erschema's eid
+    """
+    # reset erschema permissions here to avoid getting yams default anyway
+    erschema.permissions = dict((action, ()) for action in erschema.ACTIONS)
+    try:
+        thispermsdict = permsidx[erschema.eid]
+    except KeyError:
+        return
+    for action, somethings in thispermsdict.items():
+        erschema.permissions[action] = tuple(
+            isinstance(p, tuple) and erschema.rql_expression(*p) or p
+            for p in somethings)
+
+
+# schema / perms serialization ################################################
+
+def serialize_schema(cnx, schema):
+    """synchronize schema and permissions in the database according to
+    current schema
+    """
+    _title = '-> storing the schema in the database '
+    print(_title, end=' ')
+    execute = cnx.execute
+    eschemas = schema.entities()
+    pb_size = (len(eschemas + schema.relations())
+               + len(CONSTRAINTS)
+               + len([x for x in eschemas if x.specializes()]))
+    if sys.stdout.isatty():
+        pb = ProgressBar(pb_size, title=_title)
+    else:
+        pb = DummyProgressBar()
+    groupmap = group_mapping(cnx, interactive=False)
+    # serialize all entity types, assuring CWEType is serialized first for proper
+    # is / is_instance_of insertion
+    eschemas.remove(schema.eschema('CWEType'))
+    eschemas.insert(0, schema.eschema('CWEType'))
+    for eschema in eschemas:
+        execschemarql(execute, eschema, eschema2rql(eschema, groupmap))
+        pb.update()
+    # serialize constraint types
+    cstrtypemap = {}
+    rql = 'INSERT CWConstraintType X: X name %(ct)s'
+    for cstrtype in CONSTRAINTS:
+        cstrtypemap[cstrtype] = execute(rql, {'ct': text_type(cstrtype)},
+                                        build_descr=False)[0][0]
+        pb.update()
+    # serialize relations
+    for rschema in schema.relations():
+        # skip virtual relations such as eid, has_text and identity
+        if rschema in VIRTUAL_RTYPES:
+            pb.update()
+            continue
+        if rschema.rule:
+            execschemarql(execute, rschema, crschema2rql(rschema, groupmap))
+            pb.update()
+            continue
+        execschemarql(execute, rschema, rschema2rql(rschema, addrdef=False))
+        if rschema.symmetric:
+            rdefs = [rdef for k, rdef in rschema.rdefs.items()
+                     if (rdef.subject, rdef.object) == k]
+        else:
+            rdefs = rschema.rdefs.values()
+        for rdef in rdefs:
+            execschemarql(execute, rdef,
+                          rdef2rql(rdef, cstrtypemap, groupmap))
+        pb.update()
+    # serialize unique_together constraints
+    for eschema in eschemas:
+        if eschema._unique_together:
+            execschemarql(execute, eschema, uniquetogether2rqls(eschema))
+    # serialize yams inheritance relationships
+    for rql, kwargs in specialize2rql(schema):
+        execute(rql, kwargs, build_descr=False)
+        pb.update()
+    print()
+
+
+# high level serialization functions
+
+def execschemarql(execute, schema, rqls):
+    for rql, kwargs in rqls:
+        kwargs['x'] = schema.eid
+        rset = execute(rql, kwargs, build_descr=False)
+        if schema.eid is None:
+            schema.eid = rset[0][0]
+        else:
+            assert rset
+
+def erschema2rql(erschema, groupmap):
+    if isinstance(erschema, schemamod.EntitySchema):
+        return eschema2rql(erschema, groupmap=groupmap)
+    return rschema2rql(erschema, groupmap=groupmap)
+
+def specialize2rql(schema):
+    for eschema in schema.entities():
+        if eschema.final:
+            continue
+        for rql, kwargs in eschemaspecialize2rql(eschema):
+            yield rql, kwargs
+
+# etype serialization
+
+def eschema2rql(eschema, groupmap=None):
+    """return a list of rql insert statements to enter an entity schema
+    in the database as an CWEType entity
+    """
+    relations, values = eschema_relations_values(eschema)
+    # NOTE: 'specializes' relation can't be inserted here since there's no
+    # way to make sure the parent type is inserted before the child type
+    yield 'INSERT CWEType X: %s' % ','.join(relations) , values
+    # entity permissions
+    if groupmap is not None:
+        for rql, args in _erperms2rql(eschema, groupmap):
+            yield rql, args
+
+def eschema_relations_values(eschema):
+    values = _ervalues(eschema)
+    relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
+    return relations, values
+
+def eschemaspecialize2rql(eschema):
+    specialized_type = eschema.specializes()
+    if specialized_type:
+        values = {'x': eschema.eid, 'et': specialized_type.eid}
+        yield 'SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s', values
+
+def uniquetogether2rqls(eschema):
+    rql_args = []
+    # robustness against duplicated CWUniqueTogetherConstraint (pre 3.18)
+    columnset = set()
+    for columns in eschema._unique_together:
+        if columns in columnset:
+            print('schemaserial: skipping duplicate unique together %r %r' %
+                  (eschema.type, columns))
+            continue
+        columnset.add(columns)
+        rql, args = _uniquetogether2rql(eschema, columns)
+        args['name'] = y2sql.unique_index_name(eschema, columns)
+        rql_args.append((rql, args))
+    return rql_args
+
+def _uniquetogether2rql(eschema, unique_together):
+    relations = []
+    restrictions = []
+    substs = {}
+    for i, name in enumerate(unique_together):
+        rschema = eschema.schema.rschema(name)
+        rtype = 'T%d' % i
+        substs[rtype] = text_type(rschema.type)
+        relations.append('C relations %s' % rtype)
+        restrictions.append('%(rtype)s name %%(%(rtype)s)s' % {'rtype': rtype})
+    relations = ', '.join(relations)
+    restrictions = ', '.join(restrictions)
+    rql = ('INSERT CWUniqueTogetherConstraint C: C name %%(name)s, C constraint_of X, %s '
+           'WHERE X eid %%(x)s, %s')
+    return rql % (relations, restrictions), substs
+
+
+def _ervalues(erschema):
+    try:
+        type_ = text_type(erschema.type)
+    except UnicodeDecodeError as e:
+        raise Exception("can't decode %s [was %s]" % (erschema.type, e))
+    try:
+        desc = text_type(erschema.description) or u''
+    except UnicodeDecodeError as e:
+        raise Exception("can't decode %s [was %s]" % (erschema.description, e))
+    return {
+        'name': type_,
+        'final': erschema.final,
+        'description': desc,
+        }
+
+# rtype serialization
+
+def rschema2rql(rschema, cstrtypemap=None, addrdef=True, groupmap=None):
+    """generate rql insert statements to enter a relation schema
+    in the database as an CWRType entity
+    """
+    if rschema.type == 'has_text':
+        return
+    relations, values = rschema_relations_values(rschema)
+    yield 'INSERT CWRType X: %s' % ','.join(relations), values
+    if addrdef:
+        assert cstrtypemap
+        # sort for testing purpose
+        for rdef in sorted(rschema.rdefs.values(),
+                           key=lambda x: (x.subject, x.object)):
+            for rql, values in rdef2rql(rdef, cstrtypemap, groupmap):
+                yield rql, values
+
+def rschema_relations_values(rschema):
+    values = _ervalues(rschema)
+    values['final'] = rschema.final
+    values['symmetric'] = rschema.symmetric
+    values['inlined'] = rschema.inlined
+    if PY2 and isinstance(rschema.fulltext_container, str):
+        values['fulltext_container'] = unicode(rschema.fulltext_container)
+    else:
+        values['fulltext_container'] = rschema.fulltext_container
+    relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
+    return relations, values
+
+def crschema2rql(crschema, groupmap):
+    relations, values = crschema_relations_values(crschema)
+    yield 'INSERT CWComputedRType X: %s' % ','.join(relations), values
+    if groupmap:
+        for rql, args in _erperms2rql(crschema, groupmap):
+            yield rql, args
+
+def crschema_relations_values(crschema):
+    values = _ervalues(crschema)
+    values['rule'] = text_type(crschema.rule)
+    # XXX why oh why?
+    del values['final']
+    relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
+    return relations, values
+
+# rdef serialization
+
+def rdef2rql(rdef, cstrtypemap, groupmap=None):
+    # don't serialize inferred relations
+    if rdef.infered:
+        return
+    relations, values = _rdef_values(rdef)
+    relations.append('X relation_type ER,X from_entity SE,X to_entity OE')
+    values.update({'se': rdef.subject.eid, 'rt': rdef.rtype.eid, 'oe': rdef.object.eid})
+    if rdef.final:
+        etype = 'CWAttribute'
+    else:
+        etype = 'CWRelation'
+    yield 'INSERT %s X: %s WHERE SE eid %%(se)s,ER eid %%(rt)s,OE eid %%(oe)s' % (
+        etype, ','.join(relations), ), values
+    for rql, values in constraints2rql(cstrtypemap, rdef.constraints):
+        yield rql, values
+    # no groupmap means "no security insertion"
+    if groupmap:
+        for rql, args in _erperms2rql(rdef, groupmap):
+            yield rql, args
+
+_IGNORED_PROPS = ['eid', 'constraints', 'uid', 'infered', 'permissions']
+
+def _rdef_values(rdef):
+    amap = {'order': 'ordernum', 'default': 'defaultval'}
+    values = {}
+    extra = {}
+    for prop in rdef.rproperty_defs(rdef.object):
+        if prop in _IGNORED_PROPS:
+            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)
+        elif prop == 'ordernum':
+            value = int(value)
+        elif PY2 and isinstance(value, str):
+            value = unicode(value)
+        if value is not None and prop == 'default':
+            value = Binary.zpickle(value)
+        values[amap.get(prop, prop)] = value
+    if extra:
+        values['extra_props'] = Binary(json.dumps(extra).encode('ascii'))
+    relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
+    return relations, values
+
+def constraints2rql(cstrtypemap, constraints, rdefeid=None):
+    for constraint in constraints:
+        values = {'ct': cstrtypemap[constraint.type()],
+                  'value': text_type(constraint.serialize()),
+                  'x': rdefeid} # when not specified, will have to be set by the caller
+        yield 'INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE \
+CT eid %(ct)s, EDEF eid %(x)s', values
+
+
+def _erperms2rql(erschema, groupmap):
+    """return rql insert statements to enter the entity or relation
+    schema's permissions in the database as
+    [read|add|delete|update]_permission relations between CWEType/CWRType
+    and CWGroup entities
+    """
+    for action in erschema.ACTIONS:
+        try:
+            grantedto = erschema.action_permissions(action)
+        except KeyError:
+            # may occurs when modifying persistent schema
+            continue
+        for group_or_rqlexpr in grantedto:
+            if isinstance(group_or_rqlexpr, string_types):
+                # group
+                try:
+                    yield ('SET X %s_permission Y WHERE Y eid %%(g)s, X eid %%(x)s' % action,
+                           {'g': groupmap[group_or_rqlexpr]})
+                except KeyError:
+                    print("WARNING: group %s used in permissions for %s was ignored because it doesn't exist."
+                          " You may want to add it into a precreate.py file" % (group_or_rqlexpr, erschema))
+                    continue
+            else:
+                # rqlexpr
+                rqlexpr = group_or_rqlexpr
+                yield ('INSERT RQLExpression E: E expression %%(e)s, E exprtype %%(t)s, '
+                       'E mainvars %%(v)s, X %s_permission E WHERE X eid %%(x)s' % action,
+                       {'e': text_type(rqlexpr.expression),
+                        'v': text_type(','.join(sorted(rqlexpr.mainvars))),
+                        't': text_type(rqlexpr.__class__.__name__)})
+
+# update functions
+
+def updateeschema2rql(eschema, eid):
+    relations, values = eschema_relations_values(eschema)
+    values['x'] = eid
+    yield 'SET %s WHERE X eid %%(x)s' % ','.join(relations), values
+
+def updaterschema2rql(rschema, eid):
+    if rschema.rule:
+        yield ('SET X rule %(r)s WHERE X eid %(x)s',
+               {'x': eid, 'r': text_type(rschema.rule)})
+    else:
+        relations, values = rschema_relations_values(rschema)
+        values['x'] = eid
+        yield 'SET %s WHERE X eid %%(x)s' % ','.join(relations), values
+
+def updaterdef2rql(rdef, eid):
+    relations, values = _rdef_values(rdef)
+    values['x'] = eid
+    yield 'SET %s WHERE X eid %%(x)s' % ','.join(relations), values