e45844c9d8b9d63ee85a7acf03b8dd4b3b9e15aa
[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
155     def to_sql(self, platform='', multiline=False):
156         get_params_str = lambda : ', '.join(['%s = %r' % (k, v) for k, v in self.get_params().items()])
157         get_select_str = lambda : ', '.join(self.get_select()) 
158
159         table  = self.get_from()
160         select = 'SELECT %s' % (get_select_str()    if self.get_select()    else '*')
161         where  = 'WHERE %s'  % self.get_where()     if self.get_where()     else ''
162         at     = 'AT %s '    % self.get_timestamp() if self.get_timestamp() else ''
163         params = 'SET %s'    % get_params_str()     if self.get_params()    else ''
164
165         sep = ' ' if not multiline else '\n  '
166         if platform: platform = "%s:" % platform
167         strmap = {
168             'get'   : '%(select)s%(sep)s%(at)sFROM %(platform)s%(table)s%(sep)s%(where)s%(sep)s',                                           
169             'update': 'UPDATE %(platform)s%(table)s%(sep)s%(params)s%(sep)s%(where)s%(sep)s%(select)s',       
170             'create': 'INSERT INTO %(platform)s%(table)s%(sep)s%(params)s%(sep)s%(select)s',
171             'delete': 'DELETE FROM %(platform)s%(table)s%(sep)s%(where)s'
172         }
173
174         return strmap[self.action] % locals()
175
176     @returns(StringTypes)
177     def __str__(self):
178         return self.to_sql(multiline=True)
179
180     @returns(StringTypes)
181     def __repr__(self):
182         return self.to_sql()
183
184     def __key(self):
185         return (self.action, self.object, self.filters, frozendict(self.params), frozenset(self.fields))
186
187     def __hash__(self):
188         return hash(self.__key())
189
190     #--------------------------------------------------------------------------- 
191     # Conversion
192     #--------------------------------------------------------------------------- 
193
194     def to_dict(self):
195         return {
196             'action': self.action,
197             'object': self.object,
198             'timestamp': self.timestamp,
199             'filters': self.filters,
200             'params': self.params,
201             'fields': list(self.fields)
202         }
203
204     def to_json (self, analyzed_query=None):
205         query_uuid=self.query_uuid
206         a=self.action
207         o=self.object
208         t=self.timestamp
209         f=json.dumps (self.filters.to_list())
210         p=json.dumps (self.params)
211         c=json.dumps (list(self.fields))
212         # xxx unique can be removed, but for now we pad the js structure
213         unique=0
214
215         if not analyzed_query:
216             aq = 'null'
217         else:
218             aq = analyzed_query.to_json()
219         sq="{}"
220         
221         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()
222         if debug: print 'ManifoldQuery.to_json:',result
223         return result
224     
225     # this builds a ManifoldQuery object from a dict as received from javascript through its ajax request 
226     # we use a json-encoded string - see manifold.js for the sender part 
227     # e.g. here's what I captured from the server's output
228     # 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":{}}']}>
229     def fill_from_POST (self, POST_dict):
230         try:
231             json_string=POST_dict['json']
232             dict=json.loads(json_string)
233             for (k,v) in dict.iteritems(): 
234                 setattr(self,k,v)
235         except:
236             print "Could not decode incoming ajax request as a Query, POST=",POST_dict
237             if (debug):
238                 import traceback
239                 traceback.print_exc()
240
241     #--------------------------------------------------------------------------- 
242     # Accessors
243     #--------------------------------------------------------------------------- 
244
245     @returns(StringTypes)
246     def get_action(self):
247         return self.action
248
249     @returns(frozenset)
250     def get_select(self):
251         return frozenset(self.fields)
252
253     @returns(StringTypes)
254     def get_from(self):
255         return self.object
256
257     @returns(Filter)
258     def get_where(self):
259         return self.filters
260
261     @returns(dict)
262     def get_params(self):
263         return self.params
264
265     @returns(StringTypes)
266     def get_timestamp(self):
267         return self.timestamp
268
269 #DEPRECATED#
270 #DEPRECATED#    def make_filters(self, filters):
271 #DEPRECATED#        return Filter(filters)
272 #DEPRECATED#
273 #DEPRECATED#    def make_fields(self, fields):
274 #DEPRECATED#        if isinstance(fields, (list, tuple)):
275 #DEPRECATED#            return set(fields)
276 #DEPRECATED#        else:
277 #DEPRECATED#            raise Exception, "Invalid field specification"
278
279     #--------------------------------------------------------------------------- 
280     # LINQ-like syntax
281     #--------------------------------------------------------------------------- 
282
283     @classmethod
284     #@returns(Query)
285     def action(self, action, object):
286         """
287         (Internal usage). Craft a Query according to an action name 
288         See methods: get, update, delete, execute.
289         Args:
290             action: A String among {"get", "update", "delete", "execute"}
291             object: The name of the queried object (String)
292         Returns:
293             The corresponding Query instance
294         """
295         query = Query()
296         query.action = action
297         query.object = object
298         return query
299
300     @classmethod
301     #@returns(Query)
302     def get(self, object):
303         """
304         Craft the Query which fetches the records related to a given object
305         Args:
306             object: The name of the queried object (String)
307         Returns:
308             The corresponding Query instance
309         """
310         return self.action("get", object)
311
312     @classmethod
313     #@returns(Query)
314     def update(self, object):
315         """
316         Craft the Query which updates the records related to a given object
317         Args:
318             object: The name of the queried object (String)
319         Returns:
320             The corresponding Query instance
321         """
322         return self.action("update", object)
323     
324     @classmethod
325     #@returns(Query)
326     def create(self, object):
327         """
328         Craft the Query which create the records related to a given object
329         Args:
330             object: The name of the queried object (String)
331         Returns:
332             The corresponding Query instance
333         """
334         return self.action("create", object)
335     
336     @classmethod
337     #@returns(Query)
338     def delete(self, object):
339         """
340         Craft the Query which delete the records related to a given object
341         Args:
342             object: The name of the queried object (String)
343         Returns:
344             The corresponding Query instance
345         """
346         return self.action("delete", object)
347     
348     @classmethod
349     #@returns(Query)
350     def execute(self, object):
351         """
352         Craft the Query which execute a processing related to a given object
353         Args:
354             object: The name of the queried object (String)
355         Returns:
356             The corresponding Query instance
357         """
358         return self.action("execute", object)
359
360     #@returns(Query)
361     def at(self, timestamp):
362         """
363         Set the timestamp carried by the query
364         Args:
365             timestamp: The timestamp (it may be a python timestamp, a string
366                 respecting the "%Y-%m-%d %H:%M:%S" python format, or "now")
367         Returns:
368             The self Query instance
369         """
370         self.timestamp = timestamp
371         return self
372
373     def filter_by(self, *args):
374         if len(args) == 1:
375             filters = args[0]
376             if filters == None:
377                 self.filters = Filter()
378                 return self
379             if not isinstance(filters, (set, list, tuple, Filter)):
380                 filters = [filters]
381             for predicate in filters:
382                 self.filters.add(predicate)
383         elif len(args) == 3: 
384             predicate = Predicate(*args)
385             self.filters.add(predicate)
386         else:
387             raise Exception, 'Invalid expression for filter'
388         return self
389             
390     def select(self, *fields):
391         if not fields:
392             # Delete all fields
393             self.fields = set()
394             return self
395
396         # Accept passing iterables
397         if len(fields) == 1:
398             tmp, = fields
399             if isinstance(tmp, (list, tuple, set, frozenset)):
400                 fields = tuple(tmp)
401
402         for field in fields:
403             self.fields.add(field)
404         return self
405
406     def set(self, params):
407         self.params.update(params)
408         return self
409
410     def __or__(self, query):
411         assert self.action == query.action
412         assert self.object == query.object
413         assert self.timestamp == query.timestamp # XXX
414         filter = self.filters | query.filters
415         # fast dict union
416         # http://my.safaribooksonline.com/book/programming/python/0596007973/python-shortcuts/pythoncook2-chp-4-sect-17
417         params = dict(self.params, **query.params)
418         fields = self.fields | query.fields
419         return Query.action(self.action, self.object).filter_by(filter).select(fields)
420
421     def __and__(self, query):
422         assert self.action == query.action
423         assert self.object == query.object
424         assert self.timestamp == query.timestamp # XXX
425         filter = self.filters & query.filters
426         # fast dict intersection
427         # http://my.safaribooksonline.com/book/programming/python/0596007973/python-shortcuts/pythoncook2-chp-4-sect-17
428         params =  dict.fromkeys([x for x in self.params if x in query.params])
429         fields = self.fields & query.fields
430         return Query.action(self.action, self.object).filter_by(filter).select(fields)
431
432     def __le__(self, query):
433         return ( self == self & query ) or ( query == self | query )
434
435 class AnalyzedQuery(Query):
436
437     # XXX we might need to propagate special parameters sur as DEBUG, etc.
438
439     def __init__(self, query=None, metadata=None):
440         self.clear()
441         self.metadata = metadata
442         if query:
443             self.query_uuid = query.query_uuid
444             self.analyze(query)
445         else:
446             self.query_uuid = uniqid()
447
448     @returns(StringTypes)
449     def __str__(self):
450         out = []
451         fields = self.get_select()
452         fields = ", ".join(fields) if fields else '*'
453         out.append("SELECT %s FROM %s WHERE %s" % (
454             fields,
455             self.get_from(),
456             self.get_where()
457         ))
458         cpt = 1
459         for method, subquery in self.subqueries():
460             out.append('  [SQ #%d : %s] %s' % (cpt, method, str(subquery)))
461             cpt += 1
462
463         return "\n".join(out)
464
465     def clear(self):
466         super(AnalyzedQuery, self).clear()
467         self._subqueries = {}
468
469     def subquery(self, method):
470         # Allows for the construction of a subquery
471         if not method in self._subqueries:
472             analyzed_query = AnalyzedQuery(metadata=self.metadata)
473             analyzed_query.action = self.action
474             try:
475                 type = self.metadata.get_field_type(self.object, method)
476             except ValueError ,e: # backwards 1..N
477                 type = method
478             analyzed_query.object = type
479             self._subqueries[method] = analyzed_query
480         return self._subqueries[method]
481
482     def get_subquery(self, method):
483         return self._subqueries.get(method, None)
484
485     def remove_subquery(self, method):
486         del self._subqueries[method]
487
488     def get_subquery_names(self):
489         return set(self._subqueries.keys())
490
491     def get_subqueries(self):
492         return self._subqueries
493
494     def subqueries(self):
495         for method, subquery in self._subqueries.iteritems():
496             yield (method, subquery)
497
498     def filter_by(self, filters):
499         if not isinstance(filters, (set, list, tuple, Filter)):
500             filters = [filters]
501         for predicate in filters:
502             if predicate and '.' in predicate.key:
503                 method, subkey = predicate.key.split('.', 1)
504                 # Method contains the name of the subquery, we need the type
505                 # XXX type = self.metadata.get_field_type(self.object, method)
506                 sub_pred = Predicate(subkey, predicate.op, predicate.value)
507                 self.subquery(method).filter_by(sub_pred)
508             else:
509                 super(AnalyzedQuery, self).filter_by(predicate)
510         return self
511
512     def select(self, *fields):
513
514         # XXX passing None should reset fields in all subqueries
515
516         # Accept passing iterables
517         if len(fields) == 1:
518             tmp, = fields
519             if isinstance(tmp, (list, tuple, set, frozenset)):
520                 fields = tuple(tmp)
521
522         for field in fields:
523             if field and '.' in field:
524                 method, subfield = field.split('.', 1)
525                 # Method contains the name of the subquery, we need the type
526                 # XXX type = self.metadata.get_field_type(self.object, method)
527                 self.subquery(method).select(subfield)
528             else:
529                 super(AnalyzedQuery, self).select(field)
530         return self
531
532     def set(self, params):
533         for param, value in self.params.items():
534             if '.' in param:
535                 method, subparam = param.split('.', 1)
536                 # Method contains the name of the subquery, we need the type
537                 # XXX type = self.metadata.get_field_type(self.object, method)
538                 self.subquery(method).set({subparam: value})
539             else:
540                 super(AnalyzedQuery, self).set({param: value})
541         return self
542         
543     def analyze(self, query):
544         self.clear()
545         self.action = query.action
546         self.object = query.object
547         self.filter_by(query.filters)
548         self.set(query.params)
549         self.select(query.fields)
550
551     def to_json (self):
552         query_uuid=self.query_uuid
553         a=self.action
554         o=self.object
555         t=self.timestamp
556         f=json.dumps (self.filters.to_list())
557         p=json.dumps (self.params)
558         c=json.dumps (list(self.fields))
559         # xxx unique can be removed, but for now we pad the js structure
560         unique=0
561
562         aq = 'null'
563         sq=", ".join ( [ "'%s':%s" % (object, subquery.to_json())
564                   for (object, subquery) in self._subqueries.iteritems()])
565         sq="{%s}"%sq
566         
567         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()
568         if debug: print 'ManifoldQuery.to_json:',result
569         return result