> 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 )