> I will try and clean up the code today and post it along with some
> examples. Its just a proof of concept, so it can only deal with simple
> forward foreign keys, but I think this approach might just work.

here it is:

very simple example:

>>> from django.contrib.admin import models
>>> import query
>>> q = query.Query( models.LogEntry )
>>> q.filter( user__username='aa')
>>> q.order_by( '-user__id' )
>>> print q
SELECT
"t0"."id", "t0"."action_time", "t0"."user_id", "t0"."content_type_id",
"t0"."object_id", "t0"."object_repr", "t0"."action_flag",
"t0"."change_message"
FROM
"django_admin_log" as "t0" INNER JOIN "auth_user" as "user" ON
"t0"."user_id" = "user"."id"
WHERE
"user"."username" = aa
ORDER BY
"user"."id" DESC

>>>


In the code you can find FIXME and TODO marks, FIXME marks something
broken, TODO stands for something missing.

Looking forward to any comments...

-- 
Honza Král
E-Mail: [EMAIL PROTECTED]
ICQ#:   107471613
Phone:  +420 606 678585

--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Django developers" group.
To post to this group, send email to django-developers@googlegroups.com
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/django-developers?hl=en
-~----------~----~----~----~------~----~------~--~---

from django.db import backend
from django.utils.datastructures import SortedDict

class Model( object ):
    """
        Represent a table belonging to a model. Can be joined with another table/model via a field.
    """
    def __init__( self, query, model, alias=None, joined=False ):
        self.query, self.model, self.joined = query, model, joined
        self.name = model._meta.db_table
        self.joins = SortedDict()
        self._alias = alias

    def __str__( self ):
        # FIXME: move this to Query.__str__()
        output = [ '%s as %s' % ( backend.quote_name( self.name ), backend.quote_name( self.alias ) ) ]
        if self.joins:
            for join_type, table, source, target in self.joins.values():
                output.append( '%s JOIN %s ON %s = %s' % (
                                    join_type, table,
                                    source.alias,
                                    target.alias,
                    ) )
        return ' '.join( output )

    def _get_alias( self ):
        if not self._alias:
            self._alias = self.query.get_alias( self )
        return backend.quote_name( self._alias )

    def _set_alias( self, alias ):
        self._alias = alias
    alias = property( _get_alias, _set_alias )

    def join( self, field, path ):
        # FIXME: add support for other things than just simple ForeignKey
        # probably in Query.lookup_path though
        label = '__'.join( path )

        if self.joins.has_key( label ):
            # already joined
            return
        else:
            # register the table, but mark it as joined, so it doesn't make it to FROM on its own
            table = Model( self.query, field.rel.to, alias=label, joined=True )
            self.query.tables[label] = table

            # the join itself
            self.joins[label] = (
                    field.null and 'LEFT' or 'INNER', 
                    table,
                    Field( self.query, self.model, field, path ),
                    Field( self.query, field.rel.to, field.rel.to._meta.pk, path + ['pk'] ),
                )

class Field( object ):
    """
        represent a column/field
    """
    def __init__( self, query, model, field, path=None ):
        self.query, self.model, self.field, self.path = query, model, field, path

        # TODO: check for DB specific limitations
        if path:
            self._alias = '__'.join( path )
        else:
            self._alias = self.query.get_table( self.model ).name + '_' + self.field.column

    def __str__( self ):
        # FIXME: move the rendering to Query.__str__
        # TODO: add some meaningful alias
        return self.alias
    def get_select( self ):
        return self.__str__()

    def _get_alias( self ):
        return '%s.%s' % ( self.query.get_table( self.model ).alias, backend.quote_name( self.field.column ) )

    alias = property( _get_alias )

# FIXME: quoting and params use, since query is available from every object, simply call add_param() and put a placeholder

# TODO: add a simple mechanism to register own SQL operators, a simple function, that will get a field and any object
OPERATORS = {
    'lt' : lambda f,v: '%s < %s' % (f.alias, v),
    'exact' : lambda f,v: '%s = %s' % (f.alias, v),
    'icontains' : lambda f,v: "%s ILIKE '%%%s%%'" % (f.alias, v),
}
DEFAULT_OPERATOR = OPERATORS['exact']
QOR = lambda x,y: '((%s) OR (%s))' % ( x, y )
QNOT = lambda x,y: 'NOT (%s)' % ' AND '.join( operator( field, value ) for operator, field, value in y )

