sohm 0.0.1
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 +7 -0
- data/.gems +4 -0
- data/.gitignore +3 -0
- data/CHANGELOG.md +312 -0
- data/LICENSE +19 -0
- data/README.md +10 -0
- data/benchmarks/common.rb +33 -0
- data/benchmarks/create.rb +21 -0
- data/benchmarks/delete.rb +13 -0
- data/examples/activity-feed.rb +162 -0
- data/examples/chaining.rb +162 -0
- data/examples/json-hash.rb +75 -0
- data/examples/one-to-many.rb +124 -0
- data/examples/philosophy.rb +137 -0
- data/examples/redis-logging.txt +179 -0
- data/examples/slug.rb +149 -0
- data/examples/tagging.rb +237 -0
- data/lib/sample.rb +14 -0
- data/lib/sohm/command.rb +51 -0
- data/lib/sohm/json.rb +17 -0
- data/lib/sohm/lua/delete.lua +72 -0
- data/lib/sohm/lua/save.lua +13 -0
- data/lib/sohm.rb +1576 -0
- data/makefile +4 -0
- data/sohm.gemspec +18 -0
- data/test/association.rb +33 -0
- data/test/command.rb +55 -0
- data/test/connection.rb +16 -0
- data/test/core.rb +24 -0
- data/test/counters.rb +67 -0
- data/test/enumerable.rb +79 -0
- data/test/filtering.rb +185 -0
- data/test/hash_key.rb +31 -0
- data/test/helper.rb +23 -0
- data/test/indices.rb +133 -0
- data/test/json.rb +62 -0
- data/test/list.rb +83 -0
- data/test/model.rb +789 -0
- data/test/set.rb +37 -0
- data/test/thread_safety.rb +67 -0
- data/test/to_hash.rb +29 -0
- data/test/uniques.rb +98 -0
- metadata +142 -0
data/examples/slug.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
### All Kinds of Slugs
|
2
|
+
|
3
|
+
# The problem of making semantic URLs have definitely been a prevalent one.
|
4
|
+
# There has been quite a lot of solutions around this theme, so we'll discuss
|
5
|
+
# a few simple ways to handle slug generation.
|
6
|
+
|
7
|
+
#### ID Prefixed slugs
|
8
|
+
|
9
|
+
# This is by far the simplest (and most cost-effective way) of generating
|
10
|
+
# slugs. Implementing this is pretty simple too.
|
11
|
+
|
12
|
+
# Let's first require `Ohm`.
|
13
|
+
require "ohm"
|
14
|
+
|
15
|
+
# Now let's define our `Post` model, with just a single
|
16
|
+
# `attribute` *title*.
|
17
|
+
class Post < Ohm::Model
|
18
|
+
attribute :title
|
19
|
+
|
20
|
+
# To make it more convenient, we override the finder syntax,
|
21
|
+
# so doing a `Post["1-my-post-title"]` will in effect just call
|
22
|
+
# `Post[1]`.
|
23
|
+
def self.[](id)
|
24
|
+
super(id.to_i)
|
25
|
+
end
|
26
|
+
|
27
|
+
# This pattern was mostly borrowed from Rails' style of generating
|
28
|
+
# URLs. Here we just concatenate the `id` and a sanitized form
|
29
|
+
# of our title.
|
30
|
+
def to_param
|
31
|
+
"#{id}-#{title.to_s.gsub(/\p{^Alnum}/u, " ").gsub(/\s+/, "-").downcase}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Let's verify our code using the
|
36
|
+
# [Cutest](http://github.com/djanowski/cutest)
|
37
|
+
# testing framework.
|
38
|
+
require "cutest"
|
39
|
+
|
40
|
+
# Also we ensure every test run is guaranteed to have a clean
|
41
|
+
# *Redis* instance.
|
42
|
+
prepare { Ohm.flush }
|
43
|
+
|
44
|
+
# For each and every test, we create a post with
|
45
|
+
# the title "ID Prefixed Slugs". Since it's the last
|
46
|
+
# line of our `setup`, it will also be yielded to
|
47
|
+
# each of our test blocks.
|
48
|
+
setup do
|
49
|
+
Post.create(:title => "ID Prefixed Slugs")
|
50
|
+
end
|
51
|
+
|
52
|
+
# Now let's verify the behavior of our `to_param` method.
|
53
|
+
# Note that we make it dash-separated and lowercased.
|
54
|
+
test "to_param" do |post|
|
55
|
+
assert "1-id-prefixed-slugs" == post.to_param
|
56
|
+
end
|
57
|
+
|
58
|
+
# We also check that our easier finder syntax works.
|
59
|
+
test "finding the post" do |post|
|
60
|
+
assert post == Post[post.to_param]
|
61
|
+
end
|
62
|
+
|
63
|
+
#### We don't have to code it everytime
|
64
|
+
|
65
|
+
# Because of the prevalence, ease of use, and efficiency of this style of slug
|
66
|
+
# generation, it has been extracted to a module in
|
67
|
+
# [Ohm::Contrib](http://github.com/cyx/ohm-contrib/) called `Ohm::Slug`.
|
68
|
+
|
69
|
+
# Let's create a different model to demonstrate how to use it.
|
70
|
+
# (Run `[sudo] gem install ohm-contrib` to install ohm-contrib).
|
71
|
+
|
72
|
+
# When using `ohm-contrib`, we simply require it, and then
|
73
|
+
# directly reference the specific module. In this case, we
|
74
|
+
# use `Ohm::Slug`.
|
75
|
+
require "ohm/contrib"
|
76
|
+
|
77
|
+
class Video < Ohm::Model
|
78
|
+
include Ohm::Slug
|
79
|
+
|
80
|
+
attribute :title
|
81
|
+
|
82
|
+
# `Ohm::Slug` just uses the value of the object's `to_s`.
|
83
|
+
def to_s
|
84
|
+
title.to_s
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Now to quickly verify that everything works similar to our
|
89
|
+
# example above!
|
90
|
+
test "video slugging" do
|
91
|
+
video = Video.create(:title => "A video about ohm")
|
92
|
+
|
93
|
+
assert "1-a-video-about-ohm" == video.to_param
|
94
|
+
assert video == Video[video.id]
|
95
|
+
end
|
96
|
+
|
97
|
+
# That's it, and it works similarly to the example above.
|
98
|
+
|
99
|
+
#### What if I want a slug without an ID prefix?
|
100
|
+
|
101
|
+
# For this case, we can still make use of `Ohm::Slug`'s ability to
|
102
|
+
# make a clean string.
|
103
|
+
|
104
|
+
# Let's create an `Article` class which has a single attribute `title`.
|
105
|
+
class Article < Ohm::Model
|
106
|
+
include Ohm::Callbacks
|
107
|
+
|
108
|
+
attribute :title
|
109
|
+
|
110
|
+
# Now before creating this object, we just call `Ohm::Slug.slug` directly.
|
111
|
+
# We also check if the generated slug exists, and repeatedly try
|
112
|
+
# appending numbers.
|
113
|
+
protected
|
114
|
+
def before_create
|
115
|
+
temp = Ohm::Slug.slug(title)
|
116
|
+
self.id = temp
|
117
|
+
|
118
|
+
counter = 0
|
119
|
+
while Article.exists?(id)
|
120
|
+
self.id = "%s-%d" % [temp, counter += 1]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# We now verify the behavior of our `Article` class
|
126
|
+
# by creating an article with the same title 3 times.
|
127
|
+
test "create an article with the same title" do
|
128
|
+
a1 = Article.create(:title => "All kinds of slugs")
|
129
|
+
a2 = Article.create(:title => "All kinds of slugs")
|
130
|
+
a3 = Article.create(:title => "All kinds of slugs")
|
131
|
+
|
132
|
+
assert a1.id == "all-kinds-of-slugs"
|
133
|
+
assert a2.id == "all-kinds-of-slugs-1"
|
134
|
+
assert a3.id == "all-kinds-of-slugs-2"
|
135
|
+
end
|
136
|
+
|
137
|
+
#### Conclusion
|
138
|
+
|
139
|
+
# Slug generation comes in all different flavors.
|
140
|
+
#
|
141
|
+
# 1. The first solution is good enough for most cases. The primary advantage
|
142
|
+
# of this solution is that we don't have to check for ID clashes.
|
143
|
+
#
|
144
|
+
# 2. The second solution may be needed for cases where you must make
|
145
|
+
# the URLs absolutely clean and readable, and you hate having those
|
146
|
+
# number prefixes.
|
147
|
+
#
|
148
|
+
# *NOTE:* The example we used for the second solution has potential
|
149
|
+
# race conditions. I'll leave fixing it as an exercise to you.
|
data/examples/tagging.rb
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
### Tagging
|
2
|
+
|
3
|
+
#### Intro
|
4
|
+
|
5
|
+
# When building a Web 2.0 application, tagging will probably come up
|
6
|
+
# as one of the most requested features. Popularized by Delicious,
|
7
|
+
# it has quickly become a useful way to organize crowd sourced data.
|
8
|
+
|
9
|
+
#### How it was done
|
10
|
+
|
11
|
+
# Typically, when you do tagging using an RDBMS, you'll probably end up
|
12
|
+
# having a taggings and a tags table, hence a many-to-many design.
|
13
|
+
# Here is a quick sketch just to illustrate:
|
14
|
+
#
|
15
|
+
#
|
16
|
+
#
|
17
|
+
# Post Taggings Tag
|
18
|
+
# ---- -------- ---
|
19
|
+
# id tag_id id
|
20
|
+
# title post_id name
|
21
|
+
#
|
22
|
+
# As you can see, this design leads to a lot of problems:
|
23
|
+
#
|
24
|
+
# 1. Trying to find the tags of a post will have to go through taggings, and
|
25
|
+
# then individually find the actual tag.
|
26
|
+
# 2. One might be inclined to use a JOIN query, but we all know
|
27
|
+
# [joins are evil](http://stackoverflow.com/questions/1020847).
|
28
|
+
# 3. Building a tag cloud or some form of tag ranking is unintuitive.
|
29
|
+
|
30
|
+
#### The Ohm approach
|
31
|
+
|
32
|
+
# Here is a basic outline of what we'll need:
|
33
|
+
#
|
34
|
+
# 1. We should be able to tag a post (separated by commas).
|
35
|
+
# 2. We should be able to find a post with a given tag.
|
36
|
+
|
37
|
+
#### Beginning with our Post model
|
38
|
+
|
39
|
+
# Let's first require ohm.
|
40
|
+
require 'ohm'
|
41
|
+
|
42
|
+
# We then declare our class, inheriting from `Ohm::Model` in the process.
|
43
|
+
class Post < Ohm::Model
|
44
|
+
|
45
|
+
# The structure, fields, and other associations are defined in a declarative
|
46
|
+
# manner. Ohm allows us to declare *attributes*, *sets*, *lists* and
|
47
|
+
# *counters*. For our usecase here, only two *attributes* will get the job
|
48
|
+
# done. The `body` will just
|
49
|
+
# be a plain string, and the `tags` will contain our comma-separated list of
|
50
|
+
# words, i.e. "ruby, redis, ohm". We then declare an `index` (which can be
|
51
|
+
# an `attribute` or just a plain old method), which we point to our method
|
52
|
+
# `tag`.
|
53
|
+
attribute :body
|
54
|
+
attribute :tags
|
55
|
+
index :tag
|
56
|
+
|
57
|
+
# One very interesting thing about Ohm indexes is that it can either be a
|
58
|
+
# *String* or an *Enumerable* data structure. When we declare it as an
|
59
|
+
# *Enumerable*, `Ohm` will create an index for every element. So if `tag`
|
60
|
+
# returned `[ruby, redis, ohm]` then we can search it using any of the
|
61
|
+
# following:
|
62
|
+
#
|
63
|
+
# 1. ruby
|
64
|
+
# 2. redis
|
65
|
+
# 3. ohm
|
66
|
+
# 4. ruby, redis
|
67
|
+
# 5. ruby, ohm
|
68
|
+
# 6. redis, ohm
|
69
|
+
# 7. ruby, redis, ohm
|
70
|
+
#
|
71
|
+
# Pretty neat ain't it?
|
72
|
+
def tag
|
73
|
+
tags.to_s.split(/\s*,\s*/).uniq
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
#### Testing it out
|
78
|
+
|
79
|
+
# It's a very good habit to test all the time. In the Ruby community,
|
80
|
+
# a lot of test frameworks have been created.
|
81
|
+
|
82
|
+
# For our purposes in this example, we'll use cutest.
|
83
|
+
require "cutest"
|
84
|
+
|
85
|
+
# Cutest allows us to define callbacks which are guaranteed to be executed
|
86
|
+
# every time a new `test` begins. Here, we just make sure that the Redis
|
87
|
+
# instance of `Ohm` is empty everytime.
|
88
|
+
prepare { Ohm.flush }
|
89
|
+
|
90
|
+
# Next, let's create a simple `Post` instance. The return value of the `setup`
|
91
|
+
# block will be passed to every `test` block, so we don't actually have to
|
92
|
+
# assign it to an instance variable.
|
93
|
+
setup do
|
94
|
+
Post.create(:body => "Ohm Tagging", :tags => "tagging, ohm, redis")
|
95
|
+
end
|
96
|
+
|
97
|
+
# For our first run, let's verify the fact that we can find a `Post`
|
98
|
+
# using any of the tags we gave.
|
99
|
+
test "find using a single tag" do |p|
|
100
|
+
assert Post.find(tag: "tagging").include?(p)
|
101
|
+
assert Post.find(tag: "ohm").include?(p)
|
102
|
+
assert Post.find(tag: "redis").include?(p)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Now we verify our claim earlier, that it is possible to find a tag
|
106
|
+
# using any one of the combinations for the given set of tags.
|
107
|
+
#
|
108
|
+
# We also verify that if we pass in a non-existent tag name that
|
109
|
+
# we'll fail to find the `Post` we just created.
|
110
|
+
test "find using an intersection of multiple tag names" do |p|
|
111
|
+
assert Post.find(tag: ["tagging", "ohm"]).include?(p)
|
112
|
+
assert Post.find(tag: ["tagging", "redis"]).include?(p)
|
113
|
+
assert Post.find(tag: ["ohm", "redis"]).include?(p)
|
114
|
+
assert Post.find(tag: ["tagging", "ohm", "redis"]).include?(p)
|
115
|
+
|
116
|
+
assert ! Post.find(tag: ["tagging", "foo"]).include?(p)
|
117
|
+
end
|
118
|
+
|
119
|
+
#### Adding a Tag model
|
120
|
+
|
121
|
+
# Let's pretend that the client suddenly requested that we keep track
|
122
|
+
# of the number of times a tag has been used. It's a pretty fair requirement
|
123
|
+
# after all. Updating our requirements, we will now have:
|
124
|
+
#
|
125
|
+
# 1. We should be able to tag a post (separated by commas).
|
126
|
+
# 2. We should be able to find a post with a given tag.
|
127
|
+
# 3. We should be able to find top tags, and their count.
|
128
|
+
|
129
|
+
# Continuing from our example above, let's require `ohm-contrib`, which we
|
130
|
+
# will be using for callbacks.
|
131
|
+
require "ohm/contrib"
|
132
|
+
|
133
|
+
# Let's quickly re-open our Post class.
|
134
|
+
class Post
|
135
|
+
# When we want our class to have extended functionality like callbacks,
|
136
|
+
# we simply include the necessary modules, in this case `Ohm::Callbacks`,
|
137
|
+
# which will be responsible for inserting `before_*` and `after_*` methods
|
138
|
+
# in the object's lifecycle.
|
139
|
+
include Ohm::Callbacks
|
140
|
+
|
141
|
+
# To make our code more concise, we just quickly change our implementation
|
142
|
+
# of `tag` to receive a default parameter:
|
143
|
+
def tag(tags = self.tags)
|
144
|
+
tags.to_s.split(/\s*,\s*/).uniq
|
145
|
+
end
|
146
|
+
|
147
|
+
# For all but the most simple cases, we would probably need to define
|
148
|
+
# callbacks. When we included `Ohm::Callbacks` above, it actually gave us
|
149
|
+
# the following:
|
150
|
+
#
|
151
|
+
# 1. `before_validate` and `after_validate`
|
152
|
+
# 2. `before_create` and `after_create`
|
153
|
+
# 3. `before_update` and `after_update`
|
154
|
+
# 4. `before_save` and `after_save`
|
155
|
+
# 5. `before_delete` and `after_delete`
|
156
|
+
|
157
|
+
# For our scenario, we only need a `before_update` and `after_save`.
|
158
|
+
# The idea for our `before_update` is to decrement the `total` of
|
159
|
+
# all existing tags. We use `get(:tags)` the original tags for the
|
160
|
+
# record and use assigned one on save.
|
161
|
+
protected
|
162
|
+
def before_update
|
163
|
+
assigned_tags = tags
|
164
|
+
tag(get(:tags)).map(&Tag).each { |t| t.decr :total }
|
165
|
+
self.tags = assigned_tags
|
166
|
+
end
|
167
|
+
|
168
|
+
# And of course, we increment all new tags for a particular record
|
169
|
+
# after successfully saving it.
|
170
|
+
def after_save
|
171
|
+
tag.map(&Tag).each { |t| t.incr :total }
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
#### Our Tag model
|
176
|
+
|
177
|
+
# The `Tag` model has only one type, which is a `counter` for the `total`.
|
178
|
+
# Since `Ohm` allows us to use any kind of ID (not just numeric sequences),
|
179
|
+
# we can actually use the tag name to identify a `Tag`.
|
180
|
+
class Tag < Ohm::Model
|
181
|
+
counter :total
|
182
|
+
|
183
|
+
# The syntax for finding a record by its ID is `Tag["ruby"]`. The standard
|
184
|
+
# behavior in `Ohm` is to return `nil` when the ID does not exist.
|
185
|
+
#
|
186
|
+
# To simplify our code, we override `Tag["ruby"]`, and make it create a
|
187
|
+
# new `Tag` if it doesn't exist yet. One important implementation detail
|
188
|
+
# though is that we need to encode the tag name, so special characters
|
189
|
+
# and spaces won't produce an invalid key.
|
190
|
+
def self.[](id)
|
191
|
+
encoded_id = id.encode
|
192
|
+
super(encoded_id) || create(:id => encoded_id)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
#### Verifying our third requirement
|
197
|
+
|
198
|
+
# Continuing from our test cases above, let's add test coverage for the
|
199
|
+
# behavior of counting tags.
|
200
|
+
|
201
|
+
# For each and every tag we initially create, we need to make sure they have a
|
202
|
+
# total of 1.
|
203
|
+
test "verify total to be exactly 1" do
|
204
|
+
assert 1 == Tag["ohm"].total
|
205
|
+
assert 1 == Tag["redis"].total
|
206
|
+
assert 1 == Tag["tagging"].total
|
207
|
+
end
|
208
|
+
|
209
|
+
# If we try and create another post tagged "ruby", "redis", `Tag["redis"]`
|
210
|
+
# should then have a total of 2. All of the other tags will still have
|
211
|
+
# a total of 1.
|
212
|
+
test "verify totals increase" do
|
213
|
+
Post.create(:body => "Ruby & Redis", :tags => "ruby, redis")
|
214
|
+
|
215
|
+
assert 1 == Tag["ohm"].total
|
216
|
+
assert 1 == Tag["tagging"].total
|
217
|
+
assert 1 == Tag["ruby"].total
|
218
|
+
assert 2 == Tag["redis"].total
|
219
|
+
end
|
220
|
+
|
221
|
+
# Finally, let's verify the scenario where we create a `Post` tagged
|
222
|
+
# "ruby", "redis" and update it to only have the tag "redis",
|
223
|
+
# effectively removing the tag "ruby" from our `Post`.
|
224
|
+
test "updating an existing post decrements the tags removed" do
|
225
|
+
p = Post.create(:body => "Ruby & Redis", :tags => "ruby, redis")
|
226
|
+
p.update(:tags => "redis")
|
227
|
+
|
228
|
+
assert 0 == Tag["ruby"].total
|
229
|
+
assert 2 == Tag["redis"].total
|
230
|
+
end
|
231
|
+
|
232
|
+
## Conclusion
|
233
|
+
|
234
|
+
# Most of the time we tend to think in terms of an RDBMS way, and this is in
|
235
|
+
# no way a negative thing. However, it is important to try and switch your
|
236
|
+
# frame of mind when working with Ohm (and Redis) because it will greatly save
|
237
|
+
# you time, and possibly lead to a great design.
|
data/lib/sample.rb
ADDED
data/lib/sohm/command.rb
ADDED
@@ -0,0 +1,51 @@
|
|
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(nido, redis)
|
20
|
+
newkey(nido, redis) do |key|
|
21
|
+
redis.call(@operation, key, *params(nido, redis))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def clean
|
26
|
+
keys.each do |key, redis|
|
27
|
+
redis.call("DEL", key)
|
28
|
+
end
|
29
|
+
|
30
|
+
subcommands.each { |cmd| cmd.clean }
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def subcommands
|
35
|
+
args.select { |arg| arg.respond_to?(:call) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def params(nido, redis)
|
39
|
+
args.map { |arg| arg.respond_to?(:call) ? arg.call(nido, redis) : arg }
|
40
|
+
end
|
41
|
+
|
42
|
+
def newkey(nido, redis)
|
43
|
+
key = nido[SecureRandom.hex(32)]
|
44
|
+
keys << [key, redis]
|
45
|
+
|
46
|
+
yield key
|
47
|
+
|
48
|
+
return key
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/sohm/json.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module Ohm
|
4
|
+
class Model
|
5
|
+
# Export a JSON representation of the model by encoding `to_hash`.
|
6
|
+
def to_json(*args)
|
7
|
+
to_hash.to_json(*args)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Collection
|
12
|
+
# Sugar for to_a.to_json for all types of Sets
|
13
|
+
def to_json(*args)
|
14
|
+
to_a.to_json(*args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
-- This script receives three parameters, all encoded with
|
2
|
+
-- MessagePack. The decoded values are used for deleting a model
|
3
|
+
-- instance in Redis and removing any reference to it in sets
|
4
|
+
-- (indices) and hashes (unique indices).
|
5
|
+
--
|
6
|
+
-- # model
|
7
|
+
--
|
8
|
+
-- Table with three attributes:
|
9
|
+
-- id (model instance id)
|
10
|
+
-- key (hash where the attributes will be saved)
|
11
|
+
-- name (model name)
|
12
|
+
--
|
13
|
+
-- # uniques
|
14
|
+
--
|
15
|
+
-- Fields and values to be removed from the unique indices.
|
16
|
+
--
|
17
|
+
-- # tracked
|
18
|
+
--
|
19
|
+
-- Keys that share the lifecycle of this model instance, that
|
20
|
+
-- should be removed as this object is deleted.
|
21
|
+
--
|
22
|
+
local model = cmsgpack.unpack(ARGV[1])
|
23
|
+
local uniques = cmsgpack.unpack(ARGV[2])
|
24
|
+
local tracked = cmsgpack.unpack(ARGV[3])
|
25
|
+
|
26
|
+
local function remove_indices(model)
|
27
|
+
local memo = model.key .. ":_indices"
|
28
|
+
local existing = redis.call("SMEMBERS", memo)
|
29
|
+
|
30
|
+
for _, key in ipairs(existing) do
|
31
|
+
redis.call("SREM", key, model.id)
|
32
|
+
redis.call("SREM", memo, key)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
local function remove_uniques(model, uniques)
|
37
|
+
local memo = model.key .. ":_uniques"
|
38
|
+
|
39
|
+
for field, _ in pairs(uniques) do
|
40
|
+
local key = model.name .. ":uniques:" .. field
|
41
|
+
|
42
|
+
redis.call("HDEL", key, redis.call("HGET", memo, key))
|
43
|
+
redis.call("HDEL", memo, key)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
local function remove_tracked(model, tracked)
|
48
|
+
for _, tracked_key in ipairs(tracked) do
|
49
|
+
local key = model.key .. ":" .. tracked_key
|
50
|
+
|
51
|
+
redis.call("DEL", key)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
local function delete(model)
|
56
|
+
local keys = {
|
57
|
+
model.key .. ":counters",
|
58
|
+
model.key .. ":_indices",
|
59
|
+
model.key .. ":_uniques",
|
60
|
+
model.key
|
61
|
+
}
|
62
|
+
|
63
|
+
redis.call("SREM", model.name .. ":all", model.id)
|
64
|
+
redis.call("DEL", unpack(keys))
|
65
|
+
end
|
66
|
+
|
67
|
+
remove_indices(model)
|
68
|
+
remove_uniques(model, uniques)
|
69
|
+
remove_tracked(model, tracked)
|
70
|
+
delete(model)
|
71
|
+
|
72
|
+
return model.id
|
@@ -0,0 +1,13 @@
|
|
1
|
+
local ctoken = redis.call('HGET', KEYS[1], '_cas')
|
2
|
+
if (not ctoken) or ctoken == ARGV[2] then
|
3
|
+
local ntoken
|
4
|
+
if not ctoken then
|
5
|
+
ntoken = 1
|
6
|
+
else
|
7
|
+
ntoken = tonumber(ctoken) + 1
|
8
|
+
end
|
9
|
+
redis.call('HMSET', KEYS[1], '_sdata', ARGV[1], '_cas', ntoken)
|
10
|
+
return ntoken
|
11
|
+
else
|
12
|
+
error('cas_error')
|
13
|
+
end
|