65ddca60042b615a70dd972a29352a87392e0be3
[sliver-openvswitch.git] / python / ovs / db / schema.py
1 # Copyright (c) 2009, 2010, 2011 Nicira Networks
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at:
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 import re
16 import sys
17
18 from ovs.db import error
19 import ovs.db.parser
20 from ovs.db import types
21
22 def _check_id(name, json):
23     if name.startswith('_'):
24         raise error.Error('names beginning with "_" are reserved', json)
25     elif not ovs.db.parser.is_identifier(name):
26         raise error.Error("name must be a valid id", json)
27
28 class DbSchema(object):
29     """Schema for an OVSDB database."""
30
31     def __init__(self, name, version, tables):
32         self.name = name
33         self.version = version
34         self.tables = tables
35
36         # "isRoot" was not part of the original schema definition.  Before it
37         # was added, there was no support for garbage collection.  So, for
38         # backward compatibility, if the root set is empty then assume that
39         # every table is in the root set.
40         if self.__root_set_size() == 0:
41             for table in self.tables.itervalues():
42                 table.is_root = True
43
44         # Validate that all ref_tables refer to the names of tables
45         # that exist.
46         #
47         # Also force certain columns to be persistent, as explained in
48         # __check_ref_table().  This requires 'is_root' to be known, so this
49         # must follow the loop updating 'is_root' above.
50         for table in self.tables.itervalues():
51             for column in table.columns.itervalues():
52                 self.__check_ref_table(column, column.type.key, "key")
53                 self.__check_ref_table(column, column.type.value, "value")
54
55     def __root_set_size(self):
56         """Returns the number of tables in the schema's root set."""
57         n_root = 0
58         for table in self.tables.itervalues():
59             if table.is_root:
60                 n_root += 1
61         return n_root
62
63     @staticmethod
64     def from_json(json):
65         parser = ovs.db.parser.Parser(json, "database schema")
66         name = parser.get("name", ['id'])
67         version = parser.get_optional("version", [unicode])
68         parser.get_optional("cksum", [unicode])
69         tablesJson = parser.get("tables", [dict])
70         parser.finish()
71
72         if (version is not None and
73             not re.match('[0-9]+\.[0-9]+\.[0-9]+$', version)):
74             raise error.Error('schema version "%s" not in format x.y.z'
75                               % version)
76
77         tables = {}
78         for tableName, tableJson in tablesJson.iteritems():
79             _check_id(tableName, json)
80             tables[tableName] = TableSchema.from_json(tableJson, tableName)
81
82         return DbSchema(name, version, tables)
83
84     def to_json(self):
85         # "isRoot" was not part of the original schema definition.  Before it
86         # was added, there was no support for garbage collection.  So, for
87         # backward compatibility, if every table is in the root set then do not
88         # output "isRoot" in table schemas.
89         default_is_root = self.__root_set_size() == len(self.tables)
90
91         tables = {}
92         for table in self.tables.itervalues():
93             tables[table.name] = table.to_json(default_is_root)
94         json = {"name": self.name, "tables": tables}
95         if self.version:
96             json["version"] = self.version
97         return json
98
99     def __check_ref_table(self, column, base, base_name):
100         if not base or base.type != types.UuidType or not base.ref_table:
101             return
102
103         ref_table = self.tables.get(base.ref_table)
104         if not ref_table:
105             raise error.Error("column %s %s refers to undefined table %s"
106                               % (column.name, base_name, base.ref_table),
107                               tag="syntax error")
108
109         if base.is_strong_ref() and not ref_table.is_root:
110             # We cannot allow a strong reference to a non-root table to be
111             # ephemeral: if it is the only reference to a row, then replaying
112             # the database log from disk will cause the referenced row to be
113             # deleted, even though it did exist in memory.  If there are
114             # references to that row later in the log (to modify it, to delete
115             # it, or just to point to it), then this will yield a transaction
116             # error.
117             column.persistent = True
118
119 class IdlSchema(DbSchema):
120     def __init__(self, name, version, tables, idlPrefix, idlHeader):
121         DbSchema.__init__(self, name, version, tables)
122         self.idlPrefix = idlPrefix
123         self.idlHeader = idlHeader
124
125     @staticmethod
126     def from_json(json):
127         parser = ovs.db.parser.Parser(json, "IDL schema")
128         idlPrefix = parser.get("idlPrefix", [unicode])
129         idlHeader = parser.get("idlHeader", [unicode])
130
131         subjson = dict(json)
132         del subjson["idlPrefix"]
133         del subjson["idlHeader"]
134         schema = DbSchema.from_json(subjson)
135
136         return IdlSchema(schema.name, schema.version, schema.tables,
137                          idlPrefix, idlHeader)
138
139 def column_set_from_json(json, columns):
140     if json is None:
141         return tuple(columns)
142     elif type(json) != list:
143         raise error.Error("array of distinct column names expected", json)
144     else:
145         for column_name in json:
146             if type(column_name) not in [str, unicode]:
147                 raise error.Error("array of distinct column names expected",
148                                   json)
149             elif column_name not in columns:
150                 raise error.Error("%s is not a valid column name"
151                                   % column_name, json)
152         if len(set(json)) != len(json):
153             # Duplicate.
154             raise error.Error("array of distinct column names expected", json)
155         return tuple([columns[column_name] for column_name in json])
156
157 class TableSchema(object):
158     def __init__(self, name, columns, mutable=True, max_rows=sys.maxint,
159                  is_root=True, indexes=[]):
160         self.name = name
161         self.columns = columns
162         self.mutable = mutable
163         self.max_rows = max_rows
164         self.is_root = is_root
165         self.indexes = indexes
166
167     @staticmethod
168     def from_json(json, name):
169         parser = ovs.db.parser.Parser(json, "table schema for table %s" % name)
170         columns_json = parser.get("columns", [dict])
171         mutable = parser.get_optional("mutable", [bool], True)
172         max_rows = parser.get_optional("maxRows", [int])
173         is_root = parser.get_optional("isRoot", [bool], False)
174         indexes_json = parser.get_optional("indexes", [list], [])
175         parser.finish()
176
177         if max_rows == None:
178             max_rows = sys.maxint
179         elif max_rows <= 0:
180             raise error.Error("maxRows must be at least 1", json)
181
182         if not columns_json:
183             raise error.Error("table must have at least one column", json)
184
185         columns = {}
186         for column_name, column_json in columns_json.iteritems():
187             _check_id(column_name, json)
188             columns[column_name] = ColumnSchema.from_json(column_json,
189                                                           column_name)
190
191         indexes = []
192         for index_json in indexes_json:
193             index = column_set_from_json(index_json, columns)
194             if not index:
195                 raise error.Error("index must have at least one column", json)
196             elif len(index) == 1:
197                 index[0].unique = True
198             for column in index:
199                 if not column.persistent:
200                     raise error.Error("ephemeral columns (such as %s) may "
201                                       "not be indexed" % column.name, json)
202             indexes.append(index)
203
204         return TableSchema(name, columns, mutable, max_rows, is_root, indexes)
205
206     def to_json(self, default_is_root=False):
207         """Returns this table schema serialized into JSON.
208
209         The "isRoot" member is included in the JSON only if its value would
210         differ from 'default_is_root'.  Ordinarily 'default_is_root' should be
211         false, because ordinarily a table would be not be part of the root set
212         if its "isRoot" member is omitted.  However, garbage collection was not
213         orginally included in OVSDB, so in older schemas that do not include
214         any "isRoot" members, every table is implicitly part of the root set.
215         To serialize such a schema in a way that can be read by older OVSDB
216         tools, specify 'default_is_root' as True.
217         """
218         json = {}
219         if not self.mutable:
220             json["mutable"] = False
221         if default_is_root != self.is_root:
222             json["isRoot"] = self.is_root
223
224         json["columns"] = columns = {}
225         for column in self.columns.itervalues():
226             if not column.name.startswith("_"):
227                 columns[column.name] = column.to_json()
228
229         if self.max_rows != sys.maxint:
230             json["maxRows"] = self.max_rows
231
232         if self.indexes:
233             json["indexes"] = []
234             for index in self.indexes:
235                 json["indexes"].append([column.name for column in index])
236
237         return json
238
239 class ColumnSchema(object):
240     def __init__(self, name, mutable, persistent, type_):
241         self.name = name
242         self.mutable = mutable
243         self.persistent = persistent
244         self.type = type_
245         self.unique = False
246
247     @staticmethod
248     def from_json(json, name):
249         parser = ovs.db.parser.Parser(json, "schema for column %s" % name)
250         mutable = parser.get_optional("mutable", [bool], True)
251         ephemeral = parser.get_optional("ephemeral", [bool], False)
252         type_ = types.Type.from_json(parser.get("type", [dict, unicode]))
253         parser.finish()
254
255         return ColumnSchema(name, mutable, not ephemeral, type_)
256
257     def to_json(self):
258         json = {"type": self.type.to_json()}
259         if not self.mutable:
260             json["mutable"] = False
261         if not self.persistent:
262             json["ephemeral"] = True
263         return json
264