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
@@ -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