Initial slice view with users
[myslice.git] / manifold / core / query.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # Query representation
5 #
6 # Copyright (C) UPMC Paris Universitas
7 # Authors:
8 #   Jordan AugĂ©         <jordan.auge@lip6.fr>
9 #   Marc-Olivier Buob   <marc-olivier.buob@lip6.fr>
10 #   Thierry Parmentelat <thierry.parmentelat@inria.fr>
11
12 from types                      import StringTypes
13 from manifold.core.filter         import Filter, Predicate
14 from manifold.util.frozendict     import frozendict
15 from manifold.util.type           import returns, accepts
16 import copy
17
18 import json
19 import uuid
20
21 def uniqid (): 
22     return uuid.uuid4().hex
23
24 debug=False
25 debug=True
26
27 class ParameterError(StandardError): pass
28
29 class Query(object):
30     """
31     Implements a TopHat query.
32
33     We assume this is a correct DAG specification.
34
35     1/ A field designates several tables = OR specification.
36     2/ The set of fields specifies a AND between OR clauses.
37     """
38
39     #--------------------------------------------------------------------------- 
40     # Constructor
41     #--------------------------------------------------------------------------- 
42
43     def __init__(self, *args, **kwargs):
44
45         self.query_uuid = uniqid()
46
47         # Initialize optional parameters
48         self.clear()
49     
50         #l = len(kwargs.keys())
51         len_args = len(args)
52
53         if len(args) == 1:
54             if isinstance(args[0], dict):
55                 kwargs = args[0]
56                 args = []
57
58         # Initialization from a tuple
59
60         if len_args in range(2, 7) and type(args) == tuple:
61             # Note: range(x,y) <=> [x, y[
62
63             # XXX UGLY
64             if len_args == 3:
65                 self.action = 'get'
66                 self.params = {}
67                 self.timestamp     = 'now'
68                 self.object, self.filters, self.fields = args
69             elif len_args == 4:
70                 self.object, self.filters, self.params, self.fields = args
71                 self.action = 'get'
72                 self.timestamp     = 'now'
73             else:
74                 self.action, self.object, self.filters, self.params, self.fields, self.timestamp = args
75
76         # Initialization from a dict
77         elif "object" in kwargs:
78             if "action" in kwargs:
79                 self.action = kwargs["action"]
80                 del kwargs["action"]
81             else:
82                 print "W: defaulting to get action"
83                 self.action = "get"
84
85
86             self.object = kwargs["object"]
87             del kwargs["object"]
88
89             if "filters" in kwargs:
90                 self.filters = kwargs["filters"]
91                 del kwargs["filters"]
92             else:
93                 self.filters = Filter([])
94
95             if "fields" in kwargs:
96                 self.fields = set(kwargs["fields"])
97                 del kwargs["fields"]
98             else:
99                 self.fields = set([])
100
101             # "update table set x = 3" => params == set
102             if "params" in kwargs:
103                 self.params = kwargs["params"]
104                 del kwargs["params"]
105             else:
106                 self.params = {}
107
108             if "timestamp" in kwargs:
109                 self.timestamp = kwargs["timestamp"]
110                 del kwargs["timestamp"]
111             else:
112                 self.timestamp = "now" 
113
114             if kwargs:
115                 raise ParameterError, "Invalid parameter(s) : %r" % kwargs.keys()
116         #else:
117         #        raise ParameterError, "No valid constructor found for %s : args = %r" % (self.__class__.__name__, args)
118
119         if not self.filters: self.filters = Filter([])
120         if not self.params:  self.params  = {}
121         if not self.fields:  self.fields  = set([])
122         if not self.timestamp:      self.timestamp      = "now" 
123
124         if isinstance(self.filters, list):
125             f = self.filters
126             self.filters = Filter([])
127             for x in f:
128                 pred = Predicate(x)
129                 self.filters.add(pred)
130
131         if isinstance(self.fields, list):
132             self.fields = set(self.fields)
133
134         for field in self.fields:
135             if not isinstance(field, StringTypes):
136                 raise TypeError("Invalid field name %s (string expected, got %s)" % (field, type(field)))
137
138     #--------------------------------------------------------------------------- 
139     # Helpers
140     #--------------------------------------------------------------------------- 
141
142     def copy(self):
143         return copy.deepcopy(self)
144
145     def clear(self):
146         self.action = 'get'
147         self.object = None
148         self.filters = Filter([])
149         self.params  = {}
150         self.fields  = set([])
151         self.timestamp      = "now" 
152         self.timestamp  = 'now' # ignored for now
153
154     @returns(StringTypes)
155     def __str__(self):
156         return "SELECT %s FROM %s WHERE %s" % (
157             ", ".join(self.get_select()) if self.get_select() else '*',
158             self.get_from(),
159             self.get_where()
160         )
161
162     @returns(StringTypes)
163     def __repr__(self):
164         return self.__str__()
165
166     def __key(self):
167         return (self.action, self.object, self.filters, frozendict(self.params), frozenset(self.fields))
168
169     def __hash__(self):
170         print "HASH", self.__key()
171         return hash(self.__key())
172
173     #--------------------------------------------------------------------------- 
174     # Conversion
175     #--------------------------------------------------------------------------- 
176
177     def to_dict(self):
178         return {
179             'action': self.action,
180             'object': self.object,
181             'timestamp': self.timestamp,
182             'filters': self.filters,
183             'params': self.params,
184             'fields': list(self.fields)
185         }
186
187     def to_json (self, analyzed_query=None):
188         query_uuid=self.query_uuid
189         a=self.action
190         o=self.object
191         t=self.timestamp
192         f=json.dumps (self.filters.to_list())
193         p=json.dumps (self.params)
194         c=json.dumps (list(self.fields))
195         # xxx unique can be removed, but for now we pad the js structure
196         unique=0
197
198         if not analyzed_query:
199             aq = 'null'
200         else:
201             aq = analyzed_query.to_json()
202         sq="{}"
203         
204         result= """ new ManifoldQuery('%(a)s', '%(o)s', '%(t)s', %(f)s, %(p)s, %(c)s, %(unique)s, '%(query_uuid)s', %(aq)s, %(sq)s)"""%locals()
205         if debug: print 'ManifoldQuery.to_json:',result
206         return result
207     
208     # this builds a ManifoldQuery object from a dict as received from javascript through its ajax request 
209     # we use a json-encoded string - see manifold.js for the sender part 
210     # e.g. here's what I captured from the server's output
211     # manifoldproxy.proxy: request.POST <QueryDict: {u'json': [u'{"action":"get","object":"resource","timestamp":"latest","filters":[["slice_hrn","=","ple.inria.omftest"]],"params":[],"fields":["hrn","hostname"],"unique":0,"query_uuid":"436aae70a48141cc826f88e08fbd74b1","analyzed_query":null,"subqueries":{}}']}>
212     def fill_from_POST (self, POST_dict):
213         try:
214             json_string=POST_dict['json']
215             dict=json.loads(json_string)
216             for (k,v) in dict.iteritems(): 
217                 setattr(self,k,v)
218         except:
219             print "Could not decode incoming ajax request as a Query, POST=",POST_dict
220             if (debug):
221                 import traceback
222                 traceback.print_exc()
223
224     #--------------------------------------------------------------------------- 
225     # Accessors
226     #--------------------------------------------------------------------------- 
227
228     @returns(StringTypes)
229     def get_action(self):
230         return self.action
231
232     @returns(frozenset)
233     def get_select(self):
234         return frozenset(self.fields)
235
236     @returns(StringTypes)
237     def get_from(self):
238         return self.object
239
240     @returns(Filter)
241     def get_where(self):
242         return self.filters
243
244     @returns(dict)
245     def get_params(self):
246         return self.params
247
248     @returns(StringTypes)
249     def get_timestamp(self):
250         return self.timestamp
251
252 #DEPRECATED#
253 #DEPRECATED#    def make_filters(self, filters):
254 #DEPRECATED#        return Filter(filters)
255 #DEPRECATED#
256 #DEPRECATED#    def make_fields(self, fields):
257 #DEPRECATED#        if isinstance(fields, (list, tuple)):
258 #DEPRECATED#            return set(fields)
259 #DEPRECATED#        else:
260 #DEPRECATED#            raise Exception, "Invalid field specification"
261
262     #--------------------------------------------------------------------------- 
263     # LINQ-like syntax
264     #--------------------------------------------------------------------------- 
265
266     @classmethod
267     def action(self, action, object):
268         query = Query()
269         query.action = action
270         query.object = object
271         return query
272
273     @classmethod
274     def get(self, object): return self.action('get', object)
275
276     @classmethod
277     def update(self, object): return self.action('update', object)
278     
279     @classmethod
280     def create(self, object): return self.action('create', object)
281     
282     @classmethod
283     def delete(self, object): return self.action('delete', object)
284     
285     @classmethod
286     def execute(self, object): return self.action('execute', object)
287
288     def filter_by(self, *args):
289         if len(args) == 1:
290             filters = args[0]
291             if not isinstance(filters, (set, list, tuple, Filter)):
292                 filters = [filters]
293             for predicate in filters:
294                 self.filters.add(predicate)
295         elif len(args) == 3: 
296             predicate = Predicate(*args)
297             self.filters.add(predicate)
298         else:
299             raise Exception, 'Invalid expression for filter'
300         return self
301             
302     def select(self, fields):
303         if not isinstance(fields, (set, list, tuple)):
304             fields = [fields]
305         for field in fields:
306             self.fields.add(field)
307         return self
308
309     def set(self, params):
310         self.params.update(params)
311         return self
312
313 class AnalyzedQuery(Query):
314
315     # XXX we might need to propagate special parameters sur as DEBUG, etc.
316
317     def __init__(self, query=None):
318         self.clear()
319         if query:
320             self.query_uuid = query.query_uuid
321             self.analyze(query)
322         else:
323             self.query_uuid = uniqid()
324
325     @returns(StringTypes)
326     def __str__(self):
327         out = []
328         out.append("SELECT %s FROM %s WHERE %s" % (
329             ", ".join(self.get_select()),
330             self.get_from(),
331             self.get_where()
332         ))
333         cpt = 1
334         for method, subquery in self.subqueries():
335             out.append('  [SQ #%d : %s] %s' % (cpt, method, str(subquery)))
336             cpt += 1
337
338         return "\n".join(out)
339
340     def clear(self):
341         super(AnalyzedQuery, self).clear()
342         self._subqueries = {}
343
344     def subquery(self, method):
345         # Allows for the construction of a subquery
346         if not method in self._subqueries:
347             analyzed_query = AnalyzedQuery()
348             analyzed_query.action = self.action
349             analyzed_query.object = method
350             self._subqueries[method] = analyzed_query
351         return self._subqueries[method]
352
353     def subqueries(self):
354         for method, subquery in self._subqueries.iteritems():
355             yield (method, subquery)
356
357     def filter_by(self, filters):
358         if not filters: return self
359         if not isinstance(filters, (set, list, tuple, Filter)):
360             filters = [filters]
361         for predicate in filters:
362             if '.' in predicate.key:
363                 method, subkey = pred.key.split('.', 1)
364                 sub_pred = Predicate(subkey, pred.op, pred.value)
365                 self.subquery(method).filter_by(sub_pred)
366             else:
367                 super(AnalyzedQuery, self).filter_by(predicate)
368         return self
369
370     def select(self, fields):
371         if not isinstance(fields, (set, list, tuple)):
372             fields = [fields]
373         for field in fields:
374             if '.' in field:
375                 method, subfield = field.split('.', 1)
376                 self.subquery(method).select(subfield)
377             else:
378                 super(AnalyzedQuery, self).select(field)
379         return self
380
381     def set(self, params):
382         for param, value in self.params.items():
383             if '.' in param:
384                 method, subparam = param.split('.', 1)
385                 self.subquery(method).set({subparam: value})
386             else:
387                 super(AnalyzedQuery, self).set({param: value})
388         return self
389         
390     def analyze(self, query):
391         self.clear()
392         self.action = query.action
393         self.object = query.object
394         self.filter_by(query.filters)
395         self.set(query.params)
396         self.select(query.fields)
397
398     def to_json (self):
399         query_uuid=self.query_uuid
400         a=self.action
401         o=self.object
402         t=self.timestamp
403         f=json.dumps (self.filters.to_list())
404         p=json.dumps (self.params)
405         c=json.dumps (list(self.fields))
406         # xxx unique can be removed, but for now we pad the js structure
407         unique=0
408
409         aq = 'null'
410         sq=", ".join ( [ "'%s':%s" % (object, subquery.to_json())
411                   for (object, subquery) in self._subqueries.iteritems()])
412         sq="{%s}"%sq
413         
414         result= """ new ManifoldQuery('%(a)s', '%(o)s', '%(t)s', %(f)s, %(p)s, %(c)s, %(unique)s, '%(query_uuid)s', %(aq)s, %(sq)s)"""%locals()
415         if debug: print 'ManifoldQuery.to_json:',result
416         return result