Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

I think I've seen this ORM somewhere before...

root/trunk/dejavu/storage/multischema.py

Revision 477 (checked in by fumanchu, 6 years ago)

Sort Modeler columns, plus comments and some lazier imports.

  • Property svn:eol-style set to native
Line 
1 """A database Storage Manager which mixes multiple schemas.
2
3 This SM depends on each Dejavu class in the model possessing
4 an additional '_dbschema_name' attribute.
5 """
6
7 import threading
8
9 import geniusql
10 import dejavu
11 from dejavu import storage, logflags, xray
12 from dejavu.storage import db
13
14
15 # --------------------------- Storage Manager --------------------------- #
16
17
18 class MultiSchemaStorageManagerDB(db.StorageManagerDB):
19     """StoreManager base class to save and retrieve Units using a DB."""
20    
21     def __init__(self, allOptions={}):
22         storage.StorageManager.__init__(self, allOptions)
23         self.reserve_lock = threading.Lock()
24        
25         # Config Overrides
26         def get_option(name):
27             item = allOptions.get(name)
28             if isinstance(item, basestring):
29                 item = xray.classes(item)
30             return item
31        
32         dbclass = get_option('Database Class')
33         if dbclass:
34             self.databaseclass = dbclass
35        
36         allOptions = dict([(str(k), v) for k, v in allOptions.iteritems()])
37        
38         self.db = self.databaseclass(**allOptions)
39         self.schemas = {}
40         self._table_map = {}
41        
42         def logger(msg):
43             if self.logflags & logflags.SQL:
44                 self.log(logflags.SQL.message(msg))
45         self.db.log = logger
46    
47     def _seq_UnitSequencerInteger(self, unit):
48         """Reserve a unit (using the table's autoincrement fields)."""
49         # Grab the new ID. This is threadsafe because reserve has a mutex.
50         newids = self._table_map[unit.__class__].insert(**unit._properties)
51         for k, v in newids.iteritems():
52             setattr(unit, k, v)
53    
54     def _manual_reserve(self, unit):
55         """Use when the DB cannot automatically generate an identifier.
56         The identifiers will be supplied by UnitSequencer.assign().
57         """
58         t = self._table_map[unit.__class__]
59         if not unit.sequencer.valid_id(unit.identity()):
60             # Examine all existing IDs and grant the "next" one.
61             data = list(self.db.select((t, unit.identifiers)))
62             unit.sequencer.assign(unit, data)
63         t.insert(**unit._properties)
64    
65     def save(self, unit, forceSave=False):
66         """Update storage from unit's data (if unit.dirty())."""
67         if self.logflags & logflags.SAVE:
68             self.log(logflags.SAVE.message(unit, forceSave))
69        
70         if forceSave or unit.dirty():
71             self._table_map[unit.__class__].save(**unit._properties)
72             unit.cleanse()
73    
74     def destroy(self, unit):
75         """Delete the unit."""
76         if self.logflags & logflags.DESTROY:
77             self.log(logflags.DESTROY.message(unit))
78         self._table_map[unit.__class__].delete(**unit._properties)
79    
80    
81     #                                Views                                #
82    
83     def tablejoin(self, join):
84         """Return a geniusql Join tree for the given UnitJoin."""
85         t1, t2 = join.class1, join.class2
86        
87         if isinstance(t1, dejavu.UnitJoin):
88             wt1 = self.tablejoin(t1)
89         else:
90             wt1 = self._table_map[t1]
91        
92         if isinstance(t2, dejavu.UnitJoin):
93             wt2 = self.tablejoin(t2)
94         else:
95             wt2 = self._table_map[t2]
96        
97         uj = geniusql.sqlwriters.Join(wt1, wt2, join.leftbiased)
98         # if the original UnitJoin had a custom association path,
99         # copy it to the new Join instance
100         uj.path = join.path
101         return uj
102    
103     def _geniusql_query(self, query):
104         """Return a Geniusql Query object for the given Dejavu Query."""
105         rel = query.relation
106         if isinstance(rel, dejavu.UnitJoin):
107             rel = self.tablejoin(rel)
108         else:
109             rel = self._table_map[rel]
110         return geniusql.sqlwriters.Query(rel, query.attributes, query.restriction)
111    
112     def insert_into(self, name, query, distinct=False):
113         """INSERT matching data INTO a new class and return the class."""
114         if not isinstance(query, dejavu.Query):
115             query = dejavu.Query(*query)
116        
117         self.db.insert_into(name, self._geniusql_query(query),
118                             distinct=distinct)
119         if isinstance(query.relation, dejavu.UnitJoin):
120             for cls in query.relation:
121                 schema = self._table_map[cls].schema
122                 break
123         else:
124             schema = self._table_map[query.relation].schema
125         return Modeler(schema).make_class(name)
126    
127     def make_class(self, name):
128         """Return a (new) Unit class for the given storage name."""
129         # TODO:
130         raise NotImplementedError
131    
132    
133     #                               Schemas                               #
134    
135     def create_database(self):
136         if self.logflags & logflags.DDL:
137             self.log(logflags.DDL.message("create database"))
138         self.db.create()
139         for s in self.schemas.itervalues():
140             s.create()
141    
142     def drop_database(self):
143         if self.logflags & logflags.DDL:
144             self.log(logflags.DDL.message("drop database"))
145         for s in self.schemas.itervalues():
146             s.drop()
147         self.db.drop()
148    
149     def _make_table(self, cls):
150         """Create and return a Table object for the given class."""
151         s = self.schemas[getattr(cls, "_dbschema_name", "public")]
152         self._table_map[cls] = t = s.table(cls.__name__)
153        
154         indices = cls.indices()
155         fields = []
156         for key in cls.properties:
157             t[key] = self._make_column(cls, key)
158             if key in indices:
159                 t.add_index(key)
160        
161         # Copy associations to table.references.
162         for k, v in cls._associations.iteritems():
163             t.references[k] = (v.nearKey, v.farClass.__name__, v.farKey)
164        
165         return t
166    
167     def create_storage(self, cls):
168         """Create storage for the given class."""
169         if self.logflags & logflags.DDL:
170             self.log(logflags.DDL.message("create storage %s" % cls))
171         # Attach to a schema, which should call CREATE TABLE.
172         s = self.schemas[getattr(cls, "_dbschema_name", "public")]
173         s[cls.__name__] = self._make_table(cls)
174    
175     def _make_column(self, cls, key):
176         s = self._table_map[cls].schema
177         prop = getattr(cls, key)
178         col = s.column(prop.type, default=prop.default, hints=prop.hints)
179         if key in cls.identifiers:
180             col.key = True
181             if isinstance(cls.sequencer, dejavu.UnitSequencerInteger):
182                 col.autoincrement = True
183                 col.initial = cls.sequencer.initial
184         return col
185    
186     def has_storage(self, cls):
187         s = self.schemas[getattr(cls, "_dbschema_name", "public")]
188         return cls.__name__ in s
189    
190     def drop_storage(self, cls):
191         if self.logflags & logflags.DDL:
192             self.log(logflags.DDL.message("drop storage %s" % cls))
193         s = self._table_map[cls].schema
194         del s[cls.__name__]
195    
196     def rename_storage(self, oldname, newname):
197         # TODO:
198         return NotImplementedError
199    
200     def add_property(self, cls, name):
201         if self.logflags & logflags.DDL:
202             self.log(logflags.DDL.message("add property %s %s" %
203                                           (cls, name)))
204         if not self.has_property(cls, name):
205             s = self._table_map[cls].schema
206             s[cls.__name__][name] = self._make_column(cls, name)
207    
208     def has_property(self, cls, name):
209         return name in self._table_map[cls][cls.__name__]
210    
211     def drop_property(self, cls, name):
212         if self.logflags & logflags.DDL:
213             self.log(logflags.DDL.message("drop property %s %s" %
214                                           (cls, name)))
215         if self.has_property(cls, name):
216             del self._table_map[cls][name]
217    
218     def rename_property(self, cls, oldname, newname):
219         if self.logflags & logflags.DDL:
220             self.log(logflags.DDL.message(
221                 "rename property %s from %s to %s" %
222                 (cls, oldname, newname)))
223         t = self._table_map[cls]
224         s = t.schema
225        
226         # Sometimes, a Dejavu Schema will change a code model first, and
227         # then change the database afterward. So it's possible that the
228         # column we're trying to rename hasn't been loaded, because the
229         # model layer no longer references it. So if table[oldname]
230         # raises a KeyError, try to find a column that matches oldkey.
231         tempcol = None
232         try:
233             t[oldname]
234         except KeyError:
235             c = [x for x in s._get_columns(t.name)
236                  if x.name == s._column_name(t.name, oldname)]
237             if not c:
238                 raise KeyError("Rename failed. Old column %r not found in %r."
239                                % (oldname, t.name))
240             oldcol = c[0]
241             # Use the superclass call to avoid DROP COLUMN/ADD COLUMN.
242             dict.__setitem__(t, oldname, oldcol)
243        
244         t.rename(oldname, newname)
245    
246     def add_index(self, cls, name):
247         self._table_map[cls].add_index(name)
248    
249     def has_index(self, cls, name):
250         return name in self._table_map[cls].indices
251    
252     def drop_index(self, cls, name):
253         del self._table_map[cls].indices[name]
254    
255     auto_discover = True
256    
257     def map(self, classes, conflict_mode='error'):
258         """Map classes to internal storage.
259         
260         If self.auto_discover is True (the default), then Table/Column/Index
261         objects will be formed by inspecting the underlying database using
262         self.sync().
263         
264         If auto_discover is False, then mock Table/Column/Index objects
265         will be used instead; this provides a performance improvement
266         in scenarios where the model maps perfectly to the database
267         and changes to the database are not expected outside the model.
268         
269         conflict_mode: This argument determines what happens when there are
270         discrepancies between the Dejavu model and the actual database.
271             
272             If 'error' (the default), MappingError is raised for the
273             first issue and the sync process is aborted.
274             
275             If 'warn', then a warning is raised (instead of an error)
276             for each issue, and the sync process is not aborted. This
277             allows you to see all errors at once, without having to stop
278             and fix each one and then execute the process again.
279             
280             If 'repair', then each issue will be resolved by changing
281             the database to match the model.
282         """
283         if self.auto_discover:
284             self.sync(classes, conflict_mode)
285         else:
286             for cls in classes:
287                 s = self.schemas[getattr(cls, "_dbschema_name", "public")]
288                 if cls.__name__ in s:
289                     # If our consumer-side key is already present, skip this cls.
290                     # This allows callers to auto-sync class by class
291                     # without making a new Table object each time.
292                     continue
293                
294                 t = self._make_table(cls)
295                
296                 # Use the superclass call to avoid DROP/CREATE TABLE
297                 dict.__setitem__(s, cls.__name__, t)
298    
299     def sync(self, classes, conflict_mode='error'):
300         """Map classes to existing Table objects (found via discovery).
301         
302         conflict_mode: This argument determines what happens when there are
303         discrepancies between the Dejavu model and the actual database.
304             
305             If 'error' (the default), MappingError is raised for the
306             first issue and the sync process is aborted.
307             
308             If 'warn', then a warning is raised (instead of an error)
309             for each issue, and the sync process is not aborted. This
310             allows you to see all errors at once, without having to stop
311             and fix each one and then execute the process again.
312             
313             If 'repair', then each issue will be resolved by changing
314             the database to match the model.
315             
316             If 'ignore', then each issue will be silently ignored.
317         """
318         for cls in classes:
319             clsname = cls.__name__
320            
321             s = self.schemas[getattr(cls, "_dbschema_name", "public")]
322             if clsname in s:
323                 # If our consumer-side key is already present, skip this cls.
324                 # This allows callers to auto-sync class by class
325                 # without calling the expensive discover() func each time.
326                 continue
327            
328             self._table_map[cls] = self._find_table(s, cls, conflict_mode)
329
Note: See TracBrowser for help on using the browser.