stal 0.0.1 → 0.1.0

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.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -20
  3. data/lib/stal.rb +54 -38
  4. data/stal.gemspec +1 -1
  5. data/test/all.rb +24 -13
  6. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 01f32692fdb539b8eebbc7d2964ba275aecef27b
4
- data.tar.gz: a971d29478038e640c0e94fd2636e0a57edd807f
3
+ metadata.gz: e0c73d92dba5eab545ca813c4e47885b77c78e34
4
+ data.tar.gz: daf465500d116a4ead3f79d5532aabecda8a67d3
5
5
  SHA512:
6
- metadata.gz: dc8a947141e7a9d84d8c78ba58097d5cdb002b2493fdbd5a9752cb98306cd661c4f49e6c2b353c4371bb28ff45f30368b255f200d56a467bfe265c69fe620bfd
7
- data.tar.gz: 47e6bb79573cde80bce0ed2554ef135de38f52c45ce5303761886d41f2b14b4bb4994632d45007d342685f21c530241d111dc8abe7a2984ef2beebc4361b082d
6
+ metadata.gz: 6a656ad854d7bb0027d45988772d3b81dfe49775344301424f4dc876977027d6ec1a207835e9570f8cf15434abf66e402e75e646ec5fdf9ab2ebdce23443911d
7
+ data.tar.gz: 30851cd6f82f3072f2526359b73608a90dc07d1459944e681591453d02101f37c420feac75879b2dfcdb327bc00c799741a7147c316ed0f3048937953f4042e7
data/README.md CHANGED
@@ -49,44 +49,39 @@ redis.call("SADD", "qux", "x", "y", "z")
49
49
  Now we can perform some set operations with `Stal`:
50
50
 
51
51
  ```ruby
52
- expr = [:union, "qux", [:diff, [:inter, "foo", "bar"], "baz"]]
52
+ expr = [:SUNION, "qux", [:SDIFF, [:SINTER, "foo", "bar"], "baz"]]
53
53
 
54
54
  Stal.solve(redis, expr)
55
55
  #=> ["b", "x", "y", "z"]
56
56
  ```
57
57
 
58
- `Stal` translates the shortcuts `:union`, `:diff` and `:inter` into
59
- `SDIFFSTORE`, `SINTERSTORE` and `SUNIONSTORE` to perform the
60
- underlying operations. You can also use the explicit command
61
- (lowercase works too).
58
+ `Stal` translates the internal calls to `:SUNION`, `:SDIFF` and
59
+ `:SINTER` into `SDIFFSTORE`, `SINTERSTORE` and `SUNIONSTORE` to
60
+ perform the underlying operations, and it takes care of generating
61
+ and deleting any temporary keys.
62
+
63
+ Note that the only valid names for the internal commands are
64
+ `:SUNION`, `:SDIFF` and `:SINTER`. Any other internal command will
65
+ raise an error. The outmost command can be any set operation, for
66
+ example:
62
67
 
63
68
  ```ruby
64
- expr = [:SUNIONSTORE, "qux", [:SDIFFSTORE, [:SINTERSTORE, "foo", "bar"], "baz"]]
69
+ expr = [:SCARD, [:SINTER, "foo", "bar"]]
65
70
 
66
71
  Stal.solve(redis, expr)
67
- #=> ["b", "x", "y", "z"]
72
+ #=> 2
68
73
  ```
69
74
 
70
75
  If you want to preview the commands `Stal` will send to generate
71
76
  the results, you can use `Stal.compile`:
72
77
 
73
78
  ```ruby
74
- Stal.compile([:inter, [:union, "foo", "bar"], "baz"])
75
- # [[:SUNIONSTORE, "stal:55f631dc-...", "foo", "bar"],
76
- # [:SINTERSTORE,
77
- # "stal:fe5aaec9-...",
78
- # "stal:55f631dc-...",
79
- # "baz"],
80
- # [:SMEMBERS, "stal:fe5aaec9-..."],
81
- # [:DEL,
82
- # "stal:fe5aaec9-...",
83
- # "stal:55f631dc-..."]]
79
+ Stal.explain([:SINTER, [:SUNION, "foo", "bar"], "baz"])
80
+ # [["SUNIONSTORE", "stal:0", "foo", "bar"],
81
+ # [:SINTER, "stal:0", "baz"]]
84
82
  ```
85
83
 
86
84
  All commands are pipelined and wrapped in a `MULTI/EXEC` transaction.
87
- The temporary keys, which have been shortened in the example, are
88
- deleted immediately.
89
-
90
85
 
91
86
  Installation
92
87
  ------------
@@ -1,69 +1,85 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  require "redic"
4
- require "securerandom"
5
4
 
6
5
  module Stal
