X-Git-Url: http://git.onelab.eu/?a=blobdiff_plain;f=PLC%2FFilter.py;h=5d0d22dc1da53b3766249349e48f1ef55a014c38;hb=fe81b2c91b436b1882f63023413c7f51b29538ed;hp=195c4e67ff97c48de0590ff5a44449bb9a9f5c45;hpb=a7c033397aba357a21336361f0b0f41df26866b1;p=plcapi.git diff --git a/PLC/Filter.py b/PLC/Filter.py index 195c4e6..5d0d22d 100644 --- a/PLC/Filter.py +++ b/PLC/Filter.py @@ -1,3 +1,5 @@ +# $Id$ +# $URL$ from types import StringTypes try: set @@ -18,8 +20,9 @@ class Filter(Parameter, dict): as well as sorting and clipping. - fields should be a dictionary of field names and types - Only filters on non-sequence type fields are supported. + fields should be a dictionary of field names and types. + As of PLCAPI-4.3-26, we provide support for filtering on + sequence types as well, with the special '&' and '|' modifiers. example : fields = {'node_id': Parameter(int, "Node identifier"), 'hostname': Parameter(int, "Fully qualified hostname", max = 255), ...} @@ -28,12 +31,28 @@ class Filter(Parameter, dict): filter should be a dictionary of field names and values representing the criteria for filtering. example : filter = { 'hostname' : '*.edu' , site_id : [34,54] } + + Whether the filter represents an intersection (AND) or a union (OR) - of these criteria is determined by the join_with argument - provided to the sql method below + of these criteria is determined as follows: + * if the dictionnary has the '-AND' or the '-OR' key, this is chosen + * otherwise, the join_with argument, as provided to the sql method below, + is expected to hold the 'AND' or 'OR' string + this argument defaults to 'AND' and in most of the code, this default applies + as the join_with argument is left unspecified + Special features: + * a field starting with '&' or '|' should refer to a sequence type + the semantic is then that the object value (expected to be a list) + should contain all (&) or any (|) value specified in the corresponding + filter value. See other examples below. + example : filter = { '|role_ids' : [ 20, 40 ] } + example : filter = { '|roles' : ['tech', 'pi'] } + example : filter = { '&roles' : ['admin', 'tech'] } + example : filter = { '&roles' : 'tech' } + * a field starting with the ~ character means negation. example : filter = { '~peer_id' : None } @@ -53,7 +72,7 @@ class Filter(Parameter, dict): SQL wildcard character. example : filter = { 'hostname' : '*.jp' } - * fields starting with - are special and relate to row selection, i.e. sorting and clipping + * the filter's keys starting with '-' are special and relate to sorting and clipping * '-SORT' : a field name, or an ordered list of field names that are used for sorting these fields may start with + (default) or - for denoting increasing or decreasing order example : filter = { '-SORT' : [ '+node_id', '-hostname' ] } @@ -61,11 +80,29 @@ class Filter(Parameter, dict): * '-LIMIT' : the amount of rows to be returned example : filter = { '-OFFSET' : 100, '-LIMIT':25} - A realistic example would read - GetNodes ( { 'hostname' : '*.edu' , '-SORT' : 'hostname' , '-OFFSET' : 30 , '-LIMIT' : 25 } ) - and that would return nodes matching '*.edu' in alphabetical order from 31th to 55th + + Here are a few realistic examples + + GetNodes ( { 'node_type' : 'regular' , 'hostname' : '*.edu' , + '-SORT' : 'hostname' , '-OFFSET' : 30 , '-LIMIT' : 25 } ) + would return regular (usual) nodes matching '*.edu' in alphabetical order from 31th to 55th + + GetNodes ( { '~peer_id' : None } ) + returns the foreign nodes - that have an integer peer_id + + GetPersons ( { '|role_ids' : [ 20 , 40] } ) + would return all persons that have either pi (20) or tech (40) roles + + GetPersons ( { '&role_ids' : 10 } ) + GetPersons ( { '&role_ids' : 10 } ) + GetPersons ( { '|role_ids' : [ 10 ] } ) + GetPersons ( { '|role_ids' : [ 10 ] } ) + all 4 forms are equivalent and would return all admin users in the system """ + debug=False +# debug=True + def __init__(self, fields = {}, filter = {}, doc = "Attribute filter"): # Store the filter in our dict instance dict.__init__(self, filter) @@ -73,33 +110,26 @@ class Filter(Parameter, dict): # Declare ourselves as a type of parameter that can take # either a value or a list of values for each of the specified # fields. - self.fields = {} - - for field, expected in fields.iteritems(): - # Cannot filter on sequences - if python_type(expected) in (list, tuple, set): - continue - - # Accept either a value or a list of values of the specified type - self.fields[field] = Mixed(expected, [expected]) + self.fields = dict ( [ ( field, Mixed (expected, [expected])) + for (field,expected) in fields.iteritems() ] ) # Null filter means no filter Parameter.__init__(self, self.fields, doc = doc, nullok = True) - # this code is not used anymore - # at some point the select in the DB for event objects was done on - # the events table directly, that is stored as a timestamp, thus comparisons - # needed to be done based on SQL timestamps as well - def unix2timestamp (self,unix): - s = time.gmtime(unix) - return "TIMESTAMP'%04d-%02d-%02d %02d:%02d:%02d'" % (s.tm_year,s.tm_mon,s.tm_mday, - s.tm_hour,s.tm_min,s.tm_sec) - def sql(self, api, join_with = "AND"): """ Returns a SQL conditional that represents this filter. """ + if self.has_key('-AND'): + del self['-AND'] + join_with='AND' + if self.has_key('-OR'): + del self['-OR'] + join_with='OR' + + self.join_with=join_with + # So that we always return something if join_with == "AND": conditionals = ["True"] @@ -120,35 +150,39 @@ class Filter(Parameter, dict): '<' : False, '>' : False, '[' : False, ']' : False, '-' : False, + '&' : False, '|' : False, } - - for char in modifiers.keys(): - if field[0] == char: - modifiers[char]=True; - field = field[1:] - break + def check_modifiers(field): + if field[0] in modifiers.keys(): + modifiers[field[0]] = True + field = field[1:] + return check_modifiers(field) + return field + field = check_modifiers(field) # filter on fields if not modifiers['-']: if field not in self.fields: raise PLCInvalidArgument, "Invalid filter field '%s'" % field - if isinstance(value, (list, tuple, set)): - # Turn empty list into (NULL) instead of invalid () - if not value: - value = [None] + # handling array fileds always as compound values + if modifiers['&'] or modifiers['|']: + if not isinstance(value, (list, tuple, set)): + value = [value,] - operator = "IN" - value = map(str, map(api.db.quote, value)) - value = "(%s)" % ", ".join(value) - else: + def get_op_and_val(value): if value is None: operator = "IS" value = "NULL" elif isinstance(value, StringTypes) and \ (value.find("*") > -1 or value.find("%") > -1): operator = "LIKE" - value = str(api.db.quote(value.replace("*", "%"))) + # insert *** in pattern instead of either * or % + # we dont use % as requests are likely to %-expansion later on + # actual replacement to % done in PostgreSQL.py + value = value.replace ('*','***') + value = value.replace ('%','***') + value = str(api.db.quote(value)) else: operator = "=" if modifiers['<']: @@ -159,10 +193,61 @@ class Filter(Parameter, dict): operator='<=' if modifiers[']']: operator='>=' + value = str(api.db.quote(value)) + return (operator, value) + + if isinstance(value, (list, tuple, set)): + # handling filters like '~slice_id':[] + # this should return true, as it's the opposite of 'slice_id':[] which is false + # prior to this fix, 'slice_id':[] would have returned ``slice_id IN (NULL) '' which is unknown + # so it worked by coincidence, but the negation '~slice_ids':[] would return false too + if not value: + if modifiers['&'] or modifiers['|']: + operator = "=" + value = "'{}'" else: - value = str(api.db.quote(value)) - - clause = "%s %s %s" % (field, operator, value) + field="" + operator="" + value = "FALSE" + clause = "%s %s %s" % (field, operator, value) + else: + value = map(str, map(api.db.quote, value)) + do_join = True + vals = {} + for val in value: + base_op, val = get_op_and_val(val) + if base_op != '=': + do_join = False + if base_op in vals: + vals[base_op].append(val) + else: + vals[base_op] = [val] + if do_join: + if modifiers['&']: + operator = "@>" + value = "ARRAY[%s]" % ", ".join(value) + elif modifiers['|']: + operator = "&&" + value = "ARRAY[%s]" % ", ".join(value) + else: + operator = "IN" + value = "(%s)" % ", ".join(value) + clause = "%s %s %s" % (field, operator, value) + else: + # We need something more complex + subclauses = [] + for operator in vals.keys(): + if operator == '=': + subclauses.append("(%s IN (%s))" % (field, ",".join(vals[operator]))) + elif operator == 'IS': + subclauses.append("(%s IS NULL)" % field) + else: + for value in vals[operator]: + subclauses.append("(%s %s %s)" % (field, operator, value)) + clause = "(" + " OR ".join(subclauses) + ")" + else: + operator, value = get_op_and_val(value) + clause = "%s %s %s" % (field, operator, value) if modifiers['~']: clause = " ( NOT %s ) " % (clause) @@ -199,5 +284,5 @@ class Filter(Parameter, dict): clip_part += " ORDER BY " + ",".join(sorts) if clips: clip_part += " " + " ".join(clips) -# print 'where_part=',where_part,'clip_part',clip_part + if Filter.debug: print 'Filter.sql: where_part=',where_part,'clip_part',clip_part return (where_part,clip_part)