nkallen-cache-money 0.2.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.
- data/README +1 -0
- data/TODO +20 -0
- data/UNSUPPORTED_FEATURES +14 -0
- data/config/environment.rb +6 -0
- data/config/memcache.yml +6 -0
- data/db/schema.rb +11 -0
- data/lib/cash.rb +53 -0
- data/lib/cash/accessor.rb +78 -0
- data/lib/cash/buffered.rb +126 -0
- data/lib/cash/config.rb +64 -0
- data/lib/cash/finders.rb +40 -0
- data/lib/cash/index.rb +207 -0
- data/lib/cash/local.rb +59 -0
- data/lib/cash/lock.rb +52 -0
- data/lib/cash/mock.rb +86 -0
- data/lib/cash/query/abstract.rb +162 -0
- data/lib/cash/query/calculation.rb +45 -0
- data/lib/cash/query/primary_key.rb +51 -0
- data/lib/cash/query/select.rb +16 -0
- data/lib/cash/transactional.rb +42 -0
- data/lib/cash/util/array.rb +9 -0
- data/lib/cash/write_through.rb +72 -0
- data/spec/cash/accessor_spec.rb +133 -0
- data/spec/cash/active_record_spec.rb +190 -0
- data/spec/cash/calculations_spec.rb +67 -0
- data/spec/cash/finders_spec.rb +343 -0
- data/spec/cash/lock_spec.rb +87 -0
- data/spec/cash/order_spec.rb +166 -0
- data/spec/cash/transactional_spec.rb +574 -0
- data/spec/cash/window_spec.rb +195 -0
- data/spec/cash/write_through_spec.rb +223 -0
- data/spec/spec_helper.rb +55 -0
- metadata +100 -0
@@ -0,0 +1,195 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
module Cash
|
4
|
+
describe 'Windows' do
|
5
|
+
LIMIT, BUFFER = 5, 2
|
6
|
+
|
7
|
+
before :suite do
|
8
|
+
Fable = Class.new(Story)
|
9
|
+
Fable.index :title, :limit => LIMIT, :buffer => BUFFER
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#find(...)' do
|
13
|
+
before do
|
14
|
+
@fables = []
|
15
|
+
10.times { @fables << Fable.create!(:title => @title = 'title') }
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'when the cache is populated' do
|
19
|
+
describe "#find(:all, :conditions => ...)" do
|
20
|
+
it "uses the database, not the cache" do
|
21
|
+
mock(Fable).get.never
|
22
|
+
Fable.find(:all, :conditions => { :title => @title }).should == @fables
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#find(:all, :conditions => ..., :limit => ...) and query limit > index limit" do
|
27
|
+
it "uses the database, not the cache" do
|
28
|
+
mock(Fable).get.never
|
29
|
+
Fable.find(:all, :conditions => { :title => @title }, :limit => LIMIT + 1).should == @fables[0, LIMIT + 1]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#find(:all, :conditions => ..., :limit => ..., :offset => ...) and query limit + offset > index limit" do
|
34
|
+
it "uses the database, not the cache" do
|
35
|
+
mock(Fable).get.never
|
36
|
+
Fable.find(:all, :conditions => { :title => @title }, :limit => 1, :offset => LIMIT).should == @fables[LIMIT, 1]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#find(:all, :conditions => ..., :limit => ...) and query limit <= index limit" do
|
41
|
+
it "does not use the database" do
|
42
|
+
mock(Fable.connection).execute.never
|
43
|
+
Fable.find(:all, :conditions => { :title => @title }, :limit => LIMIT - 1).should == @fables[0, LIMIT - 1]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'when the cache is not populated' do
|
49
|
+
describe "#find(:all, :conditions => ..., :limit => ...) and query limit <= index limit" do
|
50
|
+
describe 'when there are fewer than limit + buffer items' do
|
51
|
+
it "populates the cache with all items" do
|
52
|
+
Fable.find(:all, :limit => deleted = @fables.size - LIMIT - BUFFER + 1).collect(&:destroy)
|
53
|
+
$memcache.flush_all
|
54
|
+
Fable.find(:all, :conditions => { :title => @title }, :limit => LIMIT).should == @fables[deleted, LIMIT]
|
55
|
+
Fable.get("title/#{@title}").should == @fables[deleted, @fables.size - deleted].collect(&:id)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe 'when there are more than limit + buffer items' do
|
60
|
+
it "populates the cache with limit + buffer items" do
|
61
|
+
$memcache.flush_all
|
62
|
+
Fable.find(:all, :conditions => { :title => @title }, :limit => 5).should == @fables[0, 5]
|
63
|
+
Fable.get("title/#{@title}").should == @fables[0, LIMIT + BUFFER].collect(&:id)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#create!' do
|
71
|
+
describe 'when the cache is populated' do
|
72
|
+
describe 'when the count of records in the database is > limit + buffer items' do
|
73
|
+
it 'truncates' do
|
74
|
+
fables, title = [], 'title'
|
75
|
+
(LIMIT + BUFFER).times { fables << Fable.create!(:title => title) }
|
76
|
+
Fable.get("title/#{title}").should == fables.collect(&:id)
|
77
|
+
Fable.create!(:title => title)
|
78
|
+
Fable.get("title/#{title}").should == fables.collect(&:id)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe 'when the count of records in the database is < limit + buffer items' do
|
83
|
+
it 'appends to the list' do
|
84
|
+
fables, title = [], 'title'
|
85
|
+
(LIMIT + BUFFER - 1).times { fables << Fable.create!(:title => title) }
|
86
|
+
Fable.get("title/#{title}").should == fables.collect(&:id)
|
87
|
+
fable = Fable.create!(:title => title)
|
88
|
+
Fable.get("title/#{title}").should == (fables << fable).collect(&:id)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe 'when the cache is not populated' do
|
94
|
+
describe 'when the count of records in the database is > limit + buffer items' do
|
95
|
+
it 'truncates the index' do
|
96
|
+
fables, title = [], 'title'
|
97
|
+
(LIMIT + BUFFER).times { fables << Fable.create!(:title => title) }
|
98
|
+
$memcache.flush_all
|
99
|
+
Fable.create!(:title => title)
|
100
|
+
Fable.get("title/#{title}").should == fables.collect(&:id)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe 'when the count of records in the database is < limit + buffer items' do
|
105
|
+
it 'appends to the list' do
|
106
|
+
fables, title = [], 'title'
|
107
|
+
(LIMIT + BUFFER - 1).times { fables << Fable.create!(:title => title) }
|
108
|
+
$memcache.flush_all
|
109
|
+
fable = Fable.create!(:title => title)
|
110
|
+
Fable.get("title/#{title}").should == (fables << fable).collect(&:id)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe '#destroy' do
|
117
|
+
describe 'when the cache is populated' do
|
118
|
+
describe 'when the index size is <= limit of items' do
|
119
|
+
describe 'when the count of records in the database is <= limit of items' do
|
120
|
+
it 'deletes from the list without refreshing from the database' do
|
121
|
+
fables, title = [], 'title'
|
122
|
+
LIMIT.times { fables << Fable.create!(:title => title) }
|
123
|
+
Fable.get("title/#{title}").size.should <= LIMIT
|
124
|
+
|
125
|
+
mock(Fable.connection).select.never
|
126
|
+
fables.shift.destroy
|
127
|
+
Fable.get("title/#{title}").should == fables.collect(&:id)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe 'when the count of records in the database is >= limit of items' do
|
132
|
+
it 'refreshes the list (from the database)' do
|
133
|
+
fables, title = [], 'title'
|
134
|
+
(LIMIT + BUFFER + 1).times { fables << Fable.create!(:title => title) }
|
135
|
+
BUFFER.times { fables.shift.destroy }
|
136
|
+
Fable.get("title/#{title}").size.should == LIMIT
|
137
|
+
|
138
|
+
fables.shift.destroy
|
139
|
+
Fable.get("title/#{title}").should == fables.collect(&:id)
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe 'when the index size is > limit of items' do
|
146
|
+
it 'deletes from the list' do
|
147
|
+
fables, title = [], 'title'
|
148
|
+
(LIMIT + 1).times { fables << Fable.create!(:title => title) }
|
149
|
+
Fable.get("title/#{title}").size.should > LIMIT
|
150
|
+
|
151
|
+
fables.shift.destroy
|
152
|
+
Fable.get("title/#{title}").should == fables.collect(&:id)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
describe 'when the cache is not populated' do
|
158
|
+
describe 'when count of records in the database is <= limit of items' do
|
159
|
+
it 'deletes from the index' do
|
160
|
+
fables, title = [], 'title'
|
161
|
+
LIMIT.times { fables << Fable.create!(:title => title) }
|
162
|
+
$memcache.flush_all
|
163
|
+
|
164
|
+
fables.shift.destroy
|
165
|
+
Fable.get("title/#{title}").should == fables.collect(&:id)
|
166
|
+
end
|
167
|
+
|
168
|
+
describe 'when the count of records in the database is between limit and limit + buffer items' do
|
169
|
+
it 'populates the index' do
|
170
|
+
fables, title = [], 'title'
|
171
|
+
(LIMIT + BUFFER + 1).times { fables << Fable.create!(:title => title) }
|
172
|
+
BUFFER.times { fables.shift.destroy }
|
173
|
+
$memcache.flush_all
|
174
|
+
|
175
|
+
fables.shift.destroy
|
176
|
+
Fable.get("title/#{title}").should == fables.collect(&:id)
|
177
|
+
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
describe 'when the count of records in the database is > limit + buffer items' do
|
182
|
+
it 'populates the index with limit + buffer items' do
|
183
|
+
fables, title = [], 'title'
|
184
|
+
(LIMIT + BUFFER + 2).times { fables << Fable.create!(:title => title) }
|
185
|
+
$memcache.flush_all
|
186
|
+
|
187
|
+
fables.shift.destroy
|
188
|
+
Fable.get("title/#{title}").should == fables[0, LIMIT + BUFFER].collect(&:id)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
module Cash
|
4
|
+
describe WriteThrough do
|
5
|
+
describe 'ClassMethods' do
|
6
|
+
describe 'after create' do
|
7
|
+
it "inserts all indexed attributes into the cache" do
|
8
|
+
story = Story.create!(:title => "I am delicious")
|
9
|
+
Story.get("title/#{story.title}").should == [story.id]
|
10
|
+
Story.get("id/#{story.id}").should == [story]
|
11
|
+
end
|
12
|
+
|
13
|
+
describe 'multiple objects' do
|
14
|
+
it "inserts multiple objects into the same cache key" do
|
15
|
+
story1 = Story.create!(:title => "I am delicious")
|
16
|
+
story2 = Story.create!(:title => "I am delicious")
|
17
|
+
Story.get("title/#{story1.title}").should == [story1.id, story2.id]
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'when the cache has been cleared after some objects were created' do
|
21
|
+
before do
|
22
|
+
@story1 = Story.create!(:title => "I am delicious")
|
23
|
+
$memcache.flush_all
|
24
|
+
@story2 = Story.create!(:title => "I am delicious")
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'inserts legacy objects into the cache' do
|
28
|
+
Story.get("title/#{@story1.title}").should == [@story1.id, @story2.id]
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'initializes the count to account for the legacy objects' do
|
32
|
+
Story.get("title/#{@story1.title}/count", :raw => true).should =~ /2/
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it "does not write through the cache on non-indexed attributes" do
|
38
|
+
story = Story.create!(:title => "Story 1", :subtitle => "Subtitle")
|
39
|
+
Story.get("subtitle/#{story.subtitle}").should == nil
|
40
|
+
end
|
41
|
+
|
42
|
+
it "indexes on combinations of attributes" do
|
43
|
+
story = Story.create!(:title => "Sam")
|
44
|
+
Story.get("id/#{story.id}/title/#{story.title}").should == [story.id]
|
45
|
+
end
|
46
|
+
|
47
|
+
it "does not cache associations" do
|
48
|
+
story = Story.new(:title => 'I am lugubrious')
|
49
|
+
story.characters.build(:name => 'How am I holy?')
|
50
|
+
story.save!
|
51
|
+
Story.get("id/#{story.id}").first.characters.loaded?.should_not be
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'increments the count' do
|
55
|
+
story = Story.create!(:title => "Sam")
|
56
|
+
Story.get("title/#{story.title}/count", :raw => true).should =~ /1/
|
57
|
+
story = Story.create!(:title => "Sam")
|
58
|
+
Story.get("title/#{story.title}/count", :raw => true).should =~ /2/
|
59
|
+
end
|
60
|
+
|
61
|
+
describe 'when the value is nil' do
|
62
|
+
it "does not write through the cache on indexed attributes" do
|
63
|
+
story = Story.create!(:title => nil)
|
64
|
+
Story.get("title/").should == nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe 'after update' do
|
70
|
+
it "overwrites the primary cache" do
|
71
|
+
story = Story.create!(:title => "I am delicious")
|
72
|
+
Story.get(cache_key = "id/#{story.id}").first.title.should == "I am delicious"
|
73
|
+
story.update_attributes(:title => "I am fabulous")
|
74
|
+
Story.get(cache_key).first.title.should == "I am fabulous"
|
75
|
+
end
|
76
|
+
|
77
|
+
it "populates empty caches" do
|
78
|
+
story = Story.create!(:title => "I am delicious")
|
79
|
+
$memcache.flush_all
|
80
|
+
story.update_attributes(:title => "I am fabulous")
|
81
|
+
Story.get("title/#{story.title}").should == [story.id]
|
82
|
+
end
|
83
|
+
|
84
|
+
it "removes from the affected index caches on update" do
|
85
|
+
story = Story.create!(:title => "I am delicious")
|
86
|
+
Story.get(cache_key = "title/#{story.title}").should == [story.id]
|
87
|
+
story.update_attributes(:title => "I am fabulous")
|
88
|
+
Story.get(cache_key).should == []
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'increments/decrements the counts of affected indices' do
|
92
|
+
story = Story.create!(:title => original_title = "I am delicious")
|
93
|
+
story.update_attributes(:title => new_title = "I am fabulous")
|
94
|
+
Story.get("title/#{original_title}/count", :raw => true).should =~ /0/
|
95
|
+
Story.get("title/#{new_title}/count", :raw => true).should =~ /1/
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe 'after destroy' do
|
100
|
+
it "removes from the primary cache" do
|
101
|
+
story = Story.create!(:title => "I am delicious")
|
102
|
+
Story.get(cache_key = "id/#{story.id}").should == [story]
|
103
|
+
story.destroy
|
104
|
+
Story.get(cache_key).should == []
|
105
|
+
end
|
106
|
+
|
107
|
+
it "removes from the the cache on keys matching the original values of attributes" do
|
108
|
+
story = Story.create!(:title => "I am delicious")
|
109
|
+
Story.get(cache_key = "title/#{story.title}").should == [story.id]
|
110
|
+
story.title = "I am not delicious"
|
111
|
+
story.destroy
|
112
|
+
Story.get(cache_key).should == []
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'decrements the count' do
|
116
|
+
story = Story.create!(:title => "I am delicious")
|
117
|
+
story.destroy
|
118
|
+
Story.get("title/#{story.title}/count", :raw => true).should =~ /0/
|
119
|
+
end
|
120
|
+
|
121
|
+
describe 'when there are multiple items in the index' do
|
122
|
+
it "only removes one item from the affected indices, not all of them" do
|
123
|
+
story1 = Story.create!(:title => "I am delicious")
|
124
|
+
story2 = Story.create!(:title => "I am delicious")
|
125
|
+
Story.get(cache_key = "title/#{story1.title}").should == [story1.id, story2.id]
|
126
|
+
story1.destroy
|
127
|
+
Story.get(cache_key).should == [story2.id]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe 'when the object is a new record' do
|
132
|
+
it 'does nothing' do
|
133
|
+
story1 = Story.new
|
134
|
+
mock(Story).set.never
|
135
|
+
story1.destroy
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
describe 'when the cache is not yet populated' do
|
140
|
+
it "populates the cache with data" do
|
141
|
+
story1 = Story.create!(:title => "I am delicious")
|
142
|
+
story2 = Story.create!(:title => "I am delicious")
|
143
|
+
$memcache.flush_all
|
144
|
+
Story.get(cache_key = "title/#{story1.title}").should == nil
|
145
|
+
story1.destroy
|
146
|
+
Story.get(cache_key).should == [story2.id]
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe 'when the value is nil' do
|
151
|
+
it "does not delete through the cache on indexed attributes when the value is nil" do
|
152
|
+
story = Story.create!(:title => nil)
|
153
|
+
story.destroy
|
154
|
+
Story.get("title/").should == nil
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe 'InstanceMethods' do
|
160
|
+
describe '#expire_caches' do
|
161
|
+
it 'deletes the index' do
|
162
|
+
story = Story.create!(:title => "I am delicious")
|
163
|
+
Story.get(cache_key = "id/#{story.id}").should == [story]
|
164
|
+
story.expire_caches
|
165
|
+
Story.get(cache_key).should be_nil
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
describe "Locking" do
|
172
|
+
it "acquires and releases locks, in order, for all indices to be written" do
|
173
|
+
pending
|
174
|
+
|
175
|
+
story = Story.create!(:title => original_title = "original title")
|
176
|
+
story.title = tentative_title = "tentative title"
|
177
|
+
keys = ["id/#{story.id}", "title/#{original_title}", "title/#{story.title}", "id/#{story.id}/title/#{original_title}", "id/#{story.id}/title/#{tentative_title}"]
|
178
|
+
|
179
|
+
locks_should_be_acquired_and_released_in_order($lock, keys)
|
180
|
+
story.save!
|
181
|
+
end
|
182
|
+
|
183
|
+
it "acquires and releases locks on destroy" do
|
184
|
+
pending
|
185
|
+
|
186
|
+
story = Story.create!(:title => "title")
|
187
|
+
keys = ["id/#{story.id}", "title/#{story.title}", "id/#{story.id}/title/#{story.title}"]
|
188
|
+
|
189
|
+
locks_should_be_acquired_and_released_in_order($lock, keys)
|
190
|
+
story.destroy
|
191
|
+
end
|
192
|
+
|
193
|
+
def locks_should_be_acquired_and_released_in_order(lock, keys)
|
194
|
+
mock = keys.sort!.inject(mock = mock($lock)) do |mock, key|
|
195
|
+
mock.acquire_lock.with(Story.cache_key(key)).then
|
196
|
+
end
|
197
|
+
keys.inject(mock) do |mock, key|
|
198
|
+
mock.release_lock.with(Story.cache_key(key)).then
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
describe "Single Table Inheritence" do
|
204
|
+
describe 'A subclass' do
|
205
|
+
it "writes to indices of all superclasses" do
|
206
|
+
oral = Oral.create!(:title => 'title')
|
207
|
+
Story.get("title/#{oral.title}").should == [oral.id]
|
208
|
+
Epic.get("title/#{oral.title}").should == [oral.id]
|
209
|
+
Oral.get("title/#{oral.title}").should == [oral.id]
|
210
|
+
end
|
211
|
+
|
212
|
+
describe 'when one ancestor has its own indices' do
|
213
|
+
it "it only populates those indices for that ancestor" do
|
214
|
+
oral = Oral.create!(:subtitle => 'subtitle')
|
215
|
+
Story.get("subtitle/#{oral.subtitle}").should be_nil
|
216
|
+
Epic.get("subtitle/#{oral.subtitle}").should be_nil
|
217
|
+
Oral.get("subtitle/#{oral.subtitle}").should == [oral.id]
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
dir = File.dirname(__FILE__)
|
2
|
+
$LOAD_PATH.unshift "#{dir}/../lib"
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'spec'
|
6
|
+
require 'pp'
|
7
|
+
require 'cash'
|
8
|
+
require 'memcache'
|
9
|
+
require File.join(dir, '../config/environment')
|
10
|
+
|
11
|
+
Spec::Runner.configure do |config|
|
12
|
+
config.mock_with :rr
|
13
|
+
config.before :suite do
|
14
|
+
load File.join(dir, "../db/schema.rb")
|
15
|
+
|
16
|
+
config = YAML.load(IO.read((File.expand_path(File.dirname(__FILE__) + "/../config/memcache.yml"))))['test']
|
17
|
+
$memcache = MemCache.new(config)
|
18
|
+
$memcache.servers = config['servers']
|
19
|
+
$lock = Cash::Lock.new($memcache)
|
20
|
+
end
|
21
|
+
|
22
|
+
config.before :each do
|
23
|
+
$memcache.flush_all
|
24
|
+
Story.delete_all
|
25
|
+
Character.delete_all
|
26
|
+
end
|
27
|
+
|
28
|
+
config.before :suite do
|
29
|
+
ActiveRecord::Base.class_eval do
|
30
|
+
is_cached :repository => Cash::Transactional.new($memcache, $lock)
|
31
|
+
end
|
32
|
+
|
33
|
+
Character = Class.new(ActiveRecord::Base)
|
34
|
+
Story = Class.new(ActiveRecord::Base)
|
35
|
+
Story.has_many :characters
|
36
|
+
|
37
|
+
Story.class_eval do
|
38
|
+
index :title
|
39
|
+
index [:id, :title]
|
40
|
+
end
|
41
|
+
|
42
|
+
Epic = Class.new(Story)
|
43
|
+
Oral = Class.new(Epic)
|
44
|
+
|
45
|
+
Character.class_eval do
|
46
|
+
index [:name, :story_id]
|
47
|
+
index [:id, :story_id]
|
48
|
+
index [:id, :name, :story_id]
|
49
|
+
end
|
50
|
+
|
51
|
+
Oral.class_eval do
|
52
|
+
index :subtitle
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|