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