python: Implement write support in Python IDL for OVSDB.
[sliver-openvswitch.git] / python / ovs / db / schema.py
index e0e0daf..675f4ec 100644 (file)
@@ -19,6 +19,12 @@ from ovs.db import error
 import ovs.db.parser
 from ovs.db import types
 
+def _check_id(name, json):
+    if name.startswith('_'):
+        raise error.Error('names beginning with "_" are reserved', json)
+    elif not ovs.db.parser.is_identifier(name):
+        raise error.Error("name must be a valid id", json)
+
 class DbSchema(object):
     """Schema for an OVSDB database."""
 
@@ -27,13 +33,6 @@ class DbSchema(object):
         self.version = version
         self.tables = tables
 
-        # Validate that all ref_tables refer to the names of tables
-        # that exist.
-        for table in self.tables.itervalues():
-            for column in table.columns.itervalues():
-                self.__check_ref_table(column, column.type.key, "key")
-                self.__check_ref_table(column, column.type.value, "value")
-
         # "isRoot" was not part of the original schema definition.  Before it
         # was added, there was no support for garbage collection.  So, for
         # backward compatibility, if the root set is empty then assume that
@@ -42,6 +41,16 @@ class DbSchema(object):
             for table in self.tables.itervalues():
                 table.is_root = True
 
+        # Find the "ref_table"s referenced by "ref_table_name"s.
+        #
+        # Also force certain columns to be persistent, as explained in
+        # __check_ref_table().  This requires 'is_root' to be known, so this
+        # must follow the loop updating 'is_root' above.
+        for table in self.tables.itervalues():
+            for column in table.columns.itervalues():
+                self.__follow_ref_table(column, column.type.key, "key")
+                self.__follow_ref_table(column, column.type.value, "value")
+
     def __root_set_size(self):
         """Returns the number of tables in the schema's root set."""
         n_root = 0
@@ -54,23 +63,19 @@ class DbSchema(object):
     def from_json(json):
         parser = ovs.db.parser.Parser(json, "database schema")
         name = parser.get("name", ['id'])
-        version = parser.get_optional("version", [unicode])
-        parser.get_optional("cksum", [unicode])
+        version = parser.get_optional("version", [str, unicode])
+        parser.get_optional("cksum", [str, unicode])
         tablesJson = parser.get("tables", [dict])
         parser.finish()
 
         if (version is not None and
             not re.match('[0-9]+\.[0-9]+\.[0-9]+$', version)):
-            raise error.Error("schema version \"%s\" not in format x.y.z"
+            raise error.Error('schema version "%s" not in format x.y.z'
                               % version)
 
         tables = {}
         for tableName, tableJson in tablesJson.iteritems():
-            if tableName.startswith('_'):
-                raise error.Error("names beginning with \"_\" are reserved",
-                                  json)
-            elif not ovs.db.parser.is_identifier(tableName):
-                raise error.Error("name must be a valid id", json)
+            _check_id(tableName, json)
             tables[tableName] = TableSchema.from_json(tableJson, tableName)
 
         return DbSchema(name, version, tables)
@@ -90,13 +95,29 @@ class DbSchema(object):
             json["version"] = self.version
         return json
 
-    def __check_ref_table(self, column, base, base_name):
-        if (base and base.type == types.UuidType and base.ref_table and
-            base.ref_table not in self.tables):
+    def copy(self):
+        return DbSchema.from_json(self.to_json())
+
+    def __follow_ref_table(self, column, base, base_name):
+        if not base or base.type != types.UuidType or not base.ref_table_name:
+            return
+
+        base.ref_table = self.tables.get(base.ref_table_name)
+        if not base.ref_table:
             raise error.Error("column %s %s refers to undefined table %s"
-                              % (column.name, base_name, base.ref_table),
+                              % (column.name, base_name, base.ref_table_name),
                               tag="syntax error")
 
+        if base.is_strong_ref() and not base.ref_table.is_root:
+            # We cannot allow a strong reference to a non-root table to be
+            # ephemeral: if it is the only reference to a row, then replaying
+            # the database log from disk will cause the referenced row to be
+            # deleted, even though it did exist in memory.  If there are
+            # references to that row later in the log (to modify it, to delete
+            # it, or just to point to it), then this will yield a transaction
+            # error.
+            column.persistent = True
+
 class IdlSchema(DbSchema):
     def __init__(self, name, version, tables, idlPrefix, idlHeader):
         DbSchema.__init__(self, name, version, tables)
@@ -106,8 +127,8 @@ class IdlSchema(DbSchema):
     @staticmethod
     def from_json(json):
         parser = ovs.db.parser.Parser(json, "IDL schema")
-        idlPrefix = parser.get("idlPrefix", [unicode])
-        idlHeader = parser.get("idlHeader", [unicode])
+        idlPrefix = parser.get("idlPrefix", [str, unicode])
+        idlHeader = parser.get("idlHeader", [str, unicode])
 
         subjson = dict(json)
         del subjson["idlPrefix"]
@@ -117,22 +138,42 @@ class IdlSchema(DbSchema):
         return IdlSchema(schema.name, schema.version, schema.tables,
                          idlPrefix, idlHeader)
 
+def column_set_from_json(json, columns):
+    if json is None:
+        return tuple(columns)
+    elif type(json) != list:
+        raise error.Error("array of distinct column names expected", json)
+    else:
+        for column_name in json:
+            if type(column_name) not in [str, unicode]:
+                raise error.Error("array of distinct column names expected",
+                                  json)
+            elif column_name not in columns:
+                raise error.Error("%s is not a valid column name"
+                                  % column_name, json)
+        if len(set(json)) != len(json):
+            # Duplicate.
+            raise error.Error("array of distinct column names expected", json)
+        return tuple([columns[column_name] for column_name in json])
+
 class TableSchema(object):
     def __init__(self, name, columns, mutable=True, max_rows=sys.maxint,
-                 is_root=True):
+                 is_root=True, indexes=[]):
         self.name = name
         self.columns = columns
         self.mutable = mutable
         self.max_rows = max_rows
         self.is_root = is_root
