improve phony execute_sql()
[matthijs/upstream/django-ldapdb.git] / ldapdb / backends / ldap / compiler.py
1 # -*- coding: utf-8 -*-
2
3 # django-ldapdb
4 # Copyright (c) 2009-2010, Bolloré telecom
5 # All rights reserved.
6
7 # See AUTHORS file for a full list of contributors.
8
9 # Redistribution and use in source and binary forms, with or without modification,
10 # are permitted provided that the following conditions are met:
11
12 #     1. Redistributions of source code must retain the above copyright notice, 
13 #        this list of conditions and the following disclaimer.
14 #     
15 #     2. Redistributions in binary form must reproduce the above copyright 
16 #        notice, this list of conditions and the following disclaimer in the
17 #        documentation and/or other materials provided with the distribution.
18
19 #     3. Neither the name of Bolloré telecom nor the names of its contributors
20 #        may be used to endorse or promote products derived from this software
21 #        without specific prior written permission.
22
23 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
24 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
27 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
28 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
30 # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
31 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
32 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 #
34
35 import ldap
36
37 from django.db.models.sql import aggregates, compiler
38 from django.db.models.sql.where import AND, OR
39
40 def get_lookup_operator(lookup_type):
41     if lookup_type == 'gte':
42         return '>='
43     elif lookup_type == 'lte':
44         return '<='
45     else:
46         return '='
47
48 def query_as_ldap(query):
49     filterstr = ''.join(['(objectClass=%s)' % cls for cls in query.model.object_classes])
50     sql, params = where_as_ldap(query.where)
51     filterstr += sql
52     return '(&%s)' % filterstr
53
54 def where_as_ldap(self):
55     bits = []
56     for item in self.children:
57         if hasattr(item, 'as_sql'):
58             sql, params = where_as_ldap(item)
59             bits.append(sql)
60             continue
61
62         constraint, lookup_type, y, values = item
63         comp = get_lookup_operator(lookup_type)
64         if lookup_type == 'in':
65             equal_bits = [ "(%s%s%s)" % (constraint.col, comp, value) for value in values ]
66             clause = '(|%s)' % ''.join(equal_bits)
67         else:
68             clause = "(%s%s%s)" % (constraint.col, comp, values)
69
70         bits.append(clause)
71
72     if not len(bits):
73         return '', []
74
75     if len(bits) == 1:
76         sql_string = bits[0]
77     elif self.connector == AND:
78         sql_string = '(&%s)' % ''.join(bits)
79     elif self.connector == OR:
80         sql_string = '(|%s)' % ''.join(bits)
81     else:
82         raise Exception("Unhandled WHERE connector: %s" % self.connector)
83
84     if self.negated:
85         sql_string = ('(!%s)' % sql_string)
86
87     return sql_string, []
88
89 class SQLCompiler(object):
90     def __init__(self, query, connection, using):
91         self.query = query
92         self.connection = connection
93         self.using = using
94
95     def execute_sql(self, result_type=compiler.MULTI):
96         if result_type !=compiler.SINGLE:
97             raise Exception("LDAP does not support MULTI queries")
98
99         for key, aggregate in self.query.aggregate_select.items():
100             if not isinstance(aggregate, aggregates.Count):
101                 raise Exception("Unsupported aggregate %s" % aggregate)
102
103         try:
104             vals = self.connection.search_s(
105                 self.query.model.base_dn,
106                 self.query.model.search_scope,
107                 filterstr=query_as_ldap(self.query),
108                 attrlist=['dn'],
109             )
110         except ldap.NO_SUCH_OBJECT:
111             vals = []
112
113         if not vals:
114             return None
115
116         output = []
117         for alias, col in self.query.extra_select.iteritems():
118             output.append(col[0])
119         for key, aggregate in self.query.aggregate_select.items():
120             if isinstance(aggregate, aggregates.Count):
121                 output.append(len(vals))
122             else:
123                 output.append(None)
124         return output
125
126     def results_iter(self):
127         if self.query.select_fields:
128             fields = self.query.select_fields
129         else:
130             fields = self.query.model._meta.fields
131
132         attrlist = [ x.db_column for x in fields if x.db_column ]
133
134         try:
135             vals = self.connection.search_s(
136                 self.query.model.base_dn,
137                 self.query.model.search_scope,
138                 filterstr=query_as_ldap(self.query),
139                 attrlist=attrlist,
140             )
141         except ldap.NO_SUCH_OBJECT:
142             return
143
144         # perform sorting
145         if self.query.extra_order_by:
146             ordering = self.query.extra_order_by
147         elif not self.query.default_ordering:
148             ordering = self.query.order_by
149         else:
150             ordering = self.query.order_by or self.query.model._meta.ordering
151         def cmpvals(x, y):
152             for fieldname in ordering:
153                 if fieldname.startswith('-'):
154                     fieldname = fieldname[1:]
155                     negate = True
156                 else:
157                     negate = False
158                 field = self.query.model._meta.get_field(fieldname)
159                 attr_x = field.from_ldap(x[1].get(field.db_column, []), connection=self.connection)
160                 attr_y = field.from_ldap(y[1].get(field.db_column, []), connection=self.connection)
161                 # perform case insensitive comparison
162                 if hasattr(attr_x, 'lower'):
163                     attr_x = attr_x.lower()
164                 if hasattr(attr_y, 'lower'):
165                     attr_y = attr_y.lower()
166                 val = negate and cmp(attr_y, attr_x) or cmp(attr_x, attr_y)
167                 if val:
168                     return val
169             return 0
170         vals = sorted(vals, cmp=cmpvals)
171
172         # process results
173         pos = 0
174         for dn, attrs in vals:
175             # FIXME : This is not optimal, we retrieve more results than we need
176             # but there is probably no other options as we can't perform ordering
177             # server side.
178             if (self.query.low_mark and pos < self.query.low_mark) or \
179                (self.query.high_mark is not None and pos >= self.query.high_mark):
180                 pos += 1
181                 continue
182             row = []
183             for field in iter(fields):
184                 if field.attname == 'dn':
185                     row.append(dn)
186                 elif hasattr(field, 'from_ldap'):
187                     row.append(field.from_ldap(attrs.get(field.db_column, []), connection=self.connection))
188                 else:
189                     row.append(None)
190             yield row
191             pos += 1
192
193 class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler):
194     pass
195
196 class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler):
197     def execute_sql(self, result_type=compiler.MULTI):
198         try:
199             vals = self.connection.search_s(
200                 self.query.model.base_dn,
201                 self.query.model.search_scope,
202                 filterstr=query_as_ldap(self.query),
203                 attrlist=['dn'],
204             )
205         except ldap.NO_SUCH_OBJECT:
206             return
207
208         # FIXME : there is probably a more efficient way to do this 
209         for dn, attrs in vals:
210             self.connection.delete_s(dn)
211
212 class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler):
213     pass
214
215 class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler):
216     pass
217
218 class SQLDateCompiler(compiler.SQLDateCompiler, SQLCompiler):
219     pass
220