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.
@@ -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