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
16 from manifold.util.clause import Clause
23 return uuid.uuid4().hex
28 class ParameterError(StandardError): pass
32 Implements a TopHat query.
34 We assume this is a correct DAG specification.
36 1/ A field designates several tables = OR specification.
37 2/ The set of fields specifies a AND between OR clauses.
40 #---------------------------------------------------------------------------
42 #---------------------------------------------------------------------------
44 def __init__(self, *args, **kwargs):
46 self.query_uuid = uniqid()
48 # Initialize optional parameters
51 #l = len(kwargs.keys())
55 if isinstance(args[0], dict):
59 # Initialization from a tuple
61 if len_args in range(2, 7) and type(args) == tuple:
62 # Note: range(x,y) <=> [x, y[
68 self.timestamp = 'now'
69 self.object, self.filters, self.fields = args
71 self.object, self.filters, self.params, self.fields = args
73 self.timestamp = 'now'
75 self.action, self.object, self.filters, self.params, self.fields, self.timestamp = args
77 # Initialization from a dict
78 elif "object" in kwargs:
79 if "action" in kwargs:
80 self.action = kwargs["action"]
83 print "W: defaulting to get action"
87 self.object = kwargs["object"]
90 if "filters" in kwargs:
91 self.filters = kwargs["filters"]
94 self.filters = Filter()
96 if "fields" in kwargs:
97 self.fields = set(kwargs["fields"])
102 # "update table set x = 3" => params == set
103 if "params" in kwargs:
104 self.params = kwargs["params"]
109 if "timestamp" in kwargs:
110 self.timestamp = kwargs["timestamp"]
111 del kwargs["timestamp"]
113 self.timestamp = "now"
116 raise ParameterError, "Invalid parameter(s) : %r" % kwargs.keys()
118 # raise ParameterError, "No valid constructor found for %s : args = %r" % (self.__class__.__name__, args)
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"
128 if isinstance(self.filters, list):
130 self.filters = Filter()
133 self.filters.add(pred)
134 elif isinstance(self.filters, Clause):
135 self.filters = Filter.from_clause(self.filters)
137 if isinstance(self.fields, list):
138 self.fields = set(self.fields)
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)))
144 #---------------------------------------------------------------------------
146 #---------------------------------------------------------------------------
149 return copy.deepcopy(self)
154 self.filters = Filter()
157 self.timestamp = 'now' # ignored for now
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())
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 ''
169 sep = ' ' if not multiline else '\n '
170 if platform: platform = "%s:" % platform
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'
178 return strmap[self.action] % locals()
180 @returns(StringTypes)
182 return self.to_sql(multiline=True)
184 @returns(StringTypes)
189 return (self.action, self.object, self.filters, frozendict(self.params), frozenset(self.fields))
192 return hash(self.__key())
194 #---------------------------------------------------------------------------
196 #---------------------------------------------------------------------------
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)
208 def to_json (self, analyzed_query=None):
209 query_uuid=self.query_uuid
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
219 if not analyzed_query:
222 aq = analyzed_query.to_json()
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
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):
235 json_string=POST_dict['json']
236 dict=json.loads(json_string)
237 for (k,v) in dict.iteritems():
240 print "Could not decode incoming ajax request as a Query, POST=",POST_dict
243 traceback.print_exc()
246 #---------------------------------------------------------------------------
248 #---------------------------------------------------------------------------
250 @returns(StringTypes)
251 def get_action(self):
255 def get_select(self):
256 return frozenset(self.fields)
258 @returns(StringTypes)
267 def get_params(self):
270 @returns(StringTypes)
271 def get_timestamp(self):
272 return self.timestamp
275 #DEPRECATED# def make_filters(self, filters):
276 #DEPRECATED# return Filter(filters)
278 #DEPRECATED# def make_fields(self, fields):
279 #DEPRECATED# if isinstance(fields, (list, tuple)):
280 #DEPRECATED# return set(fields)
282 #DEPRECATED# raise Exception, "Invalid field specification"
284 #---------------------------------------------------------------------------
286 #---------------------------------------------------------------------------
290 def action(self, action, object):
292 (Internal usage). Craft a Query according to an action name
293 See methods: get, update, delete, execute.
295 action: A String among {"get", "update", "delete", "execute"}
296 object: The name of the queried object (String)
298 The corresponding Query instance
301 query.action = action
302 query.object = object
307 def get(self, object):
309 Craft the Query which fetches the records related to a given object
311 object: The name of the queried object (String)
313 The corresponding Query instance
315 return self.action("get", object)
319 def update(self, object):
321 Craft the Query which updates the records related to a given object
323 object: The name of the queried object (String)
325 The corresponding Query instance
327 return self.action("update", object)
331 def create(self, object):
333 Craft the Query which create the records related to a given object
335 object: The name of the queried object (String)
337 The corresponding Query instance
339 return self.action("create", object)
343 def delete(self, object):
345 Craft the Query which delete the records related to a given object
347 object: The name of the queried object (String)
349 The corresponding Query instance
351 return self.action("delete", object)
355 def execute(self, object):
357 Craft the Query which execute a processing related to a given object
359 object: The name of the queried object (String)
361 The corresponding Query instance
363 return self.action("execute", object)
366 def at(self, timestamp):
368 Set the timestamp carried by the query
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")
373 The self Query instance
375 self.timestamp = timestamp
378 def filter_by(self, *args):
382 - the parts of a Predicate (key, op, value)
385 - a set/list/tuple of Predicate instances
390 self.filters = Filter()
392 if not isinstance(filters, (set, list, tuple, Filter)):
394 for predicate in filters:
395 self.filters.add(predicate)
397 predicate = Predicate(*args)
398 self.filters.add(predicate)
400 raise Exception, 'Invalid expression for filter'
403 def select(self, *fields):
405 # Accept passing iterables
410 elif isinstance(tmp, (list, tuple, set, frozenset)):
419 self.fields.add(field)
422 def set(self, params):
423 self.params.update(params)
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
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)
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)
448 def __le__(self, query):
449 return ( self == self & query ) or ( query == self | query )
451 class AnalyzedQuery(Query):
453 # XXX we might need to propagate special parameters sur as DEBUG, etc.
455 def __init__(self, query=None, metadata=None):
457 self.metadata = metadata
459 self.query_uuid = query.query_uuid
462 self.query_uuid = uniqid()
464 @returns(StringTypes)
467 fields = self.get_select()
468 fields = ", ".join(fields) if fields else '*'
469 out.append("SELECT %s FROM %s WHERE %s" % (
475 for method, subquery in self.subqueries():
476 out.append(' [SQ #%d : %s] %s' % (cpt, method, str(subquery)))
479 return "\n".join(out)
482 super(AnalyzedQuery, self).clear()
483 self._subqueries = {}
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
491 type = self.metadata.get_field_type(self.object, method)
492 except ValueError ,e: # backwards 1..N
494 analyzed_query.object = type
495 self._subqueries[method] = analyzed_query
496 return self._subqueries[method]
498 def get_subquery(self, method):
499 return self._subqueries.get(method, None)
501 def remove_subquery(self, method):
502 del self._subqueries[method]
504 def get_subquery_names(self):
505 return set(self._subqueries.keys())
507 def get_subqueries(self):
508 return self._subqueries
510 def subqueries(self):
511 for method, subquery in self._subqueries.iteritems():
512 yield (method, subquery)
514 def filter_by(self, filters):
515 if not isinstance(filters, (set, list, tuple, Filter)):
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)
525 super(AnalyzedQuery, self).filter_by(predicate)
528 def select(self, *fields):
530 # XXX passing None should reset fields in all subqueries
532 # Accept passing iterables
535 if isinstance(tmp, (list, tuple, set, frozenset)):
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)
545 super(AnalyzedQuery, self).select(field)
548 def set(self, params):
549 for param, value in self.params.items():
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})
556 super(AnalyzedQuery, self).set({param: value})
559 def analyze(self, query):
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)
568 query_uuid=self.query_uuid
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
579 sq=", ".join ( [ "'%s':%s" % (object, subquery.to_json())
580 for (object, subquery) in self._subqueries.iteritems()])
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