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
@@ -24,11 +24,13 @@ module DataMapper
24
24
 
25
25
  TABLE_QUOTING_CHARACTER = '"'.freeze
26
26
  COLUMN_QUOTING_CHARACTER = '"'.freeze
27
+ TRUE_ALIASES << 't'.freeze
28
+ FALSE_ALIASES << 'f'.freeze
27
29
 
28
30
  def create_connection
29
31
  conn = DataObject::Sqlite3::Connection.new("dbname=#{@configuration.database}")
30
32
  conn.logger = self.logger
31
- conn.open
33
+ conn.open if conn.respond_to?(:open)
32
34
  return conn
33
35
  end
34
36
 
@@ -65,6 +67,11 @@ module DataMapper
65
67
  PRAGMA TABLE_INFO(?)
66
68
  EOS
67
69
  end
70
+
71
+ def to_truncate_sql
72
+ "DELETE FROM #{to_sql}"
73
+ end
74
+
68
75
  alias_method :to_columns_sql, :to_column_exists_sql
69
76
 
70
77
  def unquote_default(default)
@@ -148,4 +155,4 @@ module DataMapper
148
155
  end # class Sqlite3Adapter
149
156
 
150
157
  end # module Adapters
151
- end # module DataMapper
158
+ end # module DataMapper
@@ -5,25 +5,97 @@ require 'data_mapper/associations/has_and_belongs_to_many_association'
5
5
 
6
6
  module DataMapper
7
7
  module Associations
8
-
8
+
9
+ # Extends +base+ with methods for setting up associations between different models.
9
10
  def self.included(base)
10
11
  base.extend(ClassMethods)
11
12
  end
12
13
 
13
14
  module ClassMethods
14
-
15
+ # Adds the following methods for query of a single associated object:
16
+ # * <tt>collection(</tt> - returns a set containing the associated objects. Returns
17
+ # an empty set if no objects are found.
18
+ # * <tt>collection << object</tt> - adds an object to the collection.
19
+ # * <tt>collection = [objects]</tt> - replaces the collections content by deleting and
20
+ # adding objects as appropriate.
21
+ # * <tt>collection.empty?</tt> - returns +true+ if there is no associated objects.
22
+ # * <tt>collection.size</tt> - returns the number of associated objects.
23
+ #
24
+ # Options are:
25
+ # * <tt>:class</tt> - specify the class name of the association. So has_many :animals will by
26
+ # default be linked to the Animal class, but if you want the association to use a
27
+ # different class, you'll have to specify it with this option. DM also lets you specify
28
+ # this with <tt>:class_name</tt>, for AR compability.
29
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default
30
+ # this is guessed to be the name of this class in lower-case and _id suffixed.
31
+ # * <tt>:dependent</tt> - if set to :destroy, the associated objects have their destroy! methods
32
+ # called in a chain meaning all callbacks are also called for each object.
33
+ # if set to :delete, the associated objects are deleted from the database
34
+ # without their callbacks being triggered.
35
+ # if set to :protect and the collection is not empty an AssociatedProtectedError will be raised.
36
+ # if set to :nullify, the associated objects foreign key is set to NULL.
37
+ # default is :nullify
38
+ #
39
+ # Option examples:
40
+ # has_many :favourite_fruits, :class => 'Fruit', :dependent => :destroy
15
41
  def has_many(association_name, options = {})
16
42
  database.schema[self].associations << HasManyAssociation.new(self, association_name, options)
17
43
  end
18
44
 
