2 # -*- coding: utf-8 -*-
6 # Copyright (C) UPMC Paris Universitas
8 # Jordan Augé <jordan.auge@lip6.fr>
9 # Marc-Olivier Buob <marc-olivier.buob@lip6.fr>
10 # Thierry Parmentelat <thierry.parmentelat@inria.fr>
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
22 return uuid.uuid4().hex
27 class ParameterError(StandardError): pass
31 Implements a TopHat query.
33 We assume this is a correct DAG specification.
35 1/ A field designates several tables = OR specification.
36 2/ The set of fields specifies a AND between OR clauses.
39 #---------------------------------------------------------------------------
41 #---------------------------------------------------------------------------
43 def __init__(self, *args, **kwargs):
45 self.query_uuid = uniqid()
47 # Initialize optional parameters
50 #l = len(kwargs.keys())
54 if isinstance(args[0], dict):
58 # Initialization from a tuple
60 if len_args in range(2, 7) and type(args) == tuple:
61 # Note: range(x,y) <=> [x, y[
67 self.timestamp = 'now'
68 self.object, self.filters, self.fields = args
70 self.object, self.filters, self.params, self.fields = args
72 self.timestamp = 'now'
74 self.action, self.object, self.filters, self.params, self.fields, self.timestamp = args
76 # Initialization from a dict
77 elif "object" in kwargs:
78 if "action" in kwargs:
79 self.action = kwargs["action"]
82 print "W: defaulting to get action"
86 self.object = kwargs["object"]
89 if "filters" in kwargs:
90 self.filters = kwargs["filters"]
93 self.filters = Filter()
95 if "fields" in kwargs:
96 self.fields = set(kwargs["fields"])
101 # "update table set x = 3" => params == set
102 if "params" in kwargs:
103 self.params = kwargs["params"]
108 if "timestamp" in kwargs:
109 self.timestamp = kwargs["timestamp"]
110 del kwargs["timestamp"]
112 self.timestamp = "now"
115 raise ParameterError, "Invalid parameter(s) : %r" % kwargs.keys()
117 # raise ParameterError, "No valid constructor found for %s : args = %r" % (self.__class__.__name__, args)
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"
124 if isinstance(self.filters, list):
126 self.filters = Filter()
129 self.filters.add(pred)
131 if isinstance(self.fields, list):
132 self.fields = set(self.fields)
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)))
138 #---------------------------------------------------------------------------
140 #---------------------------------------------------------------------------
143 return copy.deepcopy(self)
148 self.filters = Filter()
151 self.timestamp = "now"
152 self.timestamp = 'now' # ignored for now
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())
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 ''
165 sep = ' ' if not multiline else '\n '
166 if platform: platform = "%s:" % platform
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'
174 return strmap[self.action] % locals()
176 @returns(StringTypes)
178 return self.to_sql(multiline=True)
180 @returns(StringTypes)
185 return (self.action, self.object, self.filters, frozendict(self.params), frozenset(self.fields))
188 return hash(self.__key())
190 #---------------------------------------------------------------------------
192 #---------------------------------------------------------------------------
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)
204 def to_json (self, analyzed_query=None):
205 query_uuid=self.query_uuid
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
215 if not analyzed_query:
218 aq = analyzed_query.to_json()
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
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):
231 json_string=POST_dict['json']
232 dict=json.loads(json_string)
233 for (k,v) in dict.iteritems():
236 print "Could not decode incoming ajax request as a Query, POST=",POST_dict
239 traceback.print_exc()
241 #---------------------------------------------------------------------------
243 #---------------------------------------------------------------------------
245 @returns(StringTypes)
246 def get_action(self):
250 def get_select(self):
251 return frozenset(self.fields)
253 @returns(StringTypes)
262 def get_params(self):
265 @returns(StringTypes)
266 def get_timestamp(self):
267 return self.timestamp
270 #DEPRECATED# def make_filters(self, filters):
271 #DEPRECATED# return Filter(filters)
273 #DEPRECATED# def make_fields(self, fields):
274 #DEPRECATED# if isinstance(fields, (list, tuple)):
275 #DEPRECATED# return set(fields)
277 #DEPRECATED# raise Exception, "Invalid field specification"
279 #---------------------------------------------------------------------------
281 #---------------------------------------------------------------------------
285 def action(self, action, object):
287 (Internal usage). Craft a Query according to an action name
288 See methods: get, update, delete, execute.
290 action: A String among {"get", "update", "delete", "execute"}
291 object: The name of the queried object (String)
293 The corresponding Query instance
296 query.action = action
297 query.object = object
302 def get(self, object):
304 Craft the Query which fetches the records related to a given object
306 object: The name of the queried object (String)
308 The corresponding Query instance
310 return self.action("get", object)
314 def update(self, object):
316 Craft the Query which updates the records related to a given object
318 object: The name of the queried object (String)
320 The corresponding Query instance
322 return self.action("update", object)
326 def create(self, object):
328 Craft the Query which create the records related to a given object
330 object: The name of the queried object (String)
332 The corresponding Query instance
334 return self.action("create", object)
338 def delete(self, object):
340 Craft the Query which delete the records related to a given object
342 object: The name of the queried object (String)
344 The corresponding Query instance
346 return self.action("delete", object)
350 def execute(self, object):
352 Craft the Query which execute a processing related to a given object
354 object: The name of the queried object (String)
356 The corresponding Query instance
358 return self.action("execute", object)
361 def at(self, timestamp):
363 Set the timestamp carried by the query
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")
368 The self Query instance
370 self.timestamp = timestamp
373 def filter_by(self, *args):
377 self.filters = Filter()
379 if not isinstance(filters, (set, list, tuple, Filter)):
381 for predicate in filters:
382 self.filters.add(predicate)
384 predicate = Predicate(*args)
385 self.filters.add(predicate)
387 raise Exception, 'Invalid expression for filter'
390 def select(self, *fields):
396 # Accept passing iterables
399 if isinstance(tmp, (list, tuple, set, frozenset)):
403 self.fields.add(field)
406 def set(self, params):
407 self.params.update(params)
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
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)
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)
432 def __le__(self, query):
433 return ( self == self & query ) or ( query == self | query )
435 class AnalyzedQuery(Query):
437 # XXX we might need to propagate special parameters sur as DEBUG, etc.
439 def __init__(self, query=None, metadata=None):
441 self.metadata = metadata
443 self.query_uuid = query.query_uuid
446 self.query_uuid = uniqid()
448 @returns(StringTypes)
451 fields = self.get_select()
452 fields = ", ".join(fields) if fields else '*'
453 out.append("SELECT %s FROM %s WHERE %s" % (
459 for method, subquery in self.subqueries():
460 out.append(' [SQ #%d : %s] %s' % (cpt, method, str(subquery)))
463 return "\n".join(out)
466 super(AnalyzedQuery, self).clear()
467 self._subqueries = {}
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
475 type = self.metadata.get_field_type(self.object, method)
476 except ValueError ,e: # backwards 1..N
478 analyzed_query.object = type
479 self._subqueries[method] = analyzed_query
480 return self._subqueries[method]
482 def get_subquery(self, method):
483 return self._subqueries.get(method, None)
485 def remove_subquery(self, method):
486 del self._subqueries[method]
488 def get_subquery_names(self):
489 return set(self._subqueries.keys())
491 def get_subqueries(self):
492 return self._subqueries
494 def subqueries(self):
495 for method, subquery in self._subqueries.iteritems():
496 yield (method, subquery)
498 def filter_by(self, filters):
499 if not isinstance(filters, (set, list, tuple, Filter)):
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)
509 super(AnalyzedQuery, self).filter_by(predicate)
512 def select(self, *fields):
514 # XXX passing None should reset fields in all subqueries
516 # Accept passing iterables
519 if isinstance(tmp, (list, tuple, set, frozenset)):
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)
529 super(AnalyzedQuery, self).select(field)
532 def set(self, params):
533 for param, value in self.params.items():
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})
540 super(AnalyzedQuery, self).set({param: value})
543 def analyze(self, query):
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)
552 query_uuid=self.query_uuid
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
563 sq=", ".join ( [ "'%s':%s" % (object, subquery.to_json())
564 for (object, subquery) in self._subqueries.iteritems()])
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