ohm 1.0.2 → 1.1.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/ohm/command.rb +47 -0
- data/lib/ohm.rb +36 -114
- data/test/command.rb +57 -0
- data/test/connection.rb +6 -2
- data/test/filtering.rb +144 -0
- metadata +11 -11
- data/test/lua-save.rb +0 -193
- data/test/lua.rb +0 -47
data/lib/ohm/command.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module Ohm
|
2
|
+
class Command
|
3
|
+
def self.[](operation, head, *tail)
|
4
|
+
return head if tail.empty?
|
5
|
+
|
6
|
+
new(operation, head, *tail)
|
7
|
+
end
|
8
|
+
|
9
|
+
attr :operation
|
10
|
+
attr :args
|
11
|
+
attr :keys
|
12
|
+
|
13
|
+
def initialize(operation, *args)
|
14
|
+
@operation = operation
|
15
|
+
@args = args
|
16
|
+
@keys = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(nest, db)
|
20
|
+
newkey(nest) do |key|
|
21
|
+
db.send(@operation, key, *params(nest, db))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def clean
|
26
|
+
keys.each { |key| key.del }
|
27
|
+
subcommands.each { |cmd| cmd.clean }
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def subcommands
|
32
|
+
args.select { |arg| arg.respond_to?(:call) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def params(nest, db)
|
36
|
+
args.map { |arg| arg.respond_to?(:call) ? arg.call(nest, db) : arg }
|
37
|
+
end
|
38
|
+
|
39
|
+
def newkey(nest)
|
40
|
+
key = nest[SecureRandom.hex(32)]
|
41
|
+
keys << key
|
42
|
+
|
43
|
+
yield key
|
44
|
+
return key
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/ohm.rb
CHANGED
@@ -5,6 +5,7 @@ require "redis"
|
|
5
5
|
require "securerandom"
|
6
6
|
require "scrivener"
|
7
7
|
require "ohm/transaction"
|
8
|
+
require "ohm/command"
|
8
9
|
|
9
10
|
module Ohm
|
10
11
|
|
@@ -66,6 +67,16 @@ module Ohm
|
|
66
67
|
else name
|
67
68
|
end
|
68
69
|
end
|
70
|
+
|
71
|
+
if Redis::VERSION >= "3.0.0"
|
72
|
+
def self.dict(dict)
|
73
|
+
dict
|
74
|
+
end
|
75
|
+
else
|
76
|
+
def self.dict(arr)
|
77
|
+
Hash[*arr]
|
78
|
+
end
|
79
|
+
end
|
69
80
|
end
|
70
81
|
|
71
82
|
class Connection
|
@@ -142,7 +153,7 @@ module Ohm
|
|
142
153
|
return res if arr.nil?
|
143
154
|
|
144
155
|
arr.each_with_index do |atts, idx|
|
145
|
-
res << model.new(
|
156
|
+
res << model.new(Utils.dict(atts).update(:id => ids[idx]))
|
146
157
|
end
|
147
158
|
|
148
159
|
res
|
@@ -417,9 +428,9 @@ module Ohm
|
|
417
428
|
# set.find(:age => 30)
|
418
429
|
#
|
419
430
|
def find(dict)
|
420
|
-
|
421
|
-
|
422
|
-
|
431
|
+
MultiSet.new(
|
432
|
+
namespace, model, Command[:sinterstore, key, *model.filters(dict)]
|
433
|
+
)
|
423
434
|
end
|
424
435
|
|
425
436
|
# Reduce the set using any number of filters.
|
@@ -433,7 +444,7 @@ module Ohm
|
|
433
444
|
# User.find(:name => "John").except(:country => "US")
|
434
445
|
#
|
435
446
|
def except(dict)
|
436
|
-
MultiSet.new(namespace, model
|
447
|
+
MultiSet.new(namespace, model, key).except(dict)
|
437
448
|
end
|
438
449
|
|
439
450
|
# Do a union to the existing set using any number of filters.
|
@@ -447,7 +458,7 @@ module Ohm
|
|
447
458
|
# User.find(:name => "John").union(:name => "Jane")
|
448
459
|
#
|
449
460
|
def union(dict)
|
450
|
-
MultiSet.new(namespace, model
|
461
|
+
MultiSet.new(namespace, model, key).union(dict)
|
451
462
|
end
|
452
463
|
|
453
464
|
private
|
@@ -529,15 +540,9 @@ module Ohm
|
|
529
540
|
# User.find(:name => "John", :age => 30).kind_of?(Ohm::MultiSet)
|
530
541
|
# # => true
|
531
542
|
#
|
532
|
-
class MultiSet < Struct.new(:namespace, :model)
|
543
|
+
class MultiSet < Struct.new(:namespace, :model, :command)
|
533
544
|
include Collection
|
534
545
|
|
535
|
-
def append(operation, list)
|
536
|
-
filters.push([operation, list])
|
537
|
-
|
538
|
-
return self
|
539
|
-
end
|
540
|
-
|
541
546
|
# Chain new fiters on an existing set.
|
542
547
|
#
|
543
548
|
# Example:
|
@@ -546,9 +551,9 @@ module Ohm
|
|
546
551
|
# set.find(:status => 'pending')
|
547
552
|
#
|
548
553
|
def find(dict)
|
549
|
-
|
550
|
-
|
551
|
-
|
554
|
+
MultiSet.new(
|
555
|
+
namespace, model, Command[:sinterstore, command, intersected(dict)]
|
556
|
+
)
|
552
557
|
end
|
553
558
|
|
554
559
|
# Reduce the set using any number of filters.
|
@@ -562,9 +567,9 @@ module Ohm
|
|
562
567
|
# User.find(:name => "John").except(:country => "US")
|
563
568
|
#
|
564
569
|
def except(dict)
|
565
|
-
|
566
|
-
|
567
|
-
|
570
|
+
MultiSet.new(
|
571
|
+
namespace, model, Command[:sdiffstore, command, intersected(dict)]
|
572
|
+
)
|
568
573
|
end
|
569
574
|
|
570
575
|
# Do a union to the existing set using any number of filters.
|
@@ -578,9 +583,9 @@ module Ohm
|
|
578
583
|
# User.find(:name => "John").union(:name => "Jane")
|
579
584
|
#
|
580
585
|
def union(dict)
|
581
|
-
|
582
|
-
|
583
|
-
|
586
|
+
MultiSet.new(
|
587
|
+
namespace, model, Command[:sunionstore, command, intersected(dict)]
|
588
|
+
)
|
584
589
|
end
|
585
590
|
|
586
591
|
private
|
@@ -588,70 +593,27 @@ module Ohm
|
|
588
593
|
model.db
|
589
594
|
end
|
590
595
|
|
591
|
-
def
|
592
|
-
|
593
|
-
end
|
594
|
-
|
595
|
-
def temp_keys
|
596
|
-
@temp_keys ||= []
|
597
|
-
end
|
598
|
-
|
599
|
-
def clean_temp_keys
|
600
|
-
db.del(*temp_keys)
|
601
|
-
temp_keys.clear
|
602
|
-
end
|
603
|
-
|
604
|
-
def generate_temp_key
|
605
|
-
key = namespace[:temp][SecureRandom.hex(32)]
|
606
|
-
temp_keys << key
|
607
|
-
key
|
596
|
+
def intersected(dict)
|
597
|
+
Command[:sinterstore, *model.filters(dict)]
|
608
598
|
end
|
609
599
|
|
610
600
|
def execute
|
611
|
-
|
612
|
-
#
|
613
|
-
|
614
|
-
|
615
|
-
filters.each do |operation, list|
|
616
|
-
|
617
|
-
# Operation can be sinterstore, sdiffstore, or sunionstore.
|
618
|
-
# each operation we do, i.e. `.union(...)`, will be considered
|
619
|
-
# one intersected set, hence we need to `sinterstore` all
|
620
|
-
# the filters in a temporary set.
|
621
|
-
temp = generate_temp_key
|
622
|
-
db.sinterstore(temp, *list)
|
623
|
-
|
624
|
-
# If this is the first set, we simply assign the generated
|
625
|
-
# set to main, which could possibly be the return value
|
626
|
-
# for simple filters like one `.find(...)`.
|
627
|
-
if main.nil?
|
628
|
-
main = temp
|
629
|
-
else
|
630
|
-
|
631
|
-
# Append the generated temporary set using the operation.
|
632
|
-
# i.e. if we have (mood=happy & book=1) and we have an
|
633
|
-
# `sunionstore`, we do (mood=happy & book=1) | (mood=sad & book=1)
|
634
|
-
#
|
635
|
-
# Here we dynamically call the stored command, e.g.
|
636
|
-
#
|
637
|
-
# SUNIONSTORE main main temp
|
638
|
-
#
|
639
|
-
db.send(operation, main, main, temp)
|
640
|
-
end
|
641
|
-
end
|
601
|
+
# namespace[:tmp] is where all the temp keys should be stored in.
|
602
|
+
# db will be where all the commands are executed against.
|
603
|
+
res = command.call(namespace[:tmp], db)
|
642
604
|
|
643
605
|
begin
|
644
606
|
|
645
607
|
# At this point, we have the final aggregated set, which we yield
|
646
608
|
# to the caller. the caller can do all the normal set operations,
|
647
609
|
# i.e. SCARD, SMEMBERS, etc.
|
648
|
-
yield
|
610
|
+
yield res
|
649
611
|
|
650
612
|
ensure
|
651
613
|
|
652
614
|
# We have to make sure we clean up the temporary keys to avoid
|
653
615
|
# memory leaks and the unintended explosion of memory usage.
|
654
|
-
|
616
|
+
command.clean
|
655
617
|
end
|
656
618
|
end
|
657
619
|
end
|
@@ -768,7 +730,7 @@ module Ohm
|
|
768
730
|
# Note: The use of this should be a last resort for your actual
|
769
731
|
# application runtime, or for simply debugging in your console. If
|
770
732
|
# you care about performance, you should pipeline your reads. For
|
771
|
-
# more information checkout the implementation of Ohm::
|
733
|
+
# more information checkout the implementation of Ohm::List#fetch.
|
772
734
|
#
|
773
735
|
def self.to_proc
|
774
736
|
lambda { |id| self[id] }
|
@@ -840,7 +802,7 @@ module Ohm
|
|
840
802
|
if keys.size == 1
|
841
803
|
Ohm::Set.new(keys.first, key, self)
|
842
804
|
else
|
843
|
-
Ohm::MultiSet.new(key, self
|
805
|
+
Ohm::MultiSet.new(key, self, Command.new(:sinterstore, *keys))
|
844
806
|
end
|
845
807
|
end
|
846
808
|
|
@@ -1522,44 +1484,4 @@ module Ohm
|
|
1522
1484
|
end
|
1523
1485
|
end
|
1524
1486
|
end
|
1525
|
-
|
1526
|
-
class Lua
|
1527
|
-
attr :dir
|
1528
|
-
attr :redis
|
1529
|
-
attr :files
|
1530
|
-
attr :scripts
|
1531
|
-
|
1532
|
-
def initialize(dir, redis)
|
1533
|
-
@dir = dir
|
1534
|
-
@redis = redis
|
1535
|
-
@files = Hash.new { |h, cmd| h[cmd] = read(cmd) }
|
1536
|
-
@scripts = {}
|
1537
|
-
end
|
1538
|
-
|
1539
|
-
def run_file(file, options)
|
1540
|
-
run(files[file], options)
|
1541
|
-
end
|
1542
|
-
|
1543
|
-
def run(script, options)
|
1544
|
-
keys = options[:keys]
|
1545
|
-
argv = options[:argv]
|
1546
|
-
|
1547
|
-
params = keys + argv
|
1548
|
-
|
1549
|
-
begin
|
1550
|
-
redis.evalsha(sha(script), keys.size, *params)
|
1551
|
-
rescue RuntimeError
|
1552
|
-
redis.eval(script, keys.size, *params)
|
1553
|
-
end
|
1554
|
-
end
|
1555
|
-
|
1556
|
-
private
|
1557
|
-
def read(file)
|
1558
|
-
File.read("%s/%s.lua" % [dir, file])
|
1559
|
-
end
|
1560
|
-
|
1561
|
-
def sha(script)
|
1562
|
-
Digest::SHA1.hexdigest(script)
|
1563
|
-
end
|
1564
|
-
end
|
1565
1487
|
end
|
data/test/command.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require_relative "helper"
|
2
|
+
|
3
|
+
scope do
|
4
|
+
setup do
|
5
|
+
redis = Redis.connect
|
6
|
+
redis.flushdb
|
7
|
+
|
8
|
+
# require 'logger'
|
9
|
+
# redis.client.logger = Logger.new(STDOUT)
|
10
|
+
nest = Nest.new("User:tmp", redis)
|
11
|
+
|
12
|
+
[1, 2, 3].each { |i| redis.sadd("A", i) }
|
13
|
+
[1, 4, 5].each { |i| redis.sadd("B", i) }
|
14
|
+
|
15
|
+
[10, 11, 12].each { |i| redis.sadd("C", i) }
|
16
|
+
[11, 12, 13].each { |i| redis.sadd("D", i) }
|
17
|
+
[12, 13, 14].each { |i| redis.sadd("E", i) }
|
18
|
+
|
19
|
+
[10, 11, 12].each { |i| redis.sadd("F", i) }
|
20
|
+
[11, 12, 13].each { |i| redis.sadd("G", i) }
|
21
|
+
[12, 13, 14].each { |i| redis.sadd("H", i) }
|
22
|
+
|
23
|
+
[redis, nest]
|
24
|
+
end
|
25
|
+
|
26
|
+
test "special condition: single argument returns that arg" do
|
27
|
+
assert_equal "A", Ohm::Command[:sinterstore, "A"]
|
28
|
+
end
|
29
|
+
|
30
|
+
test "full stack test" do |redis, nest|
|
31
|
+
cmd1 = Ohm::Command[:sinterstore, "A", "B"]
|
32
|
+
|
33
|
+
res = cmd1.call(nest, redis)
|
34
|
+
assert_equal ["1"], res.smembers
|
35
|
+
|
36
|
+
cmd1.clean
|
37
|
+
assert ! res.exists
|
38
|
+
|
39
|
+
cmd2 = Ohm::Command[:sinterstore, "C", "D", "E"]
|
40
|
+
cmd3 = Ohm::Command[:sunionstore, cmd1, cmd2]
|
41
|
+
|
42
|
+
res = cmd3.call(nest, redis)
|
43
|
+
assert_equal ["1", "12"], res.smembers
|
44
|
+
|
45
|
+
cmd3.clean
|
46
|
+
assert redis.keys(nest["*"]).empty?
|
47
|
+
|
48
|
+
cmd4 = Ohm::Command[:sinterstore, "F", "G", "H"]
|
49
|
+
cmd5 = Ohm::Command[:sdiffstore, cmd3, cmd4]
|
50
|
+
|
51
|
+
res = cmd5.call(nest, redis)
|
52
|
+
assert_equal ["1"], res.smembers
|
53
|
+
|
54
|
+
cmd5.clean
|
55
|
+
assert redis.keys(nest["*"]).empty?
|
56
|
+
end
|
57
|
+
end
|
data/test/connection.rb
CHANGED
@@ -2,6 +2,10 @@
|
|
2
2
|
|
3
3
|
require File.expand_path("./helper", File.dirname(__FILE__))
|
4
4
|
|
5
|
+
unless defined?(Redis::CannotConnectError)
|
6
|
+
Redis::CannotConnectError = Errno::ECONNREFUSED
|
7
|
+
end
|
8
|
+
|
5
9
|
prepare.clear
|
6
10
|
|
7
11
|
test "no rewriting of settings hash when using Ohm.connect" do
|
@@ -24,7 +28,7 @@ test "connects lazily" do
|
|
24
28
|
begin
|
25
29
|
Ohm.redis.get "foo"
|
26
30
|
rescue => e
|
27
|
-
assert_equal
|
31
|
+
assert_equal Redis::CannotConnectError, e.class
|
28
32
|
end
|
29
33
|
end
|
30
34
|
|
@@ -54,7 +58,7 @@ test "supports connecting by URL" do
|
|
54
58
|
begin
|
55
59
|
Ohm.redis.get "foo"
|
56
60
|
rescue => e
|
57
|
-
assert_equal
|
61
|
+
assert_equal Redis::CannotConnectError, e.class
|
58
62
|
end
|
59
63
|
end
|
60
64
|
|
data/test/filtering.rb
CHANGED
@@ -59,6 +59,11 @@ test "#except" do |john, jane|
|
|
59
59
|
assert_equal 2, res.size
|
60
60
|
assert res.include?(john)
|
61
61
|
assert res.include?(jane)
|
62
|
+
|
63
|
+
res = User.all.except(:status => "inactive")
|
64
|
+
|
65
|
+
assert_equal 2, res.size
|
66
|
+
assert res.include?(jane)
|
62
67
|
end
|
63
68
|
|
64
69
|
test "indices bug related to a nil attribute" do |john, jane|
|
@@ -154,3 +159,142 @@ scope do
|
|
154
159
|
assert_equal 2, res.size
|
155
160
|
end
|
156
161
|
end
|
162
|
+
|
163
|
+
# test precision of filtering commands
|
164
|
+
require "logger"
|
165
|
+
require "stringio"
|
166
|
+
scope do
|
167
|
+
class Post < Ohm::Model
|
168
|
+
attribute :author
|
169
|
+
index :author
|
170
|
+
|
171
|
+
attribute :mood
|
172
|
+
index :mood
|
173
|
+
end
|
174
|
+
|
175
|
+
setup do
|
176
|
+
io = StringIO.new
|
177
|
+
|
178
|
+
Post.connect(:logger => Logger.new(io))
|
179
|
+
|
180
|
+
Post.create(author: "matz", mood: "happy")
|
181
|
+
Post.create(author: "rich", mood: "mad")
|
182
|
+
|
183
|
+
io
|
184
|
+
end
|
185
|
+
|
186
|
+
def read(io)
|
187
|
+
io.rewind
|
188
|
+
io.read
|
189
|
+
end
|
190
|
+
|
191
|
+
test "SINTERSTORE a b" do |io|
|
192
|
+
Post.find(author: "matz").find(mood: "happy").to_a
|
193
|
+
|
194
|
+
# This is the simple case. We should only do one SINTERSTORE
|
195
|
+
# given two direct keys. Anything more and we're performing badly.
|
196
|
+
expected = "SINTERSTORE Post:tmp:[a-f0-9]{64} " +
|
197
|
+
"Post:indices:author:matz Post:indices:mood:happy"
|
198
|
+
|
199
|
+
assert(read(io) =~ Regexp.new(expected))
|
200
|
+
end
|
201
|
+
|
202
|
+
test "SUNIONSTORE a b" do |io|
|
203
|
+
Post.find(author: "matz").union(mood: "happy").to_a
|
204
|
+
|
205
|
+
# Another simple case where we must only do one operation at maximum.
|
206
|
+
expected = "SUNIONSTORE Post:tmp:[a-f0-9]{64} " +
|
207
|
+
"Post:indices:author:matz Post:indices:mood:happy"
|
208
|
+
|
209
|
+
assert(read(io) =~ Regexp.new(expected))
|
210
|
+
end
|
211
|
+
|
212
|
+
test "SUNIONSTORE c (SINTERSTORE a b)" do |io|
|
213
|
+
Post.find(author: "matz").find(mood: "happy").union(author: "rich").to_a
|
214
|
+
|
215
|
+
# For this case we need an intermediate key. This will
|
216
|
+
# contain the intersection of matz + happy.
|
217
|
+
expected = "SINTERSTORE (Post:tmp:[a-f0-9]{64}) " +
|
218
|
+
"Post:indices:author:matz Post:indices:mood:happy"
|
219
|
+
|
220
|
+
assert(read(io) =~ Regexp.new(expected))
|
221
|
+
|
222
|
+
# The next operation is simply doing a UNION of the previously
|
223
|
+
# generated intermediate key and the additional single key.
|
224
|
+
expected = "SUNIONSTORE (Post:tmp:[a-f0-9]{64}) " +
|
225
|
+
"%s Post:indices:author:rich" % $1
|
226
|
+
|
227
|
+
assert(read(io) =~ Regexp.new(expected))
|
228
|
+
end
|
229
|
+
|
230
|
+
test "SUNIONSTORE (SINTERSTORE c d) (SINTERSTORE a b)" do |io|
|
231
|
+
Post.find(author: "matz").find(mood: "happy").
|
232
|
+
union(author: "rich", mood: "sad").to_a
|
233
|
+
|
234
|
+
# Similar to the previous case, we need to do an intermediate
|
235
|
+
# operation.
|
236
|
+
expected = "SINTERSTORE (Post:tmp:[a-f0-9]{64}) " +
|
237
|
+
"Post:indices:author:matz Post:indices:mood:happy"
|
238
|
+
|
239
|
+
match1 = read(io).match(Regexp.new(expected))
|
240
|
+
assert match1
|
241
|
+
|
242
|
+
# But now, we need to also hold another intermediate key for the
|
243
|
+
# condition of author: rich AND mood: sad.
|
244
|
+
expected = "SINTERSTORE (Post:tmp:[a-f0-9]{64}) " +
|
245
|
+
"Post:indices:author:rich Post:indices:mood:sad"
|
246
|
+
|
247
|
+
match2 = read(io).match(Regexp.new(expected))
|
248
|
+
assert match2
|
249
|
+
|
250
|
+
# Now we expect that it does a UNION of those two previous
|
251
|
+
# intermediate keys.
|
252
|
+
expected = sprintf(
|
253
|
+
"SUNIONSTORE (Post:tmp:[a-f0-9]{64}) %s %s",
|
254
|
+
match1[1], match2[1]
|
255
|
+
)
|
256
|
+
|
257
|
+
assert(read(io) =~ Regexp.new(expected))
|
258
|
+
end
|
259
|
+
|
260
|
+
test do |io|
|
261
|
+
Post.create(author: "kent", mood: "sad")
|
262
|
+
|
263
|
+
Post.find(author: "kent", mood: "sad").
|
264
|
+
union(author: "matz", mood: "happy").
|
265
|
+
except(mood: "sad", author: "rich").to_a
|
266
|
+
|
267
|
+
expected = "SINTERSTORE (Post:tmp:[a-f0-9]{64}) " +
|
268
|
+
"Post:indices:author:kent Post:indices:mood:sad"
|
269
|
+
|
270
|
+
match1 = read(io).match(Regexp.new(expected))
|
271
|
+
assert match1
|
272
|
+
|
273
|
+
expected = "SINTERSTORE (Post:tmp:[a-f0-9]{64}) " +
|
274
|
+
"Post:indices:author:matz Post:indices:mood:happy"
|
275
|
+
|
276
|
+
match2 = read(io).match(Regexp.new(expected))
|
277
|
+
assert match2
|
278
|
+
|
279
|
+
expected = sprintf(
|
280
|
+
"SUNIONSTORE (Post:tmp:[a-f0-9]{64}) %s %s",
|
281
|
+
match1[1], match2[1]
|
282
|
+
)
|
283
|
+
|
284
|
+
match3 = read(io).match(Regexp.new(expected))
|
285
|
+
assert match3
|
286
|
+
|
287
|
+
expected = "SINTERSTORE (Post:tmp:[a-f0-9]{64}) " +
|
288
|
+
"Post:indices:mood:sad Post:indices:author:rich"
|
289
|
+
|
290
|
+
match4 = read(io).match(Regexp.new(expected))
|
291
|
+
assert match4
|
292
|
+
|
293
|
+
expected = sprintf(
|
294
|
+
"SDIFFSTORE (Post:tmp:[a-f0-9]{64}) %s %s",
|
295
|
+
match3[1], match4[1]
|
296
|
+
)
|
297
|
+
|
298
|
+
assert(read(io) =~ Regexp.new(expected))
|
299
|
+
end
|
300
|
+
end
|
metadata
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ohm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
5
|
-
prerelease:
|
4
|
+
version: 1.1.0.rc1
|
5
|
+
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Michel Martens
|
@@ -11,24 +11,24 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2012-
|
14
|
+
date: 2012-07-11 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: redis
|
18
18
|
requirement: !ruby/object:Gem::Requirement
|
19
19
|
none: false
|
20
20
|
requirements:
|
21
|
-
- -
|
21
|
+
- - ! '>='
|
22
22
|
- !ruby/object:Gem::Version
|
23
|
-
version: '
|
23
|
+
version: '0'
|
24
24
|
type: :runtime
|
25
25
|
prerelease: false
|
26
26
|
version_requirements: !ruby/object:Gem::Requirement
|
27
27
|
none: false
|
28
28
|
requirements:
|
29
|
-
- -
|
29
|
+
- - ! '>='
|
30
30
|
- !ruby/object:Gem::Version
|
31
|
-
version: '
|
31
|
+
version: '0'
|
32
32
|
- !ruby/object:Gem::Dependency
|
33
33
|
name: nest
|
34
34
|
requirement: !ruby/object:Gem::Requirement
|
@@ -88,6 +88,7 @@ executables: []
|
|
88
88
|
extensions: []
|
89
89
|
extra_rdoc_files: []
|
90
90
|
files:
|
91
|
+
- lib/ohm/command.rb
|
91
92
|
- lib/ohm/json.rb
|
92
93
|
- lib/ohm/transaction.rb
|
93
94
|
- lib/ohm.rb
|
@@ -96,6 +97,7 @@ files:
|
|
96
97
|
- Rakefile
|
97
98
|
- test/1.8.6_test.rb
|
98
99
|
- test/association.rb
|
100
|
+
- test/command.rb
|
99
101
|
- test/connection.rb
|
100
102
|
- test/core.rb
|
101
103
|
- test/counters.rb
|
@@ -107,8 +109,6 @@ files:
|
|
107
109
|
- test/issue-52.rb
|
108
110
|
- test/json.rb
|
109
111
|
- test/list.rb
|
110
|
-
- test/lua-save.rb
|
111
|
-
- test/lua.rb
|
112
112
|
- test/model.rb
|
113
113
|
- test/pipeline-performance.rb
|
114
114
|
- test/transactions.rb
|
@@ -130,9 +130,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
130
130
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
131
131
|
none: false
|
132
132
|
requirements:
|
133
|
-
- - ! '
|
133
|
+
- - ! '>'
|
134
134
|
- !ruby/object:Gem::Version
|
135
|
-
version:
|
135
|
+
version: 1.3.1
|
136
136
|
requirements: []
|
137
137
|
rubyforge_project: ohm
|
138
138
|
rubygems_version: 1.8.23
|
data/test/lua-save.rb
DELETED
@@ -1,193 +0,0 @@
|
|
1
|
-
require File.expand_path("./helper", File.dirname(__FILE__))
|
2
|
-
|
3
|
-
def redis
|
4
|
-
Ohm.redis
|
5
|
-
end
|
6
|
-
|
7
|
-
begin
|
8
|
-
Ohm.redis.script("flush")
|
9
|
-
rescue RuntimeError
|
10
|
-
# We're running on Redis < 2.6, so we
|
11
|
-
# skip all the test.
|
12
|
-
else
|
13
|
-
setup do
|
14
|
-
Ohm.redis.script("flush")
|
15
|
-
|
16
|
-
redis.sadd("User:uniques", "email")
|
17
|
-
redis.sadd("User:indices", "fname")
|
18
|
-
redis.sadd("User:indices", "lname")
|
19
|
-
redis.hset("User:uniques:email", "foo@bar.com", 1)
|
20
|
-
|
21
|
-
Ohm::Lua.new("./test/lua", redis)
|
22
|
-
end
|
23
|
-
|
24
|
-
test "empty email doesn't choke" do |lua|
|
25
|
-
res = lua.run_file("save",
|
26
|
-
:keys => ["User"],
|
27
|
-
:argv => ["email", nil])
|
28
|
-
|
29
|
-
assert_equal [200, ["id", "1"]], res
|
30
|
-
assert_equal "1", redis.hget("User:uniques:email", nil)
|
31
|
-
end
|
32
|
-
|
33
|
-
test "empty fname / lname doesn't choke" do |lua|
|
34
|
-
res = lua.run_file("save",
|
35
|
-
:keys => ["User"],
|
36
|
-
:argv => ["email", nil, "fname", nil, "lname", nil])
|
37
|
-
|
38
|
-
assert_equal [200, ["id", "1"]], res
|
39
|
-
assert redis.sismember("User:indices:fname:", 1)
|
40
|
-
assert redis.sismember("User:indices:lname:", 1)
|
41
|
-
end
|
42
|
-
|
43
|
-
test "returns the unique constraint error" do |lua|
|
44
|
-
res = lua.run_file("save",
|
45
|
-
:keys => ["User"],
|
46
|
-
:argv => ["email", "foo@bar.com"])
|
47
|
-
|
48
|
-
assert_equal [500, ["email", "not_unique"]], res
|
49
|
-
end
|
50
|
-
|
51
|
-
test "persists the unique entry properly" do |lua|
|
52
|
-
lua.run_file("save",
|
53
|
-
:keys => ["User"],
|
54
|
-
:argv => ["email", "bar@baz.com"])
|
55
|
-
|
56
|
-
assert_equal "1", redis.hget("User:uniques:email", "bar@baz.com")
|
57
|
-
end
|
58
|
-
|
59
|
-
test "adds the entry to User:all" do |lua|
|
60
|
-
lua.run_file("save",
|
61
|
-
:keys => ["User"],
|
62
|
-
:argv => ["email", "bar@baz.com"])
|
63
|
-
|
64
|
-
assert_equal 1, redis.scard("User:all")
|
65
|
-
end
|
66
|
-
|
67
|
-
|
68
|
-
test "saves the attributes" do |lua|
|
69
|
-
lua.run_file("save",
|
70
|
-
:keys => ["User"],
|
71
|
-
:argv => ["email", "bar@baz.com", "fname", "John", "lname", "Doe"])
|
72
|
-
|
73
|
-
assert_equal "bar@baz.com", redis.hget("User:1", "email")
|
74
|
-
assert_equal "John", redis.hget("User:1", "fname")
|
75
|
-
assert_equal "Doe", redis.hget("User:1", "lname")
|
76
|
-
end
|
77
|
-
|
78
|
-
test "indexes fname / lname" do |lua|
|
79
|
-
lua.run_file("save",
|
80
|
-
:keys => ["User"],
|
81
|
-
:argv => ["email", "bar@baz.com", "fname", "John", "lname", "Doe"])
|
82
|
-
|
83
|
-
assert redis.sismember("User:indices:fname:John", 1)
|
84
|
-
assert redis.sismember("User:indices:lname:Doe", 1)
|
85
|
-
end
|
86
|
-
|
87
|
-
test "unique constraint during update" do |lua|
|
88
|
-
lua.run_file("save",
|
89
|
-
:keys => ["User"],
|
90
|
-
:argv => ["email", "bar@baz.com", "fname", "John", "lname", "Doe"])
|
91
|
-
|
92
|
-
res = lua.run_file("save",
|
93
|
-
:keys => ["User", "User:1"],
|
94
|
-
:argv => ["email", "bar@baz.com", "fname", "John", "lname", "Doe"])
|
95
|
-
|
96
|
-
assert_equal [200, ["id", "1"]], res
|
97
|
-
|
98
|
-
res = lua.run_file("save",
|
99
|
-
:keys => ["User", "User:1"],
|
100
|
-
:argv => ["email", "foo@bar.com", "fname", "Jane", "lname", "Doe"])
|
101
|
-
|
102
|
-
assert_equal [200, ["id", "1"]], res
|
103
|
-
end
|
104
|
-
|
105
|
-
test "cleanup of existing indices during update" do |lua|
|
106
|
-
lua.run_file("save",
|
107
|
-
:keys => ["User"],
|
108
|
-
:argv => ["email", "bar@baz.com", "fname", "John", "lname", "Doe"])
|
109
|
-
|
110
|
-
res = lua.run_file("save",
|
111
|
-
:keys => ["User", "User:1"],
|
112
|
-
:argv => ["email", "foo@bar.com", "fname", "Jane", "lname", "Smith"])
|
113
|
-
|
114
|
-
assert ! redis.sismember("User:indices:fname:John", 1)
|
115
|
-
assert ! redis.sismember("User:indices:fname:Doe", 1)
|
116
|
-
end
|
117
|
-
|
118
|
-
test "cleanup of existing uniques during update" do |lua|
|
119
|
-
lua.run_file("save",
|
120
|
-
:keys => ["User"],
|
121
|
-
:argv => ["email", "bar@baz.com", "fname", "John", "lname", "Doe"])
|
122
|
-
|
123
|
-
res = lua.run_file("save",
|
124
|
-
:keys => ["User", "User:1"],
|
125
|
-
:argv => ["email", "foo@bar.com", "fname", "Jane", "lname", "Smith"])
|
126
|
-
|
127
|
-
assert_equal nil, redis.hget("User:uniques:email", "bar@baz.com")
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
__END__
|
132
|
-
$VERBOSE = false
|
133
|
-
|
134
|
-
test "stress test for lua scripting" do |lua|
|
135
|
-
require "benchmark"
|
136
|
-
|
137
|
-
class User < Ohm::Model
|
138
|
-
attribute :email
|
139
|
-
attribute :fname
|
140
|
-
attribute :lname
|
141
|
-
|
142
|
-
index :email
|
143
|
-
index :fname
|
144
|
-
index :lname
|
145
|
-
end
|
146
|
-
|
147
|
-
t = Benchmark.measure do
|
148
|
-
threads = 100.times.map do |i|
|
149
|
-
Thread.new do
|
150
|
-
User.create(:email => "foo#{i}@bar.com",
|
151
|
-
:fname => "Jane#{i}",
|
152
|
-
:lname => "Smith#{i}")
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
threads.each(&:join)
|
157
|
-
end
|
158
|
-
|
159
|
-
puts t
|
160
|
-
end
|
161
|
-
|
162
|
-
test "stress test for postgres + sequel (as a comparison)" do
|
163
|
-
require "sequel"
|
164
|
-
|
165
|
-
DB = Sequel.connect("postgres://cyx@localhost/postgres")
|
166
|
-
DB[:users].truncate
|
167
|
-
t = Benchmark.measure do
|
168
|
-
threads = 100.times.map do |i|
|
169
|
-
Thread.new do
|
170
|
-
DB[:users].insert(:email => "foo#{i}@bar.com",
|
171
|
-
:fname => "John#{i}",
|
172
|
-
:lname => "Doe#{i}")
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
threads.each(&:join)
|
177
|
-
end
|
178
|
-
|
179
|
-
puts t
|
180
|
-
end
|
181
|
-
|
182
|
-
## Result for 100 threads:
|
183
|
-
# 0.040000 0.010000 0.050000 ( 0.061512) - lua script
|
184
|
-
# 0.150000 0.180000 0.330000 ( 0.259676) - postgres
|
185
|
-
#
|
186
|
-
## Result for 100 linear executions:
|
187
|
-
#
|
188
|
-
# 0.010000 0.010000 0.020000 ( 0.032064) - lua script
|
189
|
-
# 0.010000 0.010000 0.020000 ( 0.059540) - postgres
|
190
|
-
#
|
191
|
-
## It's also important to note that with 1K concurrent threads,
|
192
|
-
# postgres throws a Sequel::PoolTimeout
|
193
|
-
end
|
data/test/lua.rb
DELETED
@@ -1,47 +0,0 @@
|
|
1
|
-
# encoding: UTF-8
|
2
|
-
|
3
|
-
require File.expand_path("./helper", File.dirname(__FILE__))
|
4
|
-
|
5
|
-
begin
|
6
|
-
Ohm.redis.script("flush")
|
7
|
-
rescue RuntimeError
|
8
|
-
# We're running on Redis < 2.6, so we
|
9
|
-
# skip all the test.
|
10
|
-
else
|
11
|
-
setup do
|
12
|
-
Ohm::Lua.new("./test/lua", Ohm.redis)
|
13
|
-
end
|
14
|
-
|
15
|
-
test do |lua|
|
16
|
-
lua.redis.set("foo", "baz")
|
17
|
-
|
18
|
-
res = lua.run_file("getset", :keys => ["foo"], :argv => ["bar"])
|
19
|
-
assert_equal ["baz", "bar"], res
|
20
|
-
end
|
21
|
-
|
22
|
-
test do |lua|
|
23
|
-
res = lua.run_file("ohm-save",
|
24
|
-
:keys => ["User"],
|
25
|
-
:argv => ["fname", "John", "lname", "Doe"])
|
26
|
-
|
27
|
-
assert lua.redis.sismember("User:all", 1)
|
28
|
-
assert_equal({ "fname" => "John", "lname" => "Doe" },
|
29
|
-
lua.redis.hgetall("User:1"))
|
30
|
-
end
|
31
|
-
|
32
|
-
test do |lua|
|
33
|
-
lua.redis.sadd("User:indices", "fname")
|
34
|
-
lua.redis.sadd("User:indices", "lname")
|
35
|
-
|
36
|
-
res = lua.run_file("save-with-indices",
|
37
|
-
:keys => ["User:1", "User:all", "User:indices"],
|
38
|
-
:argv => ["fname", "John", "lname", "Doe"])
|
39
|
-
|
40
|
-
assert lua.redis.sismember("User:all", 1)
|
41
|
-
|
42
|
-
assert lua.redis.sismember("User:fname:John", 1)
|
43
|
-
assert lua.redis.sismember("User:lname:Doe", 1)
|
44
|
-
assert lua.redis.sismember("User:1:_indices", "User:fname:John")
|
45
|
-
assert lua.redis.sismember("User:1:_indices", "User:lname:Doe")
|
46
|
-
end
|
47
|
-
end
|