datastax_rails 1.0.5

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 (148) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +62 -0
  3. data/Rakefile +34 -0
  4. data/config/schema.xml +266 -0
  5. data/config/schema.xml.erb +70 -0
  6. data/config/solrconfig.xml +1564 -0
  7. data/config/stopwords.txt +58 -0
  8. data/lib/datastax_rails/associations/association.rb +224 -0
  9. data/lib/datastax_rails/associations/association_scope.rb +25 -0
  10. data/lib/datastax_rails/associations/belongs_to_association.rb +64 -0
  11. data/lib/datastax_rails/associations/builder/association.rb +56 -0
  12. data/lib/datastax_rails/associations/builder/belongs_to.rb +30 -0
  13. data/lib/datastax_rails/associations/builder/collection_association.rb +48 -0
  14. data/lib/datastax_rails/associations/builder/has_and_belongs_to_many.rb +36 -0
  15. data/lib/datastax_rails/associations/builder/has_many.rb +54 -0
  16. data/lib/datastax_rails/associations/builder/has_one.rb +52 -0
  17. data/lib/datastax_rails/associations/builder/singular_association.rb +56 -0
  18. data/lib/datastax_rails/associations/collection_association.rb +274 -0
  19. data/lib/datastax_rails/associations/collection_proxy.rb +118 -0
  20. data/lib/datastax_rails/associations/has_and_belongs_to_many_association.rb +44 -0
  21. data/lib/datastax_rails/associations/has_many_association.rb +58 -0
  22. data/lib/datastax_rails/associations/has_one_association.rb +68 -0
  23. data/lib/datastax_rails/associations/singular_association.rb +58 -0
  24. data/lib/datastax_rails/associations.rb +86 -0
  25. data/lib/datastax_rails/attribute_methods/definition.rb +20 -0
  26. data/lib/datastax_rails/attribute_methods/dirty.rb +43 -0
  27. data/lib/datastax_rails/attribute_methods/typecasting.rb +50 -0
  28. data/lib/datastax_rails/attribute_methods.rb +104 -0
  29. data/lib/datastax_rails/base.rb +587 -0
  30. data/lib/datastax_rails/batches.rb +35 -0
  31. data/lib/datastax_rails/callbacks.rb +37 -0
  32. data/lib/datastax_rails/collection.rb +9 -0
  33. data/lib/datastax_rails/connection.rb +21 -0
  34. data/lib/datastax_rails/consistency.rb +33 -0
  35. data/lib/datastax_rails/cql/base.rb +15 -0
  36. data/lib/datastax_rails/cql/column_family.rb +38 -0
  37. data/lib/datastax_rails/cql/consistency.rb +13 -0
  38. data/lib/datastax_rails/cql/create_column_family.rb +63 -0
  39. data/lib/datastax_rails/cql/create_keyspace.rb +30 -0
  40. data/lib/datastax_rails/cql/delete.rb +41 -0
  41. data/lib/datastax_rails/cql/drop_column_family.rb +13 -0
  42. data/lib/datastax_rails/cql/drop_keyspace.rb +13 -0
  43. data/lib/datastax_rails/cql/insert.rb +53 -0
  44. data/lib/datastax_rails/cql/select.rb +51 -0
  45. data/lib/datastax_rails/cql/truncate.rb +13 -0
  46. data/lib/datastax_rails/cql/update.rb +68 -0
  47. data/lib/datastax_rails/cql/use_keyspace.rb +13 -0
  48. data/lib/datastax_rails/cql.rb +25 -0
  49. data/lib/datastax_rails/cursor.rb +90 -0
  50. data/lib/datastax_rails/errors.rb +16 -0
  51. data/lib/datastax_rails/identity/abstract_key_factory.rb +26 -0
  52. data/lib/datastax_rails/identity/custom_key_factory.rb +36 -0
  53. data/lib/datastax_rails/identity/hashed_natural_key_factory.rb +10 -0
  54. data/lib/datastax_rails/identity/natural_key_factory.rb +37 -0
  55. data/lib/datastax_rails/identity/uuid_key_factory.rb +23 -0
  56. data/lib/datastax_rails/identity.rb +53 -0
  57. data/lib/datastax_rails/log_subscriber.rb +37 -0
  58. data/lib/datastax_rails/migrations/migration.rb +15 -0
  59. data/lib/datastax_rails/migrations.rb +36 -0
  60. data/lib/datastax_rails/mocking.rb +15 -0
  61. data/lib/datastax_rails/persistence.rb +133 -0
  62. data/lib/datastax_rails/railtie.rb +20 -0
  63. data/lib/datastax_rails/reflection.rb +472 -0
  64. data/lib/datastax_rails/relation/finder_methods.rb +184 -0
  65. data/lib/datastax_rails/relation/modification_methods.rb +80 -0
  66. data/lib/datastax_rails/relation/search_methods.rb +349 -0
  67. data/lib/datastax_rails/relation/spawn_methods.rb +107 -0
  68. data/lib/datastax_rails/relation.rb +393 -0
  69. data/lib/datastax_rails/schema/migration.rb +106 -0
  70. data/lib/datastax_rails/schema/migration_proxy.rb +25 -0
  71. data/lib/datastax_rails/schema/migrator.rb +212 -0
  72. data/lib/datastax_rails/schema.rb +37 -0
  73. data/lib/datastax_rails/scoping.rb +394 -0
  74. data/lib/datastax_rails/serialization.rb +6 -0
  75. data/lib/datastax_rails/tasks/column_family.rb +162 -0
  76. data/lib/datastax_rails/tasks/ds.rake +63 -0
  77. data/lib/datastax_rails/tasks/keyspace.rb +57 -0
  78. data/lib/datastax_rails/timestamps.rb +19 -0
  79. data/lib/datastax_rails/type.rb +16 -0
  80. data/lib/datastax_rails/types/array_type.rb +77 -0
  81. data/lib/datastax_rails/types/base_type.rb +26 -0
  82. data/lib/datastax_rails/types/binary_type.rb +15 -0
  83. data/lib/datastax_rails/types/boolean_type.rb +22 -0
  84. data/lib/datastax_rails/types/date_type.rb +17 -0
  85. data/lib/datastax_rails/types/float_type.rb +18 -0
  86. data/lib/datastax_rails/types/integer_type.rb +18 -0
  87. data/lib/datastax_rails/types/string_type.rb +16 -0
  88. data/lib/datastax_rails/types/text_type.rb +16 -0
  89. data/lib/datastax_rails/types/time_type.rb +17 -0
  90. data/lib/datastax_rails/types.rb +9 -0
  91. data/lib/datastax_rails/validations/uniqueness.rb +119 -0
  92. data/lib/datastax_rails/validations.rb +48 -0
  93. data/lib/datastax_rails/version.rb +3 -0
  94. data/lib/datastax_rails.rb +87 -0
  95. data/lib/solr_no_escape.rb +28 -0
  96. data/spec/datastax_rails/associations/belongs_to_association_spec.rb +7 -0
  97. data/spec/datastax_rails/associations/has_many_association_spec.rb +37 -0
  98. data/spec/datastax_rails/associations_spec.rb +22 -0
  99. data/spec/datastax_rails/attribute_methods_spec.rb +23 -0
  100. data/spec/datastax_rails/base_spec.rb +15 -0
  101. data/spec/datastax_rails/cql/select_spec.rb +12 -0
  102. data/spec/datastax_rails/cql/update_spec.rb +0 -0
  103. data/spec/datastax_rails/relation/finder_methods_spec.rb +54 -0
  104. data/spec/datastax_rails/relation/modification_methods_spec.rb +41 -0
  105. data/spec/datastax_rails/relation/search_methods_spec.rb +117 -0
  106. data/spec/datastax_rails/relation/spawn_methods_spec.rb +28 -0
  107. data/spec/datastax_rails/relation_spec.rb +130 -0
  108. data/spec/datastax_rails/validations/uniqueness_spec.rb +41 -0
  109. data/spec/datastax_rails_spec.rb +5 -0
  110. data/spec/dummy/Rakefile +8 -0
  111. data/spec/dummy/app/assets/javascripts/application.js +9 -0
  112. data/spec/dummy/app/assets/stylesheets/application.css +7 -0
  113. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  114. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  115. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  116. data/spec/dummy/config/application.rb +47 -0
  117. data/spec/dummy/config/boot.rb +10 -0
  118. data/spec/dummy/config/database.yml +25 -0
  119. data/spec/dummy/config/datastax.yml +18 -0
  120. data/spec/dummy/config/environment.rb +5 -0
  121. data/spec/dummy/config/environments/development.rb +30 -0
  122. data/spec/dummy/config/environments/production.rb +60 -0
  123. data/spec/dummy/config/environments/test.rb +39 -0
  124. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  125. data/spec/dummy/config/initializers/inflections.rb +10 -0
  126. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  127. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  128. data/spec/dummy/config/initializers/session_store.rb +8 -0
  129. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  130. data/spec/dummy/config/locales/en.yml +5 -0
  131. data/spec/dummy/config/routes.rb +58 -0
  132. data/spec/dummy/config/sunspot.yml +17 -0
  133. data/spec/dummy/config.ru +4 -0
  134. data/spec/dummy/ks/migrate/20111117224534_models.rb +20 -0
  135. data/spec/dummy/ks/schema.json +180 -0
  136. data/spec/dummy/log/development.log +298 -0
  137. data/spec/dummy/log/production.log +0 -0
  138. data/spec/dummy/log/test.log +20307 -0
  139. data/spec/dummy/public/404.html +26 -0
  140. data/spec/dummy/public/422.html +26 -0
  141. data/spec/dummy/public/500.html +26 -0
  142. data/spec/dummy/public/favicon.ico +0 -0
  143. data/spec/dummy/script/rails +6 -0
  144. data/spec/spec.opts +5 -0
  145. data/spec/spec_helper.rb +29 -0
  146. data/spec/support/datastax_test_hook.rb +14 -0
  147. data/spec/support/models.rb +72 -0
  148. metadata +353 -0
