cequel 0.0.0 → 0.4.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.
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