datamapper 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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