class Q( object ):
    def __init__( self, query, **kwargs ):
        self.query = query

        self.conditions = []
        self.filter( **kwargs )

    def __and__( self, other ):
        new = Q( self.query )
        new.conditions = self.conditions + other.conditions
        return new

    def __or__( self, other ):
        new = Q( self.query )
        new.conditions = [ ( self, QOR, other ) ]
        return new


    def __nonzero__( self ):
        return bool( self.conditions )

    def filter( self, **kwargs ):
        self.conditions.extend( self.parse( **kwargs ) )

    def exclude( self, **kwargs ):
        self.conditions.append( (None, QNOT, self.parse( **kwargs ) ) )

    def parse( self, **kwargs ):
        to_append = []
        for lookup, value in kwargs.items():
            path = lookup.split( '__' )
            if path[-1] in OPERATORS:
                operator = OPERATORS[path[-1]]
                path = path[:-1]
            else:
                operator = DEFAULT_OPERATOR
            field = self.query.lookup_path( path )
            to_append.append( ( field, operator, value ) )
        return to_append

    def __str__( self ):
        return ' AND '.join( [ operator( field, value ) for field, operator, value in self.conditions ] )


# TODO: add copy/clone behavior
class Query( object ):
    def __init__( self, model ):
        self.model = model
        main_table = Model( self, model )

        self.tables = SortedDict()
        self.tables[ '' ] = main_table
        self.fields = [ Field( self, model, f ) for f in model._meta.fields ]
        self.where = Q( self )
        self._order_by = []
        self.limit = ()

    def __str__( self ):
        # FIXME: move all rendering into this method from the individual classes (save only Q)
        output = [ 'SELECT' ]
        output.append( ', '.join( str(f) for f in self.fields ) )

        output.append( 'FROM' )
        output.append( ', '.join( str(t) for t in self.tables.values() if not t.joined ) )

        if self.where:
            output.append( 'WHERE' )
            output.append( str( self.where ) )

        if self._order_by:
            output.append( 'ORDER BY' )
            output.append( ', '.join( '%s %s' % ( f.alias, order ) for f, order in self._order_by ) )

        if self.limit:
            output.append( 'OFFSET %d LIMIT %d' % self.limit )

        return '\n'.join( output )

    def __repr__( self ):
        return self.__str__()

    def __getitem__( self, k ):
        assert (not isinstance(k, slice) and (k >= 0)) \
            or (isinstance(k, slice) and (k.start is None or k.start >= 0) and (k.stop is None or k.stop >= 0)), \
            "Negative indexing is not supported."

        # FIXME: handle already sliced stuff, handle Nones
        if isinstance( k, slice ):
            self.limit = ( k.start, k.stop-k.start )
        else:
            self.limit = ( k, 1 )

    # TODO: add support for passing Q object
    def exclude( self, **kwargs ):
        self.where.exclude( **kwargs )

    # TODO: add support for passing Q object
    def filter( self, **kwargs ):
        self.where.filter( **kwargs )

    def order_by( self, *args ):
        self._order_by = []
        for field in args:
            if field.startswith( '-' ):
                order = 'DESC'
                field = field[1:]
            else:
                order = 'ASC'
            field = self.lookup_path( field.split( '__' ) )
            self._order_by.append( ( field, order, ) )

    def get_table( self, model ):
        # TODO: add a parameter to specify PATH, so that multiple tables for one model can coexist
        # FIXME: ugly, ugly, ugly
        return [ t for t in self.tables.values() if t.model == model ][0]

    def get_alias( self, table ):
        return 't%d' % self.tables.values().index( table )

    def lookup_path( self, path ):
        # FIXME, this is where the main work lies, main problem lies in how to determine which joins are necessary
        model = self.model
        field = False
        for step in path:
            if field and field.rel:
                old_m = model
                model = field.rel.to

                table = self.get_table( old_m )
                table.join( field, path[:path.index(step)] )
            field = model._meta.get_field( step )

        return Field( self, model, field, path )

Reply via email to