45
+ # Adds the following methods for query of a single associated object:
46
+ # * <tt>association(</tt> - returns the associated object. Returns an empty set if no
47
+ # object is found.
48
+ # * <tt>association=(associate)</tt> - assigns the associate object, extracts the
49
+ # primary key, and sets it as the foreign key.
50
+ # * <tt>association.nil?</tt> - returns +true+ if there is no associated object.
51
+ #
52
+ # The declaration can also include an options hash to specialize the behavior of the
53
+ # association.
54
+ #
55
+ # Options are:
56
+ # * <tt>:class</tt> - specify the class name of the association. So has_one :animal will by
57
+ # default be linked to the Animal class, but if you want the association to use a
58
+ # different class, you'll have to specify it with this option. DM also lets you specify
59
+ # this with <tt>:class_name</tt>, for AR compability.
60
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default
61
+ # this is guessed to be the name of this class in lower-case and _id suffixed.
62
+ # * <tt>:dependent</tt> - has_one is secretly a has_many so this option performs the same
63
+ # as the has_many
64
+ #
65
+ # Option examples:
66
+ # has_one :favourite_fruit, :class => 'Fruit', :foreign_key => 'devourer_id'
19
67
  def has_one(association_name, options = {})
20
68
  database.schema[self].associations << HasManyAssociation.new(self, association_name, options)
21
69
  end
22
70
 
71
+ # Adds the following methods for query of a single associated object:
72
+ # * <tt>association(</tt> - returns the associated object. Returns an empty set if no
73
+ # object is found.
74
+ # * <tt>association=(associate)</tt> - assigns the associate object, extracts the
75
+ # primary key, and sets it as the foreign key.
76
+ # * <tt>association.nil?</tt> - returns +true+ if there is no associated object.
77
+ # * <tt>build_association</tt> - builds a new object of the associated type, without
78
+ # saving it to the database.
79
+ # * <tt>create_association</tt> - creates and saves a new object of the associated type.
23
80
  def belongs_to(association_name, options = {})
24
81
  database.schema[self].associations << BelongsToAssociation.new(self, association_name, options)
25
82
  end
26
83
 
84
+ # Associates two classes via an intermediate join table.
85
+ #
86
+ # Options are:
87
+ # * <tt>:dependent</tt> - if set to :destroy, the associated objects have their destroy! methods
88
+ # called in a chain meaning all callbacks are also called for each object. Beware that this
89
+ # is a cascading delete and will affect all records that have a remote relationship with the
90
+ # record being destroyed!
91
+ # if set to :delete, the associated objects are deleted from the database without their
92
+ # callbacks being triggered. This does NOT cascade the deletes. All associated objects will
93
+ # have their relationships removed from other records before being deleted. The record calling
94
+ # destroy will only delete those records directly associated to it.
95
+ # if set to :protect and the collection is not empty an AssociatedProtectedError will be raised.
96
+ # if set to :nullify, the join table will have the relationship records removed which is
97
+ # effectively nullifying the foreign key.
98
+ # default is :nullify
27
99
  def has_and_belongs_to_many(association_name, options = {})
28
100
  database.schema[self].associations << HasAndBelongsToManyAssociation.new(self, association_name, options)
29
101
  end
@@ -31,4 +103,4 @@ module DataMapper
31
103
  end
32
104
 
33
105
  end
34
- end
106
+ end
@@ -2,90 +2,108 @@ require 'data_mapper/associations/has_n_association'
2
2
 
3
3
  module DataMapper
4
4
  module Associations
5
-
5
+
6
6
  class BelongsToAssociation < HasNAssociation
7
7
 
8
- def define_accessor(klass)
8
+ def define_accessor(klass)
9
9
  klass.class_eval <<-EOS
10
-
10
+
11
11
  def create_#{@association_name}(options = {})
12
12
  #{@association_name}_association.create(options)
13
13
  end
14
-
14
+
15
15
  def build_#{@association_name}(options = {})
16
16
  #{@association_name}_association.build(options)
17
17
  end
18
-
18
+
19
19
  def #{@association_name}
20
20
  #{@association_name}_association.instance
21
21
  end
22
-
22
+
23
23
  def #{@association_name}=(value)
24
24
  #{@association_name}_association.set(value)
25
25
  end
26
-
26
+
27
27
  private
28
28
  def #{@association_name}_association
29
29
  @#{@association_name} || (@#{@association_name} = DataMapper::Associations::BelongsToAssociation::Instance.new(self, #{@association_name.inspect}))
30
30
  end
31
31
  EOS
32
32
  end
33
-
33
+
34
34
  # Reverse the natural order for BelongsToAssociations
35
35
  alias constant associated_constant
