12

My Product model has a jsonb field specs (which we're managing using ActiveRecord's store_accessor). Many of my products' specs have a spec in that hash called spec_options.

Before now, this spec_option field was just text. Now it needs to be an array.

The scope used before now to query products for this field was this:

scope :with_spec_options, ->(spec_options) { 
    where("'#{spec_options}'::jsonb \? (specs->>'spec_option')") 
}

Ruby equivalent (just to help understand what this is doing):

select{ |product| spec_options.include?(product.specs['spec_option']) }

ActiveRecord equivalent (if spec_option were a regular column):

where(spec_option: spec_options)

However, now that specs['spec_options'] is an array, I can't do that. I think I need to use postgres' ?| jsonb operator, but I can't work out how to get the right side of this operation into the correct format.

Ruby equivalent:

def self.with_spec_options(spec_options)
    all.select{|product| 
        if product.specs['spec_options'].present?
            product.specs['spec_options'].any?{|option|
                spec_options.include?(option)
            }
        else
            false
        end
    }
end

Anyone got ideas?

1 Answer 1

24

What you want to use is the @> operator, which tests whether your left-hand value contains the right-hand value. "Contains" works for both objects and arrays, so the following query would work:

SELECT * FROM products WHERE specs->'spec_options' @> '["spec1", "spec2"]';

Which I believe you can transform into ActiveRecord-compatible syntax like so:

scope :with_spec_options, ->(spec_options) { 
  where("specs->'spec_option' @> ?", spec_options.to_json) 
}
Sign up to request clarification or add additional context in comments.

6 Comments

Thank you! That was almost it. Both sides of the operation needed to be jsonb, so this worked: where("specs->'spec_option' @> ?::jsonb", spec_options.to_json)
Scrap that, you don't need the extra ::jsonb conversion: where("specs->'spec_option' @> ?", spec_options.to_json)
Ugh, so I thought this was a solution, but it's not quite what I need. It needs to return results if any of the queried options match. This answer only returns results with all matching.
@Obversity: try this instead: where("specs->'spec_option' ?| :options", options: spec_options).
Sorry, I meant: where("specs->'spec_option' ?| array[:options]", options: spec_options). The interpolation into proper Postgres syntax is a bit tricky.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.