datamapper 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. data/CHANGELOG +5 -1
  2. data/FAQ +96 -0
  3. data/QUICKLINKS +12 -0
  4. data/README +57 -155
  5. data/environment.rb +61 -43
  6. data/example.rb +30 -12
  7. data/lib/data_mapper.rb +6 -1
  8. data/lib/data_mapper/adapters/abstract_adapter.rb +0 -57
  9. data/lib/data_mapper/adapters/data_object_adapter.rb +203 -97
  10. data/lib/data_mapper/adapters/mysql_adapter.rb +4 -0
  11. data/lib/data_mapper/adapters/postgresql_adapter.rb +7 -1
  12. data/lib/data_mapper/adapters/sql/coersion.rb +3 -2
  13. data/lib/data_mapper/adapters/sql/commands/load_command.rb +29 -10
  14. data/lib/data_mapper/adapters/sql/mappings/associations_set.rb +4 -0
  15. data/lib/data_mapper/adapters/sql/mappings/column.rb +13 -9
  16. data/lib/data_mapper/adapters/sql/mappings/conditions.rb +172 -0
  17. data/lib/data_mapper/adapters/sql/mappings/table.rb +43 -17
  18. data/lib/data_mapper/adapters/sqlite3_adapter.rb +9 -2
  19. data/lib/data_mapper/associations.rb +75 -3
  20. data/lib/data_mapper/associations/belongs_to_association.rb +70 -36
  21. data/lib/data_mapper/associations/has_and_belongs_to_many_association.rb +195 -86
  22. data/lib/data_mapper/associations/has_many_association.rb +168 -61
  23. data/lib/data_mapper/associations/has_n_association.rb +23 -3
  24. data/lib/data_mapper/attributes.rb +73 -0
  25. data/lib/data_mapper/auto_migrations.rb +2 -6
  26. data/lib/data_mapper/base.rb +5 -9
  27. data/lib/data_mapper/database.rb +4 -3
  28. data/lib/data_mapper/embedded_value.rb +66 -30
  29. data/lib/data_mapper/identity_map.rb +1 -3
  30. data/lib/data_mapper/is/tree.rb +121 -0
  31. data/lib/data_mapper/migration.rb +155 -0
  32. data/lib/data_mapper/persistence.rb +532 -218
  33. data/lib/data_mapper/property.rb +306 -0
  34. data/lib/data_mapper/query.rb +164 -0
  35. data/lib/data_mapper/support/blank.rb +2 -2
  36. data/lib/data_mapper/support/connection_pool.rb +5 -6
  37. data/lib/data_mapper/support/enumerable.rb +3 -3
  38. data/lib/data_mapper/support/errors.rb +10 -1
  39. data/lib/data_mapper/support/inflector.rb +174 -238
  40. data/lib/data_mapper/support/object.rb +54 -0
  41. data/lib/data_mapper/support/serialization.rb +19 -1
  42. data/lib/data_mapper/support/string.rb +7 -16
  43. data/lib/data_mapper/support/symbol.rb +3 -15
  44. data/lib/data_mapper/support/typed_set.rb +68 -0
  45. data/lib/data_mapper/types/base.rb +44 -0
  46. data/lib/data_mapper/types/string.rb +34 -0
  47. data/lib/data_mapper/validations/number_validator.rb +40 -0
  48. data/lib/data_mapper/validations/string_validator.rb +20 -0
  49. data/lib/data_mapper/validations/validator.rb +13 -0
  50. data/performance.rb +26 -1
  51. data/profile_data_mapper.rb +1 -1
  52. data/rakefile.rb +42 -2
  53. data/spec/acts_as_tree_spec.rb +11 -3
  54. data/spec/adapters/data_object_adapter_spec.rb +31 -0
  55. data/spec/associations/belongs_to_association_spec.rb +98 -0
  56. data/spec/associations/has_and_belongs_to_many_association_spec.rb +377 -0
  57. data/spec/associations/has_many_association_spec.rb +337 -0
  58. data/spec/attributes_spec.rb +23 -1
  59. data/spec/auto_migrations_spec.rb +86 -29
  60. data/spec/callbacks_spec.rb +107 -0
  61. data/spec/column_spec.rb +5 -2
  62. data/spec/count_command_spec.rb +33 -1
  63. data/spec/database_spec.rb +18 -0
  64. data/spec/dependency_spec.rb +4 -2
  65. data/spec/embedded_value_spec.rb +8 -8
  66. data/spec/fixtures/people.yaml +1 -1
  67. data/spec/fixtures/projects.yaml +10 -1
  68. data/spec/fixtures/tasks.yaml +6 -0
  69. data/spec/fixtures/tasks_tasks.yaml +2 -0
  70. data/spec/fixtures/tomatoes.yaml +1 -0
  71. data/spec/is_a_tree_spec.rb +149 -0
  72. data/spec/load_command_spec.rb +71 -9
  73. data/spec/magic_columns_spec.rb +17 -2
  74. data/spec/migration_spec.rb +267 -0
  75. data/spec/models/animal.rb +1 -1
  76. data/spec/models/candidate.rb +8 -0
  77. data/spec/models/career.rb +1 -1
  78. data/spec/models/chain.rb +8 -0
  79. data/spec/models/comment.rb +1 -1
  80. data/spec/models/exhibit.rb +1 -1
  81. data/spec/models/fence.rb +7 -0
  82. data/spec/models/fruit.rb +2 -2
  83. data/spec/models/job.rb +8 -0
  84. data/spec/models/person.rb +2 -3
  85. data/spec/models/post.rb +1 -1
  86. data/spec/models/project.rb +21 -1
  87. data/spec/models/section.rb +1 -1
  88. data/spec/models/serializer.rb +1 -1
  89. data/spec/models/task.rb +9 -0
  90. data/spec/models/tomato.rb +27 -0
  91. data/spec/models/user.rb +8 -2
  92. data/spec/models/zoo.rb +2 -7
  93. data/spec/paranoia_spec.rb +1 -1
  94. data/spec/{base_spec.rb → persistence_spec.rb} +207 -18
  95. data/spec/postgres_spec.rb +48 -6
  96. data/spec/property_spec.rb +90 -9
  97. data/spec/query_spec.rb +71 -5
  98. data/spec/save_command_spec.rb +11 -0
  99. data/spec/spec_helper.rb +14 -11
  100. data/spec/support/blank_spec.rb +8 -0
  101. data/spec/support/inflector_spec.rb +41 -0
  102. data/spec/support/object_spec.rb +9 -0
  103. data/spec/{serialization_spec.rb → support/serialization_spec.rb} +1 -1
  104. data/spec/support/silence_spec.rb +15 -0
  105. data/spec/{support_spec.rb → support/string_spec.rb} +3 -3
  106. data/spec/support/struct_spec.rb +12 -0
  107. data/spec/support/typed_set_spec.rb +66 -0
  108. data/spec/table_spec.rb +3 -3
  109. data/spec/types/string.rb +81 -0
  110. data/spec/validates_uniqueness_of_spec.rb +17 -0
  111. data/spec/validations/number_validator.rb +59 -0
  112. data/spec/validations/string_validator.rb +14 -0
  113. metadata +59 -17
  114. data/do_performance.rb +0 -153
  115. data/lib/data_mapper/support/active_record_impersonation.rb +0 -103
  116. data/lib/data_mapper/support/weak_hash.rb +0 -46
  117. data/spec/active_record_impersonation_spec.rb +0 -129
  118. data/spec/associations_spec.rb +0 -232
  119. data/spec/conditions_spec.rb +0 -49
  120. data/spec/has_many_association_spec.rb +0 -173
  121. data/spec/models/animals_exhibit.rb +0 -8