36
36
  def associated_constant
37
37
  @constant
38
38
  end
39
-
39
+
40
40
  def foreign_key_name
41
41
  @foreign_key_name || @foreign_key_name = (@options[:foreign_key] || "#{name}_#{key_table.key.name}".to_sym)
42
42
  end
43
-
44
- def to_sql
43
+
44
+ def complementary_association
45
+ @complementary_association || begin
46
+ @complementary_association = key_table.associations.find do |mapping|
47
+ mapping.is_a?(HasManyAssociation) &&
48
+ mapping.foreign_key_column.name == foreign_key_column.name &&
49
+ mapping.associated_table.name == associated_table.name
50
+ end
51
+
52
+ if @complementary_association
53
+ class << self
54
+ attr_accessor :complementary_association
55
+ end
56
+ end
57
+
58
+ return @complementary_association
59
+ end
60
+ end
61
+
62
+ def to_sql # :nodoc:
45
63
  "JOIN #{key_table.to_sql} ON #{foreign_key_column.to_sql(true)} = #{primary_key_column.to_sql(true)}"
46
64
  end
47
-
65
+
48
66
  class Instance < Associations::Reference
49
-
50
- def dirty?
51
- @associated && @new_member
67
+
68
+ def dirty?(cleared = ::Set.new)
69
+ @associated && (@new_member || @key_not_set)
52
70
  end
53
-
71
+
54
72
  def validate_recursively(event, cleared)
55
73
  @associated.nil? || cleared.include?(@associated) || @associated.validate_recursively(event, cleared)
56
74
  end
57
-
58
- def save_without_validation(database_context)
75
+
76
+ def save_without_validation(database_context, cleared)
59
77
  @new_member = false
60
78
  unless @associated.nil?
61
79
  @instance.instance_variable_set(
62
80
  association.foreign_key_column.instance_variable_name,
63
81
  @associated.key
64
82
  )
65
- @instance.database_context.adapter.save_without_validation(database_context, @instance)
83
+ @instance.database_context.adapter.save_without_validation(database_context, @instance, cleared)
66
84
  end
67
85
  end
68
-
86
+
69
87
  def reload!
70
88
  @new_member = false
71
89
  @associated = nil
72
90
  instance
73
91
  end
74
-
92
+
75
93
  def instance
76
- @associated || @associated = begin
94
+ @associated || @associated = begin
77
95
  if @instance.loaded_set.nil?
78
96
  nil
79
97
  else
80
-
98
+
81
99
  # Temp variable for the instance variable name.
82
100
  fk = association.foreign_key_column.to_sym
83
-
101
+
84
102
  set = @instance.loaded_set.group_by { |instance| instance.send(fk) }
85
103
 
86
104
  @instance.database_context.all(association.constant, association.associated_table.key.to_sym => set.keys).each do |assoc|
87
105
  set[assoc.key].each do |primary_instance|
88
- primary_instance.send(setter_method, assoc)
106
+ primary_instance.send("#{@association_name}_association").shallow_append(assoc)
89
107
  end
90
108
  end
91
109
 
@@ -93,34 +111,50 @@ module DataMapper
93
111
  end
94
112
  end
95
113
  end
96
-
114
+
97
115
  def create(options)
98
116
  @associated = association.associated_constant.create(options)
99
117
  end
100
-
118
+
101
119
  def build(options)
102
120
  @associated = association.associated_constant.new(options)
103
121
  end
104
-
122
+
105
123
  def setter_method
106
124
  "#{@association_name}=".to_sym
107
125
  end
108
-
109
- def set(val)
110
- raise "RecursionError" if val == @instance
111
- @new_member = true
126
+
127
+ def set(member)
128
+ shallow_append(member)
129
+
130
+ if complement = association.complementary_association
131
+ member.send(complement.name).shallow_append(@instance)
132
+ end
133
+
134
+ return self
135
+ end
136
+
137
+ def shallow_append(val)
138
+ raise RecursionError.new if val == @instance
112
139
  @instance.instance_variable_set(association.foreign_key_column.instance_variable_name, val.key)
113
140
  @associated = val
