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.
- data/.autotest +17 -14
- data/.gitignore +3 -1
- data/FAQ +6 -5
- data/History.txt +5 -50
- data/Manifest.txt +66 -76
- data/QUICKLINKS +1 -1
- data/README.txt +21 -15
- data/Rakefile +6 -7
- data/SPECS +2 -29
- data/TODO +1 -1
- data/deps.rip +2 -0
- data/dm-core.gemspec +11 -15
- data/lib/dm-core.rb +105 -110
- data/lib/dm-core/adapters.rb +135 -16
- data/lib/dm-core/adapters/abstract_adapter.rb +251 -181
- data/lib/dm-core/adapters/data_objects_adapter.rb +482 -534
- data/lib/dm-core/adapters/in_memory_adapter.rb +90 -69
- data/lib/dm-core/adapters/mysql_adapter.rb +22 -115
- data/lib/dm-core/adapters/oracle_adapter.rb +249 -0
- data/lib/dm-core/adapters/postgres_adapter.rb +7 -173
- data/lib/dm-core/adapters/sqlite3_adapter.rb +4 -97
- data/lib/dm-core/adapters/yaml_adapter.rb +116 -0
- data/lib/dm-core/associations/many_to_many.rb +372 -90
- data/lib/dm-core/associations/many_to_one.rb +220 -73
- data/lib/dm-core/associations/one_to_many.rb +319 -255
- data/lib/dm-core/associations/one_to_one.rb +66 -53
- data/lib/dm-core/associations/relationship.rb +561 -156
- data/lib/dm-core/collection.rb +1101 -379
- data/lib/dm-core/core_ext/kernel.rb +12 -0
- data/lib/dm-core/core_ext/symbol.rb +10 -0
- data/lib/dm-core/identity_map.rb +4 -34
- data/lib/dm-core/migrations.rb +1283 -0
- data/lib/dm-core/model.rb +570 -369
- data/lib/dm-core/model/descendant_set.rb +81 -0
- data/lib/dm-core/model/hook.rb +45 -0
- data/lib/dm-core/model/is.rb +32 -0
- data/lib/dm-core/model/property.rb +247 -0
- data/lib/dm-core/model/relationship.rb +335 -0
- data/lib/dm-core/model/scope.rb +90 -0
- data/lib/dm-core/property.rb +808 -273
- data/lib/dm-core/property_set.rb +141 -98
- data/lib/dm-core/query.rb +1037 -483
- data/lib/dm-core/query/conditions/comparison.rb +872 -0
- data/lib/dm-core/query/conditions/operation.rb +221 -0
- data/lib/dm-core/query/direction.rb +43 -0
- data/lib/dm-core/query/operator.rb +84 -0
- data/lib/dm-core/query/path.rb +138 -0
- data/lib/dm-core/query/sort.rb +45 -0
- data/lib/dm-core/repository.rb +210 -94
- data/lib/dm-core/resource.rb +641 -421
- data/lib/dm-core/spec/adapter_shared_spec.rb +294 -0
- data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +106 -0
- data/lib/dm-core/support/chainable.rb +22 -0
- data/lib/dm-core/support/deprecate.rb +12 -0
- data/lib/dm-core/support/logger.rb +13 -0
- data/lib/dm-core/{naming_conventions.rb → support/naming_conventions.rb} +6 -6
- data/lib/dm-core/transaction.rb +333 -92
- data/lib/dm-core/type.rb +98 -60
- data/lib/dm-core/types/boolean.rb +1 -1
- data/lib/dm-core/types/discriminator.rb +34 -20
- data/lib/dm-core/types/object.rb +7 -4
- data/lib/dm-core/types/paranoid_boolean.rb +11 -9
- data/lib/dm-core/types/paranoid_datetime.rb +11 -9
- data/lib/dm-core/types/serial.rb +3 -3
- data/lib/dm-core/types/text.rb +3 -4
- data/lib/dm-core/version.rb +1 -1
- data/script/performance.rb +102 -109
- data/script/profile.rb +169 -38
- data/spec/lib/adapter_helpers.rb +105 -0
- data/spec/lib/collection_helpers.rb +18 -0
- data/spec/lib/counter_adapter.rb +34 -0
- data/spec/lib/pending_helpers.rb +27 -0
- data/spec/lib/rspec_immediate_feedback_formatter.rb +53 -0
- data/spec/public/associations/many_to_many_spec.rb +193 -0
- data/spec/public/associations/many_to_one_spec.rb +73 -0
- data/spec/public/associations/one_to_many_spec.rb +77 -0
- data/spec/public/associations/one_to_one_spec.rb +156 -0
- data/spec/public/collection_spec.rb +65 -0
- data/spec/public/migrations_spec.rb +359 -0
- data/spec/public/model/relationship_spec.rb +924 -0
- data/spec/public/model_spec.rb +159 -0
- data/spec/public/property_spec.rb +829 -0
- data/spec/public/resource_spec.rb +71 -0
- data/spec/public/sel_spec.rb +44 -0
- data/spec/public/setup_spec.rb +145 -0
- data/spec/public/shared/association_collection_shared_spec.rb +317 -0
- data/spec/public/shared/collection_shared_spec.rb +1670 -0
- data/spec/public/shared/finder_shared_spec.rb +1619 -0
- data/spec/public/shared/resource_shared_spec.rb +924 -0
- data/spec/public/shared/sel_shared_spec.rb +112 -0
- data/spec/public/transaction_spec.rb +129 -0
- data/spec/public/types/discriminator_spec.rb +130 -0
- data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
- data/spec/semipublic/adapters/in_memory_adapter_spec.rb +12 -0
- data/spec/semipublic/adapters/mysql_adapter_spec.rb +17 -0
- data/spec/semipublic/adapters/oracle_adapter_spec.rb +194 -0
- data/spec/semipublic/adapters/postgres_adapter_spec.rb +17 -0
- data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +17 -0
- data/spec/semipublic/adapters/yaml_adapter_spec.rb +12 -0
- data/spec/semipublic/associations/many_to_one_spec.rb +53 -0
- data/spec/semipublic/associations/relationship_spec.rb +194 -0
- data/spec/semipublic/associations_spec.rb +177 -0
- data/spec/semipublic/collection_spec.rb +142 -0
- data/spec/semipublic/property_spec.rb +61 -0
- data/spec/semipublic/query/conditions_spec.rb +528 -0
- data/spec/semipublic/query/path_spec.rb +443 -0
- data/spec/semipublic/query_spec.rb +2626 -0
- data/spec/semipublic/resource_spec.rb +47 -0
- data/spec/semipublic/shared/condition_shared_spec.rb +9 -0
- data/spec/semipublic/shared/resource_shared_spec.rb +126 -0
- data/spec/spec.opts +3 -1
- data/spec/spec_helper.rb +80 -57
- data/tasks/ci.rb +19 -31
- data/tasks/dm.rb +43 -48
- data/tasks/doc.rb +8 -11
- data/tasks/gemspec.rb +5 -5
- data/tasks/hoe.rb +15 -16
- data/tasks/install.rb +8 -10
- metadata +74 -111
- data/lib/dm-core/associations.rb +0 -207
- data/lib/dm-core/associations/relationship_chain.rb +0 -81
- data/lib/dm-core/auto_migrations.rb +0 -105
- data/lib/dm-core/dependency_queue.rb +0 -32
- data/lib/dm-core/hook.rb +0 -11
- data/lib/dm-core/is.rb +0 -16
- data/lib/dm-core/logger.rb +0 -232
- data/lib/dm-core/migrations/destructive_migrations.rb +0 -17
- data/lib/dm-core/migrator.rb +0 -29
- data/lib/dm-core/scope.rb +0 -58
- data/lib/dm-core/support.rb +0 -7
- data/lib/dm-core/support/array.rb +0 -13
- data/lib/dm-core/support/assertions.rb +0 -8
- data/lib/dm-core/support/errors.rb +0 -23
- data/lib/dm-core/support/kernel.rb +0 -11
- data/lib/dm-core/support/symbol.rb +0 -41
- data/lib/dm-core/type_map.rb +0 -80
- data/lib/dm-core/types.rb +0 -19
- data/script/all +0 -4
- data/spec/integration/association_spec.rb +0 -1382
- data/spec/integration/association_through_spec.rb +0 -203
- data/spec/integration/associations/many_to_many_spec.rb +0 -449
- data/spec/integration/associations/many_to_one_spec.rb +0 -163
- data/spec/integration/associations/one_to_many_spec.rb +0 -188
- data/spec/integration/auto_migrations_spec.rb +0 -413
- data/spec/integration/collection_spec.rb +0 -1073
- data/spec/integration/data_objects_adapter_spec.rb +0 -32
- data/spec/integration/dependency_queue_spec.rb +0 -46
- data/spec/integration/model_spec.rb +0 -197
- data/spec/integration/mysql_adapter_spec.rb +0 -85
- data/spec/integration/postgres_adapter_spec.rb +0 -731
- data/spec/integration/property_spec.rb +0 -253
- data/spec/integration/query_spec.rb +0 -514
- data/spec/integration/repository_spec.rb +0 -61
- data/spec/integration/resource_spec.rb +0 -513
- data/spec/integration/sqlite3_adapter_spec.rb +0 -352
- data/spec/integration/sti_spec.rb +0 -273
- data/spec/integration/strategic_eager_loading_spec.rb +0 -156
- data/spec/integration/transaction_spec.rb +0 -75
- data/spec/integration/type_spec.rb +0 -275
- data/spec/lib/logging_helper.rb +0 -18
- data/spec/lib/mock_adapter.rb +0 -27
- data/spec/lib/model_loader.rb +0 -100
- data/spec/lib/publicize_methods.rb +0 -28
- data/spec/models/content.rb +0 -16
- data/spec/models/vehicles.rb +0 -34
- data/spec/models/zoo.rb +0 -48
- data/spec/unit/adapters/abstract_adapter_spec.rb +0 -133
- data/spec/unit/adapters/adapter_shared_spec.rb +0 -15
- data/spec/unit/adapters/data_objects_adapter_spec.rb +0 -632
- data/spec/unit/adapters/in_memory_adapter_spec.rb +0 -98
- data/spec/unit/adapters/postgres_adapter_spec.rb +0 -133
- data/spec/unit/associations/many_to_many_spec.rb +0 -32
- data/spec/unit/associations/many_to_one_spec.rb +0 -159
- data/spec/unit/associations/one_to_many_spec.rb +0 -393
- data/spec/unit/associations/one_to_one_spec.rb +0 -7
- data/spec/unit/associations/relationship_spec.rb +0 -71
- data/spec/unit/associations_spec.rb +0 -242
- data/spec/unit/auto_migrations_spec.rb +0 -111
- data/spec/unit/collection_spec.rb +0 -182
- data/spec/unit/data_mapper_spec.rb +0 -35
- data/spec/unit/identity_map_spec.rb +0 -126
- data/spec/unit/is_spec.rb +0 -80
- data/spec/unit/migrator_spec.rb +0 -33
- data/spec/unit/model_spec.rb +0 -321
- data/spec/unit/naming_conventions_spec.rb +0 -36
- data/spec/unit/property_set_spec.rb +0 -90
- data/spec/unit/property_spec.rb +0 -753
- data/spec/unit/query_spec.rb +0 -571
- data/spec/unit/repository_spec.rb +0 -93
- data/spec/unit/resource_spec.rb +0 -649
- data/spec/unit/scope_spec.rb +0 -142
- data/spec/unit/transaction_spec.rb +0 -493
- data/spec/unit/type_map_spec.rb +0 -114
- data/spec/unit/type_spec.rb +0 -119
|
@@ -1,189 +1,23 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
+
module ManyToMany #:nodoc:
|
|
4
|
+
class Relationship < Associations::OneToMany::Relationship
|
|
5
|
+
extend Chainable
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
174
|
+
end
|
|
39
175
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
+
namespace = Object
|
|
55
185
|
|
|
56
|
-
|
|
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
|
-
|
|
192
|
+
return namespace, name
|
|
193
|
+
end
|
|
59
194
|
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
+
# Returns the inverse relationship class
|
|
238
|
+
#
|
|
239
|
+
# @api private
|
|
240
|
+
def inverse_class
|
|
241
|
+
self.class
|
|
242
|
+
end
|
|
78
243
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
393
|
+
intermediaries.all(via => targets)
|
|
122
394
|
end
|
|
123
395
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
406
|
+
return intermediary if intermediary.saved?
|
|
131
407
|
end
|
|
132
408
|
|
|
133
|
-
|
|
134
|
-
|
|
409
|
+
# TODO: document
|
|
410
|
+
# @api private
|
|
411
|
+
def through
|
|
412
|
+
relationship.through
|
|
135
413
|
end
|
|
136
414
|
|
|
137
|
-
|
|
138
|
-
|
|
415
|
+
# TODO: document
|
|
416
|
+
# @api private
|
|
417
|
+
def via
|
|
418
|
+
relationship.via
|
|
139
419
|
end
|
|
140
420
|
|
|
141
|
-
|
|
142
|
-
|
|
421
|
+
# TODO: document
|
|
422
|
+
# @api private
|
|
423
|
+
def inverse_set(*)
|
|
424
|
+
# do nothing
|
|
143
425
|
end
|
|
144
|
-
end # class
|
|
426
|
+
end # class Collection
|
|
145
427
|
end # module ManyToMany
|
|
146
428
|
end # module Associations
|
|
147
429
|
end # module DataMapper
|