dm-core 0.9.11 → 0.10.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 (194) hide show
  1. data/.autotest +17 -14
  2. data/.gitignore +3 -1
  3. data/FAQ +6 -5
  4. data/History.txt +5 -50
  5. data/Manifest.txt +66 -76
  6. data/QUICKLINKS +1 -1
  7. data/README.txt +21 -15
  8. data/Rakefile +6 -7
  9. data/SPECS +2 -29
  10. data/TODO +1 -1
  11. data/deps.rip +2 -0
  12. data/dm-core.gemspec +11 -15
  13. data/lib/dm-core.rb +105 -110
  14. data/lib/dm-core/adapters.rb +135 -16
  15. data/lib/dm-core/adapters/abstract_adapter.rb +251 -181
  16. data/lib/dm-core/adapters/data_objects_adapter.rb +482 -534
  17. data/lib/dm-core/adapters/in_memory_adapter.rb +90 -69
  18. data/lib/dm-core/adapters/mysql_adapter.rb +22 -115
  19. data/lib/dm-core/adapters/oracle_adapter.rb +249 -0
  20. data/lib/dm-core/adapters/postgres_adapter.rb +7 -173
  21. data/lib/dm-core/adapters/sqlite3_adapter.rb +4 -97
  22. data/lib/dm-core/adapters/yaml_adapter.rb +116 -0
  23. data/lib/dm-core/associations/many_to_many.rb +372 -90
  24. data/lib/dm-core/associations/many_to_one.rb +220 -73
  25. data/lib/dm-core/associations/one_to_many.rb +319 -255
  26. data/lib/dm-core/associations/one_to_one.rb +66 -53
  27. data/lib/dm-core/associations/relationship.rb +561 -156
  28. data/lib/dm-core/collection.rb +1101 -379
  29. data/lib/dm-core/core_ext/kernel.rb +12 -0
  30. data/lib/dm-core/core_ext/symbol.rb +10 -0
  31. data/lib/dm-core/identity_map.rb +4 -34
  32. data/lib/dm-core/migrations.rb +1283 -0
  33. data/lib/dm-core/model.rb +570 -369
  34. data/lib/dm-core/model/descendant_set.rb +81 -0
  35. data/lib/dm-core/model/hook.rb +45 -0
  36. data/lib/dm-core/model/is.rb +32 -0
  37. data/lib/dm-core/model/property.rb +247 -0
  38. data/lib/dm-core/model/relationship.rb +335 -0
  39. data/lib/dm-core/model/scope.rb +90 -0
  40. data/lib/dm-core/property.rb +808 -273
  41. data/lib/dm-core/property_set.rb +141 -98
  42. data/lib/dm-core/query.rb +1037 -483
  43. data/lib/dm-core/query/conditions/comparison.rb +872 -0
  44. data/lib/dm-core/query/conditions/operation.rb +221 -0
  45. data/lib/dm-core/query/direction.rb +43 -0
  46. data/lib/dm-core/query/operator.rb +84 -0
  47. data/lib/dm-core/query/path.rb +138 -0
  48. data/lib/dm-core/query/sort.rb +45 -0
  49. data/lib/dm-core/repository.rb +210 -94
  50. data/lib/dm-core/resource.rb +641 -421
  51. data/lib/dm-core/spec/adapter_shared_spec.rb +294 -0
  52. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +106 -0
  53. data/lib/dm-core/support/chainable.rb +22 -0
  54. data/lib/dm-core/support/deprecate.rb +12 -0
  55. data/lib/dm-core/support/logger.rb +13 -0
  56. data/lib/dm-core/{naming_conventions.rb → support/naming_conventions.rb} +6 -6
  57. data/lib/dm-core/transaction.rb +333 -92
  58. data/lib/dm-core/type.rb +98 -60
  59. data/lib/dm-core/types/boolean.rb +1 -1
  60. data/lib/dm-core/types/discriminator.rb +34 -20
  61. data/lib/dm-core/types/object.rb +7 -4
  62. data/lib/dm-core/types/paranoid_boolean.rb +11 -9
  63. data/lib/dm-core/types/paranoid_datetime.rb +11 -9
  64. data/lib/dm-core/types/serial.rb +3 -3
  65. data/lib/dm-core/types/text.rb +3 -4
  66. data/lib/dm-core/version.rb +1 -1
  67. data/script/performance.rb +102 -109
  68. data/script/profile.rb +169 -38
  69. data/spec/lib/adapter_helpers.rb +105 -0
  70. data/spec/lib/collection_helpers.rb +18 -0
  71. data/spec/lib/counter_adapter.rb +34 -0
  72. data/spec/lib/pending_helpers.rb +27 -0
  73. data/spec/lib/rspec_immediate_feedback_formatter.rb +53 -0
  74. data/spec/public/associations/many_to_many_spec.rb +193 -0
  75. data/spec/public/associations/many_to_one_spec.rb +73 -0
  76. data/spec/public/associations/one_to_many_spec.rb +77 -0
  77. data/spec/public/associations/one_to_one_spec.rb +156 -0
  78. data/spec/public/collection_spec.rb +65 -0
  79. data/spec/public/migrations_spec.rb +359 -0
  80. data/spec/public/model/relationship_spec.rb +924 -0
  81. data/spec/public/model_spec.rb +159 -0
  82. data/spec/public/property_spec.rb +829 -0
  83. data/spec/public/resource_spec.rb +71 -0
  84. data/spec/public/sel_spec.rb +44 -0
  85. data/spec/public/setup_spec.rb +145 -0
  86. data/spec/public/shared/association_collection_shared_spec.rb +317 -0
  87. data/spec/public/shared/collection_shared_spec.rb +1670 -0
  88. data/spec/public/shared/finder_shared_spec.rb +1619 -0
  89. data/spec/public/shared/resource_shared_spec.rb +924 -0
  90. data/spec/public/shared/sel_shared_spec.rb +112 -0
  91. data/spec/public/transaction_spec.rb +129 -0
  92. data/spec/public/types/discriminator_spec.rb +130 -0
  93. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  94. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +12 -0
  95. data/spec/semipublic/adapters/mysql_adapter_spec.rb +17 -0
  96. data/spec/semipublic/adapters/oracle_adapter_spec.rb +194 -0
  97. data/spec/semipublic/adapters/postgres_adapter_spec.rb +17 -0
  98. data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +17 -0
  99. data/spec/semipublic/adapters/yaml_adapter_spec.rb +12 -0
  100. data/spec/semipublic/associations/many_to_one_spec.rb +53 -0
  101. data/spec/semipublic/associations/relationship_spec.rb +194 -0
  102. data/spec/semipublic/associations_spec.rb +177 -0
  103. data/spec/semipublic/collection_spec.rb +142 -0
  104. data/spec/semipublic/property_spec.rb +61 -0
  105. data/spec/semipublic/query/conditions_spec.rb +528 -0
  106. data/spec/semipublic/query/path_spec.rb +443 -0
  107. data/spec/semipublic/query_spec.rb +2626 -0
  108. data/spec/semipublic/resource_spec.rb +47 -0
  109. data/spec/semipublic/shared/condition_shared_spec.rb +9 -0
  110. data/spec/semipublic/shared/resource_shared_spec.rb +126 -0
  111. data/spec/spec.opts +3 -1
  112. data/spec/spec_helper.rb +80 -57
  113. data/tasks/ci.rb +19 -31
  114. data/tasks/dm.rb +43 -48
  115. data/tasks/doc.rb +8 -11
  116. data/tasks/gemspec.rb +5 -5
  117. data/tasks/hoe.rb +15 -16
  118. data/tasks/install.rb +8 -10
  119. metadata +74 -111
  120. data/lib/dm-core/associations.rb +0 -207
  121. data/lib/dm-core/associations/relationship_chain.rb +0 -81
  122. data/lib/dm-core/auto_migrations.rb +0 -105
  123. data/lib/dm-core/dependency_queue.rb +0 -32
  124. data/lib/dm-core/hook.rb +0 -11
  125. data/lib/dm-core/is.rb +0 -16
  126. data/lib/dm-core/logger.rb +0 -232
  127. data/lib/dm-core/migrations/destructive_migrations.rb +0 -17
  128. data/lib/dm-core/migrator.rb +0 -29
  129. data/lib/dm-core/scope.rb +0 -58
  130. data/lib/dm-core/support.rb +0 -7
  131. data/lib/dm-core/support/array.rb +0 -13
  132. data/lib/dm-core/support/assertions.rb +0 -8
  133. data/lib/dm-core/support/errors.rb +0 -23
  134. data/lib/dm-core/support/kernel.rb +0 -11
  135. data/lib/dm-core/support/symbol.rb +0 -41
  136. data/lib/dm-core/type_map.rb +0 -80
  137. data/lib/dm-core/types.rb +0 -19
  138. data/script/all +0 -4
  139. data/spec/integration/association_spec.rb +0 -1382
  140. data/spec/integration/association_through_spec.rb +0 -203
  141. data/spec/integration/associations/many_to_many_spec.rb +0 -449
  142. data/spec/integration/associations/many_to_one_spec.rb +0 -163
  143. data/spec/integration/associations/one_to_many_spec.rb +0 -188
  144. data/spec/integration/auto_migrations_spec.rb +0 -413
  145. data/spec/integration/collection_spec.rb +0 -1073
  146. data/spec/integration/data_objects_adapter_spec.rb +0 -32
  147. data/spec/integration/dependency_queue_spec.rb +0 -46
  148. data/spec/integration/model_spec.rb +0 -197
  149. data/spec/integration/mysql_adapter_spec.rb +0 -85
  150. data/spec/integration/postgres_adapter_spec.rb +0 -731
  151. data/spec/integration/property_spec.rb +0 -253
  152. data/spec/integration/query_spec.rb +0 -514
  153. data/spec/integration/repository_spec.rb +0 -61
  154. data/spec/integration/resource_spec.rb +0 -513
  155. data/spec/integration/sqlite3_adapter_spec.rb +0 -352
  156. data/spec/integration/sti_spec.rb +0 -273
  157. data/spec/integration/strategic_eager_loading_spec.rb +0 -156
  158. data/spec/integration/transaction_spec.rb +0 -75
  159. data/spec/integration/type_spec.rb +0 -275
  160. data/spec/lib/logging_helper.rb +0 -18
  161. data/spec/lib/mock_adapter.rb +0 -27
  162. data/spec/lib/model_loader.rb +0 -100
  163. data/spec/lib/publicize_methods.rb +0 -28
  164. data/spec/models/content.rb +0 -16
  165. data/spec/models/vehicles.rb +0 -34
  166. data/spec/models/zoo.rb +0 -48
  167. data/spec/unit/adapters/abstract_adapter_spec.rb +0 -133
  168. data/spec/unit/adapters/adapter_shared_spec.rb +0 -15
  169. data/spec/unit/adapters/data_objects_adapter_spec.rb +0 -632
  170. data/spec/unit/adapters/in_memory_adapter_spec.rb +0 -98
  171. data/spec/unit/adapters/postgres_adapter_spec.rb +0 -133
  172. data/spec/unit/associations/many_to_many_spec.rb +0 -32
  173. data/spec/unit/associations/many_to_one_spec.rb +0 -159
  174. data/spec/unit/associations/one_to_many_spec.rb +0 -393
  175. data/spec/unit/associations/one_to_one_spec.rb +0 -7
  176. data/spec/unit/associations/relationship_spec.rb +0 -71
  177. data/spec/unit/associations_spec.rb +0 -242
  178. data/spec/unit/auto_migrations_spec.rb +0 -111
  179. data/spec/unit/collection_spec.rb +0 -182
  180. data/spec/unit/data_mapper_spec.rb +0 -35
  181. data/spec/unit/identity_map_spec.rb +0 -126
  182. data/spec/unit/is_spec.rb +0 -80
  183. data/spec/unit/migrator_spec.rb +0 -33
  184. data/spec/unit/model_spec.rb +0 -321
  185. data/spec/unit/naming_conventions_spec.rb +0 -36
  186. data/spec/unit/property_set_spec.rb +0 -90
  187. data/spec/unit/property_spec.rb +0 -753
  188. data/spec/unit/query_spec.rb +0 -571
  189. data/spec/unit/repository_spec.rb +0 -93
  190. data/spec/unit/resource_spec.rb +0 -649
  191. data/spec/unit/scope_spec.rb +0 -142
  192. data/spec/unit/transaction_spec.rb +0 -493
  193. data/spec/unit/type_map_spec.rb +0 -114
  194. data/spec/unit/type_spec.rb +0 -119
