terrestrial 0.1.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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