Changeset 50
- Timestamp:
- 01/08/05 07:26:45
- Files:
-
- trunk/__init__.py (modified) (3 diffs)
- trunk/doc/managing.html (modified) (1 diff)
- trunk/doc/modeling.html (modified) (5 diffs)
- trunk/storage/db.py (modified) (5 diffs)
- trunk/storage/storeado.py (modified) (1 diff)
- trunk/storage/storepypgsql.py (modified) (1 diff)
- trunk/storage/storeshelve.py (modified) (3 diffs)
- trunk/storage/zoo_fixture.py (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/__init__.py
r49 r50 414 414 return self.sandbox.unit(farClass, **kwargs) 415 415 416 def add(self, unit): 417 """add(unit) -> Auto-create a relationship between self and unit.""" 418 try: 419 key, farKey = self.__class__._associations[unit.__class__] 420 except KeyError: 421 raise AssociationError("'%s' is not associated with '%s'" 422 % (self.__class__, unit.__class__)) 423 424 nearval = getattr(self, key) 425 farval = getattr(unit, farKey) 426 if nearval is None: 427 if farval is None: 428 raise AssociationError("At least one Unit key must be set.") 416 def add(self, *units): 417 """add(*units) -> Auto-create a relation between self and unit(s).""" 418 cls = self.__class__ 419 for unit in units: 420 try: 421 key, farKey = cls._associations[unit.__class__] 422 except KeyError: 423 raise AssociationError("'%s' is not associated with '%s'" 424 % (cls, unit.__class__)) 425 426 nearval = getattr(self, key) 427 farval = getattr(unit, farKey) 428 if nearval is None: 429 if farval is None: 430 raise AssociationError("At least one Unit key must be set.") 431 else: 432 setattr(self, key, farval) 429 433 else: 430 setattr(self, key, farval) 431 else: 432 # If far key is already set, it will simply be overwritten. 433 setattr(unit, farKey, nearval) 434 # If far key is already set, it will simply be overwritten. 435 setattr(unit, farKey, nearval) 434 436 435 437 … … 661 663 unit.sandbox = None 662 664 663 def recall(self, cls, expr=None, *args): 664 """recall(cls, expr=None) -> Recall units of cls which match expr. 665 666 If additional args are supplied: 667 668 1) They shall be of the form: (cls, expr, cls, expr, cls, [expr]). 669 The final expr is optional. 670 2) Each such secondary cls/expr pair will be recalled; however, 671 only those secondary Units which are associated with Units 672 in the primary set will be returned. 673 3) Instead of single Units, the yielded value will be a tuple of 674 Units, in the same order as the cls args were supplied. This 675 facilitates consumer code like: 676 677 for invoice, price in sandbox.recall(Invoice, f, Price): 678 deal_with(invoice) 679 deal_with(price) 680 681 4) If any secondary Units are found, all combinations of those 682 Units, together with each primary Unit, will be returned. 683 5) If no secondary Units are recalled, then a token tuple will be 684 returned of the form (primary_unit, None). For example: 685 686 for invoice, price in sandbox.recall(Invoice, f, Price): 687 if price is None: 688 handle_no_prices(invoice) 689 690 """ 665 def recall(self, cls, expr=None): 666 """recall(cls, expr=None) -> Recall units of cls which match expr.""" 691 667 692 668 store = self.arena.storage(cls) 693 694 if args:695 # Deal with multiple class/expr pairs.696 697 # Format extra class/expr pairs more rigorously698 pairs = []699 args = list(args)700 while args:701 c = args.pop(0)702 if self.arena.storage(c) is not store:703 raise ValueError(u"recall() does not currently support "704 u"multiple classes in disparate stores.")705 if args:706 e = args.pop(0)707 else:708 e = None709 pairs.append((c, e))710 711 # Don't try any in-memory techniques, just flush it all,712 # and ask the SM to give us what we want.713 self.flush(cls)714 for c, e in pairs:715 self.flush(c)716 for units in store.recall(cls, expr, pairs):717 confirmed = True718 for unit in units:719 if unit is not None:720 ID = unit.ID721 cache = self._cache(unit.__class__)722 if ID not in cache:723 cache[ID] = unit724 unit.sandbox = self725 if hasattr(unit, 'on_recall'):726 try:727 unit.on_recall()728 except UnrecallableError:729 confirmed = False730 if confirmed:731 yield units732 raise StopIteration733 669 734 670 # Run through our cache first. … … 778 714 yield unit 779 715 716 def multirecall(self, *pairs): 717 """multirecall((cls1, expr1), ...) -> [[unit, ...], [unit, ...], ...] 718 Recall units of each cls which match each expr. 719 720 Units of each additional cls/expr pair will be recalled; however, 721 only those Units with associations to Units in the PRIMARY set will 722 be returned. For you database guys, it's a set of inner joins, 723 ALL of which are between the FIRST set and the subsequent set(s). 724 725 Instead of single Units, each yielded value will be a tuple of 726 Units, in the same order as the cls args were supplied. This 727 facilitates unpacking in iterative consumer code like: 728 729 for invoice, price in sandbox.multirecall(Invoice, f, Price, None): 730 deal_with(invoice) 731 deal_with(price) 732 """ 733 734 store = self.arena.storage(pairs[0][0]) 735 for c, e in pairs: 736 if self.arena.storage(c) is not store: 737 raise ValueError(u"multirecall() does not support multiple" 738 u" classes in disparate stores.") 739 740 # Don't try any in-memory mixing; just flush it all, 741 # and ask the SM to give us what we want. 742 for c, e in pairs: 743 self.flush(c) 744 for unitset in store.multirecall(*pairs): 745 confirmed = True 746 for index in xrange(len(unitset)): 747 unit = unitset[index] 748 ID = unit.ID 749 cache = self._cache(unit.__class__) 750 if ID in cache: 751 # Force using the same unit each time. 752 unitset[index] = cache[ID] 753 else: 754 cache[ID] = unit 755 unit.sandbox = self 756 if hasattr(unit, 'on_recall'): 757 try: 758 unit.on_recall() 759 except UnrecallableError: 760 confirmed = False 761 break 762 if confirmed: 763 yield unitset 764 780 765 def unit(self, cls, **kwargs): 781 766 """unit(cls, **kwargs) -> A single matching Unit, else None. trunk/doc/managing.html
r49 r50 304 304 any Rules.</p> 305 305 306 <h4> Rules</h4>306 <h4><a name='unitenginerules'>Rules</a></h4> 307 307 <p>Just like Collections and Engines, <tt>UnitEngineRule</tt> is <i>also</i> 308 308 a subclass of <tt>Unit</tt>, and can be persisted via Storage Managers. All trunk/doc/modeling.html
r49 r50 300 300 know; I might be convinced to add a <tt>recall_list</tt> method.</p> 301 301 302 <p><strike>The <tt>recall</tt> method will take additional arguments in pairs of303 <tt>cls</tt>, <tt>expr</tt>.</strike> This feature isn't fully developed yet.304 It's designed to emulate JOINs, returning units which match each expr305 and are related.</p>306 307 302 <p>If your Unit class defines an <tt>on_recall()</tt> method, it will be 308 303 called when each Unit has been loaded from storage (at the end of the … … 328 323 (although the rest are probably loaded into memory).</p> 329 324 325 <h5>multirecall()</h5> 326 <p>The <tt>multirecall</tt> method will take multiple pairs of 327 (<tt>cls</tt>, <tt>expr</tt>), and return a series of unitsets. Each 328 unitset will be a list of units, one per cls passed in to the method. 329 <pre>pubs = box.multirecall((Publisher, logic.filter(ID=4)), 330 (Publication, None))</pre> 331 This example will retrieve a list of (Publisher, Publication) pairs.</p> 332 333 <p>In database terminology, the multirecall method performs a series of 334 full inner joins between the first unit class and each subsequent class. 335 That is, class 2 is joined to class 1, then class 3 is joined to class 1, 336 etcetera. Since each join is an inner join, the result set is guaranteed 337 to contain a complete set of units for each iteration; however, repeated 338 units will reference the same object; in the example above, each Publisher 339 unit will be the same object, since we limited that expression to a single 340 Publisher. So we might iterate over "pubs" multiple times, but each time, 341 the first unit in the set will be the same unit instance.</p> 342 330 343 <h4>Forgetting and Repressing</h4> 331 344 <p>To <i>forget</i> a Unit is to destroy it forever. You have two options … … 395 408 </p> 396 409 397 <p>What does an explicit association buy for you? First, Arenas discover them 398 and fill the <tt>Arena.associations</tt> registry, so that smart consumer 399 code (like Unit Engine Rules, below) can automatically follow association 400 paths for you. Second, each Unit class has a private <tt>_associations</tt> 401 attribute, a <tt>dict</tt>. Each Unit involved in in the association gains 402 an entry in that dict: the key is the far class itself (not the class name), 410 <p>What does an explicit association buy for you? First, you can associate 411 Units without having to remember which keys are related. Second, Arenas 412 discover associations and fill the <tt>Arena.associations</tt> registry, so 413 that smart consumer code (like <a href='managing.html#unitenginerules'>Unit 414 Engine Rules</a>) can automatically follow association paths for you. 415 Third, each Unit class has a private <tt>_associations</tt> attribute, 416 a <tt>dict</tt>. Each Unit involved in in the association gains an entry 417 in that dict: the key is the far class itself (not the class name), 403 418 and the value is a tuple of (near key, far key).</p> 404 419 420 <h4><tt>Unit.add()</tt></h4> 421 <p>Once two classes have been associated, you attach Unit <i>instances</i> 422 to each other by equating their associated properties. That was a 423 mouthful. Here's an example: 424 <pre>>>> evbio = Biography() 425 >>> evbio.ArchID = Eversley.ID 426 </pre> 427 The two unit <i>instances</i> (evbio and Eversley) are now associated 428 (only their <i>classes</i> were before).</p> 429 430 <p>Rather than forcing you to remember all of the related classes and keys, 431 Dejavu Units all have an <tt>add</tt> method, which does the same thing: 432 <pre>>>> evbio = Biography() 433 >>> evbio.add(Eversley) 434 </pre> 435 The <tt>add</tt> method works in either direction, so you could just as 436 well write: 437 <pre>>>> evbio = Biography() 438 >>> Eversley.add(evbio) 439 </pre> 440 The <tt>add</tt> method will take any number of unit instances as 441 arguments, and add each one in turn. That is: 442 <pre> 443 >>> evbio1 = Biography() 444 >>> evbio2 = Biography() 445 >>> evbio3 = Biography() 446 >>> Eversley.add(evbio1, evbio2, evbio3) 447 </pre> 448 </p> 449 405 450 <h4><tt>related_units</tt> methods</h4> 406 <p> In addition, each of the two Unit classes will gain a new451 <p>To make querying easier, each of the two Unit classes will gain a new 407 452 <i>related_units</i> method which simplifies looking up related instances 408 453 of the other class. The new method for Unit_B will have the name of Unit_A, … … 418 463 <listiterator object at 0x012150D0> 419 464 >>> list(bios) 420 [] 465 [<arch.Biography object at 0x01158E10>, 466 <arch.Biography object at 0x0118B350>, 467 <arch.Biography object at 0x0118B170>] 421 468 </pre> 422 We haven't created any Biographies, so there aren't any to be recalled, 423 which is why we get an empty iterator at this point. At the other extreme 424 (when you have hundreds of Biographies to filter), you can pass an optional 425 <tt>Expression</tt> object to the related_units method. When you do, the 426 list of associated Units will be filtered accordingly.</p> 469 We've only created three Biographies at this point, so we can print the list 470 easily. At the other extreme (when you have hundreds of Biographies to filter), 471 you can pass an optional <tt>Expression</tt> object to the related_units method. 472 When you do, the list of associated Units will be filtered accordingly.</p> 427 473 428 474 <p>Because the related_units method names are formed automatically, you need 429 475 to take care not to use the names of Unit classes for your Unit properties. 430 In our example, we used "ArchID" for the name of our "foreign key". 431 If we had used "Archaeologist" instead, we would have had problems; 432 when we associated the classes, the <i>property</i> named "Archaeologist" 433 would have collided with the <i>related_units method</i> named 434 "Archaeologist". Be careful when naming your properties, and plan for the 435 future.</p>476 In our example, we used "ArchID" for the name of our "foreign key". If we 477 had used "Archaeologist" instead, we would have had problems; when we 478 associated the classes, the <i>property</i> named "Archaeologist" would 479 have collided with the <i>related_units method</i> named "Archaeologist". 480 Be careful when naming your properties, and plan for the future. The best 481 approach is probably to end your property name with "ID" every time.</p> 436 482 437 483 <p>Unlike some other ORM's, Dejavu doesn't cache far Units within the near … … 453 499 properties and call sandbox.unit() for you, returning either the first 454 500 such far Unit or None if not found.</p> 455 456 501 457 502 <h3>The Arena Object</h3> trunk/storage/db.py
r49 r50 800 800 return res.row_list, res.col_defs 801 801 802 def recall(self, cls, expr=None , pairs=None):802 def recall(self, cls, expr=None): 803 803 if expr is None: 804 804 expr = logic.Expression(lambda x: True) … … 806 806 data, col_defs = self.fetch(sql) 807 807 808 columns = {} 809 for index, col in enumerate(col_defs): 810 # name, type_code, display_size, internal_size, 811 # precision, scale, null_ok 812 columns[col[0]] = (index, col[1]) 808 columns = dict([(col[0], (index, col[1])) for index, col 809 in enumerate(col_defs)]) 813 810 814 811 # Get specs on properties. Get the ID property first in case other … … 937 934 938 935 def destroy(self, unit): 939 """ Delete the unit."""936 """destroy(unit). Delete the unit.""" 940 937 self.execute(u'DELETE * FROM %s WHERE %s = %s;' % 941 938 (self.tablename(unit), self.identifier("ID"), … … 943 940 944 941 def distinct(self, cls, fields, expr=None): 945 """ Return distinct values for specifiedfields."""942 """distinct(cls, fields, expr=None) -> Distinct values for given fields.""" 946 943 if expr is None: 947 944 expr = logic.Expression(lambda x: True) … … 965 962 for i, val in enumerate(row)]) 966 963 for row in data] 967 964 965 def multiselect(self, pairs): 966 t = self.tablename 967 i = self.identifier 968 firstcls = pairs[0][0] 969 tablenames = [] 970 # Because various databases may mangle column names, we explicitly 971 # order the requested columns (instead of using *). 972 columns = [] 973 wheres = [] 974 imps = [] 975 976 for cls, expr in pairs: 977 tablenames.append(t(cls)) 978 # Place the ID property first in case others depend upon it. 979 keys = ['ID'] + [k for k in cls.properties() if k != 'ID'] 980 columns.extend([(cls, k) for k in keys]) 981 982 if expr is None: 983 expr = logic.Expression(lambda x: True) 984 w, imp = self.where(cls, expr) 985 wheres.append(w) 986 imps.append(imp) 987 988 if cls is not firstcls: 989 spath = self.arena.associations.shortest_path(firstcls, cls) 990 # This should be firstcls in every case. 991 cls1 = spath.pop(0) 992 for cls2 in spath: 993 leftkey, rightkey = cls1._associations[cls2] 994 wheres.append("(%s.%s = %s.%s)" % 995 (t(cls1), i(leftkey), 996 t(cls2), i(rightkey))) 997 cls1 = cls2 998 999 # Remove any duplicate entries in the where clauses 1000 # (there may be several from the _join_clauses). 1001 wheres = dict.fromkeys(wheres).keys() 1002 1003 names = ["%s.%s" % (t(cls), i(key)) for cls, key in columns] 1004 tbls = u', '.join(tablenames) 1005 w = u' AND '.join(wheres) 1006 statement = "SELECT %s FROM %s WHERE %s" % (u', '.join(names), tbls, w) 1007 return statement, imps, columns 1008 1009 def multirecall(self, *pairs): 1010 """multirecall(*pairs) -> Full inner join units for each (cls, expr) pair.""" 1011 sql, imps, supplied_cols = self.multiselect(pairs) 1012 data, recvd_cols = self.fetch(sql) 1013 1014 # Get specs on properties. 1015 props = [] 1016 for sup, rec in zip(supplied_cols, recvd_cols): 1017 c, key = sup 1018 name, ftype = rec[0], rec[1] 1019 subtype = self.expanded_columns.get((c.__name__, key)) 1020 props.append((c, key, ftype, subtype)) 1021 1022 consume = self.fromAdapter.consume 1023 for row in data: 1024 index = 0 1025 units = {} 1026 for c, key, ftype, subtype in props: 1027 if c in units: 1028 unit = units[c] 1029 else: 1030 units[c] = unit = c() 1031 value = row[index] 1032 if subtype: 1033 self.load_expanded(unit, key, subtype) 1034 else: 1035 try: 1036 consume(unit, key, value, ftype) 1037 except UnicodeDecodeError, x: 1038 x.reason += "[%s][%s][%s]" % (key, value, ftype) 1039 raise x 1040 except Exception, x: 1041 x.args += (key, value, ftype) 1042 raise x 1043 index += 1 1044 1045 # If our SQL is imperfect, don't yield units to the 1046 # caller unless they pass evaluate(). 1047 acceptable = True 1048 unitset = [] 1049 for pair, imp in zip(pairs, imps): 1050 c, e = pair 1051 unit = units[c] 1052 unit.cleanse() 1053 if imp: 1054 acceptable &= e.evaluate(unit) 1055 if not acceptable: 1056 break 1057 unitset.append(unit) 1058 if acceptable: 1059 yield unitset trunk/storage/storeado.py
r49 r50 311 311 raise 312 312 313 def _join(self, path=[]):314 if not path:315 return u''316 firstcls = path.pop(0)317 if not path:318 return firstcls.__name__319 320 spath = self.arena.associations.shortest_path(firstcls, path[0])321 spath.pop(0)322 cls = spath[0]323 324 p = self.prefix325 i = self.identifier326 left = i(p, firstcls.__name__)327 right = i(p, cls.__name__)328 if len(spath) == 1:329 child = right330 else:331 child = u"(%s)" % self._join(spath)332 leftkey, rightkey = firstcls._associations[cls]333 334 return (u"%s LEFT JOIN %s ON %s.%s = %s.%s" %335 (left, child, left, leftkey, right, rightkey))336 337 def multiselect(self, firstcls, firstexpr, pairs):338 firstwhere, imp = self.where(firstcls, firstexpr)339 cols = [(firstcls, k) for k in firstcls.properties()]340 341 if len(pairs) != 1:342 raise ValueError("Multiselect does not yet work on multiple pairs.")343 344 for cls, expr in pairs:345 if expr is None:346 expr = logic.Expression(lambda x: True)347 348 j = self._join([firstcls, cls])349 350 w, new_imp = self.where(cls, expr)351 imp |= new_imp352 if w and w != "TRUE":353 w = " WHERE %s AND %s" % (w, firstwhere)354 else:355 w = " WHERE %s" % firstwhere356 357 cols += [(cls, k) for k in cls.properties()]358 colnames = ["%s.%s" % (self.identifier(self.prefix, colcls.__name__),359 self.identifier(k))360 for colcls, k in cols]361 362 statement = "SELECT %s FROM %s%s" % (u', '.join(colnames), j, w)363 364 return statement, imp, cols365 366 313 def execute(self, query, conn=None): 367 314 if conn is None: trunk/storage/storepypgsql.py
r49 r50 100 100 columns.append((res.fname(index), res.ftype(index))) 101 101 102 def iterator(): 103 for row in xrange(res.ntuples): 104 yield [res.getvalue(row, col) for col in xrange(res.nfields)] 105 # This should be more robust--needs a class with a cleanup call. 106 res.clear() 102 data = [[res.getvalue(row, col) for col in xrange(res.nfields)] 103 for row in xrange(res.ntuples)] 104 res.clear() 107 105 108 return iterator(), columns106 return data, columns 109 107 trunk/storage/storeshelve.py
r49 r50 36 36 return s, lock 37 37 38 def recall(self, cls, expr=None , pairs=None):38 def recall(self, cls, expr=None): 39 39 units = [] 40 40 data, lock = self.shelf(cls) … … 97 97 98 98 def distinct(self, cls, fields, expr=None): 99 """ Return distinct values for specifiedfields."""99 """distinct(cls, fields, expr=None) -> Distinct values for given fields.""" 100 100 if expr is None: 101 101 expr = logic.Expression(lambda x: True) … … 114 114 finally: 115 115 lock.release() 116 117 def multirecall(self, *pairs): 118 """multirecall(*pairs) -> Full inner join units for each (cls, expr) pair.""" 119 raise NotImplementedError("This method doesn't yet work for shelve.") 120 unitsets = [] 121 122 firstcls = pairs[0][0] 123 for cls, expr in pairs: 124 if cls is not firstcls: 125 spath = self.arena.associations.shortest_path(firstcls, cls) 126 # This should be firstcls in every case. 127 cls1 = spath.pop(0) 128 for cls2 in spath: 129 leftkey, rightkey = cls1._associations[cls2] 130 wheres.append("(%s.%s = %s.%s)" % 131 (t(cls1), i(leftkey), 132 t(cls2), i(rightkey))) 133 cls1 = cls2 134 join = logic.Expression() 135 expr = expr + join 136 137 unitset = [unit for unit in self.recall(cls, expr)] 138 unitsets.append(unitset) 139 140 for unitset in unitsets: 141 yield unitset 116 142 trunk/storage/zoo_fixture.py
r49 r50 60 60 box.memorize(zoo.Animal(Name='Ostrich', Legs=2, PreviousZoos=[])) 61 61 box.memorize(zoo.Animal(Name='Centipede', Legs=100)) 62 62 63 emp = zoo.Animal(Name='Emperor Penguin', Legs=2) 63 64 box.memorize(emp) … … 66 67 67 68 millipede = zoo.Animal(Name='Millipede', Legs=1000000) 68 millipede.add(SDZ)69 69 millipede.PreviousZoos = [WAP.ID] 70 70 box.memorize(millipede) 71 72 SDZ.add(emp, adelie, millipede) 71 73 72 74 # Exhibits … … 204 206 self.assertEqual(legs, [1, 2, 4, 100, 1000000]) 205 207 206 def test_4_Multithreading(self): 207 f = logic.Expression(lambda x: x.Legs == 4) 208 def thread_recall(): 209 # Notice we only do reads in this thread, not writes, since 210 # the order of thread execution can not be guaranteed. 211 box = zoo.arena.new_sandbox() 212 quadrupeds = [x for x in box.recall(zoo.Animal, f)] 213 self.assertEqual(len(quadrupeds), 4) 214 215 ts = [] 216 # PostgreSQL, for example, has a default max_connections of 100. 217 for x in range(99): 218 t = threading.Thread(target=thread_recall) 219 t.start() 220 ts.append(t) 221 for t in ts: 222 t.join() 208 def test_4_Multiselect(self): 209 box = zoo.arena.new_sandbox() 210 zooed_animals = [(z, a) for z, a in 211 box.multirecall((zoo.Zoo, logic.filter(Name='San Diego Zoo')), 212 (zoo.Animal, None))] 213 SDZ = box.unit(zoo.Zoo, Name='San Diego Zoo') 214 self.assertEqual(len(zooed_animals), 3) 215 aid = 0 216 for z, a in zooed_animals: 217 self.assertEqual(id(z), id(SDZ)) 218 self.assertNotEqual(id(a), aid) 219 aid = id(a) 220 ## 221 ## def test_5_Multithreading(self): 222 ## f = logic.Expression(lambda x: x.Legs == 4) 223 ## def thread_recall(): 224 ## # Notice we only do reads in this thread, not writes, since 225 ## # the order of thread execution can not be guaranteed. 226 ## box = zoo.arena.new_sandbox() 227 ## quadrupeds = [x for x in box.recall(zoo.Animal, f)] 228 ## self.assertEqual(len(quadrupeds), 4) 229 ## 230 ## ts = [] 231 ## # PostgreSQL, for example, has a default max_connections of 100. 232 ## for x in range(99): 233 ## t = threading.Thread(target=thread_recall) 234 ## t.start() 235 ## ts.append(t) 236 ## for t in ts: 237 ## t.join() 223 238 224 239 … … 242 257 pass 243 258 244 def run_tests(SM_class, opts ):259 def run_tests(SM_class, opts, profile=True): 245 260 import traceback 246 261 try: 247 262 try: 248 263 setup_SM(SM_class, opts) 249 unittest.main(__name__) 264 if profile: 265 import hotshot 266 prof = hotshot.Profile("zoo_fixture.prof") 267 prof.runcall(unittest.main, __name__) 268 else: 269 unittest.main(__name__) 250 270 except SystemExit: 251 271 # unittest.main normally raises SystemExit when complete.
