1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # mojo, a Python library for implementing document based databases
5 # Copyright (C) 2013-2014 by Javier Sancho Fernandez <jsf at jsancho dot org>
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
26 class Connection(object):
27 def __init__(self, *args, **kwargs):
30 def __getattr__(self, db_name):
31 return Database(self, db_name)
33 def __getitem__(self, *args, **kwargs):
34 return self.__getattr__(*args, **kwargs)
37 return "Connection(%s)" % self._db_con
39 def _get_databases(self):
42 def database_names(self):
44 return [unicode(x) for x in self._get_databases()]
48 def _get_tables(self, db_name):
51 def collection_names(self, db_name):
53 return list(set([unicode(x.split('$')[0]) for x in filter(lambda x: '$' in x, self._get_tables(db_name))]))
57 def _count_rows(self, db_name, table_name):
60 def _count(self, db_name, table_name):
62 return self._count_rows(db_name, table_name + '$_id')
66 def _create_database(self, db_name):
69 def _create_table(self, db_name, table_name, fields):
70 # [{'name': 'id', 'type': 'char', 'size': 20, 'primary': True}]
73 def _get_cursor(self, db_name, query):
74 # {'select': [('t1$_id', 'id'), {'select': [('t1$c1', 'value')], 'from': ['t1$c1'], 'where': [(('t1$c1', 'id'), '=', ('t1$_id', 'id'))]}], 'from': ['t1$_id']}
77 def _next(self, cursor):
80 def _insert(self, db_name, table_name, values):
90 class Database(object):
91 def __init__(self, connection, db_name):
92 self.connection = connection
93 self.db_name = unicode(db_name)
95 def __getattr__(self, table_name):
96 return Collection(self, table_name)
98 def __getitem__(self, *args, **kwargs):
99 return self.__getattr__(*args, **kwargs)
102 return "Database(%r, %r)" % (self.connection, self.db_name)
104 def _create_database(self):
105 return self.connection._create_database(self.db_name)
108 return (self.db_name in self.connection.database_names())
110 def collection_names(self):
111 return self.connection.collection_names(self.db_name)
114 class Collection(object):
115 def __init__(self, database, table_name):
116 self.database = database
117 self.table_name = unicode(table_name)
120 return "Collection(%r, %r)" % (self.database, self.table_name)
123 return (self.database.exists() and self.table_name in self.database.collection_names())
125 def _create_table(self):
127 {'name': 'id', 'type': 'char', 'size': 32, 'primary': True},
129 return self.database.connection._create_table(self.database.db_name, '%s$_id' % self.table_name, fields)
131 def _create_field(self, field_name):
133 {'name': 'id', 'type': 'char', 'size': 32, 'primary': True},
134 {'name': 'value', 'type': 'text', 'null': False},
135 {'name': 'number', 'type': 'float'},
137 return self.database.connection._create_table(self.database.db_name, '%s$%s' % (self.table_name, field_name), fields)
139 def _get_fields(self):
140 tables = self.database.connection._get_tables(self.database.db_name)
141 return [unicode(x[x.find('$')+1:]) for x in filter(lambda x: x.startswith('%s$' % self.table_name), tables)]
144 return self.database.connection._count(self.database.db_name, self.table_name)
146 def find(self, *args, **kwargs):
147 return Cursor(self, *args, **kwargs)
149 def insert(self, doc_or_docs):
150 if not self.database.db_name in self.database.connection.database_names():
151 self.database._create_database()
152 if not self.table_name in self.database.collection_names():
155 if not type(doc_or_docs) in (list, tuple):
161 doc['_id'] = uuid.uuid4().hex
162 self._insert_document(doc)
164 if type(doc_or_docs) in (list, tuple):
165 return [d['_id'] for d in docs]
167 return docs[0]['_id']
169 def _insert_document(self, doc):
170 table_id = '%s$_id' % self.table_name
171 fields = self._get_fields()
172 self.database.connection._insert(self.database.db_name, table_id, {'id': doc['_id']})
177 self._create_field(f)
178 table_f = '%s$%s' % (self.table_name, f)
181 'value': cPickle.dumps(doc[f]),
183 if type(doc[f]) in (int, float):
184 values['number'] = doc[f]
185 self.database.connection._insert(self.database.db_name, table_f, values)
188 class Cursor(object):
189 def __init__(self, collection, spec=None, fields=None, **kwargs):
190 if spec and not type(spec) is dict:
191 raise Exception("spec must be an instance of dict")
193 self.collection = collection
195 if self.collection.exists():
196 self.fields = self._get_fields(fields)
197 self.cursor = self._get_cursor()
205 def _get_fields(self, fields):
206 set_all_fields = set(self.collection._get_fields())
208 res_fields = list(set_all_fields)
209 elif type(fields) is dict:
210 fields_without_id = filter(lambda x: x[0] != '_id', fields.iteritems())
211 if fields_without_id[0][1]:
216 res_fields = set(set_all_fields)
217 for f in fields_without_id:
218 if f[1] and f[0] in set_all_fields:
222 raise Exception("You cannot currently mix including and excluding fields. Contact us if this is an issue.")
225 res_fields.discard(f[0])
227 raise Exception("You cannot currently mix including and excluding fields. Contact us if this is an issue.")
228 if '_id' in fields and not fields['_id']:
229 res_fields.discard('_id')
231 res_fields.add('_id')
232 res_fields = list(res_fields)
234 set_fields = set(list(fields))
235 set_fields.add('_id')
236 res_fields = list(set_all_fields.intersection(set_fields))
240 def _get_cursor(self):
242 table_id = '%s$_id' % self.collection.table_name
244 query['select'] = [(table_id, 'id')]
245 for f in filter(lambda x: x != '_id', self.fields):
246 table_f = '%s$%s' % (self.collection.table_name, f)
247 q = self._get_cursor_field(table_id, table_f)
248 query['select'].append(q)
250 query['from'] = [table_id]
254 for k, v in self.spec.iteritems():
255 table_f = '%s$%s' % (self.collection.table_name, k)
256 field_q = self._get_cursor_field(table_id, table_f)
257 query['where'].append((field_q, '=', v))
259 return self.collection.database.connection._get_cursor(self.collection.database.db_name, query)
261 def _get_cursor_field(self, table_id, table_field):
263 'select': [(table_field, 'value')],
264 'from': [table_field],
265 'where': [((table_field, 'id'), '=', (table_id, 'id'))],
269 if self.cursor is None:
273 res = self.collection.database.connection._next(self.cursor)
278 if '_id' in self.fields:
279 document['_id'] = res[0]
280 fields_without_id = filter(lambda x: x != '_id', self.fields)
281 for i in xrange(len(fields_without_id)):
282 if not res[i + 1] is None:
283 document[fields_without_id[i]] = cPickle.loads(res[i + 1])