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