cequel 0.0.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/lib/cequel.rb +16 -0
  2. data/lib/cequel/batch.rb +58 -0
  3. data/lib/cequel/cql_row_specification.rb +22 -0
  4. data/lib/cequel/data_set.rb +346 -0
  5. data/lib/cequel/errors.rb +4 -0
  6. data/lib/cequel/keyspace.rb +106 -0
  7. data/lib/cequel/model.rb +95 -0
  8. data/lib/cequel/model/associations.rb +120 -0
  9. data/lib/cequel/model/callbacks.rb +32 -0
  10. data/lib/cequel/model/class_internals.rb +48 -0
  11. data/lib/cequel/model/column.rb +20 -0
  12. data/lib/cequel/model/dictionary.rb +202 -0
  13. data/lib/cequel/model/dirty.rb +53 -0
  14. data/lib/cequel/model/dynamic.rb +31 -0
  15. data/lib/cequel/model/errors.rb +13 -0
  16. data/lib/cequel/model/inheritable.rb +48 -0
  17. data/lib/cequel/model/instance_internals.rb +23 -0
  18. data/lib/cequel/model/local_association.rb +42 -0
  19. data/lib/cequel/model/magic.rb +79 -0
  20. data/lib/cequel/model/mass_assignment_security.rb +21 -0
  21. data/lib/cequel/model/naming.rb +17 -0
  22. data/lib/cequel/model/observer.rb +42 -0
  23. data/lib/cequel/model/persistence.rb +173 -0
  24. data/lib/cequel/model/properties.rb +143 -0
  25. data/lib/cequel/model/railtie.rb +33 -0
  26. data/lib/cequel/model/remote_association.rb +40 -0
  27. data/lib/cequel/model/scope.rb +362 -0
  28. data/lib/cequel/model/scoped.rb +50 -0
  29. data/lib/cequel/model/subclass_internals.rb +45 -0
  30. data/lib/cequel/model/timestamps.rb +52 -0
  31. data/lib/cequel/model/translation.rb +17 -0
  32. data/lib/cequel/model/validations.rb +50 -0
  33. data/lib/cequel/new_relic_instrumentation.rb +22 -0
  34. data/lib/cequel/row_specification.rb +63 -0
  35. data/lib/cequel/statement.rb +23 -0
  36. data/lib/cequel/version.rb +3 -0
  37. data/spec/environment.rb +3 -0
  38. data/spec/examples/data_set_spec.rb +382 -0
  39. data/spec/examples/keyspace_spec.rb +63 -0
  40. data/spec/examples/model/associations_spec.rb +109 -0
  41. data/spec/examples/model/callbacks_spec.rb +79 -0
  42. data/spec/examples/model/dictionary_spec.rb +413 -0
  43. data/spec/examples/model/dirty_spec.rb +39 -0
  44. data/spec/examples/model/dynamic_spec.rb +41 -0
  45. data/spec/examples/model/inheritable_spec.rb +45 -0
  46. data/spec/examples/model/magic_spec.rb +199 -0
  47. data/spec/examples/model/mass_assignment_security_spec.rb +13 -0
  48. data/spec/examples/model/naming_spec.rb +9 -0
  49. data/spec/examples/model/observer_spec.rb +86 -0
  50. data/spec/examples/model/persistence_spec.rb +201 -0
  51. data/spec/examples/model/properties_spec.rb +81 -0
  52. data/spec/examples/model/scope_spec.rb +677 -0
  53. data/spec/examples/model/serialization_spec.rb +20 -0
  54. data/spec/examples/model/spec_helper.rb +12 -0
  55. data/spec/examples/model/timestamps_spec.rb +52 -0
  56. data/spec/examples/model/translation_spec.rb +23 -0
  57. data/spec/examples/model/validations_spec.rb +86 -0
  58. data/spec/examples/spec_helper.rb +9 -0
  59. data/spec/models/asset.rb +21 -0
  60. data/spec/models/asset_observer.rb +5 -0
  61. data/spec/models/blog.rb +14 -0
  62. data/spec/models/blog_posts.rb +6 -0
  63. data/spec/models/category.rb +9 -0
  64. data/spec/models/comment.rb +12 -0
  65. data/spec/models/photo.rb +5 -0
  66. data/spec/models/post.rb +88 -0
  67. data/spec/models/post_comments.rb +14 -0
  68. data/spec/models/post_observer.rb +43 -0
  69. data/spec/support/helpers.rb +26 -0
  70. data/spec/support/result_stub.rb +27 -0
  71. metadata +125 -23
