terrestrial 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile.lock +29 -24
- data/README.md +35 -17
- data/Rakefile +4 -9
- data/TODO.md +25 -18
- data/bin/test +31 -0
- data/docs/domain_object_contract.md +50 -0
- data/features/env.rb +4 -6
- data/features/example.feature +28 -28
- data/features/step_definitions/example_steps.rb +2 -2
- data/lib/terrestrial/adapters/memory_adapter.rb +241 -0
- data/lib/terrestrial/collection_mutability_proxy.rb +7 -2
- data/lib/terrestrial/dirty_map.rb +5 -0
- data/lib/terrestrial/error.rb +69 -0
- data/lib/terrestrial/graph_loader.rb +58 -35
- data/lib/terrestrial/graph_serializer.rb +37 -30
- data/lib/terrestrial/inspection_string.rb +19 -0
- data/lib/terrestrial/lazy_collection.rb +2 -2
- data/lib/terrestrial/lazy_object_proxy.rb +1 -1
- data/lib/terrestrial/many_to_one_association.rb +17 -11
- data/lib/terrestrial/public_conveniencies.rb +125 -95
- data/lib/terrestrial/relation_mapping.rb +30 -0
- data/lib/terrestrial/{mapper_facade.rb → relational_store.rb} +11 -1
- data/lib/terrestrial/version.rb +1 -1
- data/spec/config_override_spec.rb +10 -14
- data/spec/custom_serializers_spec.rb +4 -6
- data/spec/deletion_spec.rb +12 -14
- data/spec/error_handling/factory_error_handling_spec.rb +61 -0
- data/spec/error_handling/serialization_error_spec.rb +50 -0
- data/spec/error_handling/upsert_error_spec.rb +132 -0
- data/spec/graph_persistence_spec.rb +80 -24
- data/spec/graph_traversal_spec.rb +14 -6
- data/spec/new_graph_persistence_spec.rb +43 -9
- data/spec/object_identity_spec.rb +5 -7
- data/spec/ordered_association_spec.rb +4 -6
- data/spec/predefined_queries_spec.rb +4 -6
- data/spec/querying_spec.rb +4 -12
- data/spec/readme_examples_spec.rb +3 -6
- data/spec/{persistence_efficiency_spec.rb → sequel_query_efficiency_spec.rb} +101 -19
- data/spec/spec_helper.rb +24 -2
- data/spec/support/memory_adapter_test_support.rb +21 -0
- data/spec/support/{mapper_setup.rb → object_store_setup.rb} +5 -5
- data/spec/support/seed_data_setup.rb +3 -1
- data/spec/support/sequel_test_support.rb +58 -25
- data/spec/{sequel_mapper → terrestrial}/abstract_record_spec.rb +0 -0
- data/spec/{sequel_mapper → terrestrial}/collection_mutability_proxy_spec.rb +0 -0
- data/spec/{sequel_mapper → terrestrial}/deleted_record_spec.rb +0 -0
- data/spec/{sequel_mapper → terrestrial}/dirty_map_spec.rb +38 -6
- data/spec/{sequel_mapper → terrestrial}/lazy_collection_spec.rb +2 -3
- data/spec/{sequel_mapper → terrestrial}/lazy_object_proxy_spec.rb +0 -0
- data/spec/{sequel_mapper → terrestrial}/public_conveniencies_spec.rb +12 -7
- data/spec/{sequel_mapper → terrestrial}/upserted_record_spec.rb +0 -0
- data/{sequel_mapper.gemspec → terrestrial.gemspec} +3 -3
- metadata +47 -39
- data/lib/terrestrial/short_inspection_string.rb +0 -18
- data/spec/proxying_spec.rb +0 -88
- data/spec/support/mock_sequel.rb +0 -193
- data/spec/support/sequel_persistence_setup.rb +0 -19
@@ -6,11 +6,11 @@ Given(/^a database connection is established$/) do |code_sample|
|
|
6
6
|
example_eval(code_sample)
|
7
7
|
end
|
8
8
|
|
9
|
-
Given(/^the associations are defined in the
|
9
|
+
Given(/^the associations are defined in the configuration$/) do |code_sample|
|
10
10
|
example_eval(code_sample)
|
11
11
|
end
|
12
12
|
|
13
|
-
Given(/^a
|
13
|
+
Given(/^a object store is instantiated$/) do |code_sample|
|
14
14
|
example_eval(code_sample)
|
15
15
|
end
|
16
16
|
|
@@ -0,0 +1,241 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
module Adapters
|
3
|
+
end
|
4
|
+
end
|
5
|
+
|
6
|
+
class Terrestrial::Adapters::MemoryAdapter
|
7
|
+
def self.build_from_schema(schema, raw_storage)
|
8
|
+
schema.each { |name, _| raw_storage[name] = [] }
|
9
|
+
|
10
|
+
relations = Hash[schema.map { |name, columns|
|
11
|
+
[name, Relation.new(columns, raw_storage.fetch(name))]
|
12
|
+
}]
|
13
|
+
|
14
|
+
new(schema, relations)
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(schema, relations)
|
18
|
+
@schema = schema
|
19
|
+
@relations = relations
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :relations
|
23
|
+
private :relations
|
24
|
+
|
25
|
+
def schema(table_name)
|
26
|
+
@schema.fetch(table_name).map { |column_info|
|
27
|
+
[
|
28
|
+
column_info.fetch(:name),
|
29
|
+
column_info.fetch(:options, { primary_key: false }),
|
30
|
+
]
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def tables
|
35
|
+
@relations.keys
|
36
|
+
end
|
37
|
+
|
38
|
+
def rename_table(name, new_name)
|
39
|
+
relations[new_name] = relations[name]
|
40
|
+
relations.delete(name)
|
41
|
+
@schema[new_name] = @schema[name]
|
42
|
+
@schema.delete(name)
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def transaction(&block)
|
47
|
+
old_state = Marshal.load(Marshal.dump(relations))
|
48
|
+
block.call
|
49
|
+
rescue Object => e
|
50
|
+
rollback(old_state)
|
51
|
+
raise e
|
52
|
+
end
|
53
|
+
|
54
|
+
def [](table_name)
|
55
|
+
@relations.fetch(table_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def rollback(relations)
|
61
|
+
@relations = relations
|
62
|
+
end
|
63
|
+
|
64
|
+
class Query
|
65
|
+
def initialize(criteria: {}, order: [], reverse: false, &block)
|
66
|
+
if block
|
67
|
+
raise NotImplementedError.new("Block filtering not implemented")
|
68
|
+
end
|
69
|
+
|
70
|
+
@criteria = criteria
|
71
|
+
@order_columns = order
|
72
|
+
@reverse_order = reverse
|
73
|
+
end
|
74
|
+
|
75
|
+
attr_reader :criteria, :order_columns
|
76
|
+
|
77
|
+
def where(new_criteria, &block)
|
78
|
+
self.class.new(
|
79
|
+
criteria: criteria.merge(new_criteria),
|
80
|
+
order: order,
|
81
|
+
reverse: reverse,
|
82
|
+
&block
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
def order(columns)
|
87
|
+
self.class.new(
|
88
|
+
criteria: criteria,
|
89
|
+
order: columns,
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
def reverse
|
94
|
+
self.class.new(
|
95
|
+
criteria: criteria,
|
96
|
+
order: order_columns,
|
97
|
+
reverse: true,
|
98
|
+
)
|
99
|
+
end
|
100
|
+
|
101
|
+
def reverse_order?
|
102
|
+
!!@reverse_order
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class Relation
|
107
|
+
include Enumerable
|
108
|
+
|
109
|
+
def initialize(schema, all_rows, selected_columns: nil, applied_query: Query.new)
|
110
|
+
@schema = schema
|
111
|
+
@all_rows = all_rows
|
112
|
+
@applied_query = applied_query
|
113
|
+
@selected_columns = selected_columns || all_column_names
|
114
|
+
end
|
115
|
+
|
116
|
+
attr_reader :schema
|
117
|
+
attr_reader :all_rows, :selected_columns, :applied_query
|
118
|
+
private :all_rows, :selected_columns, :applied_query
|
119
|
+
|
120
|
+
def columns
|
121
|
+
all_column_names
|
122
|
+
end
|
123
|
+
|
124
|
+
def where(criteria, &block)
|
125
|
+
new_with_query(Query.new(criteria: criteria, &block))
|
126
|
+
end
|
127
|
+
|
128
|
+
def select(*new_selected_columns)
|
129
|
+
selected_columns = new_selected_columns & all_column_names
|
130
|
+
self.class.new(columns, all_rows, selected_columns: selected_columns, applied_query: applied_query)
|
131
|
+
end
|
132
|
+
|
133
|
+
def order(*columns)
|
134
|
+
new_with_query(applied_query.order(columns.flatten))
|
135
|
+
end
|
136
|
+
|
137
|
+
def reverse
|
138
|
+
new_with_query(@applied_query.reverse)
|
139
|
+
end
|
140
|
+
|
141
|
+
def each(&block)
|
142
|
+
matching_rows.each(&block)
|
143
|
+
end
|
144
|
+
|
145
|
+
def delete
|
146
|
+
matching_rows.each do |row_to_delete|
|
147
|
+
all_rows.delete(row_to_delete)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def insert(new_row)
|
152
|
+
new_row_with_empty_fields = empty_row.merge(clone(new_row))
|
153
|
+
|
154
|
+
all_rows.push(new_row_with_empty_fields)
|
155
|
+
end
|
156
|
+
|
157
|
+
def update(attrs)
|
158
|
+
all_rows
|
159
|
+
.select { |row| matching_rows.include?(row) }
|
160
|
+
.each do |row|
|
161
|
+
attrs.each do |k, v|
|
162
|
+
row[clone(k)] = clone(v)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
.count
|
166
|
+
end
|
167
|
+
|
168
|
+
def empty?
|
169
|
+
matching_rows.empty?
|
170
|
+
end
|
171
|
+
|
172
|
+
protected
|
173
|
+
|
174
|
+
def extract_values_for_sub_select(expected_columns: 1)
|
175
|
+
unless selected_columns.size == expected_columns
|
176
|
+
raise "Expected dataset with #{expected_columns} columns. Got #{selected_columns.size} columns."
|
177
|
+
end
|
178
|
+
|
179
|
+
matching_rows.flat_map(&:values)
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
def matching_rows
|
185
|
+
apply_sort(
|
186
|
+
equality_filter(all_rows, applied_query.criteria),
|
187
|
+
applied_query.order_columns,
|
188
|
+
applied_query.reverse_order?,
|
189
|
+
)
|
190
|
+
.map { |row| row.select { |k, _v| selected_columns.include?(k) } }
|
191
|
+
.map { |row| Marshal.load(Marshal.dump(row)) }
|
192
|
+
end
|
193
|
+
|
194
|
+
def apply_sort(rows, order_columns, reverse_order)
|
195
|
+
sorted_rows = rows.sort_by{ |row|
|
196
|
+
order_columns.map { |col| row.fetch(col) }
|
197
|
+
}
|
198
|
+
|
199
|
+
if reverse_order
|
200
|
+
sorted_rows.reverse
|
201
|
+
else
|
202
|
+
sorted_rows
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def equality_filter(rows, criteria)
|
207
|
+
rows.select { |row|
|
208
|
+
criteria.all? { |k, v| match(row.fetch(k), v) }
|
209
|
+
}
|
210
|
+
end
|
211
|
+
|
212
|
+
def match(value, comparitor)
|
213
|
+
case comparitor
|
214
|
+
when Relation
|
215
|
+
comparitor.extract_values_for_sub_select.include?(value)
|
216
|
+
when Enumerable
|
217
|
+
comparitor.include?(value)
|
218
|
+
when Regexp
|
219
|
+
comparitor === value
|
220
|
+
else
|
221
|
+
comparitor == value
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def empty_row
|
226
|
+
Hash[all_column_names.map { |name| [ name, nil ] }]
|
227
|
+
end
|
228
|
+
|
229
|
+
def all_column_names
|
230
|
+
schema.map { |f| f.fetch(:name) }
|
231
|
+
end
|
232
|
+
|
233
|
+
def new_with_query(query)
|
234
|
+
self.class.new(schema, all_rows, selected_columns: selected_columns, applied_query: query)
|
235
|
+
end
|
236
|
+
|
237
|
+
def clone(object)
|
238
|
+
Marshal.load(Marshal.dump(object))
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
@@ -1,10 +1,10 @@
|
|
1
1
|
require "forwardable"
|
2
|
-
require "terrestrial/
|
2
|
+
require "terrestrial/inspection_string"
|
3
3
|
|
4
4
|
module Terrestrial
|
5
5
|
class CollectionMutabilityProxy
|
6
6
|
extend Forwardable
|
7
|
-
include
|
7
|
+
include InspectionString
|
8
8
|
include Enumerable
|
9
9
|
|
10
10
|
def initialize(collection)
|
@@ -45,11 +45,16 @@ module Terrestrial
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def push(node)
|
48
|
+
force_load
|
48
49
|
@added_nodes.push(node)
|
49
50
|
end
|
50
51
|
|
51
52
|
private
|
52
53
|
|
54
|
+
def force_load
|
55
|
+
to_a
|
56
|
+
end
|
57
|
+
|
53
58
|
def loaded_enum
|
54
59
|
Enumerator.new do |yielder|
|
55
60
|
collection.each_loaded do |element|
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
Error = Module.new
|
3
|
+
|
4
|
+
class UpsertError < RuntimeError
|
5
|
+
include Error
|
6
|
+
|
7
|
+
def initialize(relation_name, record, original_error)
|
8
|
+
@relation_name = relation_name
|
9
|
+
@record = record
|
10
|
+
@original_error = original_error
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :relation_name, :record, :original_error
|
14
|
+
private :relation_name, :record, :original_error
|
15
|
+
|
16
|
+
def message
|
17
|
+
[
|
18
|
+
"Error upserting record into `#{relation_name}` with data `#{record.inspect}`.",
|
19
|
+
"Got Error: #{original_error.class.name} #{original_error.message}",
|
20
|
+
].join("\n")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class LoadError < RuntimeError
|
25
|
+
include Error
|
26
|
+
|
27
|
+
def initialize(relation_name, factory, record, original_error)
|
28
|
+
@relation_name = relation_name
|
29
|
+
@factory = factory
|
30
|
+
@record = record
|
31
|
+
@original_error = original_error
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :relation_name, :factory, :record, :original_error
|
35
|
+
private :relation_name, :factory, :record, :original_error
|
36
|
+
|
37
|
+
def message
|
38
|
+
[
|
39
|
+
"Error loading record from `#{relation_name}` relation `#{record.inspect}`.",
|
40
|
+
"Using: `#{factory.inspect}`.",
|
41
|
+
"Check that the factory is compatible.",
|
42
|
+
"Got Error: #{original_error.class.name} #{original_error.message}",
|
43
|
+
].join("\n")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class SerializationError < RuntimeError
|
48
|
+
include Error
|
49
|
+
|
50
|
+
def initialize(relation_name, serializer, object, original_error)
|
51
|
+
@relation_name = relation_name
|
52
|
+
@serializer = serializer
|
53
|
+
@object = object
|
54
|
+
@original_error = original_error
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_reader :relation_name, :serializer, :object, :original_error
|
58
|
+
private :relation_name, :serializer, :object, :original_error
|
59
|
+
|
60
|
+
def message
|
61
|
+
[
|
62
|
+
"Error serializing object with mapping `#{relation_name}` `#{object.inspect}`.",
|
63
|
+
"Using serializer: `#{serializer.inspect}`.",
|
64
|
+
"Check the specified serializer can transform objects into a Hash.",
|
65
|
+
"Got Error: #{original_error.class.name} #{original_error.message}",
|
66
|
+
].join("\n")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -8,56 +8,79 @@ module Terrestrial
|
|
8
8
|
|
9
9
|
attr_reader :datasets, :mappings, :object_load_pipeline
|
10
10
|
|
11
|
-
def call(mapping_name, record,
|
11
|
+
def call(mapping_name, record, eager_data_graph = {})
|
12
12
|
mapping = mappings.fetch(mapping_name)
|
13
13
|
|
14
|
-
load_record(mapping, record,
|
14
|
+
load_record(mapping, record, eager_data_graph)
|
15
15
|
end
|
16
16
|
|
17
17
|
private
|
18
18
|
|
19
|
-
def load_record(mapping, record,
|
20
|
-
associations = load_associations(mapping, record,
|
19
|
+
def load_record(mapping, record, eager_data_graph)
|
20
|
+
associations = load_associations(mapping, record, eager_data_graph)
|
21
21
|
|
22
22
|
object_load_pipeline.call(mapping, record, Hash[associations])
|
23
23
|
end
|
24
24
|
|
25
|
-
def load_associations(mapping, record,
|
25
|
+
def load_associations(mapping, record, eager_data_graph)
|
26
26
|
mapping.associations.map { |name, association|
|
27
|
-
|
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
|
-
]
|
27
|
+
load_association(name, association, record, eager_data_graph)
|
52
28
|
}
|
53
29
|
end
|
54
30
|
|
55
|
-
def
|
31
|
+
def load_association(name, association, record, eager_data_graph)
|
32
|
+
association_superset, deeper_eager_data = eager_or_lazy_data(
|
33
|
+
name,
|
34
|
+
association,
|
35
|
+
eager_data_graph,
|
36
|
+
)
|
37
|
+
|
38
|
+
[
|
39
|
+
name,
|
40
|
+
association.build_proxy(
|
41
|
+
record: record,
|
42
|
+
data_superset: association_superset,
|
43
|
+
loader: recursive_loader(association, deeper_eager_data),
|
44
|
+
)
|
45
|
+
]
|
46
|
+
end
|
47
|
+
|
48
|
+
def eager_or_lazy_data(name, association, eager_data_graph)
|
49
|
+
eager_data = eager_data_graph.fetch(name, {})
|
50
|
+
|
51
|
+
association_superset = eager_data.fetch(:superset) { default_dataset(association) }
|
52
|
+
deeper_eager_data = eager_data.fetch(:associations, {})
|
53
|
+
|
54
|
+
[association_superset, deeper_eager_data]
|
55
|
+
end
|
56
|
+
|
57
|
+
def default_dataset(association)
|
56
58
|
association
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
59
|
+
.mapping_names
|
60
|
+
.map { |name| mappings.fetch(name) }
|
61
|
+
.map(&:namespace)
|
62
|
+
.map { |ns| datasets[ns] }
|
63
|
+
end
|
64
|
+
|
65
|
+
def recursive_loader(association, eager_data_graph)
|
66
|
+
->(associated_record, join_records = []) {
|
67
|
+
load_and_ignore_join_records(association, join_records)
|
68
|
+
|
69
|
+
call(
|
70
|
+
association.mapping_name,
|
71
|
+
associated_record,
|
72
|
+
eager_data_graph,
|
73
|
+
)
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
def load_and_ignore_join_records(association, join_records)
|
78
|
+
join_records.each do |jr|
|
79
|
+
mapping = mappings.fetch(association.join_mapping_name)
|
80
|
+
object_load_pipeline.call(mapping, jr)
|
81
|
+
end
|
82
|
+
|
83
|
+
nil
|
61
84
|
end
|
62
85
|
end
|
63
86
|
end
|