bigrecord 0.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 (104) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +44 -0
  3. data/Rakefile +17 -0
  4. data/VERSION +1 -0
  5. data/doc/bigrecord_specs.rdoc +36 -0
  6. data/doc/getting_started.rdoc +157 -0
  7. data/examples/bigrecord.yml +25 -0
  8. data/generators/bigrecord/bigrecord_generator.rb +17 -0
  9. data/generators/bigrecord/templates/bigrecord.rake +47 -0
  10. data/generators/bigrecord_migration/bigrecord_migration_generator.rb +13 -0
  11. data/generators/bigrecord_migration/templates/migration.rb +9 -0
  12. data/generators/bigrecord_model/bigrecord_model_generator.rb +28 -0
  13. data/generators/bigrecord_model/templates/migration.rb +13 -0
  14. data/generators/bigrecord_model/templates/model.rb +7 -0
  15. data/generators/bigrecord_model/templates/model_spec.rb +12 -0
  16. data/init.rb +9 -0
  17. data/install.rb +22 -0
  18. data/lib/big_record/abstract_base.rb +1088 -0
  19. data/lib/big_record/action_view_extensions.rb +266 -0
  20. data/lib/big_record/ar_associations/association_collection.rb +194 -0
  21. data/lib/big_record/ar_associations/association_proxy.rb +158 -0
  22. data/lib/big_record/ar_associations/belongs_to_association.rb +57 -0
  23. data/lib/big_record/ar_associations/belongs_to_many_association.rb +57 -0
  24. data/lib/big_record/ar_associations/has_and_belongs_to_many_association.rb +164 -0
  25. data/lib/big_record/ar_associations/has_many_association.rb +191 -0
  26. data/lib/big_record/ar_associations/has_one_association.rb +80 -0
  27. data/lib/big_record/ar_associations.rb +1608 -0
  28. data/lib/big_record/ar_reflection.rb +223 -0
  29. data/lib/big_record/attribute_methods.rb +75 -0
  30. data/lib/big_record/base.rb +618 -0
  31. data/lib/big_record/br_associations/association_collection.rb +194 -0
  32. data/lib/big_record/br_associations/association_proxy.rb +153 -0
  33. data/lib/big_record/br_associations/belongs_to_association.rb +52 -0
  34. data/lib/big_record/br_associations/belongs_to_many_association.rb +293 -0
  35. data/lib/big_record/br_associations/cached_item_proxy.rb +194 -0
  36. data/lib/big_record/br_associations/cached_item_proxy_factory.rb +62 -0
  37. data/lib/big_record/br_associations/has_and_belongs_to_many_association.rb +168 -0
  38. data/lib/big_record/br_associations/has_one_association.rb +80 -0
  39. data/lib/big_record/br_associations.rb +978 -0
  40. data/lib/big_record/br_reflection.rb +151 -0
  41. data/lib/big_record/callbacks.rb +367 -0
  42. data/lib/big_record/connection_adapters/abstract/connection_specification.rb +279 -0
  43. data/lib/big_record/connection_adapters/abstract/database_statements.rb +175 -0
  44. data/lib/big_record/connection_adapters/abstract/quoting.rb +58 -0
  45. data/lib/big_record/connection_adapters/abstract_adapter.rb +190 -0
  46. data/lib/big_record/connection_adapters/column.rb +491 -0
  47. data/lib/big_record/connection_adapters/hbase_adapter.rb +432 -0
  48. data/lib/big_record/connection_adapters/view.rb +27 -0
  49. data/lib/big_record/connection_adapters.rb +10 -0
  50. data/lib/big_record/deletion.rb +73 -0
  51. data/lib/big_record/dynamic_schema.rb +92 -0
  52. data/lib/big_record/embedded.rb +71 -0
  53. data/lib/big_record/embedded_associations/association_proxy.rb +148 -0
  54. data/lib/big_record/family_span_columns.rb +89 -0
  55. data/lib/big_record/fixtures.rb +1025 -0
  56. data/lib/big_record/migration.rb +380 -0
  57. data/lib/big_record/routing_ext.rb +65 -0
  58. data/lib/big_record/timestamp.rb +51 -0
  59. data/lib/big_record/validations.rb +830 -0
  60. data/lib/big_record.rb +125 -0
  61. data/lib/bigrecord.rb +1 -0
  62. data/rails/init.rb +9 -0
  63. data/spec/connections/bigrecord.yml +13 -0
  64. data/spec/connections/cassandra/connection.rb +2 -0
  65. data/spec/connections/hbase/connection.rb +2 -0
  66. data/spec/debug.log +281 -0
  67. data/spec/integration/br_associations_spec.rb +80 -0
  68. data/spec/lib/animal.rb +12 -0
  69. data/spec/lib/book.rb +10 -0
  70. data/spec/lib/broken_migrations/duplicate_name/20090706182535_add_animals_table.rb +14 -0
  71. data/spec/lib/broken_migrations/duplicate_name/20090706193019_add_animals_table.rb +9 -0
  72. data/spec/lib/broken_migrations/duplicate_version/20090706190623_add_books_table.rb +9 -0
  73. data/spec/lib/broken_migrations/duplicate_version/20090706190623_add_companies_table.rb +9 -0
  74. data/spec/lib/company.rb +14 -0
  75. data/spec/lib/embedded/web_link.rb +12 -0
  76. data/spec/lib/employee.rb +33 -0
  77. data/spec/lib/migrations/20090706182535_add_animals_table.rb +13 -0
  78. data/spec/lib/migrations/20090706190623_add_books_table.rb +15 -0
  79. data/spec/lib/migrations/20090706193019_add_companies_table.rb +14 -0
  80. data/spec/lib/migrations/20090706194512_add_employees_table.rb +13 -0
  81. data/spec/lib/migrations/20090706195741_add_zoos_table.rb +13 -0
  82. data/spec/lib/novel.rb +5 -0
  83. data/spec/lib/zoo.rb +17 -0
  84. data/spec/spec.opts +4 -0
  85. data/spec/spec_helper.rb +55 -0
  86. data/spec/unit/abstract_base_spec.rb +287 -0
  87. data/spec/unit/adapters/abstract_adapter_spec.rb +56 -0
  88. data/spec/unit/adapters/adapter_shared_spec.rb +51 -0
  89. data/spec/unit/adapters/hbase_adapter_spec.rb +15 -0
  90. data/spec/unit/ar_associations_spec.rb +8 -0
  91. data/spec/unit/base_spec.rb +6 -0
  92. data/spec/unit/br_associations_spec.rb +58 -0
  93. data/spec/unit/embedded_spec.rb +43 -0
  94. data/spec/unit/find_spec.rb +34 -0
  95. data/spec/unit/hash_helper_spec.rb +44 -0
  96. data/spec/unit/migration_spec.rb +144 -0
  97. data/spec/unit/model_spec.rb +315 -0
  98. data/spec/unit/validations_spec.rb +182 -0
  99. data/tasks/bigrecord_tasks.rake +47 -0
  100. data/tasks/data_store.rb +46 -0
  101. data/tasks/gem.rb +22 -0
  102. data/tasks/rdoc.rb +8 -0
  103. data/tasks/spec.rb +34 -0
  104. metadata +189 -0
