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.
@@ -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(Hash[*atts].update(:id => ids[idx]))
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
- filters = model.filters(dict).push(key)
421
-
422
- MultiSet.new(namespace, model).append(:sinterstore, filters)
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).append(:sinterstore, key).except(dict)
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).append(:sinterstore, key).union(dict)
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
- filters.push([:sinterstore, model.filters(dict)])
550
-
551
- return self
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
- filters.push([:sdiffstore, model.filters(dict)])
566
-
567
- return self
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
- filters.push([:sunionstore, model.filters(dict)])
582
-
583
- return self
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 filters
592
- @filters ||= []
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
- # Hold the final result key for this MultiSet.
613
- main = nil
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 main
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
- clean_temp_keys
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::Set#fetch.
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).append(:sinterstore, keys)
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 Errno::ECONNREFUSED, e.class
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 Errno::ECONNREFUSED, e.class
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.2
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-05-25 00:00:00.000000000 Z
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: '2.2'
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: '2.2'
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: '0'
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