+        self.indexes = indexes
 
     @staticmethod
     def from_json(json, name):
         parser = ovs.db.parser.Parser(json, "table schema for table %s" % name)
-        columnsJson = parser.get("columns", [dict])
+        columns_json = parser.get("columns", [dict])
         mutable = parser.get_optional("mutable", [bool], True)
         max_rows = parser.get_optional("maxRows", [int])
         is_root = parser.get_optional("isRoot", [bool], False)
+        indexes_json = parser.get_optional("indexes", [list], [])
         parser.finish()
 
         if max_rows == None:
@@ -140,20 +181,29 @@ class TableSchema(object):
         elif max_rows <= 0:
             raise error.Error("maxRows must be at least 1", json)
 
-        if not columnsJson:
+        if not columns_json:
             raise error.Error("table must have at least one column", json)
 
         columns = {}
-        for columnName, columnJson in columnsJson.iteritems():
-            if columnName.startswith('_'):
-                raise error.Error("names beginning with \"_\" are reserved",
-                                  json)
-            elif not ovs.db.parser.is_identifier(columnName):
-                raise error.Error("name must be a valid id", json)
-            columns[columnName] = ColumnSchema.from_json(columnJson,
-                                                         columnName)
-
-        return TableSchema(name, columns, mutable, max_rows, is_root)
+        for column_name, column_json in columns_json.iteritems():
+            _check_id(column_name, json)
+            columns[column_name] = ColumnSchema.from_json(column_json,
+                                                          column_name)
+
+        indexes = []
+        for index_json in indexes_json:
+            index = column_set_from_json(index_json, columns)
+            if not index:
+                raise error.Error("index must have at least one column", json)
+            elif len(index) == 1:
+                index[0].unique = True
+            for column in index:
+                if not column.persistent:
+                    raise error.Error("ephemeral columns (such as %s) may "
+                                      "not be indexed" % column.name, json)
+            indexes.append(index)
+
+        return TableSchema(name, columns, mutable, max_rows, is_root, indexes)
 
     def to_json(self, default_is_root=False):
         """Returns this table schema serialized into JSON.
@@ -181,24 +231,30 @@ class TableSchema(object):
         if self.max_rows != sys.maxint:
             json["maxRows"] = self.max_rows
 
+        if self.indexes:
+            json["indexes"] = []
+            for index in self.indexes:
+                json["indexes"].append([column.name for column in index])
+
         return json
 
 class ColumnSchema(object):
-    def __init__(self, name, mutable, persistent, type):
+    def __init__(self, name, mutable, persistent, type_):
         self.name = name
         self.mutable = mutable
         self.persistent = persistent
-        self.type = type
+        self.type = type_
+        self.unique = False
 
     @staticmethod
     def from_json(json, name):
         parser = ovs.db.parser.Parser(json, "schema for column %s" % name)
         mutable = parser.get_optional("mutable", [bool], True)
         ephemeral = parser.get_optional("ephemeral", [bool], False)
-        type = types.Type.from_json(parser.get("type", [dict, unicode]))
+        type_ = types.Type.from_json(parser.get("type", [dict, str, unicode]))
         parser.finish()
 
-        return ColumnSchema(name, mutable, not ephemeral, type)
+        return ColumnSchema(name, mutable, not ephemeral, type_)
 
     def to_json(self):
         json = {"type": self.type.to_json()}