7
- ALIASES = {
8
- :diff => "SDIFFSTORE",
9
- :inter => "SINTERSTORE",
10
- :union => "SUNIONSTORE",
6
+ class InvalidCommand < ArgumentError; end
7
+
8
+ COMMANDS = {
9
+ :SDIFF => 'SDIFFSTORE',
10
+ :SINTER => 'SINTERSTORE',
11
+ :SUNION => 'SUNIONSTORE',
11
12
  }
12
13
 
13
- def self.tr(term)
14
- ALIASES.fetch(term, term)
14
+ def self.command(term)
15
+ COMMANDS.fetch(term) do
16
+ raise(InvalidCommand, term)
17
+ end
15
18
  end
16
19
 
17
- def self.compute(expr, ids, acc)
18
- id = sprintf("stal:%s", SecureRandom.uuid)
19
-
20
- # Keys we need to clean up later
21
- ids.push(id)
22
-
23
- # Add command with destination key
24
- cmd = [tr(expr[0]), id]
25
-
26
- expr[1..-1].each do |item|
20
+ # Compile expression into Redis commands
21
+ def self.compile(expr, ids, ops)
22
+ expr.map do |item|
27
23
  if Array === item
28
- cmd.push(compute(item, ids, acc))
24
+ convert(item, ids, ops)
29
25
  else
30
- cmd.push(item)
26
+ item
31
27
  end
32
28
  end
29
+ end
33
30
 
34
- acc.push(cmd)
31
+ # Transform :SDIFF, :SINTER and :SUNION commands
32
+ # into SDIFFSTORE, SINTERSTORE and SUNIONSTORE.
33
+ def self.convert(expr, ids, ops)
34
+ head, *tail = expr
35
35
 
36
- return id
37
- end
36
+ # Key where partial results will be stored
37
+ id = sprintf("stal:%s", ids.size)
38
38
 
39
- def self.compile(expr)
39
+ # Keep a reference to clean it up later
40
+ ids.push(id)
40
41
 
41
- # Commands to process
42
- acc = []
42
+ # Translate into command and destination key
43
+ op = [command(head), id]
43
44
 
44
- # Keys to cleanup
45
+ # Compile the rest recursively
46
+ op.concat(compile(tail, ids, ops))
47
+
48
+ # Append the outermost operation
49
+ ops.push(op)
50
+
51
+ return id
52
+ end
53
+
54
+ # Return commands without any wrapping added by `solve`
55
+ def self.explain(expr)
45
56
  ids = []
57
+ ops = []
46
58
 
47
- id = compute(expr, ids, acc)
59
+ ops.push(compile(expr, ids, ops))
48
60
 
49
- acc.push([:SMEMBERS, id])
50
- acc.push([:DEL, *ids])
51
- acc
61
+ return ops
52
62
  end
53
63
 
54
64
  # Evaluate expression `expr` in the Redis client `c`.
55
65
  def self.solve(c, expr)
56
- operations = compile(expr)
66
+ ids = []
67
+ ops = []
57
68
 
58
- c.queue("MULTI")
69
+ ops.push(compile(expr, ids, ops))
59
70
 
60
- operations.each do |op|
61
- c.queue(*op)
62
- end
71
+ if ops.one?
72
+ c.call(*ops[0])
73
+ else
74
+ c.queue("MULTI")
63
75
 
64
- c.queue("EXEC")
76
+ ops.each do |op|
77
+ c.queue(*op)
78
+ end
65
79
 
66
- # Return the result of SMEMBERS
67
- c.commit[-1][-2]
80
+ c.queue("DEL", *ids)
81
+ c.queue("EXEC")
82
+ c.commit[-1][-2]
83
+ end
68
84
  end
69
85
  end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "stal"
3
- s.version = "0.0.1"
3
+ s.version = "0.1.0"
4
4
  s.summary = %{Set algebra solver for Redis}
5
5
  s.description = %Q{Stal receives s-expressions and resolves the set operations in Redis}
6
6
  s.authors = ["Michel Martens"]
@@ -12,26 +12,37 @@ end
12
12
 
13
13
  test do |c|
14
14
 
15
- # Shortcut syntax
16
- expr = [:union, "qux", [:diff, [:inter, "foo", "bar"], "baz"]]
17
-
18
- # Explicit syntax
19
- expr = [:SUNIONSTORE, "qux", [:SDIFFSTORE, [:SINTERSTORE, "foo", "bar"], "baz"]]
15
+ # Example expression
16
+ expr = ["SUNION", "qux", [:SDIFF, [:SINTER, "foo", "bar"], "baz"]]
20
17
 
21
18
  assert_equal ["b", "x", "y", "z"], Stal.solve(c, expr).sort
22
19
 
23
- # Explicit syntax with strings
24
- expr = ["SUNIONSTORE", "qux", ["SDIFFSTORE", ["SINTERSTORE", "foo", "bar"], "baz"]]
20
+ # Commands in sub expressions must be symbols
21
+ expr = ["SUNION", "qux", ["SDIFF", ["SINTER", "foo", "bar"], "baz"]]
25
22
 
26
- assert_equal ["b", "x", "y", "z"], Stal.solve(c, expr).sort
23
+ assert_raise(Stal::InvalidCommand) do
24
+ Stal.solve(c, expr)
25
+ end
27
26
 
28
- # Explicit syntax with lowercase strings
29
- expr = ["sunionstore", "qux", ["sdiffstore", ["sinterstore", "foo", "bar"], "baz"]]
27
+ # Commands without sub expressions also work
28
+ expr = ["SINTER", "foo", "bar"]
30
29
 
31
- assert_equal ["b", "x", "y", "z"], Stal.solve(c, expr).sort
30
+ assert_equal ["b", "c"], Stal.solve(c, expr).sort
32
31
 
33
- assert_equal ["b", "x", "y", "z"], Stal.solve(c, expr).sort
32
+ # Only :SUNION, :SDIFF and :SINTER are supported in sub expressions
33
+ expr = ["SUNION", ["DEL", "foo"]]
34
34
 
35
- # Verify there's no pollution
35
+ assert_raise(Stal::InvalidCommand) do
36
+ Stal.solve(c, expr)
37
+ end
38
+
39
+ # Verify there's no keyspace pollution
36
40
  assert_equal ["bar", "baz", "foo", "qux"], c.call("KEYS", "*").sort
41
+
42
+ expr = ["SCARD", [:SINTER, "foo", "bar"]]
43
+
44
+ # Explain returns an array of Redis commands
45
+ expected = [["SINTERSTORE", "stal:0", "foo", "bar"], ["SCARD", "stal:0"]]
46
+
47
+ assert_equal expected, Stal.explain(expr)
37
48
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michel Martens
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-02-04 00:00:00.000000000 Z
11
+ date: 2015-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redic