cassandra_mapper 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.
- data/README.rdoc +98 -0
- data/Rakefile.rb +11 -0
- data/lib/cassandra_mapper.rb +5 -0
- data/lib/cassandra_mapper/base.rb +19 -0
- data/lib/cassandra_mapper/connection.rb +9 -0
- data/lib/cassandra_mapper/core_ext/array/extract_options.rb +29 -0
- data/lib/cassandra_mapper/core_ext/array/wrap.rb +22 -0
- data/lib/cassandra_mapper/core_ext/class/inheritable_attributes.rb +232 -0
- data/lib/cassandra_mapper/core_ext/kernel/reporting.rb +62 -0
- data/lib/cassandra_mapper/core_ext/kernel/singleton_class.rb +13 -0
- data/lib/cassandra_mapper/core_ext/module/aliasing.rb +70 -0
- data/lib/cassandra_mapper/core_ext/module/attribute_accessors.rb +66 -0
- data/lib/cassandra_mapper/core_ext/object/duplicable.rb +65 -0
- data/lib/cassandra_mapper/core_ext/string/inflections.rb +160 -0
- data/lib/cassandra_mapper/core_ext/string/multibyte.rb +72 -0
- data/lib/cassandra_mapper/exceptions.rb +10 -0
- data/lib/cassandra_mapper/identity.rb +29 -0
- data/lib/cassandra_mapper/indexing.rb +465 -0
- data/lib/cassandra_mapper/observable.rb +36 -0
- data/lib/cassandra_mapper/persistence.rb +309 -0
- data/lib/cassandra_mapper/support/callbacks.rb +136 -0
- data/lib/cassandra_mapper/support/concern.rb +31 -0
- data/lib/cassandra_mapper/support/dependencies.rb +60 -0
- data/lib/cassandra_mapper/support/descendants_tracker.rb +41 -0
- data/lib/cassandra_mapper/support/inflections.rb +58 -0
- data/lib/cassandra_mapper/support/inflector.rb +7 -0
- data/lib/cassandra_mapper/support/inflector/inflections.rb +213 -0
- data/lib/cassandra_mapper/support/inflector/methods.rb +143 -0
- data/lib/cassandra_mapper/support/inflector/transliterate.rb +99 -0
- data/lib/cassandra_mapper/support/multibyte.rb +46 -0
- data/lib/cassandra_mapper/support/multibyte/utils.rb +62 -0
- data/lib/cassandra_mapper/support/observing.rb +218 -0
- data/lib/cassandra_mapper/support/support_callbacks.rb +593 -0
- data/test/test_helper.rb +11 -0
- data/test/unit/callbacks_test.rb +100 -0
- data/test/unit/identity_test.rb +51 -0
- data/test/unit/indexing_test.rb +406 -0
- data/test/unit/observer_test.rb +56 -0
- data/test/unit/persistence_test.rb +561 -0
- metadata +192 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'cassandra_mapper/support/observing'
|
2
|
+
module CassandraMapper
|
3
|
+
module Observable
|
4
|
+
CALLBACKS = [
|
5
|
+
:after_load,
|
6
|
+
:before_create,
|
7
|
+
:after_create,
|
8
|
+
:before_update,
|
9
|
+
:after_update,
|
10
|
+
:before_save,
|
11
|
+
:after_save,
|
12
|
+
:before_destroy,
|
13
|
+
:after_destroy,
|
14
|
+
]
|
15
|
+
|
16
|
+
CALLBACKS.each do |cb|
|
17
|
+
name = cb.to_s
|
18
|
+
module_eval <<-cbnotify
|
19
|
+
def _notify_observer_#{name}; notify_observers(:#{name}); true; end
|
20
|
+
cbnotify
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.included(klass)
|
24
|
+
klass.module_eval do
|
25
|
+
include CassandraMapper::Support::Observing
|
26
|
+
CALLBACKS.each do |callback|
|
27
|
+
name = callback.to_s
|
28
|
+
send(callback, :"_notify_observer_#{name}")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Observer < CassandraMapper::Support::Observer
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,309 @@
|
|
1
|
+
require 'cassandra_mapper/support/callbacks'
|
2
|
+
module CassandraMapper::Persistence
|
3
|
+
def _determine_transform_options
|
4
|
+
options = {:string_keys => true}
|
5
|
+
is_update = false
|
6
|
+
if new_record?
|
7
|
+
options[:defined] = true
|
8
|
+
else
|
9
|
+
return false unless changed_attributes.length > 0
|
10
|
+
options[:changed] = true
|
11
|
+
is_update = true
|
12
|
+
end
|
13
|
+
[options, is_update]
|
14
|
+
end
|
15
|
+
|
16
|
+
def save(with_validation = true)
|
17
|
+
_run_save_callbacks do
|
18
|
+
uniq_key = _check_key
|
19
|
+
options, is_update = _determine_transform_options
|
20
|
+
return false unless options
|
21
|
+
if is_update
|
22
|
+
update(uniq_key, options)
|
23
|
+
else
|
24
|
+
create(uniq_key, options)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def loaded!
|
30
|
+
self.new_record = false
|
31
|
+
_run_load_callbacks
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def create(uniq_key, options)
|
36
|
+
_run_create_callbacks do
|
37
|
+
write!(uniq_key, options)
|
38
|
+
self.new_record = false
|
39
|
+
self
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def update(uniq_key, options)
|
44
|
+
_run_update_callbacks do
|
45
|
+
write!(uniq_key, options.merge({ :with_delete => true }))
|
46
|
+
self
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def destroy
|
51
|
+
unless new_record?
|
52
|
+
_run_destroy_callbacks do
|
53
|
+
self.class.delete(_check_key)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
@destroyed = true
|
57
|
+
freeze
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
def write!(uniq_key, options)
|
62
|
+
with_delete = options.delete(:with_delete)
|
63
|
+
structure = to_simple(options)
|
64
|
+
if with_delete
|
65
|
+
deletes = self.class.prune_deletes_from_simple_structure(structure)
|
66
|
+
cassandra_args = []
|
67
|
+
if ! (structure.empty? or deletes.empty?) or deletes.size >= 2
|
68
|
+
cassandra_args << {:timestamp => Time.stamp}
|
69
|
+
end
|
70
|
+
base_args = [self.class.column_family, uniq_key]
|
71
|
+
connection.insert(*(base_args + [structure] + cassandra_args)) unless structure.empty?
|
72
|
+
deletes.each {|del_args| connection.remove(*(base_args + del_args + cassandra_args))}
|
73
|
+
else
|
74
|
+
connection.insert(self.class.column_family, uniq_key, to_simple(options))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_mutation(with_validation = true, options = {})
|
79
|
+
uniq_key = _check_key.to_s
|
80
|
+
timestamp = options.delete(:timestamp) || Time.stamp
|
81
|
+
general_opts, is_update = _determine_transform_options
|
82
|
+
return false unless general_opts
|
83
|
+
options.merge!(general_opts)
|
84
|
+
{
|
85
|
+
uniq_key => {
|
86
|
+
self.class.column_family.to_s => self.class.to_mutation(to_simple(options), timestamp)
|
87
|
+
}
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def _check_key
|
92
|
+
uniq_key = self.key
|
93
|
+
raise CassandraMapper::UndefinedKeyException if uniq_key.nil?
|
94
|
+
uniq_key
|
95
|
+
end
|
96
|
+
|
97
|
+
def delete
|
98
|
+
self.class.delete(_check_key) unless new_record?
|
99
|
+
@destroyed = true
|
100
|
+
freeze
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
def destroyed?
|
105
|
+
(@destroyed && true) || false
|
106
|
+
end
|
107
|
+
|
108
|
+
module ClassMethods
|
109
|
+
# Given a single key or list of keys, returns all mapped objects found
|
110
|
+
# for thoese keys.
|
111
|
+
#
|
112
|
+
# If the row for a specified key is missing, a CassndraMapper::RecordNotFoundException
|
113
|
+
# exception is raised. This can be overridden by specifying the +:allow_missing+
|
114
|
+
# option (:allow_missing => true)
|
115
|
+
#
|
116
|
+
# Keys and options may be specified in a variety of ways:
|
117
|
+
# * Flat list
|
118
|
+
# SomeClass.find(key1, key2, key3, options)
|
119
|
+
# * Separate lists
|
120
|
+
# SomeClass.find([key1, key2, key3], options)
|
121
|
+
#
|
122
|
+
# And of course, _options_ can always be left out.
|
123
|
+
def find(*args)
|
124
|
+
single = false
|
125
|
+
case args.first
|
126
|
+
when Array
|
127
|
+
keys = args.first
|
128
|
+
when nil
|
129
|
+
raise CassandraMapper::InvalidArgumentException
|
130
|
+
else
|
131
|
+
keys = args
|
132
|
+
single = true if keys.length == 1
|
133
|
+
end
|
134
|
+
case args.last
|
135
|
+
when Hash
|
136
|
+
options = args.pop
|
137
|
+
else
|
138
|
+
options = {}
|
139
|
+
end
|
140
|
+
|
141
|
+
result = connection.multi_get(column_family, keys).values.inject([]) do |arr, hash|
|
142
|
+
if not hash.empty?
|
143
|
+
obj = new(hash)
|
144
|
+
obj.new_record = false
|
145
|
+
arr << obj
|
146
|
+
obj.loaded!
|
147
|
+
end
|
148
|
+
arr
|
149
|
+
end
|
150
|
+
raise CassandraMapper::RecordNotFoundException unless result.size == keys.size or options[:allow_missing]
|
151
|
+
single ? result.first : result
|
152
|
+
end
|
153
|
+
|
154
|
+
# Given a _key_ of a single row key or array of row keys,
|
155
|
+
# removes the rows from the Cassandra column family associated with
|
156
|
+
# the receiving class.
|
157
|
+
#
|
158
|
+
# As the underlying Cassandra client returns no information about the
|
159
|
+
# result of the operation, the result is always +nil+.
|
160
|
+
#
|
161
|
+
# The delete operation is purely database-side; no objects are instantiated
|
162
|
+
# and therefore no object callbacks take place for the deleted rows.
|
163
|
+
# This makes delete risky for classes that manage associations, indexes, etc.
|
164
|
+
def delete(key)
|
165
|
+
keys = Array === key ? key : [key]
|
166
|
+
keys.each do |key|
|
167
|
+
connection.remove(column_family, key)
|
168
|
+
end
|
169
|
+
nil
|
170
|
+
end
|
171
|
+
|
172
|
+
# Similar to +delete+; given a _key_ of a single row key or array of row keys,
|
173
|
+
# removes the rows from the Cassandra column family associated with the receiving
|
174
|
+
# class.
|
175
|
+
#
|
176
|
+
# However, unlike +delete+, +destroy+ loads up the object per row key in turn
|
177
|
+
# and invokes +destroy+ on each object. This allows for callbacks and observers
|
178
|
+
# to be executed, which is essential for index maintenance, association maintenance,
|
179
|
+
# etc.
|
180
|
+
def destroy(key)
|
181
|
+
objs = find(key)
|
182
|
+
case objs
|
183
|
+
when Array
|
184
|
+
objs.each {|obj| obj.destroy}
|
185
|
+
else
|
186
|
+
objs.destroy
|
187
|
+
end
|
188
|
+
nil
|
189
|
+
end
|
190
|
+
|
191
|
+
def connection
|
192
|
+
@cassandra_mapper_connection
|
193
|
+
end
|
194
|
+
|
195
|
+
def connection=(connection)
|
196
|
+
@cassandra_mapper_connection = connection
|
197
|
+
end
|
198
|
+
|
199
|
+
def column_family(family = nil)
|
200
|
+
@cassandra_mapper_column_family = family if ! family.nil?
|
201
|
+
@cassandra_mapper_column_family
|
202
|
+
end
|
203
|
+
|
204
|
+
def self.extended(klass)
|
205
|
+
klass.module_eval do
|
206
|
+
extend CassandraMapper::Support::Callbacks
|
207
|
+
define_model_callbacks :save, :create, :update, :destroy
|
208
|
+
define_model_callbacks :load, :only => :after
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def to_mutation(simple_structure, timestamp)
|
213
|
+
mutator.from_simple(simple_structure, timestamp)
|
214
|
+
end
|
215
|
+
|
216
|
+
def mutator
|
217
|
+
unless @mutator
|
218
|
+
mutator_class = simple_mapper.attributes.first[1].mapper ? SuperMutator : SimpleMutator
|
219
|
+
@mutator = mutator_class.new
|
220
|
+
end
|
221
|
+
@mutator
|
222
|
+
end
|
223
|
+
|
224
|
+
class SimpleMutator
|
225
|
+
def from_simple(structure, timestamp)
|
226
|
+
deletion = nil
|
227
|
+
deletion_columns = nil
|
228
|
+
structure.inject([]) do |list, pair|
|
229
|
+
key,val = pair
|
230
|
+
if val.nil?
|
231
|
+
unless deletion
|
232
|
+
deletion = CassandraThrift::Mutation.new(
|
233
|
+
:deletion => CassandraThrift::Deletion.new(
|
234
|
+
:super_column => nil,
|
235
|
+
:timestamp => timestamp,
|
236
|
+
:predicate => CassandraThrift::SlicePredicate.new(
|
237
|
+
:column_names => (deletion_columns = [])
|
238
|
+
)
|
239
|
+
)
|
240
|
+
)
|
241
|
+
list << deletion
|
242
|
+
end
|
243
|
+
deletion_columns << key
|
244
|
+
else
|
245
|
+
list << CassandraThrift::Mutation.new(
|
246
|
+
:column_or_supercolumn => CassandraThrift::ColumnOrSuperColumn.new(
|
247
|
+
:column => CassandraThrift::Column.new(
|
248
|
+
:name => key,
|
249
|
+
:value => val,
|
250
|
+
:timestamp => timestamp
|
251
|
+
)
|
252
|
+
)
|
253
|
+
)
|
254
|
+
end
|
255
|
+
list
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
class SuperMutator
|
261
|
+
def from_simple(structure, timestamp)
|
262
|
+
structure.inject([]) do |list, pair|
|
263
|
+
supercol_key, val = pair
|
264
|
+
if val and ! val.empty?
|
265
|
+
list << CassandraThrift::Mutation.new(
|
266
|
+
:column_or_supercolumn => CassandraThrift::ColumnOrSuperColumn.new(
|
267
|
+
:super_column => CassandraThrift::SuperColumn.new(
|
268
|
+
:name => supercol_key,
|
269
|
+
:columns => val.collect {|column, value|
|
270
|
+
CassandraThrift::Column.new(
|
271
|
+
:name => column,
|
272
|
+
:value => value,
|
273
|
+
:timestamp => timestamp
|
274
|
+
)
|
275
|
+
}
|
276
|
+
)
|
277
|
+
)
|
278
|
+
)
|
279
|
+
end
|
280
|
+
list
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def prune_deletes(source, deletes, context)
|
286
|
+
source.each do |k, v|
|
287
|
+
if v.nil?
|
288
|
+
deletes << context + [k]
|
289
|
+
source.delete(k)
|
290
|
+
# pre 1.9 String responds to :each; but :values_at is a
|
291
|
+
# safe bet for 1.8 and 1.9 for array, hash, but not string.
|
292
|
+
elsif v.respond_to?(:values_at)
|
293
|
+
prune_deletes(v, deletes, context + [k])
|
294
|
+
source.delete(k) if v.empty?
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def prune_deletes_from_simple_structure(structure)
|
300
|
+
deletes = []
|
301
|
+
prune_deletes(structure, deletes, [])
|
302
|
+
deletes
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def self.included(klass)
|
307
|
+
klass.extend ClassMethods
|
308
|
+
end
|
309
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'cassandra_mapper/core_ext/array/wrap'
|
2
|
+
require 'cassandra_mapper/support/support_callbacks'
|
3
|
+
|
4
|
+
module CassandraMapper
|
5
|
+
module Support
|
6
|
+
# == Active Model Callbacks
|
7
|
+
#
|
8
|
+
# Provides an interface for any class to have Active Record like callbacks.
|
9
|
+
#
|
10
|
+
# Like the Active Record methods, the callback chain is aborted as soon as
|
11
|
+
# one of the methods in the chain returns false.
|
12
|
+
#
|
13
|
+
# First, extend ActiveModel::Callbacks from the class you are creating:
|
14
|
+
#
|
15
|
+
# class MyModel
|
16
|
+
# extend ActiveModel::Callbacks
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# Then define a list of methods that you want callbacks attached to:
|
20
|
+
#
|
21
|
+
# define_model_callbacks :create, :update
|
22
|
+
#
|
23
|
+
# This will provide all three standard callbacks (before, around and after) around
|
24
|
+
# both the :create and :update methods. To implement, you need to wrap the methods
|
25
|
+
# you want callbacks on in a block so that the callbacks get a chance to fire:
|
26
|
+
#
|
27
|
+
# def create
|
28
|
+
# _run_create_callbacks do
|
29
|
+
# # Your create action methods here
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# The _run_<method_name>_callbacks methods are dynamically created when you extend
|
34
|
+
# the <tt>ActiveModel::Callbacks</tt> module.
|
35
|
+
#
|
36
|
+
# Then in your class, you can use the +before_create+, +after_create+ and +around_create+
|
37
|
+
# methods, just as you would in an Active Record module.
|
38
|
+
#
|
39
|
+
# before_create :action_before_create
|
40
|
+
#
|
41
|
+
# def action_before_create
|
42
|
+
# # Your code here
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# You can choose not to have all three callbacks by passing a hash to the
|
46
|
+
# define_model_callbacks method.
|
47
|
+
#
|
48
|
+
# define_model_callbacks :create, :only => :after, :before
|
49
|
+
#
|
50
|
+
# Would only create the after_create and before_create callback methods in your
|
51
|
+
# class.
|
52
|
+
module Callbacks
|
53
|
+
def self.extended(base)
|
54
|
+
base.class_eval do
|
55
|
+
include CassandraMapper::Support::SupportCallbacks
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# define_model_callbacks accepts the same options define_callbacks does, in case
|
60
|
+
# you want to overwrite a default. Besides that, it also accepts an :only option,
|
61
|
+
# where you can choose if you want all types (before, around or after) or just some.
|
62
|
+
#
|
63
|
+
# define_model_callbacks :initializer, :only => :after
|
64
|
+
#
|
65
|
+
# Note, the <tt>:only => <type></tt> hash will apply to all callbacks defined on
|
66
|
+
# that method call. To get around this you can call the define_model_callbacks
|
67
|
+
# method as many times as you need.
|
68
|
+
#
|
69
|
+
# define_model_callbacks :create, :only => :after
|
70
|
+
# define_model_callbacks :update, :only => :before
|
71
|
+
# define_model_callbacks :destroy, :only => :around
|
72
|
+
#
|
73
|
+
# Would create +after_create+, +before_update+ and +around_destroy+ methods only.
|
74
|
+
#
|
75
|
+
# You can pass in a class to before_<type>, after_<type> and around_<type>, in which
|
76
|
+
# case the callback will call that class's <action>_<type> method passing the object
|
77
|
+
# that the callback is being called on.
|
78
|
+
#
|
79
|
+
# class MyModel
|
80
|
+
# extend ActiveModel::Callbacks
|
81
|
+
# define_model_callbacks :create
|
82
|
+
#
|
83
|
+
# before_create AnotherClass
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# class AnotherClass
|
87
|
+
# def self.before_create( obj )
|
88
|
+
# # obj is the MyModel instance that the callback is being called on
|
89
|
+
# end
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
def define_model_callbacks(*callbacks)
|
93
|
+
options = callbacks.extract_options!
|
94
|
+
options = { :terminator => "result == false", :scope => [:kind, :name] }.merge(options)
|
95
|
+
|
96
|
+
types = Array.wrap(options.delete(:only))
|
97
|
+
types = [:before, :around, :after] if types.empty?
|
98
|
+
|
99
|
+
callbacks.each do |callback|
|
100
|
+
define_callbacks(callback, options)
|
101
|
+
|
102
|
+
types.each do |type|
|
103
|
+
send(:"_define_#{type}_model_callback", self, callback)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def _define_before_model_callback(klass, callback) #:nodoc:
|
109
|
+
klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1
|
110
|
+
def self.before_#{callback}(*args, &block)
|
111
|
+
set_callback(:#{callback}, :before, *args, &block)
|
112
|
+
end
|
113
|
+
CALLBACK
|
114
|
+
end
|
115
|
+
|
116
|
+
def _define_around_model_callback(klass, callback) #:nodoc:
|
117
|
+
klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1
|
118
|
+
def self.around_#{callback}(*args, &block)
|
119
|
+
set_callback(:#{callback}, :around, *args, &block)
|
120
|
+
end
|
121
|
+
CALLBACK
|
122
|
+
end
|
123
|
+
|
124
|
+
def _define_after_model_callback(klass, callback) #:nodoc:
|
125
|
+
klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1
|
126
|
+
def self.after_#{callback}(*args, &block)
|
127
|
+
options = args.extract_options!
|
128
|
+
options[:prepend] = true
|
129
|
+
options[:if] = Array.wrap(options[:if]) << "!halted && value != false"
|
130
|
+
set_callback(:#{callback}, :after, *(args << options), &block)
|
131
|
+
end
|
132
|
+
CALLBACK
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|