@@ -2,105 +2,156 @@ require 'data_mapper/associations/has_n_association'
2
2
 
3
3
  module DataMapper
4
4
  module Associations
5
-
5
+
6
6
  class HasManyAssociation < HasNAssociation
7
-
7
+
8
+ def dependency
9
+ @options[:dependent]
10
+ end
11
+
8
12
  # Define the association instance method (i.e. Project#tasks)
9
13
  def define_accessor(klass)
10
14
  klass.class_eval <<-EOS
11
15
  def #{@association_name}
12
16
  @#{@association_name} || (@#{@association_name} = DataMapper::Associations::HasManyAssociation::Set.new(self, #{@association_name.inspect}))
13
17
  end
14
-
18
+
15
19
  def #{@association_name}=(value)
16
20
  #{@association_name}.set(value)
17
21
  end
22
+
23
+ private
24
+ def #{@association_name}_keys=(value)
25
+ #{@association_name}.clear
26
+
27
+ associated_constant = #{@association_name}.association.associated_constant
28
+ associated_table = #{@association_name}.association.associated_table
29
+ associated_constant.all(associated_table.key => [*value]).each do |entry|
30
+ #{@association_name} << entry
31
+ end
32
+ end
18
33
  EOS
19
34
  end
20
-
35
+
21
36
  def to_disassociate_sql