@@ -0,0 +1,266 @@
1
+ require 'cgi'
2
+ require 'action_view/helpers/date_helper'
3
+ require 'action_view/helpers/tag_helper'
4
+
5
+ # Mixin a reflection method that returns self. Useful for generating
6
+ # form fields for primitive objects. It must be mixed in Object because
7
+ # the class for the type :boolean is Object.
8
+ class Object
9
+ def reflect_value
10
+ self
11
+ end
12
+ end
13
+
14
+ module ActionView
15
+ module Helpers
16
+ class InstanceTag #:nodoc:
17
+
18
+ def to_date_select_tag(options = {})
19
+ date_or_time_select(options.merge(:discard_hour => true))
20
+ end
21
+
22
+ def to_time_select_tag(options = {})
23
+ date_or_time_select options.merge(:discard_year => true, :discard_month => true)
24
+ end
25
+
26
+ def to_datetime_select_tag(options = {})
27
+ date_or_time_select options
28
+ end
29
+
30
+ def to_label_tag(text = nil, options = {})
31
+ name_and_id = options.dup
32
+ add_default_name_and_id(name_and_id)
33
+ options["for"] = name_and_id["id"]
34
+ content = (text.blank? ? nil : text.to_s) || method_name.humanize
35
+ content_tag("label", content, options)
36
+ end
37
+
38
+ def to_input_field_tag(field_type, options = {})
39
+ options = options.stringify_keys
40
+ options["size"] = options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"] unless options.key?("size")
41
+ options = DEFAULT_FIELD_OPTIONS.merge(options)
42
+ if field_type == "hidden"
43
+ options.delete("size")
44
+ end
45
+ options["type"] = field_type
46
+ options["value"] ||= value_before_type_cast(object, options) unless field_type == "file"
47
+ add_default_name_and_id(options)
48
+ tag("input", options)
49
+ end
50
+
51
+ def to_radio_button_tag(tag_value, options = {})
52
+ options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys)
53
+ options["type"] = "radio"
54
+ options["value"] = tag_value
55
+ if options.has_key?("checked")
56
+ cv = options.delete "checked"
57
+ checked = cv == true || cv == "checked"
58
+ else
59
+ checked = self.class.radio_button_checked?(value(object, options), tag_value)
60
+ end
61
+ options["checked"] = "checked" if checked
62
+ pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase
63
+ options["id"] ||= defined?(@auto_index) ?
64
+ "#{@object_name}_#{@auto_index}_#{@method_name}_#{pretty_tag_value}" :
65
+ "#{@object_name}_#{@method_name}_#{pretty_tag_value}"
66
+ add_default_name_and_id(options)
67
+ tag("input", options)
68
+ end
69
+
70
+ def to_text_area_tag(options = {})
71
+ options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys)
72
+ add_default_name_and_id(options)
73
+
74
+ if size = options.delete("size")
75
+ options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
76
+ end
77
+ content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast(object, options)), options)
78
+ end
79
+
80
+ def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
81
+ options = options.stringify_keys
82
+ options["type"] = "checkbox"
83
+ options["value"] = checked_value
84
+ if options.has_key?("checked")
85
+ cv = options.delete "checked"
86
+ checked = cv == true || cv == "checked"
87
+ else
88
+ checked = self.class.check_box_checked?(value(object, options), checked_value)
89
+ end
90
+ options["checked"] = "checked" if checked
91
+ add_default_name_and_id(options)
92
+ tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => options['disabled'] && checked ? checked_value : unchecked_value)
93
+ end
94
+
95
+ def to_date_tag()
96
+ defaults = DEFAULT_DATE_OPTIONS.dup
97
+ date = value(object, options) || Date.today
98
+ options = Proc.new { |position| defaults.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") }
99
+ html_day_select(date, options.call(3)) +
100
+ html_month_select(date, options.call(2)) +
101
+ html_year_select(date, options.call(1))
102
+ end
103
+
104
+ def to_select_tag(choices, options, html_options)
105
+ html_options = html_options.stringify_keys
106
+
107
+ # Ugly hack... how come selectors don't have the same signature as the other ones
108
+ html_options["index"] = options[:index]
109
+
110
+ add_default_name_and_id(html_options)
111
+ value = value(object, html_options)
112
+ selected_value = options.has_key?(:selected) ? options[:selected] : value
113
+ content_tag("select", add_options(options_for_select(choices, selected_value), options, selected_value), html_options)
114
+ end
115
+
116
+ def to_content_tag(tag_name, options = {})
117
+ content_tag(tag_name, value(object, options), options)
118
+ end
119
+
120
+ def object
121
+ @object || (@template_object.instance_variable_get("@#{@object_name}") rescue nil)
122
+ end
123
+
124
+ def value(object, options={})
125
+ self.class.value(object, @method_name, options)
126
+ end
127
+
128
+ def value_before_type_cast(object, options={})
129
+ self.class.value_before_type_cast(object, @method_name, options)
130
+ end
131
+
132
+ class << self
133
+ def value(object, method_name, options={})
134
+ options = options.stringify_keys
135
+ v = object.send method_name unless object.nil?
136
+ (options["index"] and v.is_a?(Array)) ? v[options["index"]] : v
137
+ end
138
+
139
+ def value_before_type_cast(object, method_name, options={})
140
+ unless object.nil?
141
+ options = options.stringify_keys
142
+ v = object.respond_to?(method_name + "_before_type_cast") ?
143
+ object.send(method_name + "_before_type_cast") :
144
+ object.send(method_name)
145
+ (options["index"] and v.is_a?(Array)) ? v[options["index"]] : v
146
+ end
147
+ end
148
+
149
+ end
150
+
151
+ private
152
+ def date_or_time_select(options)
153
+ defaults = { :discard_type => true }
154
+ options = defaults.merge(options)
155
+
156
+ datetime = value(object, options)
157
+ datetime ||= default_time_from_options(options[:default]) unless options[:include_blank]
158
+
159
+ position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
160
+
161
+ order = (options[:order] ||= [:year, :month, :day])
162
+
163
+ # Discard explicit and implicit by not being included in the :order
164
+ discard = {}
165
+ discard[:year] = true if options[:discard_year] or !order.include?(:year)
166
+ discard[:month] = true if options[:discard_month] or !order.include?(:month)
167
+ discard[:day] = true if options[:discard_day] or discard[:month] or !order.include?(:day)
168
+ discard[:hour] = true if options[:discard_hour]
169
+ discard[:minute] = true if options[:discard_minute] or discard[:hour]
170
+ discard[:second] = true unless options[:include_seconds] && !discard[:minute]
171
+
172
+ # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are valid
173
+ # (otherwise it could be 31 and february wouldn't be a valid date)
174
+ if datetime && discard[:day] && !discard[:month]
175
+ datetime = datetime.change(:day => 1)
176
+ end
177
+
178
+ # Maintain valid dates by including hidden fields for discarded elements
179
+ [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
180
+
181
+ # Ensure proper ordering of :hour, :minute and :second
182
+ [:hour, :minute, :second].each { |o| order.delete(o); order.push(o) }
183
+
184
+ date_or_time_select = ''
185
+ order.reverse.each do |param|
186
+ # Send hidden fields for discarded elements once output has started
187
+ # This ensures AR can reconstruct valid dates using ParseDate
188
+ next if discard[param] && date_or_time_select.empty?
189
+
190
+ date_or_time_select.insert(0, self.send("select_#{param}", datetime, options_with_prefix(position[param], options.merge(:use_hidden => discard[param]))))
191
+ date_or_time_select.insert(0,
192
+ case param
193
+ when :hour then (discard[:year] && discard[:day] ? "" : " &mdash; ")
194
+ when :minute then " : "
195
+ when :second then options[:include_seconds] ? " : " : ""
196
+ else ""
197
+ end)
198
+
199
+ end
200
+
201
+ date_or_time_select
202
+ end
203
+
204
+ def options_with_prefix(position, options)
205
+ prefix = "#{@object_name}"
206
+ if options[:index]
207
+ prefix << "[#{options[:index]}]"
208
+ elsif @auto_index
209
+ prefix << "[#{@auto_index}]"
210
+ end
211
+ options.merge(:prefix => "#{prefix}[#{@method_name}(#{position}i)]")
212
+ end
213
+
214
+ def tag_name
215
+ "#{@object_name}[#{@method_name}]"
216
+ end
217
+
218
+ def tag_name_with_index(index)
219
+ "#{@object_name}[#{index}][#{@method_name}]"
220
+ end
221
+
222
+ def tag_id
223
+ "#{sanitized_object_name}_#{@method_name}"
224
+ end
225
+
226
+ def tag_id_with_index(index)
227
+ "#{sanitized_object_name}_#{index}_#{@method_name}"
228
+ end
229
+
230
+ def sanitized_object_name
231
+ @object_name.gsub(/[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
232
+ end
233
+
234
+ end
235
+
236
+ class FormBuilder
237
+ # Override to allow the use of an index
238
+ def fields_for(record_or_name_or_array, *args, &block)
239
+ if options.has_key?(:index)
240
+ index = "[#{options[:index]}]"
241
+ elsif defined?(@auto_index)
242
+ self.object_name = @object_name.to_s.sub(/\[\]$/,"")
243
+ index = "[#{@auto_index}]"
244
+ else
245
+ index = ""
246
+ end
247
+
248
+ case record_or_name_or_array
249
+ when String, Symbol
250
+ name = "#{object_name}#{index}[#{record_or_name_or_array}]"
251
+ when Array
252
+ object = record_or_name_or_array.last
253
+ name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
254
+ args.unshift(object)
255
+ else
256
+ object = record_or_name_or_array
257
+ name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
258
+ args.unshift(object)
259
+ end
260
+
261
+ @template.fields_for(name, *args, &block)
262
+ end
263
+ end
264
+ end
265
+
266
+ end
@@ -0,0 +1,194 @@
1
+ require 'set'
2
+
3
+ module BigRecord
4
+ module ArAssociations
5
+ class AssociationCollection < AssociationProxy #:nodoc:
6
+ def to_ary
7
+ load_target
8
+ @target.to_ary
9
+ end
10
+
11
+ def reset
12
+ @loaded = false
13
+ reset_target!
14
+ end
15
+
16
+ # Add +records+ to this association. Returns +self+ so method calls may be chained.
17
+ # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
18
+ def <<(*records)
19
+ result = true
20
+ load_target
21
+
22
+ # HbaseAdapter doesn't support transactions
23
+ # @owner.transaction do
24
+ flatten_deeper(records).each do |record|
25
+ raise_on_type_mismatch(record)
26
+ callback(:before_add, record)
27
+ result &&= insert_record(record) unless @owner.new_record?
28
+ @target << record
29
+ callback(:after_add, record)
30
+ end
31
+ # end
32
+
33
+ result && self
34
+ end
35
+
36
+ alias_method :push, :<<
37
+ alias_method :concat, :<<
38
+
39
+ # Remove all records from this association
40
+ def delete_all
41
+ load_target
42
+ delete(@target)
43
+ reset_target!
44
+ end
45
+
46
+ # Calculate sum using SQL, not Enumerable
47
+ def sum(*args, &block)
48
+ calculate(:sum, *args, &block)
49
+ end
50
+
51
+ # Remove +records+ from this association. Does not destroy +records+.
52
+ def delete(*records)
53
+ records = flatten_deeper(records)
54
+ records.each { |record| raise_on_type_mismatch(record) }
55
+ records.reject! { |record| @target.delete(record) if record.new_record? }
56
+ return if records.empty?
57
+
58
+ # HbaseAdapter doesn't support transactions
59
+ # @owner.transaction do
60
+ records.each { |record| callback(:before_remove, record) }
61
+ delete_records(records)
62
+ records.each do |record|
63
+ @target.delete(record)
64
+ callback(:after_remove, record)
65
+ end
66
+ # end
67
+ end
68
+
69
+ # Removes all records from this association. Returns +self+ so method calls may be chained.
70
+ def clear
71
+ return self if length.zero? # forces load_target if hasn't happened already
72
+
73
+ if @reflection.options[:dependent] && @reflection.options[:dependent] == :delete_all
74
+ destroy_all
75
+ else
76
+ delete_all
77
+ end
78
+
79
+ self
80
+ end
81
+
82
+ def destroy_all
83
+ # HbaseAdapter doesn't support transactions
84
+ # @owner.transaction do
85
+ each { |record| record.destroy }
86
+ # end
87
+
88
+ reset_target!
89
+ end
90
+
91
+ def create(attributes = {})
92
+ # Can't use Base.create since the foreign key may be a protected attribute.
93
+ if attributes.is_a?(Array)
94
+ attributes.collect { |attr| create(attr) }
95
+ else
96
+ record = build(attributes)
97
+ record.save unless @owner.new_record?
98
+ record
99
+ end
100
+ end
101
+
102
+ # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
103
+ # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
104
+ # and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
105
+ def size
106
+ if loaded? && !@reflection.options[:uniq]
107
+ @target.size
108
+ elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
109
+ unsaved_records = Array(@target.detect { |r| r.new_record? })
110
+ unsaved_records.size + count_records
111
+ else
112
+ count_records
113
+ end
114
+ end
115
+
116
+ # Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
117
+ # whether the collection is empty, use collection.length.zero? instead of collection.empty?
118
+ def length
119
+ load_target.size
120
+ end
121
+
122
+ def empty?
123
+ size.zero?
124
+ end
125
+
126
+ def uniq(collection = self)
127
+ seen = Set.new
128
+ collection.inject([]) do |kept, record|
129
+ unless seen.include?(record.id)
130
+ kept << record
131
+ seen << record.id
132
+ end
133
+ kept
134
+ end
135
+ end
136
+
137
+ # Replace this collection with +other_array+
138
+ # This will perform a diff and delete/add only records that have changed.
139
+ def replace(other_array)
140
+ other_array.each { |val| raise_on_type_mismatch(val) }
141
+
142
+ load_target
143
+ other = other_array.size < 100 ? other_array : other_array.to_set
144
+ current = @target.size < 100 ? @target : @target.to_set
145
+
146
+ # HbaseAdapter doesn't support transactions
147
+ # @owner.transaction do
148
+ delete(@target.select { |v| !other.include?(v) })
149
+ concat(other_array.select { |v| !current.include?(v) })
150
+ # end
151
+ end
152
+
153
+ protected
154
+ def reset_target!
155
+ @target = Array.new
156
+ end
157
+
158
+ def find_target
159
+ records =
160
+ if @reflection.options[:finder_sql]
161
+ @reflection.klass.find_by_sql(@finder_sql)
162
+ else
163
+ find(:all)
164
+ end
165
+
166
+ @reflection.options[:uniq] ? uniq(records) : records
167
+ end
168
+
169
+ private
170
+ def callback(method, record)
171
+ callbacks_for(method).each do |callback|
172
+ case callback
173
+ when Symbol
174
+ @owner.send(callback, record)
175
+ when Proc, Method
176
+ callback.call(@owner, record)
177
+ else
178
+ if callback.respond_to?(method)
179
+ callback.send(method, @owner, record)
180
+ else
181
+ raise ActiveRecordError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method."
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ def callbacks_for(callback_name)
188
+ full_callback_name = "#{callback_name}_for_#{@reflection.name}"
189
+ @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
190
+ end
191
+
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,158 @@
1
+ module BigRecord
2
+ module ArAssociations
3
+ class AssociationProxy #:nodoc:
4
+ attr_reader :reflection
5
+ alias_method :proxy_respond_to?, :respond_to?
6
+ alias_method :proxy_extend, :extend
7
+ delegate :to_param, :to => :proxy_target
8
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_)/ }
9
+
10
+ def initialize(owner, reflection)
11
+ @owner, @reflection = owner, reflection
12
+ Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
13
+ reset
14
+ end
15
+
16
+ def proxy_owner
17
+ @owner
18
+ end
19
+
20
+ def proxy_reflection
21
+ @reflection
22
+ end
23
+
24
+ def proxy_target
25
+ @target
26
+ end
27
+
28
+ def respond_to?(symbol, include_priv = false)
29
+ proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv))
30
+ end
31
+
32
+ # Explicitly proxy === because the instance method removal above
33
+ # doesn't catch it.
34
+ def ===(other)
35
+ load_target
36
+ other === @target
37
+ end
38
+
39
+ def aliased_table_name
40
+ @reflection.klass.table_name
41
+ end
42
+
43
+ def conditions
44
+ @conditions ||= interpolate_sql(sanitize_sql(@reflection.options[:conditions])) if @reflection.options[:conditions]
45
+ end
46
+ alias :sql_conditions :conditions
47
+
48
+ def reset
49
+ @loaded = false
50
+ @target = nil
51
+ end
52
+
53
+ def reload
54
+ reset
55
+ load_target
56
+ end
57
+
58
+ def loaded?
59
+ @loaded
60
+ end
61
+
62
+ def loaded
63
+ @loaded = true
64
+ end
65
+
66
+ def target
67
+ @target
68
+ end
69
+
70
+ def target=(target)
71
+ @target = target
72
+ loaded
73
+ end
74
+
75
+ protected
76
+ def dependent?
77
+ @reflection.options[:dependent] || false
78
+ end
79
+
80
+ def quoted_record_ids(records)
81
+ records.map { |record| record.quoted_id }.join(',')
82
+ end
83
+
84
+ def interpolate_sql_options!(options, *keys)
85
+ keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
86
+ end
87
+
88
+ def interpolate_sql(sql, record = nil)
89
+ @owner.send(:interpolate_sql, sql, record)
90
+ end
91
+
92
+ def sanitize_sql(sql)
93
+ @reflection.klass.send(:sanitize_sql, sql)
94
+ end
95
+
96
+ def extract_options_from_args!(args)
97
+ @owner.send(:extract_options_from_args!, args)
98
+ end
99
+
100
+ def set_belongs_to_association_for(record)
101
+ if @reflection.options[:as]
102
+ record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
103
+ record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
104
+ else
105
+ record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
106
+ end
107
+ end
108
+
109
+ def merge_options_from_reflection!(options)
110
+ options.reverse_merge!(
111
+ :group => @reflection.options[:group],
112
+ :limit => @reflection.options[:limit],
113
+ :offset => @reflection.options[:offset],
114
+ :joins => @reflection.options[:joins],
115
+ :include => @reflection.options[:include],
116
+ :select => @reflection.options[:select]
117
+ )
118
+ end
119
+
120
+ private
121
+ def method_missing(method, *args, &block)
122
+ if load_target
123
+ @target.send(method, *args, &block)
124
+ end
125
+ end
126
+
127
+ def load_target
128
+ return nil unless defined?(@loaded)
129
+
130
+ if !loaded? and (!@owner.new_record? || foreign_key_present)
131
+ @target = find_target
132
+ end
133
+
134
+ @loaded = true
135
+ @target
136
+ rescue ActiveRecord::RecordNotFound
137
+ reset
138
+ end
139
+
140
+ # Can be overwritten by associations that might have the foreign key available for an association without
141
+ # having the object itself (and still being a new record). Currently, only belongs_to present this scenario.
142
+ def foreign_key_present
143
+ false
144
+ end
145
+
146
+ def raise_on_type_mismatch(record)
147
+ unless record.is_a?(@reflection.klass)
148
+ raise BigRecord::AssociationTypeMismatch, "#{@reflection.class_name} expected, got #{record.class}"
149
+ end
150
+ end
151
+
152
+ # Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
153
+ def flatten_deeper(array)
154
+ array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,57 @@
1
+ module BigRecord
2
+ module ArAssociations
3
+ class BelongsToAssociation < AssociationProxy #:nodoc:
4
+
5
+ def create(attributes = {})
6
+ replace(@reflection.klass.create(attributes))
7
+ end
8
+
9
+ def build(attributes = {})
10
+ replace(@reflection.klass.new(attributes))
11
+ end
12
+
13
+ def replace(record)
14
+ counter_cache_name = @reflection.counter_cache_column
15
+
16
+ if record.nil?
17
+ if counter_cache_name && @owner[counter_cache_name] && !@owner.new_record?
18
+ @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
19
+ end
20
+
21
+ @target = @owner[@reflection.primary_key_name] = nil
22
+ else
23
+ raise_on_type_mismatch(record)
24
+
25
+ if counter_cache_name && !@owner.new_record?
26
+ @reflection.klass.increment_counter(counter_cache_name, record.id)
27
+ @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
28
+ end
29
+
30
+ @target = (AssociationProxy === record ? record.target : record)
31
+ @owner[@reflection.primary_key_name] = record.id unless record.new_record?
32
+ @updated = true
33
+ end
34
+
35
+ loaded
36
+ record
37
+ end
38
+
39
+ def updated?
40
+ @updated
41
+ end
42
+
43
+ private
44
+ def find_target
45
+ @reflection.klass.find(
46
+ @owner[@reflection.primary_key_name],
47
+ :conditions => conditions,
48
+ :include => @reflection.options[:include]
49
+ )
50
+ end
51
+
52
+ def foreign_key_present
53
+ !@owner[@reflection.primary_key_name].nil?
54
+ end
55
+ end
56
+ end
57
+ end