Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

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

Changeset 50

Show
Ignore:
Timestamp:
01/08/05 07:26:45
Author:
fumanchu
Message:

1. Modified Unit.add() to take multiple units.
2. Pulled multirecall out of sandbox.recall.
3. db.SM now has multirecall, multiselect methods.
4. zoo_fixture has new multirecall test.
5. zoo_fixture now does profiling.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/__init__.py

    r49 r50  
    414414        return self.sandbox.unit(farClass, **kwargs) 
    415415     
    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) 
    429433            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) 
    434436 
    435437 
     
    661663        unit.sandbox = None 
    662664     
    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.""" 
    691667         
    692668        store = self.arena.storage(cls) 
    693          
    694         if args: 
    695             # Deal with multiple class/expr pairs. 
    696              
    697             # Format extra class/expr pairs more rigorously 
    698             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 = None 
    709                 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 = True 
    718                 for unit in units: 
    719                     if unit is not None: 
    720                         ID = unit.ID 
    721                         cache = self._cache(unit.__class__) 
    722                         if ID not in cache: 
    723                             cache[ID] = unit 
    724                         unit.sandbox = self 
    725                     if hasattr(unit, 'on_recall'): 
    726                         try: 
    727                             unit.on_recall() 
    728                         except UnrecallableError: 
    729                             confirmed = False 
    730                 if confirmed: 
    731                     yield units 
    732             raise StopIteration 
    733669         
    734670        # Run through our cache first. 
     
    778714                        yield unit 
    779715     
     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     
    780765    def unit(self, cls, **kwargs): 
    781766        """unit(cls, **kwargs) -> A single matching Unit, else None. 
  • trunk/doc/managing.html

    r49 r50  
    304304any Rules.</p> 
    305305 
    306 <h4>Rules</h4> 
     306<h4><a name='unitenginerules'>Rules</a></h4> 
    307307<p>Just like Collections and Engines, <tt>UnitEngineRule</tt> is <i>also</i> 
    308308a subclass of <tt>Unit</tt>, and can be persisted via Storage Managers. All 
  • trunk/doc/modeling.html

    r49 r50  
    300300know; I might be convinced to add a <tt>recall_list</tt> method.</p> 
    301301 
    302 <p><strike>The <tt>recall</tt> method will take additional arguments in pairs of 
    303 <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 expr 
    305 and are related.</p> 
    306  
    307302<p>If your Unit class defines an <tt>on_recall()</tt> method, it will be 
    308303called when each Unit has been loaded from storage (at the end of the 
     
    328323(although the rest are probably loaded into memory).</p> 
    329324 
     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 
     328unitset 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> 
     331This example will retrieve a list of (Publisher, Publication) pairs.</p> 
     332 
     333<p>In database terminology, the multirecall method performs a series of 
     334full inner joins between the first unit class and each subsequent class. 
     335That is, class 2 is joined to class 1, then class 3 is joined to class 1, 
     336etcetera. Since each join is an inner join, the result set is guaranteed 
     337to contain a complete set of units for each iteration; however, repeated 
     338units will reference the same object; in the example above, each Publisher 
     339unit will be the same object, since we limited that expression to a single 
     340Publisher. So we might iterate over "pubs" multiple times, but each time, 
     341the first unit in the set will be the same unit instance.</p> 
     342 
    330343<h4>Forgetting and Repressing</h4> 
    331344<p>To <i>forget</i> a Unit is to destroy it forever. You have two options 
     
    395408</p> 
    396409 
    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 
     411Units without having to remember which keys are related. Second, Arenas 
     412discover associations and fill the <tt>Arena.associations</tt> registry, so 
     413that smart consumer code (like <a href='managing.html#unitenginerules'>Unit 
     414Engine Rules</a>) can automatically follow association paths for you. 
     415Third, each Unit class has a private <tt>_associations</tt> attribute, 
     416a <tt>dict</tt>. Each Unit involved in in the association gains an entry 
     417in that dict: the key is the far class itself (not the class name), 
    403418and the value is a tuple of (near key, far key).</p> 
    404419 
     420<h4><tt>Unit.add()</tt></h4> 
     421<p>Once two classes have been associated, you attach Unit <i>instances</i> 
     422to each other by equating their associated properties. That was a 
     423mouthful. Here's an example: 
     424<pre>>>> evbio = Biography() 
     425>>> evbio.ArchID = Eversley.ID 
     426</pre> 
     427The 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, 
     431Dejavu Units all have an <tt>add</tt> method, which does the same thing: 
     432<pre>>>> evbio = Biography() 
     433>>> evbio.add(Eversley) 
     434</pre> 
     435The <tt>add</tt> method works in either direction, so you could just as 
     436well write: 
     437<pre>>>> evbio = Biography() 
     438>>> Eversley.add(evbio) 
     439</pre> 
     440The <tt>add</tt> method will take any number of unit instances as 
     441arguments, 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 
    405450<h4><tt>related_units</tt> methods</h4> 
    406 <p>In addition, each of the two Unit classes will gain a new 
     451<p>To make querying easier, each of the two Unit classes will gain a new 
    407452<i>related_units</i> method which simplifies looking up related instances 
    408453of the other class. The new method for Unit_B will have the name of Unit_A, 
     
    418463&lt;listiterator object at 0x012150D0> 
    419464>>> list(bios) 
    420 [] 
     465[&lt;arch.Biography object at 0x01158E10>, 
     466 &lt;arch.Biography object at 0x0118B350>, 
     467 &lt;arch.Biography object at 0x0118B170>] 
    421468</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> 
     469We've only created three Biographies at this point, so we can print the list 
     470easily. At the other extreme (when you have hundreds of Biographies to filter), 
     471you can pass an optional <tt>Expression</tt> object to the related_units method. 
     472When you do, the list of associated Units will be filtered accordingly.</p> 
    427473 
    428474<p>Because the related_units method names are formed automatically, you need 
    429475to 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> 
     476In our example, we used "ArchID" for the name of our "foreign key". If we 
     477had used "Archaeologist" instead, we would have had problems; when we 
     478associated the classes, the <i>property</i> named "Archaeologist" would 
     479have collided with the <i>related_units method</i> named "Archaeologist". 
     480Be careful when naming your properties, and plan for the future. The best 
     481approach is probably to end your property name with "ID" every time.</p> 
    436482 
    437483<p>Unlike some other ORM's, Dejavu doesn't cache far Units within the near 
     
    453499properties and call sandbox.unit() for you, returning either the first 
    454500such far Unit or None if not found.</p> 
    455  
    456501 
    457502<h3>The Arena Object</h3> 
  • trunk/storage/db.py

    r49 r50  
    800800        return res.row_list, res.col_defs 
    801801     
    802     def recall(self, cls, expr=None, pairs=None): 
     802    def recall(self, cls, expr=None): 
    803803        if expr is None: 
    804804            expr = logic.Expression(lambda x: True) 
     
    806806        data, col_defs = self.fetch(sql) 
    807807         
    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)]) 
    813810         
    814811        # Get specs on properties. Get the ID property first in case other 
     
    937934     
    938935    def destroy(self, unit): 
    939         """Delete the unit.""" 
     936        """destroy(unit). Delete the unit.""" 
    940937        self.execute(u'DELETE * FROM %s WHERE %s = %s;' % 
    941938                     (self.tablename(unit), self.identifier("ID"), 
     
    943940     
    944941    def distinct(self, cls, fields, expr=None): 
    945         """Return distinct values for specified fields.""" 
     942        """distinct(cls, fields, expr=None) -> Distinct values for given fields.""" 
    946943        if expr is None: 
    947944            expr = logic.Expression(lambda x: True) 
     
    965962                       for i, val in enumerate(row)]) 
    966963                 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  
    311311            raise 
    312312     
    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.prefix 
    325         i = self.identifier 
    326         left = i(p, firstcls.__name__) 
    327         right = i(p, cls.__name__) 
    328         if len(spath) == 1: 
    329             child = right 
    330         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_imp 
    352             if w and w != "TRUE": 
    353                 w = " WHERE %s AND %s" % (w, firstwhere) 
    354             else: 
    355                 w = " WHERE %s" % firstwhere 
    356              
    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, cols 
    365      
    366313    def execute(self, query, conn=None): 
    367314        if conn is None: 
  • trunk/storage/storepypgsql.py

    r49 r50  
    100100                columns.append((res.fname(index), res.ftype(index))) 
    101101         
    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() 
    107105         
    108         return iterator(), columns 
     106        return data, columns 
    109107 
  • trunk/storage/storeshelve.py

    r49 r50  
    3636        return s, lock 
    3737     
    38     def recall(self, cls, expr=None, pairs=None): 
     38    def recall(self, cls, expr=None): 
    3939        units = [] 
    4040        data, lock = self.shelf(cls) 
     
    9797     
    9898    def distinct(self, cls, fields, expr=None): 
    99         """Return distinct values for specified fields.""" 
     99        """distinct(cls, fields, expr=None) -> Distinct values for given fields.""" 
    100100        if expr is None: 
    101101            expr = logic.Expression(lambda x: True) 
     
    114114        finally: 
    115115            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 
    116142 
  • trunk/storage/zoo_fixture.py

    r49 r50  
    6060        box.memorize(zoo.Animal(Name='Ostrich', Legs=2, PreviousZoos=[])) 
    6161        box.memorize(zoo.Animal(Name='Centipede', Legs=100)) 
     62         
    6263        emp = zoo.Animal(Name='Emperor Penguin', Legs=2) 
    6364        box.memorize(emp) 
     
    6667         
    6768        millipede = zoo.Animal(Name='Millipede', Legs=1000000) 
    68         millipede.add(SDZ) 
    6969        millipede.PreviousZoos = [WAP.ID] 
    7070        box.memorize(millipede) 
     71         
     72        SDZ.add(emp, adelie, millipede) 
    7173         
    7274        # Exhibits 
     
    204206        self.assertEqual(legs, [1, 2, 4, 100, 1000000]) 
    205207     
    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() 
    223238 
    224239 
     
    242257            pass 
    243258 
    244 def run_tests(SM_class, opts): 
     259def run_tests(SM_class, opts, profile=True): 
    245260    import traceback 
    246261    try: 
    247262        try: 
    248263            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__) 
    250270        except SystemExit: 
    251271            # unittest.main normally raises SystemExit when complete.