portal: added wip for PI validation page
[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 from manifold.util.clause       import Clause
17 import copy
18
19 import json
20 import uuid
21
22 def uniqid (): 
23     return uuid.uuid4().hex
24
25 debug=False
26 debug=True
27
28 class ParameterError(StandardError): pass
29
30 class Query(object):
31     """
32     Implements a TopHat query.
33
34     We assume this is a correct DAG specification.
35
36     1/ A field designates several tables = OR specification.
37     2/ The set of fields specifies a AND between OR clauses.
38     """
39
40     #--------------------------------------------------------------------------- 
41     # Constructor
42     #--------------------------------------------------------------------------- 
43
44     def __init__(self, *args, **kwargs):
45
46         self.query_uuid = uniqid()
47
48         # Initialize optional parameters
49         self.clear()
50     
51         #l = len(kwargs.keys())
52         len_args = len(args)
53
54         if len(args) == 1:
55             if isinstance(args[0], dict):
56                 kwargs = args[0]
57                 args = []
58
59         # Initialization from a tuple
60
61         if len_args in range(2, 7) and type(args) == tuple:
62             # Note: range(x,y) <=> [x, y[
63
64             # XXX UGLY
65             if len_args == 3:
66                 self.action = 'get'
67                 self.params = {}
68                 self.timestamp     = 'now'
69                 self.object, self.filters, self.fields = args
70             elif len_args == 4:
71                 self.object, self.filters, self.params, self.fields = args
72                 self.action = 'get'
73                 self.timestamp     = 'now'
74             else:
75                 self.action, self.object, self.filters, self.params, self.fields, self.timestamp = args
76
77         # Initialization from a dict
78         elif "object" in kwargs:
79             if "action" in kwargs:
80                 self.action = kwargs["action"]
81                 del kwargs["action"]
82             else:
83                 print "W: defaulting to get action"
84                 self.action = "get"
85
86
87             self.object = kwargs["object"]
88             del kwargs["object"]
89
90             if "filters" in kwargs:
91                 self.filters = kwargs["filters"]
92                 del kwargs["filters"]
93             else:
94                 self.filters = Filter()
95
96             if "fields" in kwargs:
97                 self.fields = set(kwargs["fields"])
98                 del kwargs["fields"]
99             else:
100                 self.fields = set()
101
102             # "update table set x = 3" => params == set
103             if "params" in kwargs:
104                 self.params = kwargs["params"]
105                 del kwargs["params"]
106             else:
107                 self.params = {}
108
109             if "timestamp" in kwargs:
110                 self.timestamp = kwargs["timestamp"]
111                 del kwargs["timestamp"]
112             else:
113                 self.timestamp = "now" 
114
115             if kwargs:
116                 raise ParameterError, "Invalid parameter(s) : %r" % kwargs.keys()
117         #else:
118         #        raise ParameterError, "No valid constructor found for %s : args = %r" % (self.__class__.__name__, args)
119
120         self.sanitize()
121
122     def sanitize(self):
123         if not self.filters:   self.filters   = Filter()
124         if not self.params:    self.params    = {}
125         if not self.fields:    self.fields    = set()
126         if not self.timestamp: self.timestamp = "now" 
127
128         if isinstance(self.filters, list):
129             f = self.filters
130             self.filters = Filter()
131             for x in f:
132                 pred = Predicate(x)
133                 self.filters.add(pred)
134         elif isinstance(self.filters, Clause):
135             self.filters = Filter.from_clause(self.filters)
136
137         if isinstance(self.fields, list):
138             self.fields = set(self.fields)
139
140         for field in self.fields:
141             if not isinstance(field, StringTypes):
142                 raise TypeError("Invalid field name %s (string expected, got %s)" % (field, type(field)))
143
144     #--------------------------------------------------------------------------- 
145     # Helpers
146     #--------------------------------------------------------------------------- 
147
148     def copy(self):
149         return copy.deepcopy(self)
150
151     def clear(self):
152         self.action = 'get'
153         self.object = None
154         self.filters = Filter()
155         self.params  = {}
156         self.fields  = set()
157         self.timestamp  = 'now' # ignored for now
158
159     def to_sql(self, platform='', multiline=False):
160         get_params_str = lambda : ', '.join(['%s = %r' % (k, v) for k, v in self.get_params().items()])
161         get_select_str = lambda : ', '.join(self.get_select()) 
162
163         table  = self.get_from()
164         select = 'SELECT %s' % (get_select_str()    if self.get_select()    else '*')
165         where  = 'WHERE %s'  % self.get_where()     if self.get_where()     else ''
166         at     = 'AT %s'     % self.get_timestamp() if self.get_timestamp() else ''
167         params = 'SET %s'    % get_params_str()     if self.get_params()    else ''
168
169         sep = ' ' if not multiline else '\n  '
170         if platform: platform = "%s:" % platform
171         strmap = {
172             'get'   : '%(select)s%(sep)s%(at)s%(sep)sFROM %(platform)s%(table)s%(sep)s%(where)s%(sep)s',                                           
173             'update': 'UPDATE %(platform)s%(table)s%(sep)s%(params)s%(sep)s%(where)s%(sep)s%(select)s',       
174             'create': 'INSERT INTO %(platform)s%(table)s%(sep)s%(params)s%(sep)s%(select)s',
175             'delete': 'DELETE FROM %(platform)s%(table)s%(sep)s%(where)s'
176         }
177
178         return strmap[self.action] % locals()
179
180     @returns(StringTypes)
181     def __str__(self):
182         return self.to_sql(multiline=True)
183
184     @returns(StringTypes)
185     def __repr__(self):
186         return self.to_sql()
187
188     def __key(self):
189         return (self.action, self.object, self.filters, frozendict(self.params), frozenset(self.fields))
190
191     def __hash__(self):
192         return hash(self.__key())
193
194     #--------------------------------------------------------------------------- 
195     # Conversion
196     #--------------------------------------------------------------------------- 
197
198     def to_dict(self):
199         return {
200             'action': self.action,
201             'object': self.object,
202             'timestamp': self.timestamp,
203             'filters': self.filters.to_list(),
204             'params': self.params,
205             'fields': list(self.fields)
206         }
207
208     def to_json (self, analyzed_query=None):
209         query_uuid=self.query_uuid
210         a=self.action
211         o=self.object
212         t=self.timestamp
213         f=json.dumps (self.filters.to_list())
214         p=json.dumps (self.params)
215         c=json.dumps (list(self.fields))
216         # xxx unique can be removed, but for now we pad the js structure
217         unique=0
218
219         if not analyzed_query:
220             aq = 'null'
221         else:
222             aq = analyzed_query.to_json()
223         sq="{}"
224         
225         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()
226         if debug: print 'ManifoldQuery.to_json:',result
227         return result
228     
229     # this builds a ManifoldQuery object from a dict as received from javascript through its ajax request 
230     # we use a json-encoded string - see manifold.js for the sender part 
231     # e.g. here's what I captured from the server's output
232     # 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":{}}']}>
233     def fill_from_POST (self, POST_dict):
234         try:
235             json_string=POST_dict['json']
236             dict=json.loads(json_string)
237             for (k,v) in dict.iteritems(): 
238                 setattr(self,k,v)
239         except:
240             print "Could not decode incoming ajax request as a Query, POST=",POST_dict
241             if (debug):
242                 import traceback
243                 traceback.print_exc()
244         self.sanitize()
245
246     #--------------------------------------------------------------------------- 
247     # Accessors
248     #--------------------------------------------------------------------------- 
249
250     @returns(StringTypes)
251     def get_action(self):
252         return self.action
253
254     @returns(frozenset)
255     def get_select(self):
256         return frozenset(self.fields)
257
258     @returns(StringTypes)
259     def get_from(self):
260         return self.object
261
262     @returns(Filter)
263     def get_where(self):
264         return self.filters
265
266     @returns(dict)
267     def get_params(self):
268         return self.params
269
270     @returns(StringTypes)
271     def get_timestamp(self):
272         return self.timestamp
273
274 #DEPRECATED#
275 #DEPRECATED#    def make_filters(self, filters):
276 #DEPRECATED#        return Filter(filters)
277 #DEPRECATED#
278 #DEPRECATED#    def make_fields(self, fields):
279 #DEPRECATED#        if isinstance(fields, (list, tuple)):
280 #DEPRECATED#            return set(fields)
281 #DEPRECATED#        else:
282 #DEPRECATED#            raise Exception, "Invalid field specification"
283
284     #--------------------------------------------------------------------------- 
285     # LINQ-like syntax
286     #--------------------------------------------------------------------------- 
287
288     @classmethod
289     #@returns(Query)
290     def action(self, action, object):
291         """
292         (Internal usage). Craft a Query according to an action name 
293         See methods: get, update, delete, execute.
294         Args:
295             action: A String among {"get", "update", "delete", "execute"}
296             object: The name of the queried object (String)
297         Returns:
298             The corresponding Query instance
299         """
300         query = Query()
301         query.action = action
302         query.object = object
303         return query
304
305     @classmethod
306     #@returns(Query)
307     def get(self, object):
308         """
309         Craft the Query which fetches the records related to a given object
310         Args:
311             object: The name of the queried object (String)
312         Returns:
313             The corresponding Query instance
314         """
315         return self.action("get", object)
316
317     @classmethod
318     #@returns(Query)
319     def update(self, object):
320         """
321         Craft the Query which updates the records related to a given object
322         Args:
323             object: The name of the queried object (String)
324         Returns:
325             The corresponding Query instance
326         """
327         return self.action("update", object)
328     
329     @classmethod
330     #@returns(Query)
331     def create(self, object):
332         """
333         Craft the Query which create the records related to a given object
334         Args:
335             object: The name of the queried object (String)
336         Returns:
337             The corresponding Query instance
338         """
339         return self.action("create", object)
340     
341     @classmethod
342     #@returns(Query)
343     def delete(self, object):
344         """
345         Craft the Query which delete the records related to a given object
346         Args:
347             object: The name of the queried object (String)
348         Returns:
349             The corresponding Query instance
350         """
351         return self.action("delete", object)
352     
353     @classmethod
354     #@returns(Query)
355     def execute(self, object):
356         """
357         Craft the Query which execute a processing related to a given object
358         Args:
359             object: The name of the queried object (String)
360         Returns:
361             The corresponding Query instance
362         """
363         return self.action("execute", object)
364
365     #@returns(Query)
366     def at(self, timestamp):
367         """
368         Set the timestamp carried by the query
369         Args:
370             timestamp: The timestamp (it may be a python timestamp, a string
371                 respecting the "%Y-%m-%d %H:%M:%S" python format, or "now")
372         Returns:
373             The self Query instance
374         """
375         self.timestamp = timestamp
376         return self
377
378     def filter_by(self, *args):
379         """
380         Args:
381             args: It may be:
382                 - the parts of a Predicate (key, op, value)
383                 - None
384                 - a Filter instance
385                 - a set/list/tuple of Predicate instances
386         """
387         if len(args) == 1:
388             filters = args[0]
389             if filters == None:
390                 self.filters = Filter()
391                 return self
392             if not isinstance(filters, (set, list, tuple, Filter)):
393                 filters = [filters]
394             for predicate in filters:
395                 self.filters.add(predicate)
396         elif len(args) == 3: 
397             predicate = Predicate(*args)
398             self.filters.add(predicate)
399         else:
400             raise Exception, 'Invalid expression for filter'
401         return self
402             
403     def select(self, *fields):
404
405         # Accept passing iterables
406         if len(fields) == 1:
407             tmp, = fields
408             if not tmp:
409                 fields = None
410             elif isinstance(tmp, (list, tuple, set, frozenset)):
411                 fields = tuple(tmp)
412
413         if not fields:
414             # Delete all fields
415             self.fields = set()
416             return self
417
418         for field in fields:
419             self.fields.add(field)
420         return self
421
422     def set(self, params):
423         self.params.update(params)
424         return self
425
426     def __or__(self, query):
427         assert self.action == query.action
428         assert self.object == query.object
429         assert self.timestamp == query.timestamp # XXX
430         filter = self.filters | query.filters
431         # fast dict union
432         # http://my.safaribooksonline.com/book/programming/python/0596007973/python-shortcuts/pythoncook2-chp-4-sect-17
433         params = dict(self.params, **query.params)
434         fields = self.fields | query.fields
435         return Query.action(self.action, self.object).filter_by(filter).select(fields)
436
437     def __and__(self, query):
438         assert self.action == query.action
439         assert self.object == query.object
440         assert self.timestamp == query.timestamp # XXX
441         filter = self.filters & query.filters
442         # fast dict intersection
443         # http://my.safaribooksonline.com/book/programming/python/0596007973/python-shortcuts/pythoncook2-chp-4-sect-17
444         params =  dict.fromkeys([x for x in self.params if x in query.params])
445         fields = self.fields & query.fields
446         return Query.action(self.action, self.object).filter_by(filter).select(fields)
447
448     def __le__(self, query):
449         return ( self == self & query ) or ( query == self | query )
450
451 class AnalyzedQuery(Query):
452
453     # XXX we might need to propagate special parameters sur as DEBUG, etc.
454
455     def __init__(self, query=None, metadata=None):
456         self.clear()
457         self.metadata = metadata
458         if query:
459             self.query_uuid = query.query_uuid
460             self.analyze(query)
461         else:
462             self.query_uuid = uniqid()
463
464     @returns(StringTypes)
465     def __str__(self):
466         out = []
467         fields = self.get_select()
468         fields = ", ".join(fields) if fields else '*'
469         out.append("SELECT %s FROM %s WHERE %s" % (
470             fields,
471             self.get_from(),
472             self.get_where()
473         ))
474         cpt = 1
475         for method, subquery in self.subqueries():
476             out.append('  [SQ #%d : %s] %s' % (cpt, method, str(subquery)))
477             cpt += 1
478
479         return "\n".join(out)
480
481     def clear(self):
482         super(AnalyzedQuery, self).clear()
483         self._subqueries = {}
484
485     def subquery(self, method):
486         # Allows for the construction of a subquery
487         if not method in self._subqueries:
488             analyzed_query = AnalyzedQuery(metadata=self.metadata)
489             analyzed_query.action = self.action
490             try:
491                 type = self.metadata.get_field_type(self.object, method)
492             except ValueError ,e: # backwards 1..N
493                 type = method
494             analyzed_query.object = type
495             self._subqueries[method] = analyzed_query
496         return self._subqueries[method]
497
498     def get_subquery(self, method):
499         return self._subqueries.get(method, None)
500
501     def remove_subquery(self, method):
502         del self._subqueries[method]
503
504     def get_subquery_names(self):
505         return set(self._subqueries.keys())
506
507     def get_subqueries(self):
508         return self._subqueries
509
510     def subqueries(self):
511         for method, subquery in self._subqueries.iteritems():
512             yield (method, subquery)
513
514     def filter_by(self, filters):
515         if not isinstance(filters, (set, list, tuple, Filter)):
516             filters = [filters]
517         for predicate in filters:
518             if predicate and '.' in predicate.key:
519                 method, subkey = predicate.key.split('.', 1)
520                 # Method contains the name of the subquery, we need the type
521                 # XXX type = self.metadata.get_field_type(self.object, method)
522                 sub_pred = Predicate(subkey, predicate.op, predicate.value)
523                 self.subquery(method).filter_by(sub_pred)
524             else:
525                 super(AnalyzedQuery, self).filter_by(predicate)
526         return self
527
528     def select(self, *fields):
529
530         # XXX passing None should reset fields in all subqueries
531
532         # Accept passing iterables
533         if len(fields) == 1:
534             tmp, = fields
535             if isinstance(tmp, (list, tuple, set, frozenset)):
536                 fields = tuple(tmp)
537
538         for field in fields:
539             if field and '.' in field:
540                 method, subfield = field.split('.', 1)
541                 # Method contains the name of the subquery, we need the type
542                 # XXX type = self.metadata.get_field_type(self.object, method)
543                 self.subquery(method).select(subfield)
544             else:
545                 super(AnalyzedQuery, self).select(field)
546         return self
547
548     def set(self, params):
549         for param, value in self.params.items():
550             if '.' in param:
551                 method, subparam = param.split('.', 1)
552                 # Method contains the name of the subquery, we need the type
553                 # XXX type = self.metadata.get_field_type(self.object, method)
554                 self.subquery(method).set({subparam: value})
555             else:
556                 super(AnalyzedQuery, self).set({param: value})
557         return self
558         
559     def analyze(self, query):
560         self.clear()
561         self.action = query.action
562         self.object = query.object
563         self.filter_by(query.filters)
564         self.set(query.params)
565         self.select(query.fields)
566
567     def to_json (self):
568         query_uuid=self.query_uuid
569         a=self.action
570         o=self.object
571         t=self.timestamp
572         f=json.dumps (self.filters.to_list())
573         p=json.dumps (self.params)
574         c=json.dumps (list(self.fields))
575         # xxx unique can be removed, but for now we pad the js structure
576         unique=0
577
578         aq = 'null'
579         sq=", ".join ( [ "'%s':%s" % (object, subquery.to_json())
580                   for (object, subquery) in self._subqueries.iteritems()])
581         sq="{%s}"%sq
582         
583         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()
584         if debug: print 'ManifoldQuery.to_json:',result
585         return result