3220: Merge branch '3220-http-status-codes' closes #3220
[arvados.git] / services / api / lib / record_filters.rb
1 # Mixin module providing a method to convert filters into a list of SQL
2 # fragments suitable to be fed to ActiveRecord #where.
3 #
4 # Expects:
5 #   model_class
6 # Operates on:
7 #   @objects
8 module RecordFilters
9
10   # Input:
11   # +filters+        array of conditions, each being [column, operator, operand]
12   # +ar_table_name+  name of SQL table
13   #
14   # Output:
15   # Hash with two keys:
16   # :cond_out  array of SQL fragments for each filter expression
17   # :param_out  array of values for parameter substitution in cond_out
18   def record_filters filters, ar_table_name
19     cond_out = []
20     param_out = []
21
22     filters.each do |filter|
23       attr, operator, operand = filter
24       if !filter.is_a? Array
25         raise ArgumentError.new("Invalid element in filters array: #{filter.inspect} is not an array")
26       elsif !operator.is_a? String
27         raise ArgumentError.new("Invalid operator '#{operator}' (#{operator.class}) in filter")
28       elsif !model_class.searchable_columns(operator).index attr.to_s
29         raise ArgumentError.new("Invalid attribute '#{attr}' in filter")
30       end
31       case operator.downcase
32       when '=', '<', '<=', '>', '>=', '!=', 'like'
33         attr_type = model_class.attribute_column(attr).type
34         operator = '<>' if operator == '!='
35         if operand.is_a? String
36           if attr_type == :boolean
37             if not ['=', '<>'].include?(operator)
38               raise ArgumentError.new("Invalid operator '#{operator}' for " \
39                                       "boolean attribute '#{attr}'")
40             end
41             case operand.downcase
42             when '1', 't', 'true', 'y', 'yes'
43               operand = true
44             when '0', 'f', 'false', 'n', 'no'
45               operand = false
46             else
47               raise ArgumentError("Invalid operand '#{operand}' for " \
48                                   "boolean attribute '#{attr}'")
49             end
50           end
51           cond_out << "#{ar_table_name}.#{attr} #{operator} ?"
52           if (# any operator that operates on value rather than
53               # representation:
54               operator.match(/[<=>]/) and (attr_type == :datetime))
55             operand = Time.parse operand
56           end
57           param_out << operand
58         elsif operand.nil? and operator == '='
59           cond_out << "#{ar_table_name}.#{attr} is null"
60         elsif operand.nil? and operator == '<>'
61           cond_out << "#{ar_table_name}.#{attr} is not null"
62         elsif (attr_type == :boolean) and ['=', '<>'].include?(operator) and
63             [true, false].include?(operand)
64           cond_out << "#{ar_table_name}.#{attr} #{operator} ?"
65           param_out << operand
66         else
67           raise ArgumentError.new("Invalid operand type '#{operand.class}' "\
68                                   "for '#{operator}' operator in filters")
69         end
70       when 'in', 'not in'
71         if operand.is_a? Array
72           cond_out << "#{ar_table_name}.#{attr} #{operator} (?)"
73           param_out << operand
74           if operator == 'not in' and not operand.include?(nil)
75             # explicitly allow NULL
76             cond_out[-1] = "(#{cond_out[-1]} OR #{ar_table_name}.#{attr} IS NULL)"
77           end
78         else
79           raise ArgumentError.new("Invalid operand type '#{operand.class}' "\
80                                   "for '#{operator}' operator in filters")
81         end
82       when 'is_a'
83         operand = [operand] unless operand.is_a? Array
84         cond = []
85         operand.each do |op|
86           cl = ArvadosModel::kind_class op
87           if cl
88             cond << "#{ar_table_name}.#{attr} like ?"
89             param_out << cl.uuid_like_pattern
90           else
91             cond << "1=0"
92           end
93         end
94         cond_out << cond.join(' OR ')
95       end
96     end
97
98     {:cond_out => cond_out, :param_out => param_out}
99   end
100
101 end