141
+ @key_not_set = true if val.key.nil?
142
+ return self
143
+ end
144
+
145
+ def deactivate
114
146
  end
115
-
147
+
148
+ private
149
+
116
150
  def ensure_foreign_key!
117
151
  if @associated
118
152
  @instance.instance_variable_set(association.foreign_key.instance_variable_name, @associated.key)
119
153
  end
120
154
  end
121
-
155
+
122
156
  end # class Instance
123
157
  end
124
-
158
+
125
159
  end
126
- end
160
+ end
@@ -1,34 +1,43 @@
1
1
  module DataMapper
2
2
  module Associations
3
-
3
+
4
4
  class HasAndBelongsToManyAssociation
5
-
5
+
6
6
  attr_reader :adapter
7
-
7
+
8
8
  def initialize(klass, association_name, options)
9
9
  @adapter = database.adapter
10
10
  @key_table = adapter.table(klass)
11
+ @self_referential = (association_name.to_s == @key_table.name)
11
12
  @association_name = association_name.to_sym
12
13
  @options = options
13
-
14
+
14
15
  define_accessor(klass)
15
16
  end
16
-
17
+
17
18
  # def key_table
18
19
  # @key_table
19
20
  # end
20
-
21
+
21
22
  def name
22
23
  @association_name
23
24
  end
24
25
 
26
+ def dependency
27
+ @options[:dependent]
28
+ end
29
+
25
30
  def foreign_name
26
31
  @foreign_name || (@foreign_name = (@options[:foreign_name] || @key_table.name).to_sym)
27
32
  end
28
-
33
+
34
+ def self_referential?
35
+ @self_referential
36
+ end
37
+
29
38
  def constant
30
39
  @associated_class || @associated_class = begin
31
-
40
+
32
41
  if @options.has_key?(:class) || @options.has_key?(:class_name)
33
42
  associated_class_name = (@options[:class] || @options[:class_name])
34
43
  if associated_class_name.kind_of?(String)
@@ -39,51 +48,56 @@ module DataMapper
39
48
  else
40
49
  Kernel.const_get(Inflector.classify(@association_name))
41
50
  end
42
-
51
+
43
52
  end
44
53
  end
45
-
46
- def activate!
47
- join_table.create!
54
+
55
+ def activate!(force = false)
56
+ join_columns.each {|column| column unless join_table.mapped_column_exists?(column.name)}
57
+ join_table.create!(force)
48
58
  end
49
59
 
50
60
  def associated_columns
51
61
  associated_table.columns.reject { |column| column.lazy? } + join_columns
52
62
  end
53
-
63
+
54
64
  def join_columns
55
65
  [ left_foreign_key, right_foreign_key ]
56
66
  end
57
-
67
+
58
68
  def associated_table
59
69
  @associated_table || (@associated_table = adapter.table(constant))
60
70
  end
61
-
71
+
62
72
  def join_table
63
- @join_table || @join_table = begin
64
- join_table_name = @options[:join_table] ||
73
+ @join_table || @join_table = begin
74
+ join_table_name = @options[:join_table] ||
65
75
  [ @key_table.name.to_s, database.schema[constant].name.to_s ].sort.join('_')
66
-
76
+
67
77
  adapter.table(join_table_name)
68
- end
78
+ end
69
79
  end
70
-
80
+
71
81
  def left_foreign_key
72
82
  @left_foreign_key || @left_foreign_key = begin
73
83
  join_table.add_column(
74
84
  (@options[:left_foreign_key] || @key_table.default_foreign_key),
75
- :integer, :key => true)
85
+ :integer, :nullable => true, :key => true)
76
86
  end
77
87
  end
78
88
 
79
89
  def right_foreign_key
90
+ if self_referential?
91
+ @options[:right_foreign_key] ||= ["related_", associated_table.default_foreign_key].to_s
92
+ end
93
+
80
94
  @right_foreign_key || @right_foreign_key = begin
81
95
  join_table.add_column(
82
96
  (@options[:right_foreign_key] || associated_table.default_foreign_key),
83
- :integer, :key => true)
97
+ :integer, :nullable => true, :key => true)
84
98
  end
