stal 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/.gems +1 -1
- data/CONTRIBUTING +19 -0
- data/README.md +6 -25
- data/data/stal.lua +88 -0
- data/lib/stal.rb +11 -73
- data/stal.gemspec +3 -3
- data/test/all.rb +3 -21
- metadata +14 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6d5e97a625f23834a34722015dd285abbd3759b2
|
4
|
+
data.tar.gz: 4579c7dc32e11a5be18f0f1e42d405a4c197319b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3bd4c9ce1448d9737fbbf730598facfe4a6119a4e5f2a3a513592a7760364b9eb8fd1a90fad61bfec0030ed2abf9763cf05ab339c12f563bc9d383b8d6d469dd
|
7
|
+
data.tar.gz: 6e8ae54b05c800ae9be844835b48a3c08d396799db7fdf5fe7423200d28fa1790da7d7a0339c776fad52f5c33b1d5c89d3f54a7e84cebd2c730c75138b374a76
|
data/.gems
CHANGED
data/CONTRIBUTING
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
This code tries to solve a particular problem with a very simple
|
2
|
+
implementation. We try to keep the code to a minimum while making
|
3
|
+
it as clear as possible. The design is very likely finished, and
|
4
|
+
if some feature is missing it is possible that it was left out on
|
5
|
+
purpose. That said, new usage patterns may arise, and when that
|
6
|
+
happens we are ready to adapt if necessary.
|
7
|
+
|
8
|
+
A good first step for contributing is to meet us on IRC and discuss
|
9
|
+
ideas. We spend a lot of time on #lesscode at freenode, always ready
|
10
|
+
to talk about code and simplicity. If connecting to IRC is not an
|
11
|
+
option, you can create an issue explaining the proposed change and
|
12
|
+
a use case. We pay a lot of attention to use cases, because our
|
13
|
+
goal is to keep the code base simple. Usually the result of a
|
14
|
+
conversation is the creation of a different tool.
|
15
|
+
|
16
|
+
Please don't start the conversation with a pull request. The code
|
17
|
+
should come at last, and even though it may help to convey an idea,
|
18
|
+
more often than not it draws the attention to a particular
|
19
|
+
implementation.
|
data/README.md
CHANGED
@@ -49,39 +49,19 @@ 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 = [
|
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 internal calls to
|
59
|
-
|
58
|
+
`Stal` translates the internal calls to `SUNION`, `SDIFF` and
|
59
|
+
`SINTER` into `SDIFFSTORE`, `SINTERSTORE` and `SUNIONSTORE` to
|
60
60
|
perform the underlying operations, and it takes care of generating
|
61
61
|
and deleting any temporary keys.
|
62
62
|
|
63
|
-
|
64
|
-
|
65
|
-
raise an error. The outmost command can be any set operation, for
|
66
|
-
example:
|
67
|
-
|
68
|
-
```ruby
|
69
|
-
expr = [:SCARD, [:SINTER, "foo", "bar"]]
|
70
|
-
|
71
|
-
Stal.solve(redis, expr)
|
72
|
-
#=> 2
|
73
|
-
```
|
74
|
-
|
75
|
-
If you want to preview the commands `Stal` will send to generate
|
76
|
-
the results, you can use `Stal.compile`:
|
77
|
-
|
78
|
-
```ruby
|
79
|
-
Stal.explain([:SINTER, [:SUNION, "foo", "bar"], "baz"])
|
80
|
-
# [["SUNIONSTORE", "stal:0", "foo", "bar"],
|
81
|
-
# [:SINTER, "stal:0", "baz"]]
|
82
|
-
```
|
83
|
-
|
84
|
-
All commands are pipelined and wrapped in a `MULTI/EXEC` transaction.
|
63
|
+
For more information, refer to the repository of the [Stal][stal]
|
64
|
+
script.
|
85
65
|
|
86
66
|
Installation
|
87
67
|
------------
|
@@ -92,3 +72,4 @@ $ gem install stal
|
|
92
72
|
|
93
73
|
[redis]: http://redis.io
|
94
74
|
[redic]: https://github.com/amakawa/redic
|
75
|
+
[stal]: https://github.com/soveran/stal
|
data/data/stal.lua
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
-- Copyright (c) 2016 Michel Martens
|
2
|
+
|
3
|
+
local expr = cjson.decode(ARGV[1])
|
4
|
+
|
5
|
+
local tr = {
|
6
|
+
SDIFF = "SDIFFSTORE",
|
7
|
+
SINTER = "SINTERSTORE",
|
8
|
+
SUNION = "SUNIONSTORE",
|
9
|
+
ZINTER = "ZINTERSTORE",
|
10
|
+
ZUNION = "ZUNIONSTORE",
|
11
|
+
}
|
12
|
+
|
13
|
+
local function append(t1, t2)
|
14
|
+
for _, item in ipairs(t2) do
|
15
|
+
table.insert(t1, item)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
local function map(t, f)
|
20
|
+
local nt = {}
|
21
|
+
|
22
|
+
for k, v in pairs(t) do
|
23
|
+
nt[k] = f(v)
|
24
|
+
end
|
25
|
+
|
26
|
+
return nt
|
27
|
+
end
|
28
|
+
|
29
|
+
local compile, convert
|
30
|
+
|
31
|
+
function compile(expr, ids, ops)
|
32
|
+
return map(expr, function(v)
|
33
|
+
if (type(v) == "table") then
|
34
|
+
return convert(v, ids, ops)
|
35
|
+
else
|
36
|
+
return v
|
37
|
+
end
|
38
|
+
end)
|
39
|
+
end
|
40
|
+
|
41
|
+
function convert(expr, ids, ops)
|
42
|
+
local tail = {unpack(expr)}
|
43
|
+
local head = table.remove(tail, 1)
|
44
|
+
|
45
|
+
-- Key where partial results will be stored
|
46
|
+
local id = "stal:" .. #ids
|
47
|
+
|
48
|
+
-- Keep a reference to clean it up later
|
49
|
+
table.insert(ids, id)
|
50
|
+
|
51
|
+
-- Translate into command and destination key
|
52
|
+
local op = {tr[head] or head, id}
|
53
|
+
|
54
|
+
-- Compile the rest recursively
|
55
|
+
append(op, compile(tail, ids, ops))
|
56
|
+
|
57
|
+
-- Append the outermost operation
|
58
|
+
table.insert(ops, op)
|
59
|
+
|
60
|
+
return id
|
61
|
+
end
|
62
|
+
|
63
|
+
local function solve(expr)
|
64
|
+
local ids = {}
|
65
|
+
local ops = {}
|
66
|
+
local res = nil
|
67
|
+
|
68
|
+
table.insert(ops, compile(expr, ids, ops))
|
69
|
+
|
70
|
+
if (#ops == 1) then
|
71
|
+
return redis.call(unpack(ops[1]))
|
72
|
+
else
|
73
|
+
for _, op in ipairs(ops) do
|
74
|
+
if (#op > 1) then
|
75
|
+
res = redis.call(unpack(op))
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
redis.call("DEL", unpack(ids))
|
80
|
+
|
81
|
+
return res
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
redis.replicate_commands()
|
86
|
+
redis.set_repl(redis.REPL_NONE)
|
87
|
+
|
88
|
+
return solve(expr)
|
data/lib/stal.rb
CHANGED
@@ -1,85 +1,23 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
|
3
|
+
require "json"
|
3
4
|
require "redic"
|
4
5
|
|
5
6
|
module Stal
|
6
|
-
|
7
|
-
|
8
|
-
COMMANDS = {
|
9
|
-
:SDIFF => 'SDIFFSTORE',
|
10
|
-
:SINTER => 'SINTERSTORE',
|
11
|
-
:SUNION => 'SUNIONSTORE',
|
12
|
-
}
|
13
|
-
|
14
|
-
def self.command(term)
|
15
|
-
COMMANDS.fetch(term) do
|
16
|
-
raise(InvalidCommand, term)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
# Compile expression into Redis commands
|
21
|
-
def self.compile(expr, ids, ops)
|
22
|
-
expr.map do |item|
|
23
|
-
if Array === item
|
24
|
-
convert(item, ids, ops)
|
25
|
-
else
|
26
|
-
item
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
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
|
-
|
36
|
-
# Key where partial results will be stored
|
37
|
-
id = sprintf("stal:%s", ids.size)
|
38
|
-
|
39
|
-
# Keep a reference to clean it up later
|
40
|
-
ids.push(id)
|
41
|
-
|
42
|
-
# Translate into command and destination key
|
43
|
-
op = [command(head), id]
|
44
|
-
|
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)
|
56
|
-
ids = []
|
57
|
-
ops = []
|
58
|
-
|
59
|
-
ops.push(compile(expr, ids, ops))
|
60
|
-
|
61
|
-
return ops
|
62
|
-
end
|
7
|
+
LUA = File.expand_path("../../data/stal.lua", __FILE__)
|
8
|
+
SHA = "e98658a3aca397c69e8b452a73c6826cdd9f0577"
|
63
9
|
|
64
10
|
# Evaluate expression `expr` in the Redis client `c`.
|
65
11
|
def self.solve(c, expr)
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
c.queue("MULTI")
|
75
|
-
|
76
|
-
ops.each do |op|
|
77
|
-
c.queue(*op)
|
12
|
+
begin
|
13
|
+
c.call!("EVALSHA", SHA, 0, JSON.dump(expr))
|
14
|
+
rescue RuntimeError
|
15
|
+
if $!.message["NOSCRIPT"]
|
16
|
+
c.call!("SCRIPT", "LOAD", File.read(LUA))
|
17
|
+
c.call!("EVALSHA", SHA, 0, JSON.dump(expr))
|
18
|
+
else
|
19
|
+
raise $!
|
78
20
|
end
|
79
|
-
|
80
|
-
c.queue("DEL", *ids)
|
81
|
-
c.queue("EXEC")
|
82
|
-
c.commit[-1][-2]
|
83
21
|
end
|
84
22
|
end
|
85
23
|
end
|
data/stal.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "stal"
|
3
|
-
s.version = "0.
|
3
|
+
s.version = "0.2.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"]
|
@@ -10,6 +10,6 @@ Gem::Specification.new do |s|
|
|
10
10
|
|
11
11
|
s.files = `git ls-files`.split("\n")
|
12
12
|
|
13
|
-
s.add_dependency "redic"
|
14
|
-
s.add_development_dependency "cutest"
|
13
|
+
s.add_dependency "redic", "~> 1.5"
|
14
|
+
s.add_development_dependency "cutest", "~> 0"
|
15
15
|
end
|
data/test/all.rb
CHANGED
@@ -3,6 +3,7 @@ require_relative "helper"
|
|
3
3
|
setup do
|
4
4
|
Redic.new.tap do |c|
|
5
5
|
c.call("FLUSHDB")
|
6
|
+
c.call("SCRIPT", "FLUSH")
|
6
7
|
c.call("SADD", "foo", "a", "b", "c")
|
7
8
|
c.call("SADD", "bar", "b", "c", "d")
|
8
9
|
c.call("SADD", "baz", "c", "d", "e")
|
@@ -13,36 +14,17 @@ end
|
|
13
14
|
test do |c|
|
14
15
|
|
15
16
|
# Example expression
|
16
|
-
expr = ["SUNION", "qux", [:SDIFF, [:SINTER, "foo", "bar"], "baz"]]
|
17
|
-
|
18
|
-
assert_equal ["b", "x", "y", "z"], Stal.solve(c, expr).sort
|
19
|
-
|
20
|
-
# Commands in sub expressions must be symbols
|
21
17
|
expr = ["SUNION", "qux", ["SDIFF", ["SINTER", "foo", "bar"], "baz"]]
|
22
18
|
|
23
|
-
|
24
|
-
Stal.solve(c, expr)
|
25
|
-
end
|
19
|
+
assert_equal ["b", "x", "y", "z"], Stal.solve(c, expr).sort
|
26
20
|
|
27
21
|
# Commands without sub expressions also work
|
28
22
|
expr = ["SINTER", "foo", "bar"]
|
29
23
|
|
30
24
|
assert_equal ["b", "c"], Stal.solve(c, expr).sort
|
31
25
|
|
32
|
-
# Only :SUNION, :SDIFF and :SINTER are supported in sub expressions
|
33
|
-
expr = ["SUNION", ["DEL", "foo"]]
|
34
|
-
|
35
|
-
assert_raise(Stal::InvalidCommand) do
|
36
|
-
Stal.solve(c, expr)
|
37
|
-
end
|
38
|
-
|
39
26
|
# Verify there's no keyspace pollution
|
40
27
|
assert_equal ["bar", "baz", "foo", "qux"], c.call("KEYS", "*").sort
|
41
28
|
|
42
|
-
expr = ["SCARD", [
|
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)
|
29
|
+
expr = ["SCARD", ["SINTER", "foo", "bar"]]
|
48
30
|
end
|
metadata
CHANGED
@@ -1,41 +1,41 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.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:
|
11
|
+
date: 2016-12-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redic
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '1.5'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '1.5'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: cutest
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
description: Stal receives s-expressions and resolves the set operations in Redis
|
@@ -45,9 +45,11 @@ executables: []
|
|
45
45
|
extensions: []
|
46
46
|
extra_rdoc_files: []
|
47
47
|
files:
|
48
|
-
- .gems
|
48
|
+
- ".gems"
|
49
|
+
- CONTRIBUTING
|
49
50
|
- LICENSE
|
50
51
|
- README.md
|
52
|
+
- data/stal.lua
|
51
53
|
- lib/stal.rb
|
52
54
|
- makefile
|
53
55
|
- stal.gemspec
|
@@ -63,17 +65,17 @@ require_paths:
|
|
63
65
|
- lib
|
64
66
|
required_ruby_version: !ruby/object:Gem::Requirement
|
65
67
|
requirements:
|
66
|
-
- -
|
68
|
+
- - ">="
|
67
69
|
- !ruby/object:Gem::Version
|
68
70
|
version: '0'
|
69
71
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
72
|
requirements:
|
71
|
-
- -
|
73
|
+
- - ">="
|
72
74
|
- !ruby/object:Gem::Version
|
73
75
|
version: '0'
|
74
76
|
requirements: []
|
75
77
|
rubyforge_project:
|
76
|
-
rubygems_version: 2.
|
78
|
+
rubygems_version: 2.4.5.1
|
77
79
|
signing_key:
|
78
80
|
specification_version: 4
|
79
81
|
summary: Set algebra solver for Redis
|