diff --git a/lib/redis_model/base.rb b/lib/redis_model/base.rb index d3aaa9e..289e070 100644 --- a/lib/redis_model/base.rb +++ b/lib/redis_model/base.rb @@ -1,7 +1,10 @@ # IMPROVE: UUID identifiers? # IMPROVE: counters module RedisModel - class RecordNotFound < StandardError + class RedisModelError < StandardError + end + + class RecordNotFound < RedisModelError end class Base @@ -31,5 +34,12 @@ def new_record? def ==(other) !new_record? && self.class == other.class && self.id == other.id end + + def self.instanciate(attributes) + record = new(attributes) + record.id = attributes[:id] || attributes['id'] + record.persisted! + record + end end end diff --git a/lib/redis_model/finders.rb b/lib/redis_model/finders.rb index b503973..c23a814 100644 --- a/lib/redis_model/finders.rb +++ b/lib/redis_model/finders.rb @@ -11,64 +11,127 @@ def hkey(attr_name) key("*->#{attr_name}") end - def all - _find_all(:id) + def exists?(id) + connection.exists(key(id)) end - def find(id) + # Finds records. + # + # Examples: + # + # posts = Post.find :all + # posts = Post.find :all, :offset => 20, :limit => 10 + # + # The following calls are equivalent, and will return the same record: + # + # TODO: it actually doesn't work! + # + # post = Post.find :first, :by => :position, :order => :desc + # post = Post.find :last, :by => :position, :order => :asc + # + # Finds all comments for a given post: + # + # comments = Comment.find :all, :index => [ :post_id, 123 ], :by => :approved_at + # + # Options: + # + # - :index - either an indexed attribute name, or an array of [ attr_name, value ] (defaults to :id). + # - :by - an attribute name to sort by or :nosort to not sort result (defaults to :nosort). + # - :order - either :asc, :desc or :alpha or an array of :asc or :desc with :alpha. + # - :limit - an array of [ offset, limit ] + # - :select - an array of attribute names to get (defaults to all attributes) + # + def find(*args) + if args.first.is_a?(Symbol) + options = args.extract_options! + + index = (options[:index] || :id) + index = [ index ] unless index.kind_of?(Array) + limit = options[:limit] + by = options[:by] unless options[:by].blank? + + if options[:order].blank? + order = [ :asc ] unless by.blank? + else + order = options[:order] + order = [ order ] unless order.kind_of?(Array) + end + unless order.nil? + order << :alpha unless order.include?(:alpha) && by.nil? && [ :integer, :float ].include?(schema[by][:type]) + order = order.join(" ").upcase + end + + case args.first + when :all + when :first + return find_with_range(index, 0, 0) if by.nil? && order.blank? + limit = [ 0, 0 ] + when :last + return find_with_range(index, -1, -1) if by.nil? && order.blank? + limit = [ -1, -1 ] + else + raise RedisModelError.new("unknown find method #{args.first.inspect}") + end + + fields = (options[:select] || attribute_names).sort + results = connection.sort(index_key(*index), + :get => fields.collect { |k| hkey(k) }, + :by => by ? hkey(by) : :nosort, + :order => order, + :limit => limit + ) + collection = [] + results.each_slice(fields.size) do |values| + collection << instanciate(Hash[ *fields.zip(values).flatten ]) + end + + case args.first + when :all + collection + when :first, :last + collection.first + end + else + find_with_id(*args) + end + end + + def find_with_range(index, offset, limit) + ids = connection.lrange(index_key(*index), offset, limit) + instanciate(connection.hgetall(key(ids.first))) if ids.any? + end + + def find_with_id(id) attributes = connection.hgetall(key(id)) raise RedisModel::RecordNotFound.new("No such #{model_name} with id: #{id}") if attributes.empty? instanciate(attributes) end - def exists?(id) - connection.exists(key(id)) + def all + find(:all) end def first - ids = connection.lrange(index_key(:id), 0, 0) - instanciate(connection.hgetall(key(ids.first))) if ids.any? + find(:first) end def last - ids = connection.lrange(index_key(:id), -1, -1) - instanciate(connection.hgetall(key(ids.first))) if ids.any? + find(:last) end def method_missing(method_name, *args) if method_name.to_s =~ /^find_(all_by|by)_(.*)$/ case $1 when 'all_by' - _find_all($2, args.first) + find :all, :index => [ $2, args.first ] when 'by' +# find :first, :index = [ $2, args.first ] super end else super end end - - protected - def instanciate(attributes) - record = new(attributes) - record.id = attributes[:id] || attributes['id'] - record.persisted! - record - end - - def _find_all(attr_name, value = nil) - keys = attribute_names.sort - results = connection.sort( - index_key(attr_name, value), - :by => :nosort, - :get => keys.collect { |k| hkey(k) } - ) - collection = [] - results.each_slice(keys.size) do |values| - collection << instanciate(Hash[ *keys.zip(values).flatten ]) - end - collection - end end def reload diff --git a/test/finders_test.rb b/test/finders_test.rb index 2bbe2b3..02531e1 100644 --- a/test/finders_test.rb +++ b/test/finders_test.rb @@ -13,6 +13,39 @@ def test_find assert_raises(RedisModel::RecordNotFound) { Post.find(12346890) } end + def test_find_all + assert_equal [ posts(:welcome), posts(:post1) ], Post.find(:all) + assert_equal [], Row.find(:all) + end + + def test_find_all_with_index + assert_equal [ posts(:welcome) ], Post.find(:all, :index => [ :approved, true ]) + assert_equal [ posts(:post1) ], Post.find(:all, :index => [ :approved, false ]) + end + + def test_find_all_with_select + posts = Post.find(:all, :select => [ :id, :title ]) + assert_equal [ posts(:welcome).id, posts(:post1).id ], posts.collect(&:id) + assert_equal [ posts(:welcome).title, posts(:post1).title ], posts.collect(&:title) + assert_equal [ nil, nil ], posts.collect(&:body) + end + + def test_find_all_with_order + assert_equal [ posts(:post1), posts(:welcome) ], Post.find(:all, :by => :title) + end + + def test_find_all_with_limit + end + + def test_find_first + end + + def test_find_first_with_order + end + + def test_find_last_with_order + end + def test_all assert_equal [ posts(:welcome).id, posts(:post1).id ], Post.all.collect(&:id) assert_equal [], Row.all