85
99
  end
86
-
100
+
87
101
  def to_sql
88
102
  <<-EOS.compress_lines
89
103
  JOIN #{join_table.to_sql} ON
@@ -92,14 +106,21 @@ module DataMapper
92
106
  #{associated_table.key.to_sql(true)} = #{right_foreign_key.to_sql(true)}
93
107
  EOS
94
108
  end
95
-
109
+
96
110
  def to_shallow_sql
111
+ if self_referential?
112
+ <<-EOS.compress_lines
113
+ JOIN #{join_table.to_sql} ON
114
+ #{right_foreign_key.to_sql(true)} = #{@key_table.key.to_sql(true)}
115
+ EOS
116
+ else
97
117
  <<-EOS.compress_lines
98
118
  JOIN #{join_table.to_sql} ON
99
119
  #{left_foreign_key.to_sql(true)} = #{@key_table.key.to_sql(true)}
100
120
  EOS
121
+ end
101
122
  end
102
-
123
+
103
124
  def to_insert_sql
104
125
  <<-EOS.compress_lines
105
126
  INSERT INTO #{join_table.to_sql}
@@ -107,14 +128,29 @@ module DataMapper
107
128
  VALUES
108
129
  EOS
109
130
  end
110
-
131
+
111
132
  def to_delete_sql
112
- <<-EOS
133
+ <<-EOS.compress_lines
113
134
  DELETE FROM #{join_table.to_sql}
114
135
  WHERE #{left_foreign_key.to_sql} = ?
115
136
  EOS
116
137
  end
117
-
138
+
139
+ def to_delete_set_sql
140
+ <<-EOS.compress_lines
141
+ DELETE FROM #{join_table.to_sql}
142
+ WHERE #{left_foreign_key.to_sql} IN ?
143
+ OR #{right_foreign_key.to_sql} IN ?
144
+ EOS
145
+ end
146
+
147
+ def to_delete_members_sql
148
+ <<-EOS.compress_lines
149
+ DELETE FROM #{associated_table.to_sql}
150
+ WHERE #{associated_table.key.to_sql} IN ?
151
+ EOS
152
+ end
153
+
118
154
  def to_delete_member_sql
119
155
  <<-EOS
120
156
  DELETE FROM #{join_table.to_sql}
@@ -122,29 +158,43 @@ module DataMapper
122
158
  AND #{right_foreign_key.to_sql} = ?
123
159
  EOS
124
160
  end
125
-
161
+
162
+ def to_disassociate_sql
163
+ <<-EOS
164
+ UPDATE #{join_table.to_sql}
165
+ SET #{left_foreign_key.to_sql} = NULL
166
+ WHERE #{left_foreign_key.to_sql} = ?
167
+ EOS
168
+ end
169
+
126
170
  # Define the association instance method (i.e. Project#tasks)
127
171
  def define_accessor(klass)
128
172
  klass.class_eval <<-EOS
129
173
  def #{@association_name}
130
174
  @#{@association_name} || (@#{@association_name} = HasAndBelongsToManyAssociation::Set.new(self, #{@association_name.inspect}))
131
175
  end
132
-
176
+
133
177
  def #{@association_name}=(value)
134
178
  #{@association_name}.set(value)
135
179
  end
180
+
181
+ private
182
+ def #{@association_name}_keys=(value)
183
+ #{@association_name}.clear
184
+
185
+ associated_constant = #{@association_name}.association.constant
186
+ associated_table = #{@association_name}.association.associated_table
187
+ associated_constant.all(associated_table.key => [*value]).each do |entry|
188
+ #{@association_name} << entry
189
+ end
190
+ end
136
191
  EOS
137
192
  end
138
-
193
+
139
194
  class Set < Associations::Reference
140
-
195
+
141
196
  include Enumerable
142
-
143
- def initialize(*args)
144
- super
145
- @new_members = false
146
- end
147
-
197
+
148
198
  def each
149
199
  entries.each { |item| yield item }
150
200
  end
@@ -153,11 +203,11 @@ module DataMapper
153
203
  entries.size