@@ -1,189 +1,23 @@
1
- gem 'do_postgres', '~>0.9.11'
1
+ require DataMapper.root / 'lib' / 'dm-core' / 'adapters' / 'data_objects_adapter'
2
+
2
3
  require 'do_postgres'
3
4
 
4
5
  module DataMapper
5
6
  module Adapters
6
7
  class PostgresAdapter < DataObjectsAdapter
7
- module SQL
8
+ module SQL #:nodoc:
8
9
  private
9
10
 
11
+ # TODO: document
12
+ # @api private
10
13
  def supports_returning?
11
14
  true
12
15
  end
13
16
  end #module SQL
14
17
 
15
18
  include SQL
16
-
17
- # TODO: move to dm-more/dm-migrations (if possible)
18
- module Migration
19
- # TODO: move to dm-more/dm-migrations (if possible)
20
- def storage_exists?(storage_name)
21
- statement = <<-SQL.compress_lines
22
- SELECT COUNT(*)
23
- FROM "information_schema"."tables"
24
- WHERE "table_type" = 'BASE TABLE'
25
- AND "table_schema" = current_schema()
26
- AND "table_name" = ?
27
- SQL
28
-
29
- query(statement, storage_name).first > 0
30
- end
31
-
32
- # TODO: move to dm-more/dm-migrations (if possible)
33
- def field_exists?(storage_name, column_name)
34
- statement = <<-SQL.compress_lines
35
- SELECT COUNT(*)
36
- FROM "information_schema"."columns"
37
- WHERE "table_schema" = current_schema()
38
- AND "table_name" = ?
39
- AND "column_name" = ?
40
- SQL
41
-
42
- query(statement, storage_name, column_name).first > 0
43
- end
44
-
45
- # TODO: move to dm-more/dm-migrations
46
- def upgrade_model_storage(repository, model)
47
- add_sequences(repository, model)
48
- super
49
- end
50
-
51
- # TODO: move to dm-more/dm-migrations
52
- def create_model_storage(repository, model)
53
- add_sequences(repository, model)
54
- without_notices { super }
55
- end
56
-
57
- # TODO: move to dm-more/dm-migrations
58
- def destroy_model_storage(repository, model)
59
- return true unless storage_exists?(model.storage_name(repository.name))
60
- success = without_notices { super }
61
- model.properties(repository.name).each do |property|
62
- drop_sequence(repository, property) if property.serial?
63
- end
64
- success
65
- end
66
-
67
- protected
68
-
69
- # TODO: move to dm-more/dm-migrations
70
- def create_sequence(repository, property)
71
- return if sequence_exists?(repository, property)
72
- execute(create_sequence_statement(repository, property))
73
- end
74
-
75
- # TODO: move to dm-more/dm-migrations
76
- def drop_sequence(repository, property)
77
- without_notices { execute(drop_sequence_statement(repository, property)) }
78
- end
79
-
80
- module SQL
81
- private
82
-
83
- # TODO: move to dm-more/dm-migrations
84
- def drop_table_statement(repository, model)
85
- "DROP TABLE #{quote_table_name(model.storage_name(repository.name))}"
86
- end
87
-
88
- # TODO: move to dm-more/dm-migrations
89
- def without_notices
90
- # execute the block with NOTICE messages disabled
91
- begin
92
- execute('SET client_min_messages = warning')
93
- yield
94
- ensure
95
- execute('RESET client_min_messages')
96
- end
97
- end
98
-
99
- # TODO: move to dm-more/dm-migrations
100
- def add_sequences(repository, model)
101
- model.properties(repository.name).each do |property|
102
- create_sequence(repository, property) if property.serial?
103
- end
104
- end
105
-
106
- # TODO: move to dm-more/dm-migrations
107
- def sequence_name(repository, property)
108
- "#{property.model.storage_name(repository.name)}_#{property.field(repository.name)}_seq"
109
- end
110
-
111
- # TODO: move to dm-more/dm-migrations
112
- def sequence_exists?(repository, property)
113
- statement = <<-EOS.compress_lines
114
- SELECT COUNT(*)
115
- FROM "information_schema"."sequences"
116
- WHERE "sequence_name" = ?
117
- AND "sequence_schema" = current_schema()
118
- EOS
119
-
120
- query(statement, sequence_name(repository, property)).first > 0
121
- end
122
-
123
- # TODO: move to dm-more/dm-migrations
124
- def create_sequence_statement(repository, property)
125
- "CREATE SEQUENCE #{quote_column_name(sequence_name(repository, property))}"
126
- end
127
-
128
- # TODO: move to dm-more/dm-migrations
129
- def drop_sequence_statement(repository, property)
130
- "DROP SEQUENCE IF EXISTS #{quote_column_name(sequence_name(repository, property))}"
131
- end
132
-
133
- # TODO: move to dm-more/dm-migrations
134
- def property_schema_statement(schema)
135
- statement = super
136
-
137
- if schema.has_key?(:sequence_name)
138
- statement << " DEFAULT nextval('#{schema[:sequence_name]}') NOT NULL"
139
- end
140
-
141
- statement
142
- end
143
-
144
- # TODO: move to dm-more/dm-migrations
145
- def property_schema_hash(repository, property)
146
- schema = super
147
-
148
- if property.serial?
149
- schema.delete(:default) # the sequence will be the default
150
- schema[:sequence_name] = sequence_name(repository, property)
151
- end
152
-
153
- # TODO: see if TypeMap can be updated to set specific attributes to nil
154
- # for different adapters. precision/scale are perfect examples for
155
- # Postgres floats
156
-
157
- # Postgres does not support precision and scale for Float
158
- if property.primitive == Float
159
- schema.delete(:precision)
160
- schema.delete(:scale)
161
- end
162
-
163
- schema
164
- end
165
- end # module SQL
166
-
167
- include SQL
168
-
169
- module ClassMethods
170
- # TypeMap for PostgreSQL databases.
171
- #
172
- # @return <DataMapper::TypeMap> default TypeMap for PostgreSQL databases.
173
- #
174
- # TODO: move to dm-more/dm-migrations
175
- def type_map
176
- @type_map ||= TypeMap.new(super) do |tm|
177
- tm.map(DateTime).to('TIMESTAMP')
178
- tm.map(Integer).to('INT4')
179
- tm.map(Float).to('FLOAT8')
180
- end
181
- end
182
- end # module ClassMethods
183
- end # module Migration
184
-
185
- include Migration
186
- extend Migration::ClassMethods
187
19
  end # class PostgresAdapter
