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,194 @@
1
+ require 'set'
2
+
3
+ module BigRecord
4
+ module BrAssociations
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,153 @@
1
+ module BigRecord
2
+ module BrAssociations
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 reset
44
+ @loaded = false
45
+ @target = nil
46
+ end
47
+
48
+ def reload
49
+ reset
50
+ load_target
51
+ end
52
+
53
+ def loaded?
54
+ @loaded
55
+ end
56
+
57
+ def loaded
58
+ @loaded = true
59
+ end
60
+
61
+ def target
62
+ @target
63
+ end
64
+
65
+ def target=(target)
66
+ @target = target
67
+ loaded
68
+ end
69
+
70
+ protected
71
+ def dependent?
72
+ @reflection.options[:dependent] || false
73
+ end
74
+
75
+ def quoted_record_ids(records)
76
+ records.map { |record| record.quoted_id }.join(',')
77
+ end
78
+
79
+ # def interpolate_sql_options!(options, *keys)
80
+ # keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
81
+ # end
82
+ #
83
+ # def interpolate_sql(sql, record = nil)
84
+ # @owner.send(:interpolate_sql, sql, record)
85
+ # end
86
+ #
87
+ # def sanitize_sql(sql)
88
+ # @reflection.klass.send(:sanitize_sql, sql)
89
+ # end
90
+
91
+ def extract_options_from_args!(args)
92
+ @owner.send(:extract_options_from_args!, args)
93
+ end
94
+
95
+ def set_belongs_to_association_for(record)
96
+ if @reflection.options[:as]
97
+ record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
98
+ record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
99
+ else
100
+ record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
101
+ end
102
+ end
103
+
104
+ def merge_options_from_reflection!(options)
105
+ options.reverse_merge!(
106
+ :group => @reflection.options[:group],
107
+ :limit => @reflection.options[:limit],
108
+ :offset => @reflection.options[:offset],
109
+ :joins => @reflection.options[:joins],
110
+ :include => @reflection.options[:include],
111
+ :select => @reflection.options[:select]
112
+ )
113
+ end
114
+
115
+ private
116
+ def method_missing(method, *args, &block)
117
+ if load_target
118
+ @target.send(method, *args, &block)
119
+ end
120
+ end
121
+
122
+ def load_target
123
+ return nil unless defined?(@loaded)
124
+
125
+ if !loaded? and (!@owner.new_record? || foreign_key_present)
126
+ @target = find_target
127
+ end
128
+
129
+ @loaded = true
130
+ @target
131
+ rescue BigRecord::RecordNotFound
132
+ reset
133
+ end
134
+
135
+ # Can be overwritten by associations that might have the foreign key available for an association without
136
+ # having the object itself (and still being a new record). Currently, only belongs_to present this scenario.
137
+ def foreign_key_present
138
+ false
139
+ end
140
+
141
+ def raise_on_type_mismatch(record)
142
+ unless record.is_a?(@reflection.klass)
143
+ raise BigRecord::AssociationTypeMismatch, "#{@reflection.class_name} expected, got #{record.class}"
144
+ end
145
+ end
146
+
147
+ # Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
148
+ def flatten_deeper(array)
149
+ array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,52 @@
1
+ module BigRecord
2
+ module BrAssociations
3
+ class BelongsToAssociation < AssociationProxy #:nodoc:
4
+ def create(attributes = {})
5
+ replace(@reflection.klass.create(attributes))
6
+ end
7
+
8
+ def build(attributes = {})
9
+ replace(@reflection.klass.new(attributes))
10
+ end
11
+
12
+ def replace(record)
13
+ counter_cache_name = @reflection.counter_cache_column
14
+
15
+ if record.nil?
16
+ if counter_cache_name && @owner[counter_cache_name] && !@owner.new_record?
17
+ @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
18
+ end
19
+
20
+ @target = @owner[@reflection.primary_key_name] = nil
21
+ else
22
+ raise_on_type_mismatch(record)
23
+
24
+ if counter_cache_name && !@owner.new_record?
25
+ @reflection.klass.increment_counter(counter_cache_name, record.id)
26
+ @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
27
+ end
28
+
29
+ @target = (AssociationProxy === record ? record.target : record)
30
+ @owner[@reflection.primary_key_name] = record.id unless record.new_record?
31
+ @updated = true
32
+ end
33
+
34
+ loaded
35
+ record
36
+ end
37
+
38
+ def updated?
39
+ @updated
40
+ end
41
+
42
+ private
43
+ def find_target
44
+ @reflection.klass.find(@owner[@reflection.primary_key_name])
45
+ end
46
+
47
+ def foreign_key_present
48
+ !@owner[@reflection.primary_key_name].nil?
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,293 @@
1
+ module BigRecord
2
+ module BrAssociations
3
+ class BelongsToManyAssociation < AssociationProxy #:nodoc:
4
+ def to_ary
5
+ load_target
6
+ @target.to_ary
7
+ end
8
+
9
+ def reset
10
+ @loaded = false
11
+ reset_target!
12
+ end
13
+
14
+ # Add +records+ to this association. Returns +self+ so method calls may be chained.
15
+ # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
16
+ def <<(*records)
17
+ result = true
18
+ load_target
19
+
20
+ flatten_deeper(records).each do |record|
21
+ raise_on_type_mismatch(record)
22
+ callback(:before_add, record)
23
+ result &&= insert_record(record)
24
+ @target << record
25
+ callback(:after_add, record)
26
+ end
27
+
28
+ result && self
29
+ end
30
+
31
+ alias_method :push, :<<
32
+ alias_method :concat, :<<
33
+
34
+ # Remove all records from this association
35
+ def delete_all
36
+ load_target
37
+ delete(@target)
38
+ reset_target!
39
+ end
40
+
41
+ # # Calculate sum using SQL, not Enumerable
42
+ # def sum(*args, &block)
43
+ # calculate(:sum, *args, &block)
44
+ # end
45
+
46
+ # Remove +records+ from this association. Does not destroy +records+.
47
+ def delete(*records)
48
+ records = flatten_deeper(records)
49
+ records.each { |record| raise_on_type_mismatch(record) }
50
+ records.reject! { |record| @target.delete(record) if record.new_record? }
51
+ return if records.empty?
52
+
53
+ records.each { |record| callback(:before_remove, record) }
54
+ delete_records(records)
55
+ records.each do |record|
56
+ @target.delete(record)
57
+ callback(:after_remove, record)
58
+ end
59
+ end
60
+
61
+ # Removes all records from this association. Returns +self+ so method calls may be chained.
62
+ def clear
63
+ return self if length.zero? # forces load_target if hasn't happened already
64
+
65
+ if @reflection.options[:dependent] && @reflection.options[:dependent] == :delete_all
66
+ destroy_all
67
+ else
68
+ delete_all
69
+ end
70
+
71
+ self
72
+ end
73
+
74
+ def destroy_all
75
+ # HbaseAdapter doesn't support transactions
76
+ # @owner.transaction do
77
+ each { |record| record.destroy }
78
+ # end
79
+
80
+ reset_target!
81
+ end
82
+
83
+ def create(attributes = {})
84
+ # Can't use Base.create since the foreign key may be a protected attribute.
85
+ if attributes.is_a?(Array)
86
+ attributes.collect { |attr| create(attr) }
87
+ else
88
+ record = build(attributes)
89
+ record.save unless @owner.new_record?
90
+ record
91
+ end
92
+ end
93
+
94
+ # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
95
+ # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
96
+ # and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
97
+ def size
98
+ @target.size
99
+ end
100
+
101
+ # Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
102
+ # whether the collection is empty, use collection.length.zero? instead of collection.empty?
103
+ def length
104
+ load_target.size
105
+ end
106
+
107
+ def empty?
108
+ size.zero?
109
+ end
110
+
111
+ def uniq(collection = self)
112
+ seen = Set.new
113
+ collection.inject([]) do |kept, record|
114
+ unless seen.include?(record.id)
115
+ kept << record
116
+ seen << record.id
117
+ end
118
+ kept
119
+ end
120
+ end
121
+
122
+ # Replace this collection with +other_array+
123
+ # This will perform a diff and delete/add only records that have changed.
124
+ def replace(other_array)
125
+ if other_array.nil?
126
+ @target = @owner[@reflection.primary_key_name] = nil
127
+ elsif other_array.empty?
128
+ @target = @owner[@reflection.primary_key_name] = []
129
+ else
130
+ other_array.each { |val| raise_on_type_mismatch(val) }
131
+
132
+ load_target
133
+ other = other_array.size < 100 ? other_array : other_array.to_set
134
+ current = @target.size < 100 ? @target : @target.to_set
135
+
136
+ delete(@target.select { |v| !other.include?(v) })
137
+ concat(other_array.select { |v| !current.include?(v) })
138
+ @updated = true
139
+ end
140
+ self
141
+ end
142
+
143
+ def updated?
144
+ @updated
145
+ end
146
+
147
+ protected
148
+ def reset_target!
149
+ @target = Array.new
150
+ end
151
+
152
+ def find_target
153
+ records = find(:all)
154
+ @reflection.options[:uniq] ? uniq(records) : records
155
+ end
156
+
157
+ private
158
+ def callback(method, record)
159
+ callbacks_for(method).each do |callback|
160
+ case callback
161
+ when Symbol
162
+ @owner.send(callback, record)
163
+ when Proc, Method
164
+ callback.call(@owner, record)
165
+ else
166
+ if callback.respond_to?(method)
167
+ callback.send(method, @owner, record)
168
+ else
169
+ 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."
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ def callbacks_for(callback_name)
176
+ full_callback_name = "#{callback_name}_for_#{@reflection.name}"
177
+ @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
178
+ end
179
+
180
+
181
+
182
+
183
+
184
+
185
+
186
+ public
187
+ def build(attributes = {})
188
+ if attributes.is_a?(Array)
189
+ attributes.collect { |attr| build(attr) }
190
+ else
191
+ record = @reflection.klass.new(attributes)
192
+ set_belongs_to_association_for(record)
193
+
194
+ @target ||= [] unless loaded?
195
+ @target << record
196
+
197
+ record
198
+ end
199
+ end
200
+
201
+ # Count the number of associated records. All arguments are optional.
202
+ def count(*args)
203
+ load_target
204
+ @target.size
205
+ end
206
+
207
+ def find(*args)
208
+ raise ArgumentError, "Only find(:all) is supported" unless args == [:all]
209
+ return [] if @owner[@reflection.primary_key_name].blank?
210
+
211
+ if @reflection.options[:cache]
212
+ # Create the items proxies using the content that is cached
213
+ records = []
214
+ @owner[@reflection.primary_key_name].each do |id|
215
+ records << CachedItemProxyFactory.instance.create(id, @owner, @reflection)
216
+ end
217
+ records
218
+ else
219
+ # Don't throw an exception when the records are not found
220
+ records = []
221
+ @owner[@reflection.primary_key_name].each do |id|
222
+ begin
223
+ records << @reflection.klass.find(id)
224
+ rescue BigRecord::RecordNotFound
225
+ # do nothing
226
+ end
227
+ end
228
+ records
229
+ end
230
+ end
231
+
232
+ protected
233
+ def load_target
234
+ begin
235
+ if !loaded?
236
+ if @target.is_a?(Array) && @target.any?
237
+ @target = (find_target + @target).uniq
238
+ else
239
+ @target = find_target
240
+ end
241
+ end
242
+ rescue ActiveRecord::RecordNotFound, BigRecord::RecordNotFound
243
+ reset
244
+ end
245
+
246
+ loaded if target
247
+ target
248
+ end
249
+
250
+ def count_records
251
+ count = if has_cached_counter?
252
+ @owner.send(:read_attribute, cached_counter_attribute_name)
253
+ elsif @reflection.options[:counter_sql]
254
+ @reflection.klass.count_by_sql(@counter_sql)
255
+ else
256
+ @reflection.klass.count(:conditions => @counter_sql)
257
+ end
258
+
259
+ @target = [] and loaded if count == 0
260
+
261
+ if @reflection.options[:limit]
262
+ count = [ @reflection.options[:limit], count ].min
263
+ end
264
+
265
+ return count
266
+ end
267
+
268
+ def has_cached_counter?
269
+ @owner.attribute_present?(cached_counter_attribute_name)
270
+ end
271
+
272
+ def cached_counter_attribute_name
273
+ "#{@reflection.name}_count"
274
+ end
275
+
276
+ def insert_record(record)
277
+ @owner[@reflection.primary_key_name] ||= []
278
+ @owner[@reflection.primary_key_name] << record.id unless record.new_record?
279
+ end
280
+
281
+ def delete_records(records)
282
+ if @owner[@reflection.primary_key_name]
283
+ records.each{|r| @owner[@reflection.primary_key_name].delete(r.id)}
284
+ end
285
+ end
286
+
287
+ def target_obsolete?
288
+ false
289
+ end
290
+
291
+ end
292
+ end
293
+ end