154
204
  end
155
205
  alias length size
156
-
206
+
157
207
  def count
158
208
  entries.size
159
209
  end
160
-
210
+
161
211
  def [](key)
162
212
  entries[key]
163
213
  end
@@ -165,48 +215,49 @@ module DataMapper
165
215
  def empty?
166
216
  entries.empty?
167
217
  end
168
-
169
- def dirty?
170
- @new_members || (@entries && @entries.any? { |item| item != @instance && item.dirty? })
218
+
219
+ def dirty?(cleared = ::Set.new)
220
+ return false unless @entries
221
+ @entries.any? {|item| cleared.include?(item) || item.dirty?(cleared) } || @associated_keys != @entries.map { |entry| entry.keys }
171
222
  end
172
-
223
+
173
224
  def validate_recursively(event, cleared)
174
225
  @entries.blank? || @entries.all? { |item| cleared.include?(item) || item.validate_recursively(event, cleared) }
175
226
  end
176
-
177
- def save_without_validation(database_context)
227
+
228
+ def save_without_validation(database_context, cleared)
178
229
  unless @entries.nil?
179
-
180
- if @new_members || dirty?
230
+
231
+ if dirty?(cleared)
181
232
  adapter = @instance.database_context.adapter
182
-
233
+
183
234
  adapter.connection do |db|
184
235
  command = db.create_command(association.to_delete_sql)
185
236
  command.execute_non_query(@instance.key)
186
237
  end
187
-
238
+
188
239
  unless @entries.empty?
189
240
  if adapter.batch_insertable?
190
241
  sql = association.to_insert_sql
191
242
  values = []
192
243
  keys = []
193
-
244
+
194
245
  @entries.each do |member|
195
- adapter.save_without_validation(database_context, member)
246
+ adapter.save_without_validation(database_context, member, cleared)
196
247
  values << "(?, ?)"
197
248
  keys << @instance.key << member.key
198
249
  end
199
-
250
+
200
251
  adapter.connection do |db|
201
252
  command = db.create_command(sql << ' ' << values.join(', '))
202
253
  command.execute_non_query(*keys)
203
254
  end
204
-
255
+
205
256
  else # adapter doesn't support batch inserts...
206
257
  @entries.each do |member|
207
- adapter.save_without_validation(database_context, member)
258
+ adapter.save_without_validation(database_context, member, cleared)
208
259
  end
209
-
260
+
210
261
  # Just to keep the same flow as the batch-insert mode.
211
262
  @entries.each do |member|
212
263
  adapter.connection do |db|
@@ -216,30 +267,31 @@ module DataMapper
216
267
  end
217
268
  end # if adapter.batch_insertable?
218
269
  end # unless @entries.empty?
219
-
220
- @new_members = false
221
- end # if @new_members || dirty?
270
+ end # if dirty?
222
271
  end
223
272
  end
224
-
273
+
225
274
  def <<(member)
226
- @new_members = true
227
- entries << member unless member.nil?
275
+ return nil unless member
276
+
277
+ if member.is_a?(Enumerable)
278
+ member.each { |entry| entries << entry }
279
+ else
280
+ entries << member
281
+ end
228
282
  end
229
-
283
+
230
284
  def clear
231
- @new_members = true
232
- @entries = []
285
+ @entries = Support::TypedSet.new(association.constant)
233
286
  end
234
-
287
+
235
288
  def reload!
236
- @new_members = false
237
289
  @entries = nil
238
290
  end
239
-
291
+
240
292
  def delete(member)
241
- @new_members = true
242
- if entries.delete(member)
293
+ if found_member = entries.detect { |entry| entry == member }
294
+ entries.delete?(found_member)
243
295
  @instance.database_context.adapter.connection do |db|
244
296
  command = db.create_command(association.to_delete_member_sql)
245
297
  command.execute_non_query(@instance.key, member.key)
@@ -249,7 +301,7 @@ module DataMapper
249
301
  nil
250
302
  end
251
303
  end
252
-
304
+
253
305
  def method_missing(symbol, *args, &block)
