ohm 1.0.2 → 1.1.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|