22
37
  "UPDATE #{associated_table.to_sql} SET #{foreign_key_column.to_sql} = NULL WHERE #{foreign_key_column.to_sql} = ?"
23
38
  end
24
-
39
+
40
+ def to_delete_sql
41
+ "DELETE FROM #{associated_table.to_sql} WHERE #{foreign_key_column.to_sql} = ?"
42
+ end
43
+
25
44
  def instance_variable_name
26
45
  class << self
27
46
  attr_reader :instance_variable_name
28
47
  end
29
-
48
+
30
49
  @instance_variable_name = "@#{@association_name}"
31
50
  end
32
-
51
+
33
52
  class Set < Associations::Reference
34
-
53
+
35
54
  include Enumerable
36
-
37
- def dirty?
38
- @items && @items.any? { |item| item.dirty? }
55
+
56
+ # Returns true if the association has zero items
57
+ def nil?
58
+ loaded_members.blank?
59
+ end
60
+
61
+ def dirty?(cleared = ::Set.new)
62
+ loaded_members.any? { |member| cleared.include?(member) || member.dirty?(cleared) }
39
63
  end
40
-
64
+
41
65
  def validate_recursively(event, cleared)
42
- @items.blank? || @items.all? { |item| cleared.include?(item) || item.validate_recursively(event, cleared) }
66
+ loaded_members.all? { |member| cleared.include?(member) || member.validate_recursively(event, cleared) }
43
67
  end
44
-
45
- def save_without_validation(database_context)
46
-
68
+
69
+ def save_without_validation(database_context, cleared)
70
+
47
71
  adapter = @instance.database_context.adapter
48
-
72
+
73
+ members = loaded_members
74
+
49
75
  adapter.connection do |db|
50
- command = db.create_command(association.to_disassociate_sql)
51
- command.execute_non_query(@instance.key)
76
+
77
+ sql = association.to_disassociate_sql
78
+ parameters = [@instance.key]
79
+
80
+ member_keys = members.map { |member| member.key }.compact
81
+
82
+ unless member_keys.empty?
83
+ sql << " AND #{association.associated_table.key} NOT IN ?"
84
+ parameters << member_keys
85
+ end
86
+
87
+ db.create_command(sql).execute_non_query(*parameters)
52
88
  end
53
-
54
- unless @items.nil? || @items.empty?
55
-
56
-
89
+
90
+ unless members.blank?
91
+
57
92
  setter_method = "#{@association_name}=".to_sym
58
93
  ivar_name = association.foreign_key_column.instance_variable_name
59
- @items.each do |item|
60
- item.instance_variable_set(ivar_name, @instance.key)
61
- @instance.database_context.adapter.save_without_validation(database_context, item)
94
+ original_value_name = association.foreign_key_column.name
95
+
96
+ members.each do |member|
97
+ member.original_values.delete(original_value_name)
98
+ member.instance_variable_set(ivar_name, @instance.key)
99
+ @instance.database_context.adapter.save_without_validation(database_context, member, cleared)
62
100
  end
63
101
  end
64
102
  end
65
-
103
+
66
104
  def each
67
105
  items.each { |item| yield item }
68
106
  end
69
-
70
- def <<(associated_item)
71
- (@items || @items = []) << associated_item
72
-
73
- # TODO: Optimize!
74
- fk = association.foreign_key_column
75
- foreign_association = association.associated_table.associations.find do |mapping|
76
- mapping.is_a?(BelongsToAssociation) && mapping.foreign_key_column == fk
77
- end
78
-
79
- associated_item.send("#{foreign_association.name}=", @instance) if foreign_association
80
-
81
- return @items
82
- end
83
107
 
108
+ # Builds a new item and returns it.
84
109
  def build(options)
85
110
  item = association.associated_constant.new(options)
86
111
  self << item
87
112
  item
88
113
  end
89
114
 
115
+ # Builds and saves a new item, then returns it.
90
116
  def create(options)
91
117
  item = build(options)
92
118
  item.save
93
119
  item
94
120
  end
95
-
121
+
96
122
  def set(value)
97
123
  values = value.is_a?(Enumerable) ? value : [value]
98
- @items = []
124
+ @items = Support::TypedSet.new(association.associated_constant)
99
125
  values.each do |item|
100
126
  self << item
101
127
  end
102
128
  end