254
306
  if entries.respond_to?(symbol)
255
307
  entries.send(symbol, *args, &block)
@@ -265,21 +317,21 @@ module DataMapper
265
317
  super
266
318
  end
267
319
  end
268
-
320
+
269
321
  def entries
270
322
  @entries || @entries = begin
271
323
 
272
324
  if @instance.loaded_set.nil?
273
- []
325
+ Support::TypedSet.new(association.constant)
274
326
  else
275
-
327
+
276
328
  associated_items = Hash.new { |h,k| h[k] = [] }
277
329
  left_key_index = nil
278
330
  association_constant = association.constant
279
331
  left_foreign_key = association.left_foreign_key
280
-
332
+
281
333
  matcher = lambda do |instance,columns,row|
282
-
334
+
283
335
  # Locate the column for the left-key.
284
336
  unless left_key_index
285
337
  columns.each_with_index do |column, index|
@@ -289,40 +341,97 @@ module DataMapper
289
341
  end
290
342
  end
291
343
  end
292
-
344
+
293
345
  if instance.kind_of?(association_constant)
294
346
  associated_items[left_foreign_key.type_cast_value(row[left_key_index])] << instance
295
347
  end
296
348
  end
297
-
349
+
298
350
  @instance.database_context.all(association.constant,
299
351
  left_foreign_key => @instance.loaded_set.map(&:key),
300
352
  :shallow_include => association.foreign_name,
301
353
  :intercept_load => matcher
302
354
  )
303
-
355
+
304
356
  # do stsuff with associated_items hash.
305
357
  setter_method = "#{@association_name}=".to_sym
306
-
358
+
307
359
  @instance.loaded_set.each do |entry|
308
360
  entry.send(setter_method, associated_items[entry.key])
309
361
  end # @instance.loaded_set.each
310
-
311
- @entries
362
+
363
+ @entries
312
364
  end
313
365
  end
314
366
  end
315
367
 
316
368
  def set(results)
317
- @entries = results
369
+ if results.is_a?(Support::TypedSet)
370
+ @entries = results
371
+ else
372
+ @entries = Support::TypedSet.new(association.constant)
373
+ [*results].each { |item| @entries << item }
374
+ end
375
+ @associated_keys = @entries.map { |entry| entry.key }
376
+ return @entries
318
377
  end
319
378
 
320
379
  def inspect
321
380
  entries.inspect
322
381
  end
382
+
383
+ def first
384
+ entries.entries.first
385
+ end
386
+
387
+ def last
388
+ entries.entries.last
389
+ end
390
+
391
+ def deactivate
392
+ case association.dependency
393
+ when :destroy
394
+ entries.each do |member|
395
+ member.destroy! unless member.new_record?
396
+ end
397
+ when :delete
398
+ delete_association
399
+ when :protect
400
+ unless entries.empty?
401
+ raise AssociationProtectedError.new("You cannot delete this model while it has items associated with it.")
402
+ end
403
+ when :nullify
404
+ nullify_association
405
+ else
406
+ nullify_association
407
+ end
408
+ end
409
+
410
+ def delete_association
411
+ @instance.database_context.adapter.connection do |db|
412
+ associated_keys = entries.collect do |item|
413
+ item.key unless item.new_record?
414
+ end.compact
415
+ parameters = [@instance.key] + associated_keys
416
+
417
+ sql = association.to_delete_set_sql
418
+ db.create_command(sql).execute_non_query(*[parameters, parameters])
419
+
420
+ sql = association.to_delete_members_sql
421
+ db.create_command(sql).execute_non_query(associated_keys)
422
+ end
423
+ end
424
+
425
+ def nullify_association
426
+ @instance.database_context.adapter.connection do |db|
427
+ sql = association.to_delete_sql
428
+ parameters = [@instance.key]
429
+ db.create_command(sql).execute_non_query(*parameters)
430
+ end
431
+ end
323
432
  end
324
-
433
+
325
434
  end # class HasAndBelongsToManyAssociation
326
-
435
+
327
436
  end # module Associations
328
- end # module DataMapper
437
+ end # module DataMapper