20
+
21
+ const_added(:PostgresAdapter)
188
22
  end # module Adapters
189
23
  end # module DataMapper
@@ -1,105 +1,12 @@
1
- gem 'do_sqlite3', '~>0.9.11'
1
+ require DataMapper.root / 'lib' / 'dm-core' / 'adapters' / 'data_objects_adapter'
2
+
2
3
  require 'do_sqlite3'
3
4
 
4
5
  module DataMapper
5
6
  module Adapters
6
7
  class Sqlite3Adapter < DataObjectsAdapter
7
- module SQL
8
- private
9
-
10
- def quote_column_value(column_value)
11
- case column_value
12
- when TrueClass then quote_column_value('t')
13
- when FalseClass then quote_column_value('f')
14
- else
15
- super
16
- end
17
- end
18
- end # module SQL
19
-
20
- include SQL
21
-
22
- # TODO: move to dm-more/dm-migrations (if possible)
23
- module Migration
24
- # TODO: move to dm-more/dm-migrations (if possible)
25
- def storage_exists?(storage_name)
26
- query_table(storage_name).size > 0
27
- end
28
-
29
- # TODO: move to dm-more/dm-migrations (if possible)
30
- def field_exists?(storage_name, column_name)
31
- query_table(storage_name).any? do |row|
32
- row.name == column_name
33
- end
34
- end
35
-
36
- private
37
-
38
- # TODO: move to dm-more/dm-migrations (if possible)
39
- def query_table(table_name)
40
- query('PRAGMA table_info(?)', table_name)
41
- end
42
-
43
- module SQL
44
- # private ## This cannot be private for current migrations
45
-
46
- # TODO: move to dm-more/dm-migrations
47
- def supports_serial?
48
- sqlite_version >= '3.1.0'
49
- end
50
-
51
- # TODO: move to dm-more/dm-migrations
52
- def create_table_statement(repository, model)
53
- statement = <<-EOS.compress_lines
54
- CREATE TABLE #{quote_table_name(model.storage_name(repository.name))}
55
- (#{model.properties_with_subclasses(repository.name).map { |p| property_schema_statement(property_schema_hash(repository, p)) } * ', '}
56
- EOS
57
-
58
- # skip adding the primary key if one of the columns is serial. In
59
- # SQLite the serial column must be the primary key, so it has already
60
- # been defined
61
- unless model.properties(repository.name).any? { |p| p.serial? }
62
- if (key = model.properties(repository.name).key).any?
63
- statement << ", PRIMARY KEY(#{key.map { |p| quote_column_name(p.field(repository.name)) } * ', '})"
64
- end
65
- end
66
-
67
- statement << ')'
68
- statement
69
- end
70
-
71
- # TODO: move to dm-more/dm-migrations
72
- def property_schema_statement(schema)
73
- statement = super
74
- statement << ' PRIMARY KEY AUTOINCREMENT' if supports_serial? && schema[:serial?]
75
- statement
76
- end
77
-
78
- # TODO: move to dm-more/dm-migrations
79
- def sqlite_version
80
- @sqlite_version ||= query('SELECT sqlite_version(*)').first
81
- end
82
- end # module SQL
83
-
84
- include SQL
85
-
86
- module ClassMethods
87
- # TypeMap for SQLite 3 databases.
88
- #
89
- # @return <DataMapper::TypeMap> default TypeMap for SQLite 3 databases.
90
- #
91
- # TODO: move to dm-more/dm-migrations
92
- def type_map
93
- @type_map ||= TypeMap.new(super) do |tm|
94
- tm.map(Integer).to('INTEGER')
95
- tm.map(Class).to('VARCHAR')
96
- end
97
- end
98
- end # module ClassMethods
99
- end # module Migration
100
-
101
- include Migration
102
- extend Migration::ClassMethods
103
8
  end # class Sqlite3Adapter
9
+
10
+ const_added(:Sqlite3Adapter)
104
11
  end # module Adapters
105
12
  end # module DataMapper
@@ -0,0 +1,116 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+
4
+ module DataMapper
5
+ module Adapters
6
+ class YamlAdapter < AbstractAdapter
7
+ # TODO: document
8
+ # @api semipublic
9
+ def create(resources)
10
+ update_records(resources.first.model) do |records|
11
+ resources.each do |resource|
12
+ initialize_serial(resource, records.size.succ)
13
+ records << resource.attributes(:field)
14
+ end
15
+ end
16
+ end
17
+
18
+ # TODO: document
19
+ # @api semipublic
20
+ def read(query)
21
+ query.filter_records(records_for(query.model).dup)
22
+ end
23
+
24
+ # TODO: document
25
+ # @api semipublic
26
+ def update(attributes, collection)
27
+ attributes = attributes_as_fields(attributes)
28
+
29
+ update_records(collection.model) do |records|
30
+ records_to_update = collection.query.filter_records(records.dup)
31
+ records_to_update.each { |resource| resource.update(attributes) }.size
32
+ end
33
+ end
34
+
35
+ # TODO: document
36
+ # @api semipublic
37
+ def delete(collection)
38
+ update_records(collection.model) do |records|
39
+ records_to_delete = collection.query.filter_records(records.dup)
40
+ records.replace(records - records_to_delete)
41
+ records_to_delete.size
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # TODO: document
48
+ # @api semipublic
49
+ def initialize(name, options = {})
50
+ super
51
+ (@path = Pathname(@options[:path]).freeze).mkpath
52
+ end
53
+
54
+ # Retrieves all records for a model and yeilds them to a block.
55
+ #
56
+ # The block should make any changes to the records in-place. After
57
+ # the block executes all the records are dumped back to the file.
58
+ #
59
+ # @param [Model, #to_s] model
60
+ # Used to determine which file to read/write to
61
+ #
62
+ # @yieldparam [Hash]
63
+ # A hash of record.key => record pairs retrieved from the file
64
+ #
65
+ # @api private
66
+ def update_records(model)
67
+ records = records_for(model)
68
+ result = yield records
69
+ write_records(model, records)
70
+ result
71
+ end
72
+
73
+ # Read all records from a file for a model
74
+ #
75
+ # @param [#storage_name] model
76
+ # The model/name to retieve records for
77
+ #
78
+ # @api private
79
+ def records_for(model)
80
+ file = yaml_file(model)
81
+ file.readable? && YAML.load_file(file) || []
82
+ end
83
+
84
+ # Writes all records to a file
85
+ #
86
+ # @param [#storage_name] model
87
+ # The model/name to write the records for
88
+ #
89
+ # @param [Hash] records
90
+ # A hash of record.key => record pairs to be written
91
+ #
92
+ # @api private
93
+ def write_records(model, records)
94
+ yaml_file(model).open('w') do |fh|
95
+ YAML.dump(records, fh)
96
+ end
97
+ end
98
+
99
+ # Given a model, gives the filename to be used for record storage
100
+ #
101
+ # @example
102
+ # yaml_file(Article) #=> "/path/to/files/articles.yml"
103
+ #
104
+ # @param [#storage_name] model
105
+ # The model to be used to determine the file name.
106
+ #
107
+ # @api private
108
+ def yaml_file(model)
109
+ @path / "#{model.storage_name(name)}.yml"
110
+ end
111
+
112
+ end # class YamlAdapter
113
+
114
+ const_added(:YamlAdapter)
115
+ end # module Adapters
116
+ end # module DataMapper
@@ -1,147 +1,429 @@
1
- require File.join(File.dirname(__FILE__), "one_to_many")
2
1
  module DataMapper
3
2
  module Associations
4
- module ManyToMany
5
- extend Assertions
3
+ module ManyToMany #:nodoc:
4
+ class Relationship < Associations::OneToMany::Relationship
5
+ extend Chainable
6
6
 
7
- # Setup many to many relationship between two models
8
- # -
9
- # @api private
10
- def self.setup(name, model, options = {})
11
- assert_kind_of 'name', name, Symbol
12
- assert_kind_of 'model', model, Model
13
- assert_kind_of 'options', options, Hash
7
+ OPTIONS = superclass::OPTIONS.dup << :through << :via
14
8
 
15
- repository_name = model.repository.name
9
+ # Returns a set of keys that identify the target model
10
+ #
11
+ # @return [DataMapper::PropertySet]
12
+ # a set of properties that identify the target model
13
+ #
14
+ # @api semipublic
15
+ def child_key
16
+ return @child_key if defined?(@child_key)
16
17
 
17
- model.class_eval <<-EOS, __FILE__, __LINE__
18
- def #{name}(query = {})
19
- #{name}_association.all(query)
18
+ repository_name = child_repository_name || parent_repository_name
19
+ properties = child_model.properties(repository_name)
20
+
21
+ @child_key = if @child_properties
22
+ child_key = properties.values_at(*@child_properties)
23
+ properties.class.new(child_key).freeze
24
+ else
25
+ properties.key
26
+ end
27
+ end
28
+
29
+ # TODO: document
30
+ # @api semipublic
31
+ alias target_key child_key
32
+
33
+ # Intermediate association for through model
34
+ # relationships
35
+ #
36
+ # Example: for :bugs association in
37
+ #
38
+ # class Software::Engineer
39
+ # include DataMapper::Resource
40
+ #
41
+ # has n, :missing_tests
42
+ # has n, :bugs, :through => :missing_tests
43
+ # end
44
+ #
45
+ # through is :missing_tests
46
+ #
47
+ # TODO: document a case when
48
+ # through option is a model and
49
+ # not an association name
50
+ #
51
+ # @api semipublic
52
+ def through
53
+ return @through if defined?(@through)
54
+
55
+ if options[:through].kind_of?(Associations::Relationship)
56
+ return @through = options[:through]
57
+ end
58
+
59
+ repository_name = source_repository_name
60
+ relationships = source_model.relationships(repository_name)
61
+ name = through_relationship_name
62
+
63
+ @through = relationships[name] ||
64
+ DataMapper.repository(repository_name) do
65
+ source_model.has(min..max, name, through_model, one_to_many_options)
66
+ end
67
+
68
+ @through.child_key
69
+
70
+ @through
71
+ end
72
+
73
+ # TODO: document
74
+ # @api semipublic
75
+ def via
76
+ return @via if defined?(@via)
77
+
78
+ if options[:via].kind_of?(Associations::Relationship)
79
+ return @via = options[:via]
80
+ end
81
+
82
+ repository_name = through.relative_target_repository_name
83
+ through_model = through.target_model
84
+ relationships = through_model.relationships(repository_name)
85
+ singular_name = name.to_s.singularize.to_sym
86
+
87
+ @via = relationships[options[:via]] ||
88
+ relationships[name] ||
89
+ relationships[singular_name]
90
+
91
+ @via ||= if anonymous_through_model?
92
+ DataMapper.repository(repository_name) do
93
+ through_model.belongs_to(singular_name, target_model, many_to_one_options)
94
+ end
95
+ else
96
+ raise UnknownRelationshipError, "No relationships named #{name} or #{singular_name} in #{through_model}"
20
97
  end
21
98
 
22
- def #{name}=(children)
23
- #{name}_association.replace(children)
99
+ @via.child_key
100
+
101
+ @via
102
+ end
103
+
104
+ # TODO: document
105
+ # @api semipublic
106
+ def links
107
+ return @links if defined?(@links)
108
+
109
+ @links = []
110
+ links = [ through, via ]
111
+
112
+ while relationship = links.shift
113
+ if relationship.respond_to?(:links)
114
+ links.unshift(*relationship.links)
115
+ else
116
+ @links << relationship
117
+ end
24
118
  end
25
119
 
26
- private
120
+ @links.freeze
121
+ end
122
+
123
+ # TODO: document
124
+ # @api private
125
+ def source_scope(source)
126
+ { through.inverse => source }
127
+ end
27
128
 
28
- def #{name}_association
29
- @#{name}_association ||= begin
30
- unless relationship = model.relationships(#{repository_name.inspect})[#{name.inspect}]
31
- raise ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}"
129
+ # TODO: document
130
+ # @api private
131
+ def query
132
+ # TODO: consider making this a query_for method, so that ManyToMany::Relationship#query only
133
+ # returns the query supplied in the definition
134
+ @many_to_many_query ||= super.merge(:links => links).freeze
135
+ end
136
+
137
+ # Eager load the collection using the source as a base
138
+ #
139
+ # @param [Resource, Collection] source
140
+ # the source to query with
141
+ # @param [Query, Hash] other_query
142
+ # optional query to restrict the collection
143
+ #
144
+ # @return [ManyToMany::Collection]
145
+ # the loaded collection for the source
146
+ #
147
+ # @api private
148
+ def eager_load(source, other_query = nil)
149
+ # FIXME: enable SEL for m:m relationships
150
+ source.model.all(query_for(source, other_query))
151
+ end
152
+
153
+ private
154
+
155
+ # TODO: document
156
+ # @api private
157
+ def through_model
158
+ namespace, name = through_model_namespace_name
159
+
160
+ if namespace.const_defined?(name)
161
+ namespace.const_get(name)
162
+ else
163
+ model = Model.new do
164
+ # all properties added to the anonymous through model are keys by default
165
+ def property(name, type, options = {})
166
+ options[:key] = true unless options.key?(:key)
167
+ options.delete(:index)
168
+ super
32
169
  end
33
- association = Proxy.new(relationship, self)
34
- parent_associations << association
35
- association
36
170
  end
171
+
172
+ namespace.const_set(name, model)
37
173
  end
38
- EOS
174
+ end
39
175
 
40
- opts = options.dup
41
- opts.delete(:through)
42
- opts[:child_model] ||= opts.delete(:class_name) || Extlib::Inflection.classify(name)
43
- opts[:parent_model] = model
44
- opts[:repository_name] = repository_name
45
- opts[:remote_relationship_name] ||= opts.delete(:remote_name) || Extlib::Inflection.tableize(opts[:child_model])
46
- opts[:parent_key] = opts[:parent_key]
47
- opts[:child_key] = opts[:child_key]
48
- opts[:mutable] = true
176
+ # TODO: document
177
+ # @api private
178
+ def through_model_namespace_name
179
+ target_parts = target_model.base_model.name.split('::')
180
+ source_parts = source_model.base_model.name.split('::')
49
181
 
50
- names = [ opts[:child_model], opts[:parent_model].name ].sort
51
- model_name = names.join.gsub("::", "")
52
- storage_name = Extlib::Inflection.tableize(Extlib::Inflection.pluralize(names[0]) + names[1])
182
+ name = [ target_parts.pop, source_parts.pop ].sort.join
53
183
 
54
- opts[:near_relationship_name] = Extlib::Inflection.tableize(model_name).to_sym
184
+ namespace = Object
55
185
 
56
- model.has(model.n, opts[:near_relationship_name])
186
+ # find the common namespace between the target_model and source_model
187
+ target_parts.zip(source_parts) do |target_part, source_part|
188
+ break if target_part != source_part
189
+ namespace = namespace.const_get(target_part)
190
+ end
57
191
 
58
- relationship = model.relationships(repository_name)[name] = RelationshipChain.new(opts)
192
+ return namespace, name
193
+ end
59
194
 
60
- unless Object.const_defined?(model_name)
61
- model = DataMapper::Model.new(storage_name)
195
+ # TODO: document
196
+ # @api private
197
+ def through_relationship_name
198
+ if anonymous_through_model?
199
+ namespace = through_model_namespace_name.first
200
+ relationship_name = Extlib::Inflection.underscore(through_model.name.sub(/\A#{namespace.name}::/, '')).tr('/', '_')
201
+ relationship_name.pluralize.to_sym
202
+ else
203
+ options[:through]
204
+ end
205
+ end
62
206
 
63
- model.class_eval <<-EOS, __FILE__, __LINE__
64
- def self.name; #{model_name.inspect} end
65
- def self.default_repository_name; #{repository_name.inspect} end
66
- def self.many_to_many; true end
67
- EOS
207
+ # Check if the :through association uses an anonymous model
208
+ #
209
+ # An anonymous model means that DataMapper creates the model
210
+ # in-memory, and sets the relationships to join the source
211
+ # and the target model.
212
+ #
213
+ # @return [Boolean]
214
+ # true if the through model is anonymous
215
+ #
216
+ # @api private
217
+ def anonymous_through_model?
218
+ options[:through] == Resource
219
+ end
68
220
 
69
- names.each do |n|
70
- model.belongs_to(Extlib::Inflection.underscore(n).gsub('/', '_').to_sym)
221
+ # TODO: document
222
+ # @api semipublic
223
+ chainable do
224
+ def many_to_one_options
225
+ { :parent_key => target_key.map { |property| property.name } }
71
226
  end
227
+ end
72
228
 
73
- Object.const_set(model_name, model)
229
+ # TODO: document
230
+ # @api semipublic
231
+ chainable do
232
+ def one_to_many_options
233
+ { :parent_key => source_key.map { |property| property.name } }
234
+ end
74
235
  end
75
236
 
76
- relationship
77
- end
237
+ # Returns the inverse relationship class
238
+ #
239
+ # @api private
240
+ def inverse_class
241
+ self.class
242
+ end
78
243
 
79
- class Proxy < DataMapper::Associations::OneToMany::Proxy
80
- def delete(resource)
81
- through = near_association.get(*(@parent.key + resource.key))
82
- near_association.delete(through)
83
- orphan_resource(super)
244
+ # TODO: document
245
+ # @api private
246
+ def invert
247
+ inverse_class.new(inverse_name, parent_model, child_model, inverted_options)
84
248
  end
85
249
 
86
- def clear
87
- near_association.clear
88
- super
250
+ # TODO: document
251
+ # @api private
252
+ def inverted_options
253
+ links = self.links.dup
254
+ through = links.pop.inverse
255
+
256
+ links.reverse_each do |relationship|
257
+ inverse = relationship.inverse
258
+
259
+ through = self.class.new(
260
+ inverse.name,
261
+ inverse.child_model,
262
+ inverse.parent_model,
263
+ inverse.options.merge(:through => through)
264
+ )
265
+ end
266
+
267
+ options.only(*OPTIONS - [ :min, :max ]).update(
268
+ :through => through,
269
+ :child_key => options[:parent_key],
270
+ :parent_key => options[:child_key],
271
+ :inverse => self
272
+ )
273
+ end
274
+
275
+ # Loads association targets and sets resulting value on
276
+ # given source resource
277
+ #
278
+ # @param [Resource] source
279
+ # the source resource for the association
280
+ #
281
+ # @return [undefined]
282
+ #
283
+ # @api private
284
+ def lazy_load(source)
285
+ # FIXME: delegate to super once SEL is enabled
286
+ set!(source, collection_for(source))
89
287
  end
90
288
 
289
+ # Returns collection class used by this type of
290
+ # relationship
291
+ #
292
+ # @api private
293
+ def collection_class
294
+ ManyToMany::Collection
295
+ end
296
+ end # class Relationship
297
+
298
+ class Collection < Associations::OneToMany::Collection
299
+ # Remove every Resource in the m:m Collection from the repository
300
+ #
301
+ # This performs a deletion of each Resource in the Collection from
302
+ # the repository and clears the Collection.
303
+ #
304
+ # @return [Boolean]
305
+ # true if the resources were successfully destroyed
306
+ #
307
+ # @api public
91
308
  def destroy
92
- near_association.destroy
309
+ assert_source_saved 'The source must be saved before mass-deleting the collection'
310
+
311
+ # make sure the records are loaded so they can be found when
312
+ # the intermediaries are removed
313
+ lazy_load
314
+
315
+ unless intermediaries.destroy
316
+ return false
317
+ end
318
+
93
319
  super
94
320
  end
95
321
 
96
- def save
322
+ # Remove every Resource in the m:m Collection from the repository, bypassing validation
323
+ #
324
+ # This performs a deletion of each Resource in the Collection from
325
+ # the repository and clears the Collection while skipping
326
+ # validation.
327
+ #
328
+ # @return [Boolean]
329
+ # true if the resources were successfully destroyed
330
+ #
331
+ # @api public
332
+ def destroy!
333
+ assert_source_saved 'The source must be saved before mass-deleting the collection'
334
+
335
+ # make sure the records are loaded so they can be found when
336
+ # the intermediaries are removed
337
+ lazy_load
338
+
339
+ unless intermediaries.destroy!
340
+ return false
341
+ end
342
+
343
+ super
97
344
  end
98
345
 
99
346
  private
100
347
 
101
- def new_child(attributes)
102
- remote_relationship.parent_model.new(attributes)
348
+ # TODO: document
349
+ # @api private
350
+ def _create(safe, attributes)
351
+ if via.respond_to?(:resource_for)
352
+ resource = super
353
+ if create_intermediary(safe, via => resource)
354
+ resource
355
+ end
356
+ else
357
+ if intermediary = create_intermediary(safe)
358
+ super(safe, attributes.merge(via.inverse => intermediary))
359
+ end
360
+ end
103
361
  end
104
362
 
105
- def relate_resource(resource)
106
- assert_mutable
107
- add_default_association_values(resource)
108
- @orphans.delete(resource)
363
+ # TODO: document
364
+ # @api private
365
+ def _save(safe)
366
+ # delete only intermediaries linked to the removed targets
367
+ unless @removed.empty? || intermediaries(@removed).send(safe ? :destroy : :destroy!)
368
+ return false
369
+ end
109
370
 
110
- # TODO: fix this so it does not automatically save on append, if possible
111
- resource.save if resource.new_record?
112
- through_resource = @relationship.child_model.new
113
- @relationship.child_key.zip(@relationship.parent_key) do |child_key,parent_key|
114
- through_resource.send("#{child_key.name}=", parent_key.get(@parent))
371
+ if via.respond_to?(:resource_for)
372
+ super
373
+ loaded_entries.all? { |resource| create_intermediary(safe, via => resource) }
374
+ else
375
+ if intermediary = create_intermediary(safe)
376
+ inverse = via.inverse
377
+ loaded_entries.map { |resource| inverse.set(resource, intermediary) }
378
+ end
379
+
380
+ super
115
381
  end
116
- remote_relationship.child_key.zip(remote_relationship.parent_key) do |child_key,parent_key|
117
- through_resource.send("#{child_key.name}=", parent_key.get(resource))
382
+ end
383
+
384
+ # TODO: document
385
+ # @api private
386
+ def intermediaries(targets = self)
387
+ intermediaries = if through.loaded?(source)
388
+ through.get!(source)
389
+ else
390
+ through.set!(source, through.collection_for(source))
118
391
  end
119
- near_association << through_resource
120
392
 
121
- resource
393
+ intermediaries.all(via => targets)
122
394
  end
123
395
 
124
- def orphan_resource(resource)
125
- assert_mutable
126
- @orphans << resource
127
- resource
128
- end
396
+ # TODO: document
397
+ # @api private
398
+ def create_intermediary(safe, attributes = {})
399
+ collection = intermediaries
400
+
401
+ return unless collection.send(safe ? :save : :save!)
402
+
403
+ intermediary = collection.first(attributes) ||
404
+ collection.send(safe ? :create : :create!, attributes)
129
405
 
130
- def assert_mutable
406
+ return intermediary if intermediary.saved?
131
407
  end
132
408
 
133
- def remote_relationship
134
- @remote_relationship ||= @relationship.send(:remote_relationship)
409
+ # TODO: document
410
+ # @api private
411
+ def through
412
+ relationship.through
135
413
  end
136
414
 
137
- def near_association
138
- @near_association ||= @parent.send(near_relationship_name)
415
+ # TODO: document
416
+ # @api private
417
+ def via
418
+ relationship.via
139
419
  end
140
420
 
141
- def near_relationship_name
142
- @near_relationship_name ||= @relationship.send(:instance_variable_get, :@near_relationship_name)
421
+ # TODO: document
422
+ # @api private
423
+ def inverse_set(*)
424
+ # do nothing
143
425
  end
144
- end # class Proxy
426
+ end # class Collection
145
427
  end # module ManyToMany
146
428
  end # module Associations
147
429
  end # module DataMapper