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.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +62 -0
- data/Rakefile +34 -0
- data/config/schema.xml +266 -0
- data/config/schema.xml.erb +70 -0
- data/config/solrconfig.xml +1564 -0
- data/config/stopwords.txt +58 -0
- data/lib/datastax_rails/associations/association.rb +224 -0
- data/lib/datastax_rails/associations/association_scope.rb +25 -0
- data/lib/datastax_rails/associations/belongs_to_association.rb +64 -0
- data/lib/datastax_rails/associations/builder/association.rb +56 -0
- data/lib/datastax_rails/associations/builder/belongs_to.rb +30 -0
- data/lib/datastax_rails/associations/builder/collection_association.rb +48 -0
- data/lib/datastax_rails/associations/builder/has_and_belongs_to_many.rb +36 -0
- data/lib/datastax_rails/associations/builder/has_many.rb +54 -0
- data/lib/datastax_rails/associations/builder/has_one.rb +52 -0
- data/lib/datastax_rails/associations/builder/singular_association.rb +56 -0
- data/lib/datastax_rails/associations/collection_association.rb +274 -0
- data/lib/datastax_rails/associations/collection_proxy.rb +118 -0
- data/lib/datastax_rails/associations/has_and_belongs_to_many_association.rb +44 -0
- data/lib/datastax_rails/associations/has_many_association.rb +58 -0
- data/lib/datastax_rails/associations/has_one_association.rb +68 -0
- data/lib/datastax_rails/associations/singular_association.rb +58 -0
- data/lib/datastax_rails/associations.rb +86 -0
- data/lib/datastax_rails/attribute_methods/definition.rb +20 -0
- data/lib/datastax_rails/attribute_methods/dirty.rb +43 -0
- data/lib/datastax_rails/attribute_methods/typecasting.rb +50 -0
- data/lib/datastax_rails/attribute_methods.rb +104 -0
- data/lib/datastax_rails/base.rb +587 -0
- data/lib/datastax_rails/batches.rb +35 -0
- data/lib/datastax_rails/callbacks.rb +37 -0
- data/lib/datastax_rails/collection.rb +9 -0
- data/lib/datastax_rails/connection.rb +21 -0
- data/lib/datastax_rails/consistency.rb +33 -0
- data/lib/datastax_rails/cql/base.rb +15 -0
- data/lib/datastax_rails/cql/column_family.rb +38 -0
- data/lib/datastax_rails/cql/consistency.rb +13 -0
- data/lib/datastax_rails/cql/create_column_family.rb +63 -0
- data/lib/datastax_rails/cql/create_keyspace.rb +30 -0
- data/lib/datastax_rails/cql/delete.rb +41 -0
- data/lib/datastax_rails/cql/drop_column_family.rb +13 -0
- data/lib/datastax_rails/cql/drop_keyspace.rb +13 -0
- data/lib/datastax_rails/cql/insert.rb +53 -0
- data/lib/datastax_rails/cql/select.rb +51 -0
- data/lib/datastax_rails/cql/truncate.rb +13 -0
- data/lib/datastax_rails/cql/update.rb +68 -0
- data/lib/datastax_rails/cql/use_keyspace.rb +13 -0
- data/lib/datastax_rails/cql.rb +25 -0
- data/lib/datastax_rails/cursor.rb +90 -0
- data/lib/datastax_rails/errors.rb +16 -0
- data/lib/datastax_rails/identity/abstract_key_factory.rb +26 -0
- data/lib/datastax_rails/identity/custom_key_factory.rb +36 -0
- data/lib/datastax_rails/identity/hashed_natural_key_factory.rb +10 -0
- data/lib/datastax_rails/identity/natural_key_factory.rb +37 -0
- data/lib/datastax_rails/identity/uuid_key_factory.rb +23 -0
- data/lib/datastax_rails/identity.rb +53 -0
- data/lib/datastax_rails/log_subscriber.rb +37 -0
- data/lib/datastax_rails/migrations/migration.rb +15 -0
- data/lib/datastax_rails/migrations.rb +36 -0
- data/lib/datastax_rails/mocking.rb +15 -0
- data/lib/datastax_rails/persistence.rb +133 -0
- data/lib/datastax_rails/railtie.rb +20 -0
- data/lib/datastax_rails/reflection.rb +472 -0
- data/lib/datastax_rails/relation/finder_methods.rb +184 -0
- data/lib/datastax_rails/relation/modification_methods.rb +80 -0
- data/lib/datastax_rails/relation/search_methods.rb +349 -0
- data/lib/datastax_rails/relation/spawn_methods.rb +107 -0
- data/lib/datastax_rails/relation.rb +393 -0
- data/lib/datastax_rails/schema/migration.rb +106 -0
- data/lib/datastax_rails/schema/migration_proxy.rb +25 -0
- data/lib/datastax_rails/schema/migrator.rb +212 -0
- data/lib/datastax_rails/schema.rb +37 -0
- data/lib/datastax_rails/scoping.rb +394 -0
- data/lib/datastax_rails/serialization.rb +6 -0
- data/lib/datastax_rails/tasks/column_family.rb +162 -0
- data/lib/datastax_rails/tasks/ds.rake +63 -0
- data/lib/datastax_rails/tasks/keyspace.rb +57 -0
- data/lib/datastax_rails/timestamps.rb +19 -0
- data/lib/datastax_rails/type.rb +16 -0
- data/lib/datastax_rails/types/array_type.rb +77 -0
- data/lib/datastax_rails/types/base_type.rb +26 -0
- data/lib/datastax_rails/types/binary_type.rb +15 -0
- data/lib/datastax_rails/types/boolean_type.rb +22 -0
- data/lib/datastax_rails/types/date_type.rb +17 -0
- data/lib/datastax_rails/types/float_type.rb +18 -0
- data/lib/datastax_rails/types/integer_type.rb +18 -0
- data/lib/datastax_rails/types/string_type.rb +16 -0
- data/lib/datastax_rails/types/text_type.rb +16 -0
- data/lib/datastax_rails/types/time_type.rb +17 -0
- data/lib/datastax_rails/types.rb +9 -0
- data/lib/datastax_rails/validations/uniqueness.rb +119 -0
- data/lib/datastax_rails/validations.rb +48 -0
- data/lib/datastax_rails/version.rb +3 -0
- data/lib/datastax_rails.rb +87 -0
- data/lib/solr_no_escape.rb +28 -0
- data/spec/datastax_rails/associations/belongs_to_association_spec.rb +7 -0
- data/spec/datastax_rails/associations/has_many_association_spec.rb +37 -0
- data/spec/datastax_rails/associations_spec.rb +22 -0
- data/spec/datastax_rails/attribute_methods_spec.rb +23 -0
- data/spec/datastax_rails/base_spec.rb +15 -0
- data/spec/datastax_rails/cql/select_spec.rb +12 -0
- data/spec/datastax_rails/cql/update_spec.rb +0 -0
- data/spec/datastax_rails/relation/finder_methods_spec.rb +54 -0
- data/spec/datastax_rails/relation/modification_methods_spec.rb +41 -0
- data/spec/datastax_rails/relation/search_methods_spec.rb +117 -0
- data/spec/datastax_rails/relation/spawn_methods_spec.rb +28 -0
- data/spec/datastax_rails/relation_spec.rb +130 -0
- data/spec/datastax_rails/validations/uniqueness_spec.rb +41 -0
- data/spec/datastax_rails_spec.rb +5 -0
- data/spec/dummy/Rakefile +8 -0
- data/spec/dummy/app/assets/javascripts/application.js +9 -0
- data/spec/dummy/app/assets/stylesheets/application.css +7 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config/application.rb +47 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/datastax.yml +18 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +30 -0
- data/spec/dummy/config/environments/production.rb +60 -0
- data/spec/dummy/config/environments/test.rb +39 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +10 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/config/sunspot.yml +17 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/ks/migrate/20111117224534_models.rb +20 -0
- data/spec/dummy/ks/schema.json +180 -0
- data/spec/dummy/log/development.log +298 -0
- data/spec/dummy/log/production.log +0 -0
- data/spec/dummy/log/test.log +20307 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +26 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/datastax_test_hook.rb +14 -0
- data/spec/support/models.rb +72 -0
- 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
|