103
-
129
+
130
+ # Adds a new item to the association. The entire item collection is then returned.
131
+ def <<(member)
132
+ shallow_append(member)
133
+
134
+ if complement = association.complementary_association
135
+ member.send("#{complement.name}_association").shallow_append(@instance)
136
+ end
137
+
138
+ return self
139
+ end
140
+
141
+ def clear
142
+ @pending_members = nil
143
+ @items = Support::TypedSet.new(association.associated_constant)
144
+ end
145
+
146
+ def shallow_append(member)
147
+ if @items
148
+ self.items << member
149
+ else
150
+ pending_members << member
151
+ end
152
+ return self
153
+ end
154
+
104
155
  def method_missing(symbol, *args, &block)
105
156
  if items.respond_to?(symbol)
106
157
  items.send(symbol, *args, &block)
@@ -112,17 +163,17 @@ module DataMapper
112
163
  end
113
164
  end
114
165
  results.flatten
115
- elsif items.size == 1 && items.first.respond_to?(symbol)
116
- items.first.send(symbol, *args, &block)
166
+ elsif items.size == 1 && items.entries.first.respond_to?(symbol)
167
+ items.entries.first.send(symbol, *args, &block)
117
168
  else
118
169
  super
119
170
  end
120
171
  end
121
-
172
+
122
173
  def respond_to?(symbol)
123
174
  items.respond_to?(symbol) || super
124
175
  end
125
-
176
+
126
177
  def reload!
127
178
  @items = nil
128
179
  end
@@ -130,10 +181,10 @@ module DataMapper
130
181
  def items
131
182
  @items || begin
132
183
  if @instance.loaded_set.nil?
133
- @items = []
134
- else
184
+ @items = Support::TypedSet.new(association.associated_constant)
185
+ else
135
186
  associated_items = fetch_sets
136
-
187
+
137
188
  # This is where @items is set, by calling association=,
138
189
  # which in turn calls HasManyAssociation::Set#set.
139
190
  association_ivar_name = association.instance_variable_name
@@ -141,36 +192,92 @@ module DataMapper
141
192
  @instance.loaded_set.each do |entry|
142
193
  entry.send(setter_method, associated_items[entry.key])
143
194
  end # @instance.loaded_set.each
144
-
145
- return @items
146
195
  end # if @instance.loaded_set.nil?
196
+
197
+ if @pending_members
198
+ pending_members.each do |member|
199
+ @items << member
200
+ end
201
+
202
+ pending_members.clear
203
+ end
204
+
205
+ return @items
147
206
  end # begin
148
207
  end # def items
149
-
208
+
150
209
  def inspect
151
210
  entries.inspect
152
211
  end
153
-
212
+
213
+ def first
214
+ items.entries.first
215
+ end
216
+
217
+ def last
218
+ items.entries.last
219
+ end
220
+
154
221
  def ==(other)
155
- (items.size == 1 ? items.first : items) == other
222
+ (items.size == 1 ? first : items) == other
223
+ end
224
+
225
+ def deactivate
226
+ case association.dependency
227
+ when :destroy
228
+ items.entries.each do |member|
229
+ status = member.destroy! unless member.new_record?
230
+ return false unless status
231
+ end
232
+ when :delete
233
+ @instance.database_context.adapter.connection do |db|
234
+ sql = association.to_delete_sql
235
+ parameters = [@instance.key]
236
+ db.create_command(sql).execute_non_query(*parameters)
237
+ end
238
+ when :protect
239
+ unless items.empty?
240
+ raise AssociationProtectedError.new("You cannot delete this model while it has items associated with it.")
241
+ end
242
+ when :nullify
243
+ nullify_association
244
+ else
245
+ nullify_association
246
+ end
247
+ end
248
+
249
+ def nullify_association
250
+ @instance.database_context.adapter.connection do |db|
251
+ sql = association.to_disassociate_sql
252
+ parameters = [@instance.key]
253
+ db.create_command(sql).execute_non_query(*parameters)
254
+ end
156
255
  end
157
-
256
+
158
257
  private
258
+ def loaded_members
259
+ pending_members + @items
260
+ end
261
+
262
+ def pending_members
263
+ @pending_members || @pending_members = Support::TypedSet.new(association.associated_constant)
264
+ end
265
+
159
266
  def fetch_sets
160
267
  finder_options = { association.foreign_key_column.to_sym => @instance.loaded_set.map { |item| item.key } }
161
268
  finder_options.merge!(association.finder_options)
162
-
269
+
163
270
  foreign_key_ivar_name = association.foreign_key_column.instance_variable_name
