Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

union operation in find method, similar to the IN Operator in SQL #74

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,10 @@ User.find(:country => "Argentina").except(:status => "suspended")

# Find all users both from Argentina and Uruguay
User.find(:country => "Argentina").union(:country => "Uruguay")

# Find all users both from Argentina and Uruguay which are activated
User.find(:country => ["Argentina", "Uruguay"], :status => "activated")
User.find(:status => "activated").find(:country => ["Argentina", "Uruguay"])
```

Note that calling these methods results in new sets being created
Expand Down
93 changes: 76 additions & 17 deletions lib/ohm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -446,9 +446,16 @@ def initialize(key, namespace, model)
# set.find(:age => 30)
#
def find(dict)
MultiSet.new(
namespace, model, Command[:sinterstore, key, *model.filters(dict)]
)
model.check_params(dict)
# get all permuatatins of the hash values
dicts = model.hash_product(dict)
if dicts.size == 1
# do just an intersect if no hash value was an array
command = model.intersected(dicts.first, key)
else
command = model.unioned(dicts, key)
end
MultiSet.new(namespace, model, command)
end

# Reduce the set using any number of filters.
Expand Down Expand Up @@ -577,8 +584,17 @@ def initialize(namespace, model, command)
# set.find(:status => 'pending')
#
def find(dict)
model.check_params(dict)
# get all permuatatins of the hash values
dicts = model.hash_product(dict)
if dicts.size == 1
# do just an intersect if no hash value was an array
commands = model.intersected(dicts.first)
else
commands = model.unioned(dicts)
end
MultiSet.new(
namespace, model, Command[:sinterstore, command, intersected(dict)]
namespace, model, Command[:sinterstore, command, commands]
)
end

Expand All @@ -594,7 +610,7 @@ def find(dict)
#
def except(dict)
MultiSet.new(
namespace, model, Command[:sdiffstore, command, intersected(dict)]
namespace, model, Command[:sdiffstore, command, model.intersected(dict)]
)
end

Expand All @@ -610,7 +626,7 @@ def except(dict)
#
def union(dict)
MultiSet.new(
namespace, model, Command[:sunionstore, command, intersected(dict)]
namespace, model, Command[:sunionstore, command, model.intersected(dict)]
)
end

Expand All @@ -619,10 +635,6 @@ def db
model.db
end

def intersected(dict)
Command[:sinterstore, *model.filters(dict)]
end

def execute
# namespace[:tmp] is where all the temp keys should be stored in.
# db will be where all the commands are executed against.
Expand Down Expand Up @@ -810,6 +822,8 @@ def self.with(att, val)
# end
#
# u = User.create(name: "John", status: "pending", email: "foo@me.com")
# u2 = User.create(name: "Steven", status: "pending", email: "steven@me.com")
#
# User.find(provider: "me", name: "John", status: "pending").include?(u)
# # => true
#
Expand All @@ -819,17 +833,28 @@ def self.with(att, val)
# User.find(:tag => "python").include?(u)
# # => true
#
# User.find(:tag => ["ruby", "python"]).include?(u)
# User.find(:tag => [["ruby", "python"]]).include?(u)
# # => true
#
# User.find(:name => ["John", "Steven"]).to_a == [u, u2]
# # => true
#
def self.find(dict)
keys = filters(dict)

if keys.size == 1
Ohm::Set.new(keys.first, key, self)
check_params(dict)
# get all permuatations of the hash values
dicts = hash_product(dict)
if dicts.size == 1
# create a Ohm::Set if just one key-value pair was provided
# but not when the value is an array
if dicts.first.keys.size == 1 && !dicts.first.values.first.is_a?(Array)
return Ohm::Set.new(filters(dicts.first).first, key, self)
end
# do just an intersect if no hash value was an array
command = intersected(dicts.first)
else
Ohm::MultiSet.new(key, self, Command.new(:sinterstore, *keys))
command = unioned(dicts)
end
Ohm::MultiSet.new(key, self, command)
end

# Retrieve a set of models given an array of IDs.
Expand Down Expand Up @@ -1404,13 +1429,16 @@ def self.collections
@collections ||= []
end

def self.filters(dict)
def self.check_params(dict)
unless dict.kind_of?(Hash)
raise ArgumentError,
"You need to supply a hash with filters. " +
"If you want to find by ID, use #{self}[id] instead."
end
end

def self.filters(dict)
check_params(dict)
dict.map { |k, v| to_indices(k, v) }.flatten
end

Expand All @@ -1424,6 +1452,37 @@ def self.to_indices(att, val)
end
end

# returns a permutation of the elements of the hash values
#
# e.g.
# given { :attr0 => c, :attr1 => [a, b], :attr2 => [d, f] }
# returns:
# [{ :attr0 => c, :attr1 => a, :attr2 => d },
# { :attr0 => c, :attr1 => a, :attr2 => f },
# { :attr0 => c, :attr1 => b, :attr2 => d },
# { :attr0 => c, :attr1 => b, :attr2 => f }]
#
def self.hash_product(hsh)
# convert a value to an array if it isn't one already
attrs = hsh.values.map{ |e| e.is_a?(Array) ? e : [e] }
keys = hsh.keys
# lets do the product of the values
product = attrs[0].product(*attrs[1..-1])
product.map{ |p| Hash[keys.zip p] }
end

def self.intersected(dict, key=nil)
keys = filters(dict)
keys = keys.unshift(key) if key
Command[:sinterstore, *keys]
end

def self.unioned(dicts, key=nil)
# get redis commands for each dict
commands = dicts.map { |dict| intersected(dict, key) }
Command[:sunionstore, *commands]
end

def self.new_id
db.incr(key[:id])
end
Expand Down
29 changes: 28 additions & 1 deletion test/filtering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@ class User < Ohm::Model
[u1, u2]
end

test "findability" do |john, jane|
test "findability" do |john, jane, jsmith|
assert_equal 1, User.find(:lname => "Doe", :fname => "John").size
assert User.find(:lname => "Doe", :fname => "John").include?(john)

assert_equal 1, User.find(:lname => "Doe", :fname => "Jane").size
assert User.find(:lname => "Doe", :fname => "Jane").include?(jane)

smith = User.create(:fname => "John", :lname => "Smith", :status => "inactive")
assert User.find(:fname => %w(Jane John)).map(&:id) == [john, jane, smith].map(&:id)

assert User.find(:fname => %w(John Jane), :status => "active").map(&:id) == [john, jane].map(&:id)
end

test "sets aren't mutable" do |john, jane|
Expand Down Expand Up @@ -199,6 +204,28 @@ def read(io)
assert(read(io) =~ Regexp.new(expected))
end

test "SINTERSTORE (SINTERSTORE a c) (SINTERSTORE b c)" do |io|
Post.find(author: ["matz", "rich"], mood: "happy").to_a

# For this case we need an intermediate key. This will
# contain the intersection of matz + happy and rich + happy.
expected = "SINTERSTORE (Post:tmp:[a-f0-9]{64}) " +
"Post:indices:author:matz Post:indices:mood:happy"
assert(read(io) =~ Regexp.new(expected))
tmp_key = $1

expected = "SINTERSTORE (Post:tmp:[a-f0-9]{64}) " +
"Post:indices:author:rich Post:indices:mood:happy"
assert(read(io) =~ Regexp.new(expected))

# The next operation is simply doing a UNION of the previously
# generated intermediate keys and the additional single key.
expected = "SUNIONSTORE (Post:tmp:[a-f0-9]{64}) " +
"#{tmp_key} #{$1}"

assert(read(io) =~ Regexp.new(expected))
end

test "SUNIONSTORE a b" do |io|
Post.find(author: "matz").union(mood: "happy").to_a

Expand Down
6 changes: 3 additions & 3 deletions test/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -459,9 +459,9 @@ def tag
end

test "finding by multiple entries in the enumerable" do |entry|
assert Entry.find(:tag => ["foo", "bar"]).include?(entry)
assert Entry.find(:tag => ["bar", "baz"]).include?(entry)
assert Entry.find(:tag => ["baz", "oof"]).empty?
assert Entry.find(:tag => [["foo", "bar"]]).include?(entry)
assert Entry.find(:tag => [["bar", "baz"]]).include?(entry)
assert Entry.find(:tag => [["baz", "oof"]]).empty?
end

# Attributes of type Set
Expand Down