@@ -0,0 +1,143 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ module Properties
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ include ActiveModel::Conversion
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+ def key(key_alias, type)
16
+ key_alias = key_alias.to_sym
17
+ @_cequel.key = Column.new(key_alias, type)
18
+
19
+ module_eval(<<-RUBY, __FILE__, __LINE__+1)
20
+ def #{key_alias}
21
+ @_cequel.key
22
+ end
23
+
24
+ def #{key_alias}=(key)
25
+ @_cequel.key = key
26
+ end
27
+
28
+ def to_key
29
+ [@_cequel.key]
30
+ end
31
+ RUBY
32
+ end
33
+
34
+ def column(name, type, options = {})
35
+ name = name.to_sym
36
+ @_cequel.add_column(name, type, options.symbolize_keys)
37
+
38
+ module_eval <<-RUBY, __FILE__, __LINE__+1
39
+ def #{name}
40
+ read_attribute(#{name.inspect})
41
+ end
42
+
43
+ def #{name}=(value)
44
+ write_attribute(#{name.inspect}, value)
45
+ end
46
+ RUBY
47
+
48
+ if type == :boolean
49
+ module_eval <<-RUBY, __FILE__, __LINE__+1 if type == :boolean
50
+ def #{name}?
51
+ !!read_attribute(#{name.inspect})
52
+ end
53
+ RUBY
54
+ end
55
+ end
56
+
57
+ def key_alias
58
+ key_column.name
59
+ end
60
+
61
+ def key_column
62
+ @_cequel.key
63
+ end
64
+
65
+ def column_names
66
+ [@_cequel.key.name, *@_cequel.columns.keys]
67
+ end
68
+
69
+ def columns
70
+ [@_cequel.key, *@_cequel.columns.values]
71
+ end
72
+
73
+ def type_column
74
+ @_cequel.type_column
75
+ end
76
+
77
+ end
78
+
79
+ def initialize(attributes = {})
80
+ super()
81
+ @_cequel.key = generate_key
82
+ self.class.columns.each do |column|
83
+ default = column.default
84
+ @_cequel.attributes[column.name] = default unless default.nil?
85
+ end
86
+ self.attributes = attributes
87
+ yield self if block_given?
88
+ end
89
+
90
+ def attributes
91
+ {self.class.key_alias => @_cequel.key}.with_indifferent_access.
92
+ merge(@_cequel.attributes)
93
+ end
94
+
95
+ def attributes=(attributes)
96
+ attributes.each_pair do |column_name, value|
97
+ __send__("#{column_name}=", value)
98
+ end
99
+ end
100
+
101
+ def ==(other)
102
+ return false if self.class != other.class
103
+ self_key = self.__send__(self.class.key_column.name)
104
+ other_key = other.__send__(self.class.key_column.name)
105
+ self_key && other_key && self_key == other_key
106
+ end
107
+
108
+ def inspect
109
+ "#<#{self.class.name}".tap do |inspected|
110
+ attributes.each_pair do |column, value|
111
+ inspected_value =
112
+ case value
113
+ when CassandraCQL::UUID then value.to_guid
114
+ else value.inspect
115
+ end
116
+ inspected << " #{column}:#{inspected_value}"
117
+ end
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def write_attribute(column_name, value)
124
+ if value.nil?
125
+ @_cequel.attributes.delete(column_name)
126
+ else
127
+ @_cequel.attributes[column_name] = value
128
+ end
129
+ end
130
+
131
+ def read_attribute(column_name)
132
+ @_cequel.attributes[column_name.to_sym]
133
+ end
134
+
135
+ def generate_key
136
+ # Noop -- model classes can override if desired
137
+ end
138
+
139
+ end
140
+
141
+ end
142
+
143
+ end
@@ -0,0 +1,33 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ class Railtie < Rails::Railtie
6
+
7
+ config.cequel = Cequel::Model
8
+
9
+ initializer "cequel.configure_rails" do
10
+ config_path = Rails.root.join('config/cequel.yml').to_s
11
+
12
+ if File.exist?(config_path)
13
+ yaml = YAML.load_file(config_path)[Rails.env]
14
+ Cequel::Model.configure(yaml.symbolize_keys) if yaml
15
+ end
16
+
17
+ Cequel::Model.logger = Rails.logger
18
+ end
19
+
20
+ initializer "cequel.instantiate_observers" do
21
+ config.after_initialize do
22
+ ::Cequel::Model.instantiate_observers
23
+
24
+ ActionDispatch::Callbacks.to_prepare do
25
+ ::Cequel::Model.instantiate_observers
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,40 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ class RemoteAssociation
6
+
7
+ attr_reader :name, :class_name, :dependent
8
+
9
+ def initialize(name, owning_class, options)
10
+ @name, @owning_class = name, owning_class
11
+ @class_name = options[:class_name] || name.to_s.classify.to_sym
12
+ @foreign_key_name = options[:foreign_key]
13
+ @dependent = options[:dependent]
14
+ end
15
+
16
+ def scope(instance)
17
+ clazz.where(foreign_key_name => instance.__send__(primary_key_name))
18
+ end
19
+
20
+ def primary_key
21
+ @primary_key ||= @owning_class.key_column
22
+ end
23
+
24
+ def primary_key_name
25
+ @primary_key_name ||= primary_key.name
26
+ end
27
+
28
+ def foreign_key_name
29
+ @foreign_key_name ||= :"#{@owning_class.name.underscore}_id"
30
+ end
31
+
32
+ def clazz
33
+ @clazz ||= class_name.to_s.constantize
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,362 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ class Scope < BasicObject
6
+
7
+ include ::Enumerable
8
+
9
+ def initialize(clazz, data_sets)
10
+ @clazz, @data_sets = clazz, data_sets
11
+ @index_preference_applied = false
12
+ end
13
+
14
+ def each(&block)
15
+ if block
16
+ each_row do |row|
17
+ result = hydrate(row)
18
+ yield result if result
19
+ end
20
+ else
21
+ ::Enumerator.new(self, :each)
22
+ end
23
+ end
24
+
25
+ def each_row(&block)
26
+ if block
27
+ apply_index_preference!
28
+ @data_sets.each do |data_set|
29
+ data_set.each(&block)
30
+ end
31
+ else
32
+ ::Enumerator.new(self, :each_row)
33
+ end
34
+ end
35
+
36
+ def find_in_batches(options = {})
37
+ unless ::Kernel.block_given?
38
+ return ::Enumerator.new(self, :find_in_batches, options)
39
+ end
40
+ find_rows_in_batches(options) do |batch|
41
+ results = batch.map { |row| hydrate(row) }.compact
42
+ yield results if results.any?
43
+ end
44
+ end
45
+
46
+ def find_rows_in_batches(options = {}, &block)
47
+ return ::Enumerator.new(self, :find_rows_in_batches, options) if block.nil?
48
+ batch_size = options[:batch_size] || 1000
49
+ apply_index_preference!
50
+ @data_sets.each do |data_set|
51
+ keys = lookup_keys(data_set)
52
+ if keys
53
+ find_rows_in_key_batches(data_set, keys, batch_size, &block)
54
+ else
55
+ find_rows_in_range_batches(data_set, batch_size, &block)
56
+ end
57
+ end
58
+ nil
59
+ end
60
+
61
+ def find_each(options = {}, &block)
62
+ unless ::Kernel.block_given?
63
+ return ::Enumerator.new(self, :find_each, options)
64
+ end
65
+ find_in_batches(options) { |batch| batch.each(&block) }
66
+ end
67
+
68
+ def find_each_row(options = {}, &block)
69
+ unless ::Kernel.block_given?
70
+ return ::Enumerator.new(self, :find_each_row, options)
71
+ end
72
+ find_rows_in_batches(options) { |batch| batch.each(&block) }
73
+ end
74
+
75
+ def first
76
+ apply_index_preference!
77
+ @data_sets.each do |data_set|
78
+ row = hydrate(data_set.first)
79
+ return row if row
80
+ end
81
+ nil
82
+ end
83
+
84
+ def count
85
+ if restriction_columns == [@clazz.key_alias]
86
+ ::Kernel.raise ::Cequel::Model::InvalidQuery,
87
+ "Meaningless to perform count with key row restrictions"
88
+ end
89
+ apply_index_preference!
90
+ @data_sets.inject(0) { |count, data_set| count + data_set.count }
91
+ end
92
+
93
+ def size
94
+ count
95
+ end
96
+
97
+ def length
98
+ to_a.length
99
+ end
100
+
101
+ def update_all(changes)
102
+ keys = keys()
103
+ unless keys.empty?
104
+ @clazz.column_family.where(key_alias => keys).update(changes)
105
+ end
106
+ end
107
+
108
+ def destroy_all
109
+ each { |instance| instance.destroy }
110
+ end
111
+
112
+ def delete_all
113
+ if @data_sets.length == 1
114
+ if @data_sets.first.row_specifications.length == 0
115
+ return @data_sets.first.truncate
116
+ end
117
+ end
118
+ keys = keys()
119
+ if keys.empty?
120
+ @data_sets.each { |data_set| data_set.delete }
121
+ else
122
+ @clazz.column_family.where(key_alias => keys).delete
123
+ end
124
+ end
125
+
126
+ def find(*keys, &block)
127
+ if block then super
128
+ else with_scope(self) { @clazz.find(*keys) }
129
+ end
130
+ end
131
+
132
+ def any?(&block)
133
+ if block then super
134
+ else count > 0
135
+ end
136
+ end
137
+
138
+ def none?(&block)
139
+ if block then super
140
+ else empty?
141
+ end
142
+ end
143
+
144
+ def empty?
145
+ count == 0
146
+ end
147
+
148
+ def one?(&block)
149
+ if block then super
150
+ else count == 1
151
+ end
152
+ end
153
+
154
+ def keys
155
+ key_alias = @clazz.key_alias
156
+ [].tap do |keys|
157
+ @data_sets.each do |data_set|
158
+ lookup_keys = lookup_keys(data_set)
159
+ if lookup_keys
160
+ keys.concat(lookup_keys)
161
+ next
162
+ end
163
+ data_set.select!(key_alias).each { |row| keys << row[key_alias] }
164
+ end
165
+ end
166
+ end
167
+
168
+ def lookup_keys(data_set)
169
+ if data_set.row_specifications.length == 1
170
+ specification = data_set.row_specifications.first
171
+ if specification.respond_to?(:column)
172
+ if specification.column == key_alias
173
+ ::Kernel.Array(specification.value)
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ def inspect
180
+ to_a.inspect
181
+ end
182
+
183
+ def ==(other)
184
+ to_a == other.to_a
185
+ end
186
+
187
+ def select(*rows, &block)
188
+ if block then super
189
+ else scoped { |data_set| data_set.select(*rows) }.validate!
190
+ end
191
+ end
192
+
193
+ def select!(*rows)
194
+ scoped { |data_set| data_set.select!(*rows) }.validate!
195
+ end
196
+
197
+ def consistency(consistency)
198
+ scoped { |data_set| data_set.consistency(consistency) }
199
+ end
200
+
201
+ def where(*row_specification)
202
+ if row_specification.length == 1 && ::Hash === row_specification.first
203
+ row_specification.first.each_pair.inject(self) do |scope, (column, value)|
204
+ scope.where_column_equals(column, value)
205
+ end
206
+ else
207
+ scoped { |data_set| data_set.where(*row_specification) }
208
+ end
209
+ end
210
+
211
+ def where!(*row_specification)
212
+ scoped { |data_set| data_set.where!(*row_specification) }
213
+ end
214
+
215
+ def limit(*row_specification)
216
+ scoped { |data_set| data_set.limit(*row_specification) }
217
+ end
218
+
219
+ def scoped(&block)
220
+ new_data_sets = @data_sets.map(&block)
221
+ Scope.new(@clazz, new_data_sets)
222
+ end
223
+
224
+ def nil?
225
+ false # for ActiveSupport delegation
226
+ end
227
+
228
+ def method_missing(method, *args, &block)
229
+ if @clazz.respond_to?(method)
230
+ @clazz.with_scope(self) do
231
+ @clazz.__send__(method, *args, &block)
232
+ end
233
+ else
234
+ super
235
+ end
236
+ end
237
+
238
+ protected
239
+
240
+ def validate!
241
+ columns = restriction_columns
242
+ key_column = restriction_columns.include?(@clazz.key_alias)
243
+ non_key_column = restriction_columns.any? do |column|
244
+ column != @clazz.key_alias
245
+ end
246
+ if key_column
247
+ if non_key_column
248
+ ::Kernel.raise InvalidQuery,
249
+ "Can't select by key and non-key columns in the same query"
250
+ elsif key_only_select?
251
+ ::Kernel.raise InvalidQuery,
252
+ "Meaningless to select only key column with key row specification"
253
+ end
254
+ end
255
+ self
256
+ end
257
+
258
+ def where_column_equals(column, value)
259
+ if [] == value
260
+ Scope.new(@clazz, [])
261
+ elsif column.to_sym != @clazz.key_alias && ::Array === value
262
+ new_data_sets = []
263
+ @data_sets.each do |data_set|
264
+ value.each do |element|
265
+ new_data_sets << data_set.where(column => element)
266
+ end
267
+ end
268
+ Scope.new(@clazz, new_data_sets)
269
+ else
270
+ scoped { |data_set| data_set.where(column => value) }
271
+ end.validate!
272
+ end
273
+
274
+ private
275
+
276
+ def find_rows_in_range_batches(data_set, batch_size)
277
+ key_alias = @clazz.key_alias
278
+ key_alias = key_alias.upcase if key_alias =~ /key/i
279
+ scope = data_set.limit(batch_size)
280
+ unless data_set.select_columns.empty? ||
281
+ data_set.select_columns.include?(key_alias)
282
+
283
+ scope = scope.select(key_alias)
284
+ end
285
+
286
+ batch_scope = scope
287
+ last_key = nil
288
+ begin
289
+ batch_rows = batch_scope.to_a
290
+ break if batch_rows.empty?
291
+ if batch_rows.first[key_alias] == last_key
292
+ yield batch_rows[1..-1]
293
+ else
294
+ yield batch_rows
295
+ end
296
+ last_key = batch_rows.last[key_alias]
297
+ batch_scope =
298
+ scope.where("? > ?", key_alias, last_key)
299
+ end while batch_rows.length == batch_size
300
+ end
301
+
302
+ def find_rows_in_key_batches(data_set, keys, batch_size)
303
+ key_alias = @clazz.key_alias
304
+ keys.each_slice(batch_size) do |key_slice|
305
+ yield data_set.where!(key_alias => key_slice).to_a
306
+ end
307
+ end
308
+
309
+ def key_only_select?
310
+ key_only_select = @data_sets.all? do |data_set|
311
+ data_set.select_columns == [@clazz.key_alias]
312
+ end
313
+ end
314
+
315
+ def restriction_columns
316
+ [].tap do |columns|
317
+ @data_sets.each do |data_set|
318
+ data_set.row_specifications.each do |specification|
319
+ if specification.respond_to?(:column)
320
+ columns << specification.column
321
+ end
322
+ end
323
+ end
324
+ end
325
+ end
326
+
327
+ def hydrate(row)
328
+ return if row.nil?
329
+ key_alias = @clazz.key_alias.to_s
330
+ key_alias = key_alias.upcase if key_alias =~ /^key$/i
331
+ row.reject! { |k, v| v.nil? }
332
+ if row.keys.any? && (key_only_select? || row.keys != [key_alias])
333
+ @clazz._hydrate(row)
334
+ end
335
+ end
336
+
337
+ def apply_index_preference!
338
+ return if @index_preference_applied
339
+ # XXX seems ugly to do the in-place sort here.
340
+ preference = @clazz.index_preference_columns
341
+ @data_sets.each do |data_set|
342
+ data_set.row_specifications.sort! do |spec1, spec2|
343
+ if spec1.respond_to?(:column) && spec2.respond_to?(:column)
344
+ pref1 = preference.index(spec1.column)
345
+ pref2 = preference.index(spec2.column)
346
+ if pref1 && pref2 then pref1 - pref2
347
+ elsif pref1 then -1
348
+ elsif pref2 then 1
349
+ else 0
350
+ end
351
+ else 0
352
+ end
353
+ end
354
+ end
355
+ @index_preference_applied = true
356
+ end
357
+
358
+ end
359
+
360
+ end
361
+
362
+ end