17994: Accept a few very specific expressions as filters.
[arvados.git] / services / api / lib / record_filters.rb
index 994e8503106ad6ec3e931e555f2f28d8d455a1da..517a7fe28496d46d4352d13f9bb0463ba184cdec 100644 (file)
@@ -19,7 +19,7 @@ module RecordFilters
   # +model_class+    subclass of ActiveRecord being filtered
   #
   # Output:
-  # Hash with two keys:
+  # Hash with the following keys:
   # :cond_out  array of SQL fragments for each filter expression
   # :param_out array of values for parameter substitution in cond_out
   # :joins     array of joins: either [] or ["JOIN containers ON ..."]
@@ -31,7 +31,10 @@ module RecordFilters
     model_table_name = model_class.table_name
     filters.each do |filter|
       attrs_in, operator, operand = filter
-      if attrs_in == 'any' && operator != '@@'
+      if operator == '@@'
+        raise ArgumentError.new("Full text search operator is no longer supported")
+      end
+      if attrs_in == 'any'
         attrs = model_class.searchable_columns(operator)
       elsif attrs_in.is_a? Array
         attrs = attrs_in
@@ -54,22 +57,6 @@ module RecordFilters
         attrs = []
       end
 
-      if operator == '@@'
-        # Full-text search
-        if attrs_in != 'any'
-          raise ArgumentError.new("Full text search on individual columns is not supported")
-        end
-        if operand.is_a? Array
-          raise ArgumentError.new("Full text search not supported for array operands")
-        end
-
-        # Skip the generic per-column operator loop below
-        attrs = []
-        # Use to_tsquery since plainto_tsquery does not support prefix
-        # search. And, split operand and join the words with ' & '
-        cond_out << model_class.full_text_tsvector+" @@ to_tsquery(?)"
-        param_out << operand.split.join(' & ')
-      end
       attrs.each do |attr|
         subproperty = attr.split(".", 2)
 
@@ -140,6 +127,10 @@ module RecordFilters
               raise ArgumentError.new("Invalid operand '#{operand}' for '#{operator}' must be true or false")
             end
             param_out << proppath
+          when 'contains'
+            cond_out << "#{attr_table_name}.#{attr} @> ?::jsonb OR #{attr_table_name}.#{attr} @> ?::jsonb"
+            param_out << SafeJSON.dump({proppath => operand})
+            param_out << SafeJSON.dump({proppath => [operand]})
           else
             raise ArgumentError.new("Invalid operator for subproperty search '#{operator}'")
           end
@@ -150,6 +141,23 @@ module RecordFilters
 
           cond_out << "jsonb_exists(#{attr_table_name}.#{attr}, ?)"
           param_out << operand
+        elsif expr = /^ *\( *(\w+) *(<=?|>=?|=) *(\w+) *\) *$/.match(attr)
+          if operator != '=' || ![true,"true"].index(operand)
+            raise ArgumentError.new("Invalid expression filter '#{attr}': subsequent elements must be [\"=\", true]")
+          end
+          operator = expr[2]
+          attr1, attr2 = expr[1], expr[3]
+          allowed = attr_model_class.searchable_columns(operator)
+          [attr1, attr2].each do |tok|
+            if !allowed.index(tok)
+              raise ArgumentError.new("Invalid attribute in expression: '#{tok}'")
+            end
+            col = attr_model_class.columns.select { |c| c.name == tok }.first
+            if col.type != :integer
+              raise ArgumentError.new("Non-numeric attribute in expression: '#{tok}'")
+            end
+          end
+          cond_out << "#{attr1} #{operator} #{attr2}"
         else
           if !attr_model_class.searchable_columns(operator).index attr
             raise ArgumentError.new("Invalid attribute '#{attr}' in filter")