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