Contact: fumanchu@aminus.org

Log in as guest/geniusql to create tickets

Changeset 35

Show
Ignore:
Timestamp:
03/15/07 22:49:03
Author:
fumanchu
Message:

Preliminary support for using lambdas as select attributes.

Files:

Legend:

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

    r34 r35  
    1 """Geniusql, a Python database library. 
    2  
    3 The Column and Index classes model corresponding database objects, and are 
    4 intentionally simple. They should rarely contain any SQL or "smarts" of 
    5 any kind, besides the "qname", the quoted name, of the column or index. 
    6 At most, subclasses and consumers might put implementation-specific data 
    7 into them. 
    8  
    9 The IndexSet, Table, and Database objects are all dict-like containers, 
    10 and therefore have a key for each value. Those keys should equate to things 
    11 at the consumer layer; for example, a Database may possess a pair of the 
    12 form: {'YoYo': Table('yoyo')} -- the key is the "friendly" name, but the 
    13 Table.name is a lowercase version of that, because that's what the database 
    14 uses in SQL to refer to that table. 
    15 """ 
     1"""Geniusql, a Python database library.""" 
    162 
    173__version__ = "1.0alpha" 
  • trunk/geniusql/decompile.py

    r34 r35  
    66    'ConstWrapper', 'ColumnWrapper', 'Sentinel', 
    77    'cannot_represent', 'kw_arg', 'SQLDecompiler', 
     8    'AttributeDecompiler', 
    89    ] 
    9  
    10  
    11 # -------------------------- SQL DECOMPILATION -------------------------- # 
    1210 
    1311 
     
    382380        return "LENGTH(" + x + ")" 
    383381 
     382 
     383# ----------------------- Attribute Decompilation ----------------------- # 
     384 
     385 
     386class ColumnExpr(object): 
     387    """Wraps a column for use in AttributeDecompiler's stack. 
     388     
     389    When we hit LOAD_ATTR while decompiling, we need to keep the column 
     390    metadata around. 
     391     
     392    expr: the expression to be used for this column in the SELECT clause. 
     393        This should not contain an alias ("AS" clause); that will be 
     394        provided by the consumer, usually via the 'key' attribute. 
     395    """ 
     396     
     397    def __init__(self, table, col, key, expr): 
     398        self.table = table 
     399        self.col = col 
     400        self.key = key 
     401        self.alias = key 
     402        self.expr = expr 
     403        self.aggregate = False 
     404 
     405 
     406class AttributeDecompiler(codewalk.Visitor): 
     407    """Produce an SQL source list from a supplied Expression object. 
     408     
     409    Attributes of each argument in the signature will be mapped to table 
     410    columns. Keyword arguments should be bound using Expression.bind_args 
     411    before calling this decompiler. 
     412     
     413    After walk(), self.stack should be reduced to a list of ColumnExpr's. 
     414    """ 
     415     
     416    # Some constants are function or class objects, 
     417    # which should not be coerced. 
     418    no_coerce = (FunctionType, 
     419                 type, 
     420                 type(len),       # <type 'builtin_function_or_method'> 
     421                 ) 
     422     
     423    sql_cmp_op = ('<', '<=', '=', '!=', '>', '>=', 'in', 'not in') 
     424     
     425    def __init__(self, tables, expr, adapter): 
     426        self.tables = tables 
     427        self.expr = expr 
     428        self.adapter = adapter 
     429        self.groups = [] 
     430        codewalk.Visitor.__init__(self, expr.func) 
     431     
     432    def walk(self): 
     433        self.stack = [] 
     434        codewalk.Visitor.walk(self) 
     435     
     436    def visit_LOAD_DEREF(self, lo, hi): 
     437        raise ValueError("Illegal reference found in %s." % self.expr) 
     438     
     439    def visit_LOAD_GLOBAL(self, lo, hi): 
     440        raise ValueError("Illegal global found in %s." % self.expr) 
     441     
     442    def visit_LOAD_FAST(self, lo, hi): 
     443        arg_index = lo + (hi << 8) 
     444        if arg_index < self.co_argcount: 
     445            # We've hit a reference to a positional arg, which in our case 
     446            # implies a reference to a DB table. Append the (qname, table) 
     447            # tuple for later unpacking inside visit_LOAD_ATTR. 
     448            self.stack.append(self.tables[arg_index]) 
     449        else: 
     450            # Since lambdas don't support local bindings, 
     451            # any remaining local name must be a keyword arg. 
     452            raise ValueError("Illegal keyword arg found in %s." % self.expr) 
     453     
     454    def visit_LOAD_ATTR(self, lo, hi): 
     455        name = self.co_names[lo + (hi << 8)] 
     456        tos = self.stack.pop() 
     457        if isinstance(tos, tuple): 
     458            # The name in question refers to a DB column (see visit_LOAD_FAST). 
     459            alias, table = tos 
     460            col = table[name] 
     461            atom = ColumnExpr(table, col, name, '%s.%s' % (alias, col.qname)) 
     462        else: 
     463            # 'tos.name' will reference an attribute of the tos object. 
     464            # Stick the tos and name in a tuple for later processing 
     465            # (for example, in visit_CALL_FUNCTION). 
     466            atom = (tos, name) 
     467        self.stack.append(atom) 
     468     
     469    def visit_LOAD_CONST(self, lo, hi): 
     470        val = self.co_consts[lo + (hi << 8)] 
     471        if not isinstance(val, self.no_coerce): 
     472            val = ConstWrapper(val, self.adapter.coerce(val)) 
     473        self.stack.append(val) 
     474     
     475    def visit_CALL_FUNCTION(self, lo, hi): 
     476        kwargs = {} 
     477        for i in xrange(hi): 
     478            val = self.stack.pop() 
     479            key = self.stack.pop() 
     480            kwargs[key] = val 
     481        kwargs = [k + "=" + v for k, v in kwargs.iteritems()] 
     482         
     483        args = [] 
     484        for i in xrange(lo): 
     485            arg = self.stack.pop() 
     486            args.append(arg) 
     487        args.reverse() 
     488         
     489        if kwargs: 
     490            args += kwargs 
     491         
     492        func = self.stack.pop() 
     493         
     494        # Handle function objects. 
     495        funcname = func.__module__ + "_" + func.__name__ 
     496        funcname = funcname.replace(".", "_") 
     497        if funcname.startswith("_"): 
     498            funcname = "func" + funcname 
     499        dispatch = getattr(self, funcname) 
     500        self.stack.append(dispatch(*args)) 
     501     
     502    def visit_UNARY_NOT(self): 
     503        op = self.stack.pop() 
     504        self.stack.append("NOT (" + op + ")") 
     505     
     506    # --------------------------- Dispatchees --------------------------- # 
     507     
     508    def dejavu_day(self, x): 
     509        x.alias = "day_%s" % x.alias 
     510        x.expr = "DAY(" + x.expr + ")" 
     511        return x 
     512     
     513    def func__builtin___min(self, x): 
     514        x.aggregate = True 
     515        x.alias = "min_%s" % x.alias 
     516        x.expr = "MIN(" + x.expr + ")" 
     517        return x 
     518     
     519    def func__builtin___max(self, x): 
     520        x.aggregate = True 
     521        x.alias = "max_%s" % x.alias 
     522        x.expr = "MAX(" + x.expr + ")" 
     523        return x 
     524     
     525    def geniusql_decompile_count(self, x): 
     526        x.aggregate = True 
     527        x.alias = "count_%s" % x.alias 
     528        x.expr = "COUNT(" + x.expr + ")" 
     529        return x 
     530 
  • trunk/geniusql/objects.py

    r34 r35  
     1"""Geniusql architectural classes. 
     2 
     3The Column and Index classes model corresponding database objects, and are 
     4intentionally simple. They should rarely contain any SQL or "smarts" of 
     5any kind, besides the "qname", the quoted name, of the column or index. 
     6At most, subclasses and consumers might put implementation-specific data 
     7into them. 
     8 
     9The IndexSet, Table, and Database objects are all dict-like containers, 
     10and therefore have a key for each value. Those keys should equate to things 
     11at the consumer layer; for example, a Database may possess a pair of the 
     12form: {'YoYo': Table('yoyo')} -- the key is the "friendly" name, but the 
     13Table.name is a lowercase version of that, because that's what the database 
     14uses in SQL to refer to that table. 
     15""" 
     16 
    117import threading 
    218from dejavu import logic 
     
    756772    typeadapter = adapters.TypeAdapter() 
    757773    decompiler = decompile.SQLDecompiler 
     774    attributedecompiler = decompile.AttributeDecompiler 
    758775    joinwrapper = select.TableWrapper 
    759776    selectwriter = select.SelectWriter 
  • trunk/geniusql/providers/ado.py

    r34 r35  
    677677            query = query.encode(self.adaptertosql.encoding) 
    678678         
    679         self.log(repr((id(conn), id(getattr(conn, "conn", None)), query))
     679        self.log(query
    680680        try: 
    681681            bareconn = conn 
  • trunk/geniusql/select.py

    r34 r35  
     1from types import FunctionType 
     2 
    13from geniusql import errors 
    24from dejavu import logic 
     
    111113        must be in the same order as the tables in the relation. 
    112114     
    113     input_list: the sources mentioned for each column in the FROM clause 
    114     output_list: the final names (aliases) for each column in the FROM clause. 
    115     output_keys: the final keys for each column in the FROM clause. 
     115    input_list: a list of SQL expressions, one for each column in the 
     116        SELECT clause. These will include any "expr AS name" alias. 
     117    output_list: a list of the SQL names (aliases), one for each column. 
     118    output_keys: the keys for each column in the final table. 
    116119    """ 
    117120     
     
    149152        self.output_list = [] 
    150153        self.output_keys = [] 
     154        self._groupby = [] 
    151155        if self.attributes is None: 
    152156            # Return all columns 
    153157            for t in self.tables: 
    154158                self._get_columns(t, None) 
     159        elif isinstance(self.attributes, FunctionType): 
     160            self.attributes = logic.Expression(self.attributes) 
     161            self.decompile_attributes() 
     162        elif isinstance(self.attributes, logic.Expression): 
     163            self.decompile_attributes() 
    155164        else: 
    156165            if isinstance(relation, Join): 
     
    227236            w = "" 
    228237         
    229         return "SELECT %s%s%s FROM %s%s;" % (d, cols, into, self.fromclause, w) 
     238        if self._groupby and len(self._groupby) < len(self.input_list): 
     239            gb = " GROUP BY " + ", ".join(self._groupby) 
     240        else: 
     241            gb = "" 
     242         
     243        return ("SELECT %s%s%s FROM %s%s%s;" % 
     244                (d, cols, into, self.fromclause, w, gb)) 
    230245     
    231246    def where(self): 
     
    323338        raise errors.ReferenceError("No reference found between %s and %s." 
    324339                                    % (name1, name2)) 
    325  
     340     
     341    def decompile_attributes(self): 
     342        tpairs = [(t.alias or t.qname, t.table) for t in self.tables] 
     343        decom = self.db.attributedecompiler 
     344        decom = decom(tpairs, self.attributes, self.db.adaptertosql) 
     345        decom.walk() 
     346        for atom in decom.stack: 
     347            newcol = atom.col.copy() 
     348            newcol.key = False 
     349            newcol.autoincrement = False 
     350            newcol.sequence_name = None 
     351            newcol.initial = 1 
     352             
     353            if atom.alias in self.result: 
     354                schema = atom.table.schema 
     355                atom.alias = '%s_%s' % (schema.key_for(atom.table), atom.alias) 
     356                newcol.name = '%s_%s' % (atom.table.name, newcol.name) 
     357                newcol.qname = schema.db.quote(newcol.name) 
     358             
     359            self.output_list.append(newcol.qname) 
     360            self.output_keys.append(atom.alias) 
     361            if not atom.aggregate: 
     362                self._groupby.append(atom.expr) 
     363            self.result[atom.alias] = newcol 
     364             
     365            expr = atom.expr 
     366            if atom.alias != atom.key: 
     367                expr = '%s AS %s' % (expr, atom.alias) 
     368            self.input_list.append(expr) 
     369 
  • trunk/geniusql/test/zoo_fixture.py

    r34 r35  
    401401    def test_5_Aggregates(self): 
    402402        try: 
     403            Animal = schema['Animal'] 
     404            Visit = schema['Visit'] 
     405            Vet = schema['Vet'] 
     406             
    403407            # views 
    404             view = db.select(schema['Animal'], ['Legs']) 
     408            view = db.select(Animal, ['Legs']) 
    405409            legs = [x[0] for x in view] 
    406410            legs.sort() 
     
    419423                        'Ape': None, 
    420424                        } 
    421             for species, lifespan in db.select(schema['Animal'], ['Species', 'Lifespan']): 
     425            for species, lifespan in db.select(Animal, ['Species', 'Lifespan']): 
    422426                if expected[species] is None: 
    423427                    self.assertEqual(lifespan, None) 
     
    436440            # distinct 
    437441            legs = [x[0] for x in 
    438                     db.select(schema['Animal'], ['Legs'], distinct=True)] 
     442                    db.select(Animal, ['Legs'], distinct=True)] 
    439443            legs.sort() 
    440444            self.assertEqual(legs, [1, 2, 4, 100, 1000000]) 
     
    442446            # This may raise a warning on some DB's. 
    443447            f = (lambda x: x.Species == 'Lion') 
    444             escapees = db.select(schema['Animal'], ['Legs'], f, distinct=True) 
    445             self.assertEqual(list(escapees), [[4]]) 
     448            lionlegs = db.select(Animal, ['Legs'], f, distinct=True) 
     449            self.assertEqual(list(lionlegs), [[4]]) 
     450             
     451            # Test attribute lambdas 
     452            visits = db.select(Animal << Visit << Vet, 
     453                               lambda a, v, vet: (a.Species, vet.Name), 
     454                               lambda a, v, vet: vet.Name != None, 
     455                               distinct = True) 
     456            visits = list(visits) 
     457            visits.sort() 
     458            self.assertEqual(visits, [[u'Emperor Penguin', u'Jim McBain'], 
     459                                      [u'Tiger', u'Charles Schroeder']]) 
     460             
     461            # Test attribute lambdas with GROUP BY 
     462            firstvisit = db.select(Animal << Visit, 
     463                                   lambda a, v: (a.Species, min(v.Date))) 
     464            firstvisit = list(firstvisit) 
     465            firstvisit.sort() 
     466            self.assertEqual(firstvisit, 
     467                             [[u'Adelie Penguin', None], 
     468                              [u'Ape', None], 
     469                              [u'Bear', None], 
     470                              [u'Centipede', None], 
     471                              [u'Emperor Penguin', datetime.date(2001, 1, 1)], 
     472                              [u'Leopard', None], 
     473                              [u'Lion', None], 
     474                              [u'Millipede', None], 
     475                              [u'Ostrich', None], 
     476                              [u'Slug', None], 
     477                              [u'Tiger', datetime.date(2001, 1, 1)]] 
     478                             ) 
     479             
     480            # Test global agg funcs 
     481            visits = db.select(Animal << Visit, 
     482                               lambda a, v: (a.Species, count(v.Date))) 
     483            visits = list(visits) 
     484            visits.sort() 
     485            self.assertEqual(visits, [[]]) 
    446486        finally: 
    447487            db.connections.commit()