Search code examples
ruby-on-railspostgresqlactiverecordpostgresql-9.2hstore

Rails/Postgres query Hstore for presence


I have some HStore columns in my Rails app. Looking around the Posgresql documentation and some blog posts, I've discovered that, given an HStore value that is a hash like this:

{ some_key: 'value' }

I can query the columns like so:

Model.where("the_hstore_column -> 'some_key' = 'value'")

There are a bunch of issues with this as a sufficient querying tool, however:

  1. It really only makes sense for super simple values. If the value is itself a hash or something, I have no idea how to search for it effectively, if I don't know all its contents. (Even if I did, I'd have to turn them all to stringified JSON or something).

  2. It isn't helpful (at least, I can't make it helpful) for doing queries for the presence or non-presence of the column's content (or the content of any key under the column).

In other words (in pseudo-sql), I'd love to be able to do this:

Model.where("the_hstore_column = {}")
Model.where.not("the_hstore_column = {}")

Or:

Model.where("the_hstore_column -> 'some_key' = NULL")
Model.where.not("the_hstore_column -> 'some_key' = NULL")

Or best yet, given an HStore of value { some_key: { sub_key: 'value' } }:

Model.where("the_hstore_column -> 'some_key' INCLUDES (?)", 'sub_key')
Model.where.not("the_hstore_column -> 'some_key' INCLUDES (?)", 'sub_key')

These appear not to be working, but for the life of me, I can't find great information on how to conduct these queries. Does anyone know how to write them, or where I could look for better information?

UPDATE After more looking around, I found this post, which looked promising, but I can't get the code to actually work. For example Model.where("the_hstore_column ? :key", key: 'some_key') is returning an empty relation, even if there are many Model objects with some_key in the_hstore_column.

As requested by Andrius Buivydas, I'm pasting the relevant portion of my model below. It's relatively brief, because I decided to abstract out the Hstore-accessing into a module I wrote, essentially turning it into a hash store (which I though, apparently incorrectly, was its whole purpose). I tried using the built-in store_accessor, but it didn't work to my liking, (didn't seem to help parse saved hashes, for now obvious-seeming reasons) thus the ActiveRecord::MetaExt::HstoreAccessor module below. (ParsedHstore just takes an HStore string and parses it into a hash).

class Place.rb

  include ActiveRecord::MetaExt::HstoreAccessor
  hstore_accessor :hours, :extra

end

(In a separate file):

module ActiveRecord
  module MetaExt
    module HstoreAccessor

      module ClassMethods

        def hstore_accessor(*symbols)
          symbols.each do |field|
            class_eval do 
              define_method(field) do
                ParsedHstore.new(self[field.to_sym]).hash_value
              end

              define_method("add_to_#{field}!") do |arg|
                self[field.to_sym] = self[field.to_sym].merge(arg)
                send("#{field}_will_change!")
                save
                arg
              end

              define_method("add_to_#{field}") do |arg|
                self[field.to_sym] = self[field.to_sym].merge(arg)
                send("#{field}_will_change!")
                arg
              end

              define_method("remove_from_#{field}") do |arg|
                other = self[field].dup
                other.delete(arg.to_s); other.delete(arg.to_sym)
                self[field.to_sym] = other
                send("#{field}_will_change!")
                self[field.to_sym]
              end

              define_method("remove_from_#{field}!") do |arg|
                other = self[field].dup
                other.delete(arg.to_s); other.delete(arg.to_sym)
                self[field.to_sym] = other
                send("#{field}_will_change!")
                save
                self[field.to_sym]
              end

              define_method("set_#{field}") do |arg|
                self[field.to_sym] = arg
                send("#{field}_will_change!")
                self[field.to_sym]
              end

              define_method("set_#{field}!") do |arg|
                self[field.to_sym] = arg
                send("#{field}_will_change!")
                self[field.to_sym]
              end
            end
          end
        end
      end

      def self.included(base)
        base.extend ClassMethods
      end
    end
  end
end

The idea is that this lets me easily add/remove values to an HStore field, without having to think about the merging and _will_change! logic every time. So I could do this, for example: Place.first.add_to_extra({ key: 'value'}).

Should I have made these fields json or jsonb? I feel like I'm reinventing the wheel here, or, more aptly, trying to turn a horse into a wheel or something.

Also, I may be misunderstanding the query example. I literally tried this query in my database (which has many places with a non-empty ranking key under the extra field), and turned up with an empty relation:

Place.where("extra ? :key", key: 'ranking')

I wouldn't be surprised if I messed this syntax up, as it seems really strange. Wouldn't that replace the ? with 'ranking', turning the query into this?: Place.where("extra ranking :key")? Seems weird and emphatically different from any other SQL I've run. Or is it turning to Place.where("extra ? ranking")? But ? is usually for safe injection of variables, no?

Please let me know if something else in my model or elsewhere would be more relevant.


Solution

  • It really only makes sense for super simple values. If the value is itself a hash or something, I have no idea how to search for it effectively, if I don't know all its contents.

    Postgresql HStore stores key, values pairs that are both strings, only. So you can't store a hash, a nil or something else like an object - they will be converted to the strings.

    Model.where("the_hstore_column ? :key", key: 'some_key')

    That should work if everything is defined correctly. Could you paste an extract of the model file with the definition of the hstore column values?