- submitting Thierry enhancements. Filter now uses special form to support more flexi...
[plcapi.git] / PLC / 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
10 from PLC.Faults import *
11 from PLC.Parameter import Parameter, Mixed, python_type
12
13 class Filter(Parameter, dict):
14     """
15     A type of parameter that represents a filter on one or more
16     columns of a database table.
17
18     field should be a dictionary of field names and types, e.g.
19
20     {'node_id': Parameter(int, "Node identifier"),
21      'hostname': Parameter(int, "Fully qualified hostname", max = 255),
22      ...}
23
24     Only filters on non-sequence type fields are supported.
25
26     filter should be a dictionary of field names and values
27     representing an intersection (if join_with is AND) or union (if
28     join_with is OR) filter. If a value is a sequence type, then it
29     should represent a list of possible values for that field.
30
31     Special forms:
32     * a field starting with the ~ character means negation.
33     example :  { '~peer_id' : None }
34     * a field starting with < [  ] or > means lower than or greater than
35       < > uses strict comparison
36       [ ] is for using <= or >= instead
37     example :  { '>time' : 1178531418 }
38     example :  { ']event_id' : 2305 }
39     * a field starting with [ or ] means older than or more recent than
40       the associated value should be a given unix timestamp
41     * a (string) value containing either a * or a % character is
42       treated as a (sql) pattern; * are replaced with % that is the
43       SQL wildcard character.
44     example :  { 'hostname' : '*.jp' } 
45     """
46
47     def __init__(self, fields = {}, filter = {}, doc = "Attribute filter"):
48         # Store the filter in our dict instance
49         dict.__init__(self, filter)
50
51         # Declare ourselves as a type of parameter that can take
52         # either a value or a list of values for each of the specified
53         # fields.
54         self.fields = {}
55
56         for field, expected in fields.iteritems():
57             # Cannot filter on sequences
58             if python_type(expected) in (list, tuple, set):
59                 continue
60             
61             # Accept either a value or a list of values of the specified type
62             self.fields[field] = Mixed(expected, [expected])
63
64         # Null filter means no filter
65         Parameter.__init__(self, self.fields, doc = doc, nullok = True)
66
67     # this code is not used anymore
68     # at some point the select in the DB for event objects was done on
69     # the events table directly, that is stored as a timestamp, thus comparisons
70     # needed to be done based on SQL timestamps as well
71     def unix2timestamp (self,unix):
72         s = time.gmtime(unix)
73         return "TIMESTAMP'%04d-%02d-%02d %02d:%02d:%02d'" % (s.tm_year,s.tm_mon,s.tm_mday,
74                                                              s.tm_hour,s.tm_min,s.tm_sec)
75
76     def sql(self, api, join_with = "AND"):
77         """
78         Returns a SQL conditional that represents this filter.
79         """
80
81         # So that we always return something
82         if join_with == "AND":
83             conditionals = ["True"]
84         elif join_with == "OR":
85             conditionals = ["False"]
86         else:
87             assert join_with in ("AND", "OR")
88
89         for field, value in self.iteritems():
90             # handle negation, numeric comparisons
91             # simple, 1-depth only mechanism
92
93             modifiers={'~' : False, 
94                        '<' : False, '>' : False,
95                        '[' : False, ']' : False,
96                        }
97
98             for char in modifiers.keys():
99                 if field[0] == char:
100                     modifiers[char]=True;
101                     field = field[1:]
102                     break
103
104             if field not in self.fields:
105 #               print 'current fields',self.fields
106                 raise PLCInvalidArgument, "Invalid filter field '%s'" % field
107
108             if isinstance(value, (list, tuple, set)):
109                 # Turn empty list into (NULL) instead of invalid ()
110                 if not value:
111                     value = [None]
112
113                 operator = "IN"
114                 value = map(str, map(api.db.quote, value))
115                 value = "(%s)" % ", ".join(value)
116             else:
117                 if value is None:
118                     operator = "IS"
119                     value = "NULL"
120                 elif isinstance(value, StringTypes) and \
121                      (value.find("*") > -1 or value.find("%") > -1):
122                     operator = "LIKE"
123                     value = str(api.db.quote(value.replace("*", "%")))
124                 else:
125                     operator = "="
126                     if modifiers['<']:
127                         operator='<'
128                     if modifiers['>']:
129                         operator='>'
130                     if modifiers['[']:
131                         operator='<='
132                     if modifiers[']']:
133                         operator='>='
134                     else:
135                         value = str(api.db.quote(value))
136
137             clause = "%s %s %s" % (field, operator, value)
138
139             if modifiers['~']:
140                 clause = " ( NOT %s ) " % (clause)
141
142             conditionals.append(clause)
143
144 #       print 'sql=',(" %s " % join_with).join(conditionals)
145         return (" %s " % join_with).join(conditionals)