terrestrial 0.1.0 → 0.1.1
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -9
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +28 -0
- data/Gemfile.lock +73 -0
- data/LICENSE.txt +22 -0
- data/MissingFeatures.md +64 -0
- data/README.md +161 -16
- data/Rakefile +30 -0
- data/TODO.md +41 -0
- data/features/env.rb +60 -0
- data/features/example.feature +120 -0
- data/features/step_definitions/example_steps.rb +46 -0
- data/lib/terrestrial/abstract_record.rb +99 -0
- data/lib/terrestrial/association_loaders.rb +52 -0
- data/lib/terrestrial/collection_mutability_proxy.rb +81 -0
- data/lib/terrestrial/configurations/conventional_association_configuration.rb +186 -0
- data/lib/terrestrial/configurations/conventional_configuration.rb +302 -0
- data/lib/terrestrial/dataset.rb +49 -0
- data/lib/terrestrial/deleted_record.rb +20 -0
- data/lib/terrestrial/dirty_map.rb +42 -0
- data/lib/terrestrial/graph_loader.rb +63 -0
- data/lib/terrestrial/graph_serializer.rb +91 -0
- data/lib/terrestrial/identity_map.rb +22 -0
- data/lib/terrestrial/lazy_collection.rb +74 -0
- data/lib/terrestrial/lazy_object_proxy.rb +55 -0
- data/lib/terrestrial/many_to_many_association.rb +138 -0
- data/lib/terrestrial/many_to_one_association.rb +66 -0
- data/lib/terrestrial/mapper_facade.rb +137 -0
- data/lib/terrestrial/one_to_many_association.rb +66 -0
- data/lib/terrestrial/public_conveniencies.rb +139 -0
- data/lib/terrestrial/query_order.rb +32 -0
- data/lib/terrestrial/relation_mapping.rb +50 -0
- data/lib/terrestrial/serializer.rb +18 -0
- data/lib/terrestrial/short_inspection_string.rb +18 -0
- data/lib/terrestrial/struct_factory.rb +17 -0
- data/lib/terrestrial/subset_queries_proxy.rb +11 -0
- data/lib/terrestrial/upserted_record.rb +15 -0
- data/lib/terrestrial/version.rb +1 -1
- data/lib/terrestrial.rb +5 -2
- data/sequel_mapper.gemspec +31 -0
- data/spec/config_override_spec.rb +193 -0
- data/spec/custom_serializers_spec.rb +49 -0
- data/spec/deletion_spec.rb +101 -0
- data/spec/graph_persistence_spec.rb +313 -0
- data/spec/graph_traversal_spec.rb +121 -0
- data/spec/new_graph_persistence_spec.rb +71 -0
- data/spec/object_identity_spec.rb +70 -0
- data/spec/ordered_association_spec.rb +51 -0
- data/spec/persistence_efficiency_spec.rb +224 -0
- data/spec/predefined_queries_spec.rb +62 -0
- data/spec/proxying_spec.rb +88 -0
- data/spec/querying_spec.rb +48 -0
- data/spec/readme_examples_spec.rb +35 -0
- data/spec/sequel_mapper/abstract_record_spec.rb +244 -0
- data/spec/sequel_mapper/collection_mutability_proxy_spec.rb +135 -0
- data/spec/sequel_mapper/deleted_record_spec.rb +59 -0
- data/spec/sequel_mapper/dirty_map_spec.rb +214 -0
- data/spec/sequel_mapper/lazy_collection_spec.rb +119 -0
- data/spec/sequel_mapper/lazy_object_proxy_spec.rb +140 -0
- data/spec/sequel_mapper/public_conveniencies_spec.rb +58 -0
- data/spec/sequel_mapper/upserted_record_spec.rb +59 -0
- data/spec/spec_helper.rb +36 -0
- data/spec/support/blog_schema.rb +38 -0
- data/spec/support/have_persisted_matcher.rb +19 -0
- data/spec/support/mapper_setup.rb +221 -0
- data/spec/support/mock_sequel.rb +193 -0
- data/spec/support/object_graph_setup.rb +139 -0
- data/spec/support/seed_data_setup.rb +165 -0
- data/spec/support/sequel_persistence_setup.rb +19 -0
- data/spec/support/sequel_test_support.rb +166 -0
- metadata +207 -13
- data/.travis.yml +0 -4
- data/bin/console +0 -14
- data/bin/setup +0 -7
- data/terrestrial.gemspec +0 -23
@@ -0,0 +1,302 @@
|
|
1
|
+
require "terrestrial/configurations/conventional_association_configuration"
|
2
|
+
require "terrestrial/relation_mapping"
|
3
|
+
require "terrestrial/subset_queries_proxy"
|
4
|
+
require "terrestrial/struct_factory"
|
5
|
+
|
6
|
+
module Terrestrial
|
7
|
+
module Configurations
|
8
|
+
require "active_support/inflector"
|
9
|
+
class Inflector
|
10
|
+
include ActiveSupport::Inflector
|
11
|
+
end
|
12
|
+
|
13
|
+
INFLECTOR = Inflector.new
|
14
|
+
|
15
|
+
require "fetchable"
|
16
|
+
class ConventionalConfiguration
|
17
|
+
include Fetchable
|
18
|
+
|
19
|
+
def initialize(datastore)
|
20
|
+
@datastore = datastore
|
21
|
+
@overrides = {}
|
22
|
+
@subset_queries = {}
|
23
|
+
@associations_by_mapping = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :datastore, :mappings
|
27
|
+
private :datastore, :mappings
|
28
|
+
|
29
|
+
def [](mapping_name)
|
30
|
+
mappings[mapping_name]
|
31
|
+
end
|
32
|
+
|
33
|
+
include Enumerable
|
34
|
+
def each(&block)
|
35
|
+
mappings.each(&block)
|
36
|
+
end
|
37
|
+
|
38
|
+
def setup_mapping(mapping_name, &block)
|
39
|
+
@associations_by_mapping[mapping_name] ||= []
|
40
|
+
|
41
|
+
block.call(
|
42
|
+
RelationConfigOptionsProxy.new(
|
43
|
+
method(:add_override).to_proc.curry.call(mapping_name),
|
44
|
+
method(:add_subset).to_proc.curry.call(mapping_name),
|
45
|
+
@associations_by_mapping.fetch(mapping_name),
|
46
|
+
)
|
47
|
+
) if block
|
48
|
+
|
49
|
+
# TODO: more madness in this silly config this, kill it with fire.
|
50
|
+
explicit_settings = @overrides[mapping_name] ||= {}
|
51
|
+
explicit_settings[:factory] ||= raise_if_not_found_factory(mapping_name)
|
52
|
+
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
class RelationConfigOptionsProxy
|
59
|
+
def initialize(config_override, subset_adder, association_register)
|
60
|
+
@config_override = config_override
|
61
|
+
@subset_adder = subset_adder
|
62
|
+
@association_register = association_register
|
63
|
+
end
|
64
|
+
|
65
|
+
def relation_name(name)
|
66
|
+
@config_override.call(relation_name: name)
|
67
|
+
end
|
68
|
+
alias_method :table_name, :relation_name
|
69
|
+
|
70
|
+
def subset(subset_name, &block)
|
71
|
+
@subset_adder.call(subset_name, block)
|
72
|
+
end
|
73
|
+
|
74
|
+
def has_many(*args)
|
75
|
+
@association_register.push([:has_many, args])
|
76
|
+
end
|
77
|
+
|
78
|
+
def has_many_through(*args)
|
79
|
+
@association_register.push([:has_many_through, args])
|
80
|
+
end
|
81
|
+
|
82
|
+
def belongs_to(*args)
|
83
|
+
@association_register.push([:belongs_to, args])
|
84
|
+
end
|
85
|
+
|
86
|
+
def fields(field_names)
|
87
|
+
@config_override.call(fields: field_names)
|
88
|
+
end
|
89
|
+
|
90
|
+
def primary_key(field_names)
|
91
|
+
@config_override.call(primary_key: field_names)
|
92
|
+
end
|
93
|
+
|
94
|
+
def factory(callable)
|
95
|
+
@config_override.call(factory: callable)
|
96
|
+
end
|
97
|
+
|
98
|
+
def class(entity_class)
|
99
|
+
@config_override.call('class': entity_class)
|
100
|
+
end
|
101
|
+
|
102
|
+
def class_name(class_name)
|
103
|
+
@config_override.call(class_name: class_name)
|
104
|
+
end
|
105
|
+
|
106
|
+
def serializer(serializer_func)
|
107
|
+
@config_override.call(serializer: serializer_func)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def mappings
|
112
|
+
@mappings ||= generate_mappings
|
113
|
+
end
|
114
|
+
|
115
|
+
def add_override(mapping_name, attrs)
|
116
|
+
overrides = @overrides.fetch(mapping_name, {}).merge(attrs)
|
117
|
+
|
118
|
+
@overrides.store(mapping_name, overrides)
|
119
|
+
end
|
120
|
+
|
121
|
+
def add_subset(mapping_name, subset_name, block)
|
122
|
+
@subset_queries.store(
|
123
|
+
mapping_name,
|
124
|
+
@subset_queries.fetch(mapping_name, {}).merge(
|
125
|
+
subset_name => block,
|
126
|
+
)
|
127
|
+
)
|
128
|
+
end
|
129
|
+
|
130
|
+
def association_configurator(mappings, mapping_name)
|
131
|
+
ConventionalAssociationConfiguration.new(
|
132
|
+
mapping_name,
|
133
|
+
mappings,
|
134
|
+
datastore,
|
135
|
+
)
|
136
|
+
end
|
137
|
+
|
138
|
+
def generate_mappings
|
139
|
+
custom_mappings = @overrides.map { |mapping_name, overrides|
|
140
|
+
[mapping_name, {relation_name: mapping_name}.merge(consolidate_overrides(overrides))]
|
141
|
+
}
|
142
|
+
|
143
|
+
table_mappings = (tables - @overrides.keys).map { |table_name|
|
144
|
+
[table_name, overrides_for_table(table_name)]
|
145
|
+
}
|
146
|
+
|
147
|
+
Hash[
|
148
|
+
(table_mappings + custom_mappings).map { |(mapping_name, overrides)|
|
149
|
+
table_name = overrides.fetch(:relation_name) { raise no_table_error(mapping_name) }
|
150
|
+
|
151
|
+
[
|
152
|
+
mapping_name,
|
153
|
+
mapping(
|
154
|
+
**default_mapping_args(table_name, mapping_name).merge(overrides)
|
155
|
+
),
|
156
|
+
]
|
157
|
+
}
|
158
|
+
]
|
159
|
+
.tap { |mappings|
|
160
|
+
generate_associations_config(mappings)
|
161
|
+
}
|
162
|
+
end
|
163
|
+
|
164
|
+
def generate_associations_config(mappings)
|
165
|
+
# TODO: the ConventionalAssociationConfiguration takes all the mappings
|
166
|
+
# as a dependency and then sends mutating messages to them.
|
167
|
+
# This mutation based approach was originally a spike but now just
|
168
|
+
# seems totally bananas!
|
169
|
+
@associations_by_mapping.each do |mapping_name, associations|
|
170
|
+
associations.each do |(assoc_type, assoc_args)|
|
171
|
+
association_configurator(mappings, mapping_name)
|
172
|
+
.public_send(assoc_type, *assoc_args)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def default_mapping_args(table_name, mapping_name)
|
178
|
+
{
|
179
|
+
name: mapping_name,
|
180
|
+
relation_name: table_name,
|
181
|
+
fields: get_fields(table_name),
|
182
|
+
primary_key: get_primary_key(table_name),
|
183
|
+
factory: ok_if_it_doesnt_exist_factory(mapping_name),
|
184
|
+
serializer: hash_coercion_serializer,
|
185
|
+
associations: {},
|
186
|
+
subsets: subset_queries_proxy(@subset_queries.fetch(mapping_name, {})),
|
187
|
+
}
|
188
|
+
end
|
189
|
+
|
190
|
+
def overrides_for_table(table_name)
|
191
|
+
overrides = @overrides.values.detect { |config|
|
192
|
+
table_name == config.fetch(:relation_name, nil)
|
193
|
+
} || {}
|
194
|
+
|
195
|
+
{ relation_name: table_name }.merge(
|
196
|
+
consolidate_overrides(overrides)
|
197
|
+
)
|
198
|
+
end
|
199
|
+
|
200
|
+
def consolidate_overrides(opts)
|
201
|
+
new_opts = opts.dup
|
202
|
+
|
203
|
+
if new_opts.has_key?(:class_name)
|
204
|
+
new_opts.merge!(factory: string_to_factory(new_opts.fetch(:class_name)))
|
205
|
+
new_opts.delete(:class_name)
|
206
|
+
end
|
207
|
+
|
208
|
+
if new_opts.has_key?(:class)
|
209
|
+
new_opts.merge!(factory: class_to_factory(new_opts.fetch(:class)))
|
210
|
+
new_opts.delete(:class)
|
211
|
+
end
|
212
|
+
|
213
|
+
new_opts
|
214
|
+
end
|
215
|
+
|
216
|
+
def get_fields(table_name)
|
217
|
+
datastore[table_name].columns
|
218
|
+
end
|
219
|
+
|
220
|
+
def get_primary_key(table_name)
|
221
|
+
datastore.schema(table_name)
|
222
|
+
.select { |field_name, properties|
|
223
|
+
properties.fetch(:primary_key)
|
224
|
+
}
|
225
|
+
.map { |field_name, _| field_name }
|
226
|
+
end
|
227
|
+
|
228
|
+
def tables
|
229
|
+
(datastore.tables - [:schema_migrations])
|
230
|
+
end
|
231
|
+
|
232
|
+
def hash_coercion_serializer
|
233
|
+
->(o) { o.to_h }
|
234
|
+
end
|
235
|
+
|
236
|
+
def subset_queries_proxy(subset_map)
|
237
|
+
SubsetQueriesProxy.new(subset_map)
|
238
|
+
end
|
239
|
+
|
240
|
+
def mapping(name:, relation_name:, primary_key:, factory:, serializer:, fields:, associations:, subsets:)
|
241
|
+
RelationMapping.new(
|
242
|
+
name: name,
|
243
|
+
namespace: relation_name,
|
244
|
+
primary_key: primary_key,
|
245
|
+
factory: factory,
|
246
|
+
serializer: serializer,
|
247
|
+
fields: fields,
|
248
|
+
associations: associations,
|
249
|
+
subsets: subsets,
|
250
|
+
)
|
251
|
+
end
|
252
|
+
|
253
|
+
FactoryNotFoundError = Class.new(StandardError) do
|
254
|
+
def initialize(specified)
|
255
|
+
@specified = specified
|
256
|
+
end
|
257
|
+
|
258
|
+
def message
|
259
|
+
"Could not find factory for #{@specified}"
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
TableNameNotSpecifiedError = Class.new(StandardError) do
|
264
|
+
def initialize(mapping_name)
|
265
|
+
@message = "Error defining custom mapping `#{mapping_name}`." +
|
266
|
+
" You must provide the `table_name` configuration option."
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def raise_if_not_found_factory(name)
|
271
|
+
->(attrs) {
|
272
|
+
class_to_factory(string_to_class(name)).call(attrs)
|
273
|
+
}
|
274
|
+
end
|
275
|
+
|
276
|
+
def ok_if_it_doesnt_exist_factory(name)
|
277
|
+
->(attrs) {
|
278
|
+
factory = class_to_factory(string_to_class(name)) rescue nil
|
279
|
+
factory && factory.call(attrs)
|
280
|
+
}
|
281
|
+
end
|
282
|
+
|
283
|
+
def class_to_factory(klass)
|
284
|
+
if klass.ancestors.include?(Struct)
|
285
|
+
StructFactory.new(klass)
|
286
|
+
else
|
287
|
+
klass.method(:new)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
def string_to_class(string)
|
292
|
+
klass_name = INFLECTOR.classify(string)
|
293
|
+
|
294
|
+
Object.const_get(klass_name)
|
295
|
+
end
|
296
|
+
|
297
|
+
def no_table_error(table_name)
|
298
|
+
TableNameNotSpecifiedError.new(table_name)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
class Dataset
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def initialize(records)
|
6
|
+
@records = records
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :records
|
10
|
+
private :records
|
11
|
+
|
12
|
+
def each(&block)
|
13
|
+
records.each(&block)
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def where(criteria)
|
18
|
+
new(
|
19
|
+
records.find_all { |row|
|
20
|
+
criteria.all? { |k, v|
|
21
|
+
if v.respond_to?(:include?)
|
22
|
+
test_inclusion_in_value(row, k, v)
|
23
|
+
else
|
24
|
+
test_equality(row, k, v)
|
25
|
+
end
|
26
|
+
}
|
27
|
+
}
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def select(field)
|
32
|
+
map { |data| data.fetch(field) }
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def new(records)
|
38
|
+
self.class.new(records)
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_inclusion_in_value(row, field, values)
|
42
|
+
values.include?(row.fetch(field))
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_equality(row, field, value)
|
46
|
+
value == row.fetch(field)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "terrestrial/abstract_record"
|
2
|
+
|
3
|
+
module Terrestrial
|
4
|
+
class DeletedRecord < AbstractRecord
|
5
|
+
def if_delete(&block)
|
6
|
+
block.call(self)
|
7
|
+
self
|
8
|
+
end
|
9
|
+
|
10
|
+
def subset?(_other)
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def operation
|
17
|
+
:delete
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
class DirtyMap
|
3
|
+
def initialize(storage)
|
4
|
+
@storage = storage
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :storage
|
8
|
+
private :storage
|
9
|
+
|
10
|
+
def load(record)
|
11
|
+
storage.store(hash_key(record), deep_clone(record))
|
12
|
+
record
|
13
|
+
end
|
14
|
+
|
15
|
+
def dirty?(record)
|
16
|
+
record_as_loaded = storage.fetch(hash_key(record), NotFound)
|
17
|
+
return true if record_as_loaded == NotFound
|
18
|
+
|
19
|
+
!record.subset?(record_as_loaded)
|
20
|
+
end
|
21
|
+
|
22
|
+
def reject_unchanged_fields(record)
|
23
|
+
record_as_loaded = storage.fetch(hash_key(record), {})
|
24
|
+
|
25
|
+
record.reject { |field, value|
|
26
|
+
value == record_as_loaded.fetch(field, NotFound)
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
NotFound = Module.new
|
33
|
+
|
34
|
+
def hash_key(record)
|
35
|
+
deep_clone([record.namespace, record.identity])
|
36
|
+
end
|
37
|
+
|
38
|
+
def deep_clone(record)
|
39
|
+
Marshal.load(Marshal.dump(record))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
class GraphLoader
|
3
|
+
def initialize(datasets:, mappings:, object_load_pipeline:)
|
4
|
+
@datasets = datasets
|
5
|
+
@mappings = mappings
|
6
|
+
@object_load_pipeline = object_load_pipeline
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :datasets, :mappings, :object_load_pipeline
|
10
|
+
|
11
|
+
def call(mapping_name, record, eager_data = {})
|
12
|
+
mapping = mappings.fetch(mapping_name)
|
13
|
+
|
14
|
+
load_record(mapping, record, eager_data)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def load_record(mapping, record, eager_data)
|
20
|
+
associations = load_associations(mapping, record, eager_data)
|
21
|
+
|
22
|
+
object_load_pipeline.call(mapping, record, Hash[associations])
|
23
|
+
end
|
24
|
+
|
25
|
+
def load_associations(mapping, record, eager_data)
|
26
|
+
mapping.associations.map { |name, association|
|
27
|
+
assoc_eager_data = eager_data.fetch(name, {})
|
28
|
+
|
29
|
+
data_superset = assoc_eager_data.fetch(:superset) {
|
30
|
+
load_from_datasets(association)
|
31
|
+
}
|
32
|
+
|
33
|
+
[
|
34
|
+
name,
|
35
|
+
association.build_proxy(
|
36
|
+
record: record,
|
37
|
+
data_superset: data_superset,
|
38
|
+
loader: ->(associated_record, join_records = []) {
|
39
|
+
join_records.map { |jr|
|
40
|
+
join_mapping = mappings.fetch(association.join_mapping_name)
|
41
|
+
object_load_pipeline.call(join_mapping, jr)
|
42
|
+
}
|
43
|
+
|
44
|
+
call(
|
45
|
+
association.mapping_name,
|
46
|
+
associated_record,
|
47
|
+
assoc_eager_data.fetch(:associations, {})
|
48
|
+
)
|
49
|
+
},
|
50
|
+
)
|
51
|
+
]
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def load_from_datasets(association)
|
56
|
+
association
|
57
|
+
.mapping_names
|
58
|
+
.map { |name| mappings.fetch(name) }
|
59
|
+
.map(&:namespace)
|
60
|
+
.map { |ns| datasets[ns] }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require "terrestrial/upserted_record"
|
2
|
+
require "terrestrial/deleted_record"
|
3
|
+
|
4
|
+
module Terrestrial
|
5
|
+
class GraphSerializer
|
6
|
+
def initialize(mappings:)
|
7
|
+
@mappings = mappings
|
8
|
+
@serialization_map = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :mappings, :serialization_map
|
12
|
+
private :mappings, :serialization_map
|
13
|
+
|
14
|
+
def call(mapping_name, object, depth = 0, parent_foreign_keys = {})
|
15
|
+
if serialization_map.include?(object)
|
16
|
+
return [serialization_map.fetch(object)]
|
17
|
+
end
|
18
|
+
|
19
|
+
mapping = mappings.fetch(mapping_name)
|
20
|
+
|
21
|
+
current_record, association_fields = mapping.serialize(
|
22
|
+
object,
|
23
|
+
depth,
|
24
|
+
parent_foreign_keys,
|
25
|
+
)
|
26
|
+
|
27
|
+
serialization_map.store(object, current_record)
|
28
|
+
|
29
|
+
(
|
30
|
+
[current_record] + associated_records(mapping, current_record, association_fields, depth)
|
31
|
+
).flatten(1)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def associated_records(mapping, current_record, association_fields, depth)
|
37
|
+
mapping.associations
|
38
|
+
.map { |name, association|
|
39
|
+
[association_fields.fetch(name), association]
|
40
|
+
}
|
41
|
+
.map { |collection, association|
|
42
|
+
[nodes(collection), deleted_nodes(collection), association]
|
43
|
+
}
|
44
|
+
.map { |nodes, deleted_nodes, association|
|
45
|
+
association.dump(current_record, nodes, depth) { |assoc_mapping_name, assoc_object, foreign_key, assoc_depth|
|
46
|
+
call(assoc_mapping_name, assoc_object, assoc_depth, foreign_key).tap { |associated_record, *_join_records|
|
47
|
+
# TODO: remove this mutation
|
48
|
+
current_record.merge!(association.extract_foreign_key(associated_record))
|
49
|
+
}
|
50
|
+
} +
|
51
|
+
association.delete(current_record, deleted_nodes, depth) { |assoc_mapping_name, assoc_object, foreign_key, assoc_depth|
|
52
|
+
delete(assoc_mapping_name, assoc_object, assoc_depth, foreign_key)
|
53
|
+
}
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def delete(mapping_name, object, depth, _foreign_key)
|
58
|
+
mapping = mappings.fetch(mapping_name)
|
59
|
+
serialized_record = mapping.serializer.call(object)
|
60
|
+
|
61
|
+
[
|
62
|
+
DeletedRecord.new(
|
63
|
+
mapping.namespace,
|
64
|
+
mapping.primary_key,
|
65
|
+
serialized_record,
|
66
|
+
depth,
|
67
|
+
)
|
68
|
+
]
|
69
|
+
end
|
70
|
+
|
71
|
+
def nodes(collection)
|
72
|
+
if collection.respond_to?(:each_loaded)
|
73
|
+
collection.each_loaded
|
74
|
+
elsif collection.is_a?(Struct)
|
75
|
+
[collection]
|
76
|
+
elsif collection.respond_to?(:each)
|
77
|
+
collection.each
|
78
|
+
else
|
79
|
+
[collection]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def deleted_nodes(collection)
|
84
|
+
if collection.respond_to?(:each_deleted)
|
85
|
+
collection.each_deleted
|
86
|
+
else
|
87
|
+
[]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
class IdentityMap
|
3
|
+
def initialize(storage)
|
4
|
+
@storage = storage
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :storage
|
8
|
+
private :storage
|
9
|
+
|
10
|
+
def call(mapping, record, object)
|
11
|
+
storage.fetch(hash_key(mapping, record)) {
|
12
|
+
storage.store(hash_key(mapping, record), object)
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def hash_key(mapping, record)
|
19
|
+
[mapping.name, record.namespace, record.identity]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require "terrestrial/short_inspection_string"
|
2
|
+
|
3
|
+
module Terrestrial
|
4
|
+
class LazyCollection
|
5
|
+
include ShortInspectionString
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def initialize(database_enum, loader, queries)
|
9
|
+
@database_enum = database_enum
|
10
|
+
@loader = loader
|
11
|
+
@queries = queries
|
12
|
+
@loaded = false
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :database_enum, :loader, :queries
|
16
|
+
private :database_enum, :loader, :queries
|
17
|
+
|
18
|
+
def where(criteria)
|
19
|
+
self.class.new(database_enum.where(criteria), loader, queries)
|
20
|
+
end
|
21
|
+
|
22
|
+
def subset(name, *params)
|
23
|
+
self.class.new(
|
24
|
+
queries.execute(database_enum, name, *params),
|
25
|
+
loader,
|
26
|
+
queries,
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_ary
|
31
|
+
to_a
|
32
|
+
end
|
33
|
+
|
34
|
+
def each(&block)
|
35
|
+
enum.each(&block)
|
36
|
+
end
|
37
|
+
|
38
|
+
def each_loaded(&block)
|
39
|
+
loaded_objects.each(&block)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def enum
|
45
|
+
@enum ||= Enumerator.new { |yielder|
|
46
|
+
loaded_objects.each do |obj|
|
47
|
+
yielder.yield(obj)
|
48
|
+
end
|
49
|
+
|
50
|
+
loop do
|
51
|
+
object_enum.next.tap { |obj|
|
52
|
+
loaded_objects.push(obj)
|
53
|
+
yielder.yield(obj)
|
54
|
+
}
|
55
|
+
end
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def object_enum
|
60
|
+
@object_enum ||= database_enum.lazy.map(&loader)
|
61
|
+
end
|
62
|
+
|
63
|
+
def loaded_objects
|
64
|
+
@loaded_objects ||= []
|
65
|
+
end
|
66
|
+
|
67
|
+
def inspectable_properties
|
68
|
+
[
|
69
|
+
:database_enum,
|
70
|
+
:loaded,
|
71
|
+
]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|