require 'ripple/translation' require 'active_support/concern' module Ripple # Adds secondary-indexes to {Document} properties. module Indexes extend ActiveSupport::Concern module ClassMethods def inherited(subclass) super subclass.indexes = indexes.dup end # Indexes defined on the document. def indexes @indexes ||= {}.with_indifferent_access end def indexes=(idx) @indexes = idx end def property(key, type, options={}) if indexed = options.delete(:index) indexes[key] = Index.new(key, type, indexed) end super end def index(key, type, &block) if block_given? indexes[key] = Index.new(key, type, &block) else indexes[key] = Index.new(key, type) end end end # Returns indexes in a form suitable for persisting to Riak. # @return [Hash] indexes for this document def indexes_for_persistence(prefix = '') Hash.new {|h,k| h[k] = Set.new }.tap do |indexes| # Add embedded associations' indexes self.class.embedded_associations.each do |association| documents = instance_variable_get(association.ivar) unless documents.nil? Array(documents).each do |doc| embedded_indexes = doc.indexes_for_persistence("#{prefix}#{association.name}_") indexes.merge!(embedded_indexes) do |_,original,new| original.merge new end end end end # Add this document's indexes self.class.indexes.each do |key, index| if index.block index_value = index.to_index_value instance_exec(&index.block) else index_value = index.to_index_value send(key) end index_value = Set[index_value] unless index_value.is_a?(Enumerable) && !index_value.is_a?(String) indexes[prefix + index.index_key].merge index_value end end end # Modifies the persistence chain to set indexes on the internal # {Riak::RObject} before saving. module DocumentMethods extend ActiveSupport::Concern def update_robject robject.indexes = indexes_for_persistence super end module ClassMethods # Search for a document using an indexed column # @param [Symbol] name of the index # @param [String, Integer, Range] query to search for def find_by_index(index_name, query) if ["$bucket", "$key"].include?(index_name.to_s) self.find(Ripple.client.get_index(self.bucket.name, index_name.to_s, query)) else idx = self.indexes[index_name] raise ArgumentError, t('index_undefined', :property => index_name, :type => self.name) if idx.nil? self.find(Ripple.client.get_index(self.bucket.name, idx.index_key, query)) end end end end end # Represents a Secondary Index on a Document class Index include Translation attr_reader :key, :type, :block # Creates an index for a Document # @param [Symbol] key the attribute key # @param [Class] property_type the type of the associated property # @param ['bin', 'int', String, Integer] index_type if given, the # type of index # @yield a block that returns the value of the index def initialize(key, property_type, index_type=true, &block) @key, @type, @index, @block = key, property_type, index_type, block end # The key under which a value will be indexed def index_key "#{key}_#{index_type}" end # Converts an attribute to a value appropriate for storing in a # secondary index. # @param [Object] value a value of type {#type} # @return [String, Integer, Set] a value appropriate for storing # in a secondary index def to_index_value(value) value.to_ripple_index(index_type) end # @return ["bin", "int", nil] the type of index used for this property # @raise [ArgumentError] if the type cannot be automatically determined def index_type @index_type ||= case @index when /^bin|int$/ @index when Class determine_index_type(@index) else determine_index_type(@type) end end private def determine_index_type(itype) if String == itype || itype < String 'bin' elsif [Integer, Time, Date, ActiveSupport::TimeWithZone].any? {|t| t == itype || itype < t } 'int' else raise ArgumentError, t('index_type_unknown', :property => @key, :type => itype.name) end end end end