@@ -0,0 +1,56 @@
1
+ module DatastaxRails::Associations::Builder
2
+ class SingularAssociation < Association #:nodoc:
3
+ #self.valid_options += [:remote, :dependent, :counter_cache, :primary_key, :inverse_of]
4
+ self.valid_options += [:dependent, :denorm]
5
+
6
+ def constructable?
7
+ true
8
+ end
9
+
10
+ def define_accessors
11
+ super
12
+ define_constructors if constructable?
13
+ end
14
+
15
+ def build
16
+ relation = super
17
+ configure_denorm
18
+ relation
19
+ end
20
+
21
+ private
22
+
23
+ def configure_denorm
24
+ if options[:denorm]
25
+ unless options[:denorm].is_a?(Hash)
26
+ raise ArgumentError, "The :denorm option expects a hash in the form {:attr_on_other_model => :virtual_attr_on_this_model}"
27
+ end
28
+
29
+ # options[:denorm].each do |remote, local|
30
+ # # Default everything to a string. If it should be something different, the developer can declare the attribute manually.
31
+ # model.send(:string, local)
32
+ # model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1)
33
+ # def #{local}
34
+ # eoruby
35
+ # end
36
+
37
+ end
38
+ end
39
+
40
+ def define_constructors
41
+ name = self.name
42
+
43
+ model.redefine_method("build_#{name}") do |*params, &block|
44
+ association(name).build(*params, &block)
45
+ end
46
+
47
+ model.redefine_method("create_#{name}") do |*params, &block|
48
+ association(name).create(*params, &block)
49
+ end
50
+
51
+ model.redefine_method("create_#{name}!") do |*params, &block|
52
+ association(name).create!(*params, &block)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,274 @@
1
+ module DatastaxRails
2
+ module Associations
3
+ # = DatastaxRails Association Collection
4
+ #
5
+ # CollectionAssociation is an abstract class that provides common stuff to
6
+ # ease the implementation of association proxies that represent
7
+ # collections. See the class hierarchy in AssociationProxy.
8
+ #
9
+ # You need to be careful with assumptions regarding the target: The proxy
10
+ # does not fetch records from the database until it needs them, but new
11
+ # ones created with +build+ are added to the target. So, the target may be
12
+ # non-empty and still lack children waiting to be read from the database.
13
+ # If you look directly to the database you cannot assume that's the entire
14
+ # collection because new records may have been added to the target, etc.
15
+ #
16
+ # If you need to work on all current children, new and existing records,
17
+ # +load_target+ and the +loaded+ flag are your friends.
18
+ class CollectionAssociation < Association #:nodoc:
19
+ attr_reader :proxy
20
+
21
+ delegate :count, :size, :empty?, :any?, :many?, :first, :last, :to => :scoped
22
+
23
+ def initialize(owner, reflection)
24
+ super
25
+ @proxy = CollectionProxy.new(self)
26
+ end
27
+
28
+ # Implements the reader method, e.g. foo.items for Foo.has_many :items
29
+ def reader(force_reload = false)
30
+ if force_reload || stale_target?
31
+ reload
32
+ end
33
+
34
+ proxy
35
+ end
36
+
37
+ # Implements the writer method, e.g. foo.items= for Foo.has_many :items
38
+ def writer(records)
39
+ replace(records)
40
+ end
41
+
42
+ # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
43
+ def ids_reader
44
+ if loaded?
45
+ load_target.map do |record|
46
+ record.send(reflection.association_primary_key)
47
+ end
48
+ else
49
+ scoped.map! do |record|
50
+ record.send(reflection.association_primary_key)
51
+ end
52
+ end
53
+ end
54
+
55
+ # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
56
+ def ids_writer(ids)
57
+ ids = Array.wrap(ids).reject { |id| id.blank? }
58
+ replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
59
+ end
60
+
61
+ def reset
62
+ @loaded = false
63
+ @target = []
64
+ end
65
+
66
+ def build(attributes = {}, options = {}, &block)
67
+ if attributes.is_a?(Array)
68
+ attributes.collect { |attr| build(attr, options, &block) }
69
+ else
70
+ add_to_target(build_record(attributes, options)) do |record|
71
+ yield(record) if block_given?
72
+ end
73
+ end
74
+ end
75
+
76
+ def create(attributes = {}, options = {}, &block)
77
+ create_record(attributes, options, &block)
78
+ end
79
+
80
+ def create!(attributes = {}, options = {}, &block)
81
+ create_record(attributes, options, true, &block)
82
+ end
83
+
84
+ # Remove all records from this association
85
+ #
86
+ # See delete for more info.
87
+ def delete_all
88
+ delete(load_target).tap do
89
+ reset
90
+ loaded!
91
+ end
92
+ end
93
+
94
+ # Destroy all the records from this association.
95
+ #
96
+ # See destroy for more info.
97
+ def destroy_all
98
+ destroy(load_target).tap do
99
+ reset
100
+ loaded!
101
+ end
102
+ end
103
+
104
+ # Removes +records+ from this association calling +before_remove+ and
105
+ # +after_remove+ callbacks.
106
+ #
107
+ # This method is abstract in the sense that +delete_records+ has to be
108
+ # provided by descendants. Note this method does not imply the records
109
+ # are actually removed from the database, that depends precisely on
110
+ # +delete_records+. They are in any case removed from the collection.
111
+ def delete(*records)
112
+ delete_or_destroy(records, options[:dependent])
113
+ end
114
+
115
+ # Destroy +records+ and remove them from this association calling
116
+ # +before_remove+ and +after_remove+ callbacks.
117
+ #
118
+ # Note that this method will _always_ remove records from the database
119
+ # ignoring the +:dependent+ option.
120
+ def destroy(*records)
121
+ records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
122
+ delete_or_destroy(records, :destroy)
123
+ end
124
+
125
+ def uniq(collection = load_target)
126
+ seen = {}
127
+ collection.find_all do |record|
128
+ seen[record.id] = true unless seen.key?(record.id)
129
+ end
130
+ end
131
+
132
+ def load_target
133
+ if find_target?
134
+ @target = merge_target_lists(find_target, target)
135
+ end
136
+
137
+ loaded!
138
+ target
139
+ end
140
+
141
+ def add_to_target(record)
142
+ callback(:before_add, record)
143
+ yield(record) if block_given?
144
+
145
+ if options[:uniq] && index = @target.index(record)
146
+ @target[index] = record
147
+ else
148
+ @target << record
149
+ end
150
+
151
+ callback(:after_add, record)
152
+ set_inverse_instance(record)
153
+
154
+ record
155
+ end
156
+
157
+ private
158
+
159
+ # We have some records loaded from the database (persisted) and some that are
160
+ # in-memory (memory). The same record may be represented in the persisted array
161
+ # and in the memory array.
162
+ #
163
+ # So the task of this method is to merge them according to the following rules:
164
+ #
165
+ # * The final array must not have duplicates
166
+ # * The order of the persisted array is to be preserved
167
+ # * Any changes made to attributes on objects in the memory array are to be preserved
168
+ # * Otherwise, attributes should have the value found in the database
169
+ def merge_target_lists(persisted, memory)
170
+ return persisted if memory.empty?
171
+ return memory if persisted.empty?
172
+
173
+ persisted.map! do |record|
174
+ # Unfortunately we cannot simply do memory.delete(record) since on 1.8 this returns
175
+ # record rather than memory.at(memory.index(record)). The behavior is fixed in 1.9.
176
+ mem_index = memory.index(record)
177
+
178
+ if mem_index
179
+ mem_record = memory.delete_at(mem_index)
180
+
181
+ (record.attribute_names - mem_record.changes.keys).each do |name|
182
+ mem_record[name] = record[name]
183
+ end
184
+
185
+ mem_record
186
+ else
187
+ record
188
+ end
189
+ end
190
+
191
+ persisted + memory
192
+ end
193
+
194
+ def find_target
195
+ records = scoped.all
196
+ records = options[:uniq] ? uniq(records) : records
197
+ records.each { |record| set_inverse_instance(record) }
198
+ records
199
+ end
200
+
201
+ def create_record(attributes, options, raise = false, &block)
202
+ unless owner.persisted?
203
+ raise DatastaxRails::RecordNotSaved, "You cannot call create unless the parent is saved"
204
+ end
205
+
206
+ if attributes.is_a?(Array)
207
+ attributes.collect { |attr| create_record(attr, options, raise, &block) }
208
+ else
209
+ add_to_target(build_record(attributes, options)) do |record|
210
+ yield(record) if block_given?
211
+ insert_record(record, true, raise)
212
+ end
213
+ end
214
+ end
215
+
216
+ # Do the relevant stuff to insert the given record into the association collection.
217
+ def insert_record(record, validate = true, raise = false)
218
+ raise NotImplementedError
219
+ end
220
+
221
+ def create_scope
222
+ scoped.scope_for_create.stringify_keys
223
+ end
224
+
225
+ def delete_or_destroy(records, method)
226
+ records = records.flatten
227
+ records.each { |record| raise_on_type_mismatch(record) }
228
+ existing_records = records.reject { |r| r.new_record? }
229
+
230
+ records.each { |record| callback(:before_remove, record) }
231
+
232
+ delete_records(existing_records, method) if existing_records.any?
233
+ records.each { |record| target.delete(record) }
234
+
235
+ records.each { |record| callback(:after_remove, record) }
236
+ end
237
+
238
+ # Delete the given records from the association, using one of the methods :destroy,
239
+ # :delete_all or :nullify (or nil, in which case a default is used).
240
+ def delete_records(records, method)
241
+ raise NotImplementedError
242
+ end
243
+
244
+ def callback(method, record)
245
+ callbacks_for(method).each do |callback|
246
+ case callback
247
+ when Symbol
248
+ owner.send(callback, record)
249
+ when Proc
250
+ callback.call(owner, record)
251
+ else
252
+ callback.send(method, owner, record)
253
+ end
254
+ end
255
+ end
256
+
257
+ def callbacks_for(callback_name)
258
+ full_callback_name = "#{callback_name}_for_#{reflection.name}"
259
+ owner.class.send(full_callback_name.to_sym) || []
260
+ end
261
+
262
+ def include_in_memory?(record)
263
+ if reflection.is_a?(DatastaxRails::Reflection::ThroughReflection)
264
+ owner.send(reflection.through_reflection.name).any? { |source|
265
+ target = source.send(reflection.source_reflection.name)
266
+ target.respond_to?(:include?) ? target.include?(record) : target == record
267
+ } || target.include?(record)
268
+ else
269
+ target.include?(record)
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,118 @@
1
+ module DatastaxRails
2
+ module Associations
3
+ # Association proxies in DatastaxRails are middlemen between the object that
4
+ # holds the association, known as the <tt>@owner</tt>, and the actual associated
5
+ # object, known as the <tt>@target</tt>. The kind of association any proxy is
6
+ # about is available in <tt>@reflection</tt>. That's an instance of the class
7
+ # DatastaxRails::Reflection::AssociationReflection.
8
+ #
9
+ # For example, given
10
+ #
11
+ # class Blog < DatastaxRails::Base
12
+ # has_many :posts
13
+ # end
14
+ #
15
+ # blog = Blog.first
16
+ #
17
+ # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
18
+ # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
19
+ # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
20
+ #
21
+ # This class has most of the basic instance methods removed, and delegates
22
+ # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
23
+ # corner case, it even removes the +class+ method and that's why you get
24
+ #
25
+ # blog.posts.class # => Array
26
+ #
27
+ # though the object behind <tt>blog.posts</tt> is not an Array, but an
28
+ # DatastaxRails::Associations::HasManyAssociation.
29
+ #
30
+ # The <tt>@target</tt> object is not \loaded until needed. For example,
31
+ #
32
+ # blog.posts.count
33
+ #
34
+ # is computed directly through Solr and does not trigger by itself the
35
+ # instantiation of the actual post records.
36
+ class CollectionProxy #:nodoc:
37
+ alias :proxy_extend :extend
38
+
39
+ instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ }
40
+
41
+ delegate :order, :limit, :where, :to => :scoped
42
+ delegate :target, :load_target, :loaded?, :scoped, :to => :@association
43
+ delegate :select, :find, :first, :last, :build, :create, :create!, :destroy_all, :destroy,
44
+ :delete, :delete_all, :count, :size, :length, :empty?, :any?, :many?, :to => :@association
45
+
46
+ def initialize(association)
47
+ @association = association
48
+ Array.wrap(association.options[:extend]).each { |ext| proxy_extend(ext) }
49
+ end
50
+
51
+ alias_method :new, :build
52
+
53
+ def proxy_association
54
+ @association
55
+ end
56
+
57
+ def respond_to?(name, include_private = false)
58
+ super ||
59
+ (load_target && target.respond_to?(name, include_private)) ||
60
+ proxy_association.klass.respond_to?(name, include_private)
61
+ end
62
+
63
+ def method_missing(method, *args, &block)
64
+ match = ActiveRecord::DynamicFinderMatch.match(method)
65
+ if match && match.instantiator?
66
+ send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r|
67
+ proxy_association.send :set_owner_attributes, r
68
+ proxy_association.send :add_to_target, r
69
+ yield(r) if block_given?
70
+ end
71
+ end
72
+
73
+ if target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method))
74
+ if load_target
75
+ if target.respond_to?(method)
76
+ target.send(method, *args, &block)
77
+ else
78
+ begin
79
+ super
80
+ rescue NoMethodError => e
81
+ raise e, e.message.sub(/ for #<.*$/, " via proxy for #{target}")
82
+ end
83
+ end
84
+ end
85
+
86
+ else
87
+ scoped.send(method, *args, &block)
88
+ end
89
+ end
90
+
91
+ # Forwards <tt>===</tt> explicitly to the \target because the instance method
92
+ # removal above doesn't catch it. Loads the \target if needed.
93
+ def ===(other)
94
+ other === load_target
95
+ end
96
+
97
+ def to_ary
98
+ load_target.dup
99
+ end
100
+ alias_method :to_a, :to_ary
101
+
102
+ def <<(*records)
103
+ proxy_association.concat(records) && self
104
+ end
105
+ alias_method :push, :<<
106
+
107
+ def clear
108
+ destroy_all
109
+ self
110
+ end
111
+
112
+ def reload
113
+ proxy_association.reload
114
+ self
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,44 @@
1
+ module DatastaxRails
2
+ # = DatastaxRails Has And Belongs To Many Association
3
+ module Associations
4
+ class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc:
5
+ attr_reader :join_name
6
+
7
+ def initialize(owner, reflection)
8
+ join_name = [owner.class.name.underscore, reflection.class_name.underscore].sort.join('_')
9
+ super
10
+ end
11
+
12
+ def insert_record(record, validate = true, raise = false)
13
+ if record.new_record?
14
+ if raise
15
+ record.save!(:validate => validate)
16
+ else
17
+ return unless record.save(:validate => validate)
18
+ end
19
+ end
20
+
21
+ left, right = [[record.class.name, record.id], [owner.class.name, owner.id]].sort {|a,b|b.first <=> a.first}
22
+
23
+ DatastaxRails::Base.connection.insert(join_column_family,
24
+ SimpleUUID::UUID.new.to_guid,
25
+ {:left => "#{join_name}:#{left.last}",
26
+ :right => "#{join_name}:#{right.last}"})
27
+ end
28
+
29
+
30
+ private
31
+ def join_column_family
32
+ "many_to_many_joins"
33
+ end
34
+
35
+ def count_records
36
+ load_target.size
37
+ end
38
+
39
+ def invertible_for?(record)
40
+ false
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,58 @@
1
+ module DatastaxRails
2
+ module Associations
3
+ # This is the proxy that handles a has many association.
4
+ #
5
+ # If the association has a <tt>:through</tt> option further specialization
6
+ # is provided by its child HasManyThroughAssociation.
7
+ class HasManyAssociation < CollectionAssociation #:nodoc:
8
+ def insert_record(record, validate = true, raise = false)
9
+ set_owner_attributes(record)
10
+
11
+ if raise
12
+ record.save!(:validate => validate)
13
+ else
14
+ record.save(:validate => validate)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ # Returns the number of records in this collection.
21
+ #
22
+ # This does not depend on whether the collection has already been loaded
23
+ # or not. The +size+ method is the one that takes the loaded flag into
24
+ # account and delegates to +count_records+ if needed.
25
+ #
26
+ # If the collection is empty the target is set to an empty array and
27
+ # the loaded flag is set to true as well.
28
+ def count_records
29
+ count = scoped.count
30
+
31
+ # If there's nothing in the database and @target has no new records
32
+ # we are certain the current target is an empty array. This is a
33
+ # documented side-effect of the method that may avoid an extra SELECT.
34
+ @target ||= [] and loaded! if count == 0
35
+
36
+ count
37
+ end
38
+
39
+ # Deletes the records according to the <tt>:dependent</tt> option.
40
+ def delete_records(records, method)
41
+ if method == :destroy
42
+ records.each { |r| r.destroy }
43
+ else
44
+ keys = records.map { |r| r[reflection.association_primary_key] }
45
+ scope = scoped.where(reflection.association_primary_key => keys)
46
+
47
+ if method == :delete_all
48
+ scope.delete_all
49
+ else
50
+ # This is for :nullify which isn't actually supported yet,
51
+ # but this should work once it is
52
+ scope.update_all(reflection.foreign_key => nil)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,68 @@
1
+ module DatastaxRails
2
+ module Associations
3
+ class HasOneAssociation < SingularAssociation #:nodoc:
4
+ def replace(record, save = true)
5
+ raise_on_type_mismatch(record) if record
6
+ load_target
7
+
8
+ if target && target != record
9
+ remove_target!(options[:dependent]) unless target.destroyed?
10
+ end
11
+
12
+ if record
13
+ set_owner_attributes(record)
14
+ set_inverse_instance(record)
15
+
16
+ if owner.persisted? && save && !record.save
17
+ nullify_owner_attributes(record)
18
+ set_owner_attributes(target) if target
19
+ raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
20
+ end
21
+ end
22
+
23
+ self.target = record
24
+ end
25
+
26
+ def delete(method = options[:dependent])
27
+ if load_target
28
+ case method
29
+ when :delete
30
+ target.delete
31
+ when :destroy
32
+ target.destroy
33
+ when :nullify
34
+ target.update_attribute(reflection.foreign_key, nil)
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # The reason that the save param for replace is false, if for create (not just build),
42
+ # is because the setting of the foreign keys is actually handled by the scoping when
43
+ # the record is instantiated, and so they are set straight away and do not need to be
44
+ # updated within replace.
45
+ def set_new_record(record)
46
+ replace(record, false)
47
+ end
48
+
49
+ def remove_target!(method)
50
+ if method.in?([:delete, :destroy])
51
+ target.send(method)
52
+ else
53
+ nullify_owner_attributes(target)
54
+
55
+ if target.persisted? && owner.persisted? && !target.save
56
+ set_owner_attributes(target)
57
+ raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " +
58
+ "The record failed to save when after its foreign key was set to nil."
59
+ end
60
+ end
61
+ end
62
+
63
+ def nullify_owner_attributes(record)
64
+ record[reflection.foreign_key] = nil
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,58 @@
1
+ module DatastaxRails
2
+ module Associations
3
+ class SingularAssociation < Association #:nodoc:
4
+ # Implements the reader method, e.g. foo.bar for Foo.has_one :bar
5
+ def reader(force_reload = false)
6
+ reload if force_reload || !loaded? || stale_target?
7
+ target
8
+ end
9
+
10
+ # Implements the writer method, e.g. foo.items= for Foo.has_many :items
11
+ def writer(record)
12
+ replace(record)
13
+ end
14
+
15
+ def create(attributes = {}, options = {}, &block)
16
+ create_record(attributes, options, &block)
17
+ end
18
+
19
+ def create!(attributes = {}, options = {}, &block)
20
+ create_record(attributes, options, true, &block)
21
+ end
22
+
23
+ def build(attributes = {}, options = {})
24
+ record = build_record(attributes, options)
25
+ yield(record) if block_given?
26
+ set_new_record(record)
27
+ record
28
+ end
29
+
30
+ private
31
+ def create_scope
32
+ scoped.scope_for_create.stringify_keys.except("id")
33
+ end
34
+
35
+ def find_target
36
+ scoped.first.tap { |record| set_inverse_instance(record) }
37
+ end
38
+
39
+ # Implemented by subclasses
40
+ def replace(record)
41
+ raise NotImplementedError, "Subclasses must implement a replace(record) method"
42
+ end
43
+
44
+ def set_new_record(record)
45
+ replace(record)
46
+ end
47
+
48
+ def create_record(attributes, options, raise_error = false)
49
+ record = build_record(attributes, options)
50
+ yield(record) if block_given?
51
+ saved = record.save
52
+ set_new_record(record)
53
+ raise RecordInvalid.new(record) if !saved && raise_error
54
+ record
55
+ end
56
+ end
57
+ end
58
+ end