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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/Gemfile.lock +29 -24
  4. data/README.md +35 -17
  5. data/Rakefile +4 -9
  6. data/TODO.md +25 -18
  7. data/bin/test +31 -0
  8. data/docs/domain_object_contract.md +50 -0
  9. data/features/env.rb +4 -6
  10. data/features/example.feature +28 -28
  11. data/features/step_definitions/example_steps.rb +2 -2
  12. data/lib/terrestrial/adapters/memory_adapter.rb +241 -0
  13. data/lib/terrestrial/collection_mutability_proxy.rb +7 -2
  14. data/lib/terrestrial/dirty_map.rb +5 -0
  15. data/lib/terrestrial/error.rb +69 -0
  16. data/lib/terrestrial/graph_loader.rb +58 -35
  17. data/lib/terrestrial/graph_serializer.rb +37 -30
  18. data/lib/terrestrial/inspection_string.rb +19 -0
  19. data/lib/terrestrial/lazy_collection.rb +2 -2
  20. data/lib/terrestrial/lazy_object_proxy.rb +1 -1
  21. data/lib/terrestrial/many_to_one_association.rb +17 -11
  22. data/lib/terrestrial/public_conveniencies.rb +125 -95
  23. data/lib/terrestrial/relation_mapping.rb +30 -0
  24. data/lib/terrestrial/{mapper_facade.rb → relational_store.rb} +11 -1
  25. data/lib/terrestrial/version.rb +1 -1
  26. data/spec/config_override_spec.rb +10 -14
  27. data/spec/custom_serializers_spec.rb +4 -6
  28. data/spec/deletion_spec.rb +12 -14
  29. data/spec/error_handling/factory_error_handling_spec.rb +61 -0
  30. data/spec/error_handling/serialization_error_spec.rb +50 -0
  31. data/spec/error_handling/upsert_error_spec.rb +132 -0
  32. data/spec/graph_persistence_spec.rb +80 -24
  33. data/spec/graph_traversal_spec.rb +14 -6
  34. data/spec/new_graph_persistence_spec.rb +43 -9
  35. data/spec/object_identity_spec.rb +5 -7
  36. data/spec/ordered_association_spec.rb +4 -6
  37. data/spec/predefined_queries_spec.rb +4 -6
  38. data/spec/querying_spec.rb +4 -12
  39. data/spec/readme_examples_spec.rb +3 -6
  40. data/spec/{persistence_efficiency_spec.rb → sequel_query_efficiency_spec.rb} +101 -19
  41. data/spec/spec_helper.rb +24 -2
  42. data/spec/support/memory_adapter_test_support.rb +21 -0
  43. data/spec/support/{mapper_setup.rb → object_store_setup.rb} +5 -5
  44. data/spec/support/seed_data_setup.rb +3 -1
  45. data/spec/support/sequel_test_support.rb +58 -25
  46. data/spec/{sequel_mapper → terrestrial}/abstract_record_spec.rb +0 -0
  47. data/spec/{sequel_mapper → terrestrial}/collection_mutability_proxy_spec.rb +0 -0
  48. data/spec/{sequel_mapper → terrestrial}/deleted_record_spec.rb +0 -0
  49. data/spec/{sequel_mapper → terrestrial}/dirty_map_spec.rb +38 -6
  50. data/spec/{sequel_mapper → terrestrial}/lazy_collection_spec.rb +2 -3
  51. data/spec/{sequel_mapper → terrestrial}/lazy_object_proxy_spec.rb +0 -0
  52. data/spec/{sequel_mapper → terrestrial}/public_conveniencies_spec.rb +12 -7
  53. data/spec/{sequel_mapper → terrestrial}/upserted_record_spec.rb +0 -0
  54. data/{sequel_mapper.gemspec → terrestrial.gemspec} +3 -3
  55. metadata +47 -39
  56. data/lib/terrestrial/short_inspection_string.rb +0 -18
  57. data/spec/proxying_spec.rb +0 -88
  58. data/spec/support/mock_sequel.rb +0 -193
  59. 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 mapper configuration$/) do |code_sample|
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 mapper is instantiated$/) do |code_sample|
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/short_inspection_string"
2
+ require "terrestrial/inspection_string"
3
3
 
4
4
  module Terrestrial
5
5
  class CollectionMutabilityProxy
6
6
  extend Forwardable
7
- include ShortInspectionString
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|
@@ -7,6 +7,11 @@ module Terrestrial
7
7
  attr_reader :storage
8
8
  private :storage
9
9
 
10
+ def load_if_new(record)
11
+ storage.fetch(hash_key(record)) { self.load(record) }
12
+ record
13
+ end
14
+
10
15
  def load(record)
11
16
  storage.store(hash_key(record), deep_clone(record))
12
17
  record
@@ -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, eager_data = {})
11
+ def call(mapping_name, record, eager_data_graph = {})
12
12
  mapping = mappings.fetch(mapping_name)
13
13
 
14
- load_record(mapping, record, eager_data)
14
+ load_record(mapping, record, eager_data_graph)
15
15
  end
16
16
 
17
17
  private
18
18
 
19
- def load_record(mapping, record, eager_data)
20
- associations = load_associations(mapping, record, eager_data)
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, eager_data)
25
+ def load_associations(mapping, record, eager_data_graph)
26
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
- ]
27
+ load_association(name, association, record, eager_data_graph)
52
28
  }
53
29
  end
54
30
 
55
- def load_from_datasets(association)
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
- .mapping_names
58
- .map { |name| mappings.fetch(name) }
59
- .map(&:namespace)
60
- .map { |ns| datasets[ns] }
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