164
-
271
+
165
272
  @instance.database_context.all(
166
273
  association.associated_constant,
167
274
  finder_options
168
275
  ).group_by { |entry| entry.instance_variable_get(foreign_key_ivar_name) }
169
276
  end
170
-
277
+
171
278
  end
172
279
 
173
280
  end
174
-
281
+
175
282
  end
176
283
  end
@@ -1,6 +1,7 @@
1
1
  module DataMapper
2
2
 
3
3
  class ForeignKeyNotFoundError < StandardError; end
4
+ class AssociationProtectedError < StandardError; end
4
5
 
5
6
  module Associations
6
7
 
@@ -11,7 +12,8 @@ module DataMapper
11
12
  OPTIONS = [
12
13
  :class,
13
14
  :class_name,
14
- :foreign_key
15
+ :foreign_key,
16
+ :dependent
15
17
  ]
16
18
 
17
19
  def initialize(klass, association_name, options)
@@ -106,6 +108,24 @@ module DataMapper
106
108
  associated_table.columns.reject { |column| column.lazy? }
107
109
  end
108
110
 
111
+ def complementary_association
112
+ @complementary_association || begin
113
+ @complementary_association = associated_table.associations.find do |mapping|
114
+ mapping.is_a?(BelongsToAssociation) &&
115
+ mapping.foreign_key_column == foreign_key_column &&
116
+ mapping.key_table.name == key_table.name
117
+ end
118
+
119
+ if @complementary_association
120
+ class << self
121
+ attr_accessor :complementary_association
122
+ end
123
+ end
124
+
125
+ return @complementary_association
126
+ end
127
+ end
128
+
109
129
  def finder_options
110
130
  @finder_options || @finder_options = @options.reject { |k,v| self.class::OPTIONS.include?(k) }
111
131
  end
@@ -114,10 +134,10 @@ module DataMapper
114
134
  "JOIN #{associated_table.to_sql} ON #{foreign_key_column.to_sql(true)} = #{primary_key_column.to_sql(true)}"
115
135
  end
116
136
 
117
- def activate!
137
+ def activate!(force = false)
118
138
  foreign_key_column
119
139
  end
120
140
  end
121
141
 
122
142
  end
123
- end
143
+ end
@@ -0,0 +1,73 @@
1
+ module DataMapper
2
+
3
+ module Attributes
4
+
5
+ def self.included(klass)
6
+ klass.const_set('ATTRIBUTES', Set.new) unless klass.const_defined?('ATTRIBUTES')
7
+ end
8
+
9
+ def attributes
10
+ __get_attributes(true)
11
+ end
12
+
13
+ # Mass-assign mapped fields.
14
+ def attributes=(values_hash)
15
+ __set_attributes(values_hash, true)
16
+ end
17
+
18
+ private
19
+
20
+ def __method_defined?(name, public_only = true)
21
+ if public_only
22
+ self.class.public_method_defined?(name)
23
+ else
24
+ self.class.private_method_defined?(name) ||
25
+ self.class.protected_method_defined?(name) ||
26
+ self.class.public_method_defined?(name)
27
+ end
28
+ end
29
+
30
+ def __get_attributes(public_only)
31
+ pairs = {}
32
+
33
+ self.class::ATTRIBUTES.each do |name|
34
+ getter = if __method_defined?(name, public_only)
35
+ name
36
+ elsif __method_defined?(name.to_s.ensure_ends_with('?'), public_only)
37
+ name.to_s.ensure_ends_with('?')
38
+ else
39
+ nil
40
+ end
41
+
42
+ if getter
43
+ value = send(getter)
44
+ pairs[name] = value.is_a?(Class) ? value.to_s : value
45
+ end
46
+ end
47
+
48
+ pairs
49
+ end
50
+
51
+ def __set_attributes(values_hash, public_only)
52
+ values_hash.each_pair do |k,v|
53
+ setter_name = k.to_s.sub(/\?$/, '').ensure_ends_with('=')
54
+ if __method_defined?(setter_name, public_only)
55
+ send(setter_name, v)
56
+ end
57
+ end
58
+
59
+ self
60
+ end
61
+
62
+ # return all attributes, regardless of their visibility
63
+ def private_attributes
64
+ __get_attributes(false)
65
+ end
66
+
67
+ # private method for setting any/all attribute values, regardless of visibility
68
+ def private_attributes=(values_hash)
69
+ __set_attributes(values_hash, false)
70
+ end
71
+ end
72
+
73
+ end