ohm 0.1.5 → 1.0.0.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +19 -1
- data/lib/ohm.rb +893 -1662
- data/lib/ohm/transaction.rb +129 -0
- data/test/association.rb +33 -0
- data/test/connection.rb +72 -0
- data/test/core.rb +26 -0
- data/test/counters.rb +67 -0
- data/test/extensibility.rb +48 -0
- data/test/filtering.rb +42 -0
- data/test/{hash_key_test.rb → hash_key.rb} +0 -0
- data/test/indices.rb +97 -0
- data/test/{json_test.rb → json.rb} +10 -3
- data/test/lua-save.rb +193 -0
- data/test/lua.rb +47 -0
- data/test/{model_test.rb → model.rb} +325 -439
- data/test/pipeline-performance.rb +65 -0
- data/test/transactions.rb +241 -0
- data/test/uniques.rb +87 -0
- data/test/{associations_test.rb → unused/associations_test.rb} +3 -3
- data/test/{pattern_test.rb → unused/pattern_test.rb} +0 -0
- data/test/{upgrade_script_test.rb → unused/upgrade_script_test.rb} +0 -0
- data/test/{wrapper_test.rb → unused/wrapper_test.rb} +0 -0
- data/test/{validations_test.rb → validations.rb} +20 -70
- metadata +44 -29
- data/lib/ohm/compat-1.8.6.rb +0 -39
- data/lib/ohm/key.rb +0 -35
- data/lib/ohm/pattern.rb +0 -37
- data/lib/ohm/validations.rb +0 -213
- data/lib/ohm/version.rb +0 -5
- data/test/connection_test.rb +0 -101
- data/test/errors_test.rb +0 -120
- data/test/indices_test.rb +0 -213
- data/test/mutex_test.rb +0 -84
@@ -0,0 +1,129 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
module Ohm
|
4
|
+
|
5
|
+
# Transactions in Ohm are designed to be composable and atomic. They use
|
6
|
+
# Redis WATCH/MULTI/EXEC to perform the comands sequentially but in a single
|
7
|
+
# step.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
#
|
11
|
+
# redis = Ohm.redis
|
12
|
+
#
|
13
|
+
# t1 = Ohm::Transaction.new do |t|
|
14
|
+
# s = nil
|
15
|
+
#
|
16
|
+
# t.watch("foo")
|
17
|
+
#
|
18
|
+
# t.read do
|
19
|
+
# s = redis.type("foo")
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# t.write do
|
23
|
+
# redis.set("foo", s)
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# t2 = Ohm::Transaction.new do |t|
|
28
|
+
# t.watch("foo")
|
29
|
+
#
|
30
|
+
# t.write do
|
31
|
+
# redis.set("foo", "bar")
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# # Compose transactions by passing them to Ohm::Transaction.new.
|
36
|
+
# t3 = Ohm::Transaction.new(t1, t2)
|
37
|
+
# t3.commit(redis)
|
38
|
+
#
|
39
|
+
# # Compose transactions by appending them.
|
40
|
+
# t1.append(t2)
|
41
|
+
# t1.commit(redis)
|
42
|
+
#
|
43
|
+
# @see http://redis.io/topic/transactions Transactions in Redis.
|
44
|
+
class Transaction
|
45
|
+
class Store < BasicObject
|
46
|
+
class EntryAlreadyExistsError < ::RuntimeError
|
47
|
+
end
|
48
|
+
|
49
|
+
def method_missing(writer, value)
|
50
|
+
super unless writer[-1] == "="
|
51
|
+
|
52
|
+
reader = writer[0..-2].to_sym
|
53
|
+
|
54
|
+
__metaclass__.send(:define_method, reader) do
|
55
|
+
value
|
56
|
+
end
|
57
|
+
|
58
|
+
__metaclass__.send(:define_method, writer) do |*_|
|
59
|
+
::Kernel.raise EntryAlreadyExistsError
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
def __metaclass__
|
65
|
+
class << self; self end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
attr :phase
|
70
|
+
|
71
|
+
def initialize
|
72
|
+
@phase = Hash.new { |h, k| h[k] = ::Set.new }
|
73
|
+
|
74
|
+
yield self if block_given?
|
75
|
+
end
|
76
|
+
|
77
|
+
def append(t)
|
78
|
+
t.phase.each do |key, values|
|
79
|
+
phase[key].merge(values)
|
80
|
+
end
|
81
|
+
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def watch(*keys)
|
86
|
+
phase[:watch] += keys
|
87
|
+
end
|
88
|
+
|
89
|
+
def read(&block)
|
90
|
+
phase[:read] << block
|
91
|
+
end
|
92
|
+
|
93
|
+
def write(&block)
|
94
|
+
phase[:write] << block
|
95
|
+
end
|
96
|
+
|
97
|
+
def before(&block)
|
98
|
+
phase[:before] << block
|
99
|
+
end
|
100
|
+
|
101
|
+
def after(&block)
|
102
|
+
phase[:after] << block
|
103
|
+
end
|
104
|
+
|
105
|
+
def commit(db)
|
106
|
+
phase[:before].each(&:call)
|
107
|
+
|
108
|
+
loop do
|
109
|
+
store = Store.new
|
110
|
+
|
111
|
+
if phase[:watch].any?
|
112
|
+
db.watch(*phase[:watch])
|
113
|
+
end
|
114
|
+
|
115
|
+
run(phase[:read], store)
|
116
|
+
|
117
|
+
break if db.multi do
|
118
|
+
run(phase[:write], store)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
phase[:after].each(&:call)
|
123
|
+
end
|
124
|
+
|
125
|
+
def run(procs, store)
|
126
|
+
procs.each { |p| p.call(store) }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/test/association.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.expand_path("./helper", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class User < Ohm::Model
|
4
|
+
collection :posts, :Post
|
5
|
+
end
|
6
|
+
|
7
|
+
class Post < Ohm::Model
|
8
|
+
reference :user, :User
|
9
|
+
end
|
10
|
+
|
11
|
+
setup do
|
12
|
+
u = User.create
|
13
|
+
p = Post.create(user: u)
|
14
|
+
|
15
|
+
[u, p]
|
16
|
+
end
|
17
|
+
|
18
|
+
test "basic shake and bake" do |u, p|
|
19
|
+
assert u.posts.include?(p)
|
20
|
+
|
21
|
+
p = Post[p.id]
|
22
|
+
assert_equal u, p.user
|
23
|
+
end
|
24
|
+
|
25
|
+
test "memoization" do |u, p|
|
26
|
+
# This will read the user instance once.
|
27
|
+
p.user
|
28
|
+
assert_equal p.user, p.instance_variable_get(:@_memo)[:user]
|
29
|
+
|
30
|
+
# This will un-memoize the user instance
|
31
|
+
p.user = u
|
32
|
+
assert_equal nil, p.instance_variable_get(:@_memo)[:user]
|
33
|
+
end
|
data/test/connection.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require File.expand_path("./helper", File.dirname(__FILE__))
|
4
|
+
|
5
|
+
prepare.clear
|
6
|
+
|
7
|
+
test "connects lazily" do
|
8
|
+
Ohm.connect(:port => 9876)
|
9
|
+
|
10
|
+
begin
|
11
|
+
Ohm.redis.get "foo"
|
12
|
+
rescue => e
|
13
|
+
assert_equal Redis::CannotConnectError, e.class
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
test "provides a separate connection for each thread" do
|
18
|
+
assert Ohm.redis == Ohm.redis
|
19
|
+
|
20
|
+
conn1, conn2 = nil
|
21
|
+
|
22
|
+
threads = []
|
23
|
+
|
24
|
+
threads << Thread.new do
|
25
|
+
conn1 = Ohm.redis
|
26
|
+
end
|
27
|
+
|
28
|
+
threads << Thread.new do
|
29
|
+
conn2 = Ohm.redis
|
30
|
+
end
|
31
|
+
|
32
|
+
threads.each { |t| t.join }
|
33
|
+
|
34
|
+
assert conn1 != conn2
|
35
|
+
end
|
36
|
+
|
37
|
+
test "supports connecting by URL" do
|
38
|
+
Ohm.connect(:url => "redis://localhost:9876")
|
39
|
+
|
40
|
+
begin
|
41
|
+
Ohm.redis.get "foo"
|
42
|
+
rescue => e
|
43
|
+
assert_equal Redis::CannotConnectError, e.class
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
setup do
|
48
|
+
Ohm.connect(:url => "redis://localhost:6379/0")
|
49
|
+
end
|
50
|
+
|
51
|
+
test "connection class" do
|
52
|
+
conn = Ohm::Connection.new(:foo, :url => "redis://localhost:6379/0")
|
53
|
+
|
54
|
+
assert conn.redis.kind_of?(Redis)
|
55
|
+
end
|
56
|
+
|
57
|
+
test "model can define its own connection" do
|
58
|
+
class B < Ohm::Model
|
59
|
+
connect(:url => "redis://localhost:6379/1")
|
60
|
+
end
|
61
|
+
|
62
|
+
assert_equal B.conn.options, {:url=>"redis://localhost:6379/1"}
|
63
|
+
assert_equal Ohm.conn.options, {:url=>"redis://localhost:6379/0"}
|
64
|
+
end
|
65
|
+
|
66
|
+
test "model inherits Ohm.redis connection by default" do
|
67
|
+
Ohm.connect(:url => "redis://localhost:9876")
|
68
|
+
class C < Ohm::Model
|
69
|
+
end
|
70
|
+
|
71
|
+
assert_equal C.conn.options, Ohm.conn.options
|
72
|
+
end
|
data/test/core.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require File.expand_path("./helper", File.dirname(__FILE__))
|
4
|
+
|
5
|
+
class Event < Ohm::Model
|
6
|
+
attribute :name
|
7
|
+
attribute :location
|
8
|
+
end
|
9
|
+
|
10
|
+
test "assign attributes from the hash" do
|
11
|
+
event = Event.new(:name => "Ruby Tuesday")
|
12
|
+
assert_equal event.name, "Ruby Tuesday"
|
13
|
+
end
|
14
|
+
|
15
|
+
test "assign an ID and save the object" do
|
16
|
+
event1 = Event.create(:name => "Ruby Tuesday")
|
17
|
+
event2 = Event.create(:name => "Ruby Meetup")
|
18
|
+
|
19
|
+
assert_equal "1", event1.id
|
20
|
+
assert_equal "2", event2.id
|
21
|
+
end
|
22
|
+
|
23
|
+
test "save the attributes in UTF8" do
|
24
|
+
event = Event.create(:name => "32° Kisei-sen")
|
25
|
+
assert "32° Kisei-sen" == Event[event.id].name
|
26
|
+
end
|
data/test/counters.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require File.expand_path("./helper", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
$VERBOSE = false
|
4
|
+
|
5
|
+
class Ad < Ohm::Model
|
6
|
+
end
|
7
|
+
|
8
|
+
test "counters aren't overwritten by competing saves" do
|
9
|
+
Ad.counter :hits
|
10
|
+
|
11
|
+
instance1 = Ad.create
|
12
|
+
instance1.incr :hits
|
13
|
+
|
14
|
+
instance2 = Ad[instance1.id]
|
15
|
+
|
16
|
+
instance1.incr :hits
|
17
|
+
instance1.incr :hits
|
18
|
+
|
19
|
+
instance2.save
|
20
|
+
|
21
|
+
instance1 = Ad[instance1.id]
|
22
|
+
assert_equal 3, instance1.hits
|
23
|
+
end
|
24
|
+
|
25
|
+
test "you can increment counters even when attributes is empty" do
|
26
|
+
Ad.counter :hits
|
27
|
+
|
28
|
+
ad = Ad.create
|
29
|
+
ad = Ad[ad.id]
|
30
|
+
|
31
|
+
ex = nil
|
32
|
+
|
33
|
+
begin
|
34
|
+
ad.incr :hits
|
35
|
+
rescue ArgumentError => e
|
36
|
+
ex = e
|
37
|
+
end
|
38
|
+
|
39
|
+
assert_equal nil, ex
|
40
|
+
end
|
41
|
+
|
42
|
+
test "an attribute gets saved properly" do
|
43
|
+
Ad.attribute :name
|
44
|
+
Ad.counter :hits
|
45
|
+
|
46
|
+
ad = Ad.create(name: "foo")
|
47
|
+
ad.incr :hits, 10
|
48
|
+
assert_equal 10, ad.hits
|
49
|
+
|
50
|
+
# Now let's just load and save it.
|
51
|
+
ad = Ad[ad.id]
|
52
|
+
ad.save
|
53
|
+
|
54
|
+
# The attributes should remain the same
|
55
|
+
ad = Ad[ad.id]
|
56
|
+
assert_equal "foo", ad.name
|
57
|
+
assert_equal 10, ad.hits
|
58
|
+
|
59
|
+
# If we load and save again while we incr behind the scenes,
|
60
|
+
# the latest counter values should be respected.
|
61
|
+
ad = Ad[ad.id]
|
62
|
+
ad.incr :hits, 5
|
63
|
+
ad.save
|
64
|
+
|
65
|
+
ad = Ad[ad.id]
|
66
|
+
assert_equal 15, ad.hits
|
67
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require File.expand_path("./helper", File.dirname(__FILE__))
|
4
|
+
|
5
|
+
if defined?(Ohm::Model::PureRuby)
|
6
|
+
class User < Ohm::Model
|
7
|
+
attribute :email
|
8
|
+
|
9
|
+
attr_accessor :foo
|
10
|
+
|
11
|
+
def save
|
12
|
+
super do |t|
|
13
|
+
t.before do
|
14
|
+
self.email = email.downcase
|
15
|
+
end
|
16
|
+
|
17
|
+
t.after do
|
18
|
+
if @foo
|
19
|
+
key[:foos].sadd(@foo)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete
|
26
|
+
super do |t|
|
27
|
+
foos = nil
|
28
|
+
|
29
|
+
t.before do
|
30
|
+
foos = key[:foos].smembers
|
31
|
+
end
|
32
|
+
|
33
|
+
t.after do
|
34
|
+
foos.each { |foo| key[:foos].srem(foo) }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
test do
|
41
|
+
u = User.create(email: "FOO@BAR.COM", foo: "bar")
|
42
|
+
assert_equal "foo@bar.com", u.email
|
43
|
+
assert_equal ["bar"], u.key[:foos].smembers
|
44
|
+
|
45
|
+
u.delete
|
46
|
+
assert_equal [], User.key[u.id][:foos].smembers
|
47
|
+
end
|
48
|
+
end
|
data/test/filtering.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.expand_path("./helper", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class User < Ohm::Model
|
4
|
+
attribute :fname
|
5
|
+
attribute :lname
|
6
|
+
attribute :status
|
7
|
+
index :fname
|
8
|
+
index :lname
|
9
|
+
index :status
|
10
|
+
end
|
11
|
+
|
12
|
+
setup do
|
13
|
+
u1 = User.create(fname: "John", lname: "Doe", status: "active")
|
14
|
+
u2 = User.create(fname: "Jane", lname: "Doe", status: "active")
|
15
|
+
|
16
|
+
[u1, u2]
|
17
|
+
end
|
18
|
+
|
19
|
+
test "findability" do |john, jane|
|
20
|
+
assert_equal 1, User.find(lname: "Doe", fname: "John").size
|
21
|
+
assert User.find(lname: "Doe", fname: "John").include?(john)
|
22
|
+
|
23
|
+
assert_equal 1, User.find(lname: "Doe", fname: "Jane").size
|
24
|
+
assert User.find(lname: "Doe", fname: "Jane").include?(jane)
|
25
|
+
end
|
26
|
+
|
27
|
+
test "#first" do |john, jane|
|
28
|
+
set = User.find(lname: "Doe", status: "active")
|
29
|
+
|
30
|
+
assert_equal jane, set.first(by: "fname", order: "ALPHA")
|
31
|
+
assert_equal john, set.first(by: "fname", order: "ALPHA DESC")
|
32
|
+
|
33
|
+
assert_equal "Jane", set.first(by: "fname", order: "ALPHA", get: "fname")
|
34
|
+
assert_equal "John", set.first(by: "fname", order: "ALPHA DESC", get: "fname")
|
35
|
+
end
|
36
|
+
|
37
|
+
test "#[]" do |john, jane|
|
38
|
+
set = User.find(lname: "Doe", status: "active")
|
39
|
+
|
40
|
+
assert_equal john, set[john.id]
|
41
|
+
assert_equal jane, set[jane.id]
|
42
|
+
end
|
File without changes
|
data/test/indices.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require File.expand_path("./helper", File.dirname(__FILE__))
|
4
|
+
|
5
|
+
class User < Ohm::Model
|
6
|
+
attribute :email
|
7
|
+
attribute :update
|
8
|
+
attribute :activation_code
|
9
|
+
attribute :sandunga
|
10
|
+
index :email
|
11
|
+
index :email_provider
|
12
|
+
index :working_days
|
13
|
+
index :update
|
14
|
+
index :activation_code
|
15
|
+
|
16
|
+
def working_days
|
17
|
+
@working_days ||= []
|
18
|
+
end
|
19
|
+
|
20
|
+
def email_provider
|
21
|
+
email.split("@").last
|
22
|
+
end
|
23
|
+
|
24
|
+
def before_save
|
25
|
+
self.activation_code ||= "user:#{id}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
setup do
|
30
|
+
@user1 = User.create(email: "foo", activation_code: "bar", update: "baz")
|
31
|
+
@user2 = User.create(email: "bar")
|
32
|
+
@user3 = User.create(email: "baz qux")
|
33
|
+
end
|
34
|
+
|
35
|
+
test "be able to find by the given attribute" do
|
36
|
+
assert @user1 == User.find(email: "foo").first
|
37
|
+
end
|
38
|
+
|
39
|
+
test "raise an error if the parameter supplied is not a hash" do
|
40
|
+
begin
|
41
|
+
User.find(1)
|
42
|
+
rescue => ex
|
43
|
+
ensure
|
44
|
+
assert ex.kind_of?(ArgumentError)
|
45
|
+
assert ex.message == "You need to supply a hash with filters. If you want to find by ID, use User[id] instead."
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
test "avoid intersections with the all collection" do
|
50
|
+
assert_equal "User:indices:email:foo", User.find(email: "foo").key
|
51
|
+
end
|
52
|
+
|
53
|
+
test "cleanup the temporary key after use" do
|
54
|
+
assert User.find(:email => "foo", :activation_code => "bar").to_a
|
55
|
+
|
56
|
+
assert Ohm.redis.keys("User:temp:*").empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
test "allow multiple chained finds" do
|
60
|
+
assert 1 == User.find(:email => "foo").find(:activation_code => "bar").find(:update => "baz").size
|
61
|
+
end
|
62
|
+
|
63
|
+
test "return nil if no results are found" do
|
64
|
+
assert User.find(:email => "foobar").empty?
|
65
|
+
assert nil == User.find(:email => "foobar").first
|
66
|
+
end
|
67
|
+
|
68
|
+
test "update indices when changing attribute values" do
|
69
|
+
@user1.email = "baz"
|
70
|
+
@user1.save
|
71
|
+
|
72
|
+
assert [] == User.find(:email => "foo").to_a
|
73
|
+
assert [@user1] == User.find(:email => "baz").to_a
|
74
|
+
end
|
75
|
+
|
76
|
+
test "remove from the index after deleting" do
|
77
|
+
@user2.delete
|
78
|
+
|
79
|
+
assert [] == User.find(:email => "bar").to_a
|
80
|
+
end
|
81
|
+
|
82
|
+
test "work with attributes that contain spaces" do
|
83
|
+
assert [@user3] == User.find(:email => "baz qux").to_a
|
84
|
+
end
|
85
|
+
|
86
|
+
# Indexing arbitrary attributes
|
87
|
+
setup do
|
88
|
+
@user1 = User.create(:email => "foo@gmail.com")
|
89
|
+
@user2 = User.create(:email => "bar@gmail.com")
|
90
|
+
@user3 = User.create(:email => "bazqux@yahoo.com")
|
91
|
+
end
|
92
|
+
|
93
|
+
test "allow indexing by an arbitrary attribute" do
|
94
|
+
gmail = User.find(:email_provider => "gmail.com").to_a
|
95
|
+
assert [@user1, @user2] == gmail.sort_by { |u| u.id }
|
96
|
+
assert [@user3] == User.find(:email_provider => "yahoo.com").to_a
|
97
|
+
end
|