move init.d and cron.d one step up in the source tree
[sfa.git] / sfa / util / filter.py
1 from types import StringTypes
2 try:
3     set
4 except NameError:
5     from sets import Set
6     set = Set
7
8 try: import pgdb
9 except: pass
10  
11 from sfa.util.faults import SfaInvalidArgument
12 from sfa.util.parameter import Parameter, Mixed, python_type
13
14
15 class Filter(Parameter, dict):
16     """
17     A type of parameter that represents a filter on one or more
18     columns of a database table.
19     Special features provide support for negation, upper and lower bounds, 
20     as well as sorting and clipping.
21
22
23     fields should be a dictionary of field names and types
24     Only filters on non-sequence type fields are supported.
25     example : fields = {'node_id': Parameter(int, "Node identifier"),
26                         'hostname': Parameter(int, "Fully qualified hostname", max = 255),
27                         ...}
28
29
30     filter should be a dictionary of field names and values
31     representing  the criteria for filtering. 
32     example : filter = { 'hostname' : '*.edu' , site_id : [34,54] }
33     Whether the filter represents an intersection (AND) or a union (OR) 
34     of these criteria is determined by the join_with argument 
35     provided to the sql method below
36
37     Special features:
38
39     * a field starting with the ~ character means negation.
40     example :  filter = { '~peer_id' : None }
41
42     * a field starting with < [  ] or > means lower than or greater than
43       < > uses strict comparison
44       [ ] is for using <= or >= instead
45     example :  filter = { ']event_id' : 2305 }
46     example :  filter = { '>time' : 1178531418 }
47       in this example the integer value denotes a unix timestamp
48
49     * if a value is a sequence type, then it should represent 
50       a list of possible values for that field
51     example : filter = { 'node_id' : [12,34,56] }
52
53     * a (string) value containing either a * or a % character is
54       treated as a (sql) pattern; * are replaced with % that is the
55       SQL wildcard character.
56     example :  filter = { 'hostname' : '*.jp' } 
57
58     * fields starting with - are special and relate to row selection, i.e. sorting and clipping
59     * '-SORT' : a field name, or an ordered list of field names that are used for sorting
60       these fields may start with + (default) or - for denoting increasing or decreasing order
61     example : filter = { '-SORT' : [ '+node_id', '-hostname' ] }
62     * '-OFFSET' : the number of first rows to be ommitted
63     * '-LIMIT' : the amount of rows to be returned 
64     example : filter = { '-OFFSET' : 100, '-LIMIT':25}
65
66     A realistic example would read
67     GetNodes ( { 'node_type' : 'regular' , 'hostname' : '*.edu' , '-SORT' : 'hostname' , '-OFFSET' : 30 , '-LIMIT' : 25 } )
68     and that would return regular (usual) nodes matching '*.edu' in alphabetical order from 31th to 55th
69     """
70
71     def __init__(self, fields = {}, filter = {}, doc = "Attribute filter"):
72         # Store the filter in our dict instance
73         valid_fields = {}
74         for field in filter:
75             if field in fields:
76                 valid_fields[field] = filter[field]
77         dict.__init__(self, valid_fields)
78
79         # Declare ourselves as a type of parameter that can take
80         # either a value or a list of values for each of the specified
81         # fields.
82         self.fields = dict ( [ ( field, Mixed (expected, [expected])) 
83                                  for (field,expected) in fields.iteritems()
84                                  if python_type(expected) not in (list, tuple, set) ] )
85
86         # Null filter means no filter
87         Parameter.__init__(self, self.fields, doc = doc, nullok = True)
88
89     def quote(self, value):
90         """
91         Returns quoted version of the specified value.
92         """
93
94         # The pgdb._quote function is good enough for general SQL
95         # quoting, except for array types.
96         if isinstance(value, (list, tuple, set)):
97             return "ARRAY[%s]" % ", ".join(map(self.quote, value))
98         else:
99             return pgdb._quote(value)    
100
101     def sql(self, join_with = "AND"):
102         """
103         Returns a SQL conditional that represents this filter.
104         """
105
106         # So that we always return something
107         if join_with == "AND":
108             conditionals = ["True"]
109         elif join_with == "OR":
110             conditionals = ["False"]
111         else:
112             assert join_with in ("AND", "OR")
113
114         # init 
115         sorts = []
116         clips = []
117
118         for field, value in self.iteritems():
119             # handle negation, numeric comparisons
120             # simple, 1-depth only mechanism
121
122             modifiers={'~' : False, 
123                        '<' : False, '>' : False,
124                        '[' : False, ']' : False,
125                        '-' : False,
126                        }
127
128             for char in modifiers.keys():
129                 if field[0] == char:
130                     modifiers[char]=True
131                     field = field[1:]
132                     break
133
134             # filter on fields
135             if not modifiers['-']:
136                 if field not in self.fields:
137                     raise SfaInvalidArgument, "Invalid filter field '%s'" % field
138
139                 if isinstance(value, (list, tuple, set)):
140                     # handling filters like '~slice_id':[]
141                     # this should return true, as it's the opposite of 'slice_id':[] which is false
142                     # prior to this fix, 'slice_id':[] would have returned ``slice_id IN (NULL) '' which is unknown 
143                     # so it worked by coincidence, but the negation '~slice_ids':[] would return false too
144                     if not value:
145                         field=""
146                         operator=""
147                         value = "FALSE"
148                     else:
149                         operator = "IN"
150                         value = map(str, map(self.quote, value))
151                         value = "(%s)" % ", ".join(value)
152                 else:
153                     if value is None:
154                         operator = "IS"
155                         value = "NULL"
156                     elif isinstance(value, StringTypes) and \
157                             (value.find("*") > -1 or value.find("%") > -1):
158                         operator = "LIKE"
159                         # insert *** in pattern instead of either * or %
160                         # we dont use % as requests are likely to %-expansion later on
161                         # actual replacement to % done in PostgreSQL.py
162                         value = value.replace ('*','***')
163                         value = value.replace ('%','***')
164                         value = str(self.quote(value))
165                     else:
166                         operator = "="
167                         if modifiers['<']:
168                             operator='<'
169                         if modifiers['>']:
170                             operator='>'
171                         if modifiers['[']:
172                             operator='<='
173                         if modifiers[']']:
174                             operator='>='
175                         else:
176                             value = str(self.quote(value))
177  
178                 clause = "%s %s %s" % (field, operator, value)
179
180                 if modifiers['~']:
181                     clause = " ( NOT %s ) " % (clause)
182
183                 conditionals.append(clause)
184             # sorting and clipping
185             else:
186                 if field not in ('SORT','OFFSET','LIMIT'):
187                     raise SfaInvalidArgument, "Invalid filter, unknown sort and clip field %r"%field
188                 # sorting
189                 if field == 'SORT':
190                     if not isinstance(value,(list,tuple,set)):
191                         value=[value]
192                     for field in value:
193                         order = 'ASC'
194                         if field[0] == '+':
195                             field = field[1:]
196                         elif field[0] == '-':
197                             field = field[1:]
198                             order = 'DESC'
199                         if field not in self.fields:
200                             raise SfaInvalidArgument, "Invalid field %r in SORT filter"%field
201                         sorts.append("%s %s"%(field,order))
202                 # clipping
203                 elif field == 'OFFSET':
204                     clips.append("OFFSET %d"%value)
205                 # clipping continued
206                 elif field == 'LIMIT' :
207                     clips.append("LIMIT %d"%value)
208
209         where_part = (" %s " % join_with).join(conditionals)
210         clip_part = ""
211         if sorts:
212             clip_part += " ORDER BY " + ",".join(sorts)
213         if clips:
214             clip_part += " " + " ".join(clips)
215         return (where_part,clip_part)