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
@@ -1,28 +1,26 @@
1
1
  require "spec_helper"
2
2
 
3
3
  require "support/have_persisted_matcher"
4
- require "support/mapper_setup"
5
- require "support/sequel_persistence_setup"
4
+ require "support/object_store_setup"
6
5
  require "support/seed_data_setup"
7
6
  require "terrestrial"
8
7
 
9
8
  RSpec.describe "Graph persistence" do
10
- include_context "mapper setup"
11
- include_context "sequel persistence setup"
9
+ include_context "object store setup"
12
10
  include_context "seed data setup"
13
11
 
14
- subject(:mapper) { mappers.fetch(:users) }
12
+ subject(:user_store) { object_store.fetch(:users) }
15
13
 
16
14
  let(:user) {
17
- mapper.where(id: "users/1").first
15
+ user_store.where(id: "users/1").first
18
16
  }
19
17
 
20
18
  context "without associations" do
21
- let(:modified_email) { "bestie+modified@gmail.com" }
19
+ let(:modified_email) { "hasel+modified@gmail.com" }
22
20
 
23
21
  it "saves the root object" do
24
22
  user.email = modified_email
25
- mapper.save(user)
23
+ user_store.save(user)
26
24
 
27
25
  expect(datastore).to have_persisted(
28
26
  :users,
@@ -35,7 +33,7 @@ RSpec.describe "Graph persistence" do
35
33
 
36
34
  it "doesn't send associated objects to the database as columns" do
37
35
  user.email = modified_email
38
- mapper.save(user)
36
+ user_store.save(user)
39
37
 
40
38
  expect(datastore).not_to have_persisted(
41
39
  :users,
@@ -50,7 +48,7 @@ RSpec.describe "Graph persistence" do
50
48
  it "saves the object" do
51
49
  user.email << "MUTATED"
52
50
 
53
- mapper.save(user)
51
+ user_store.save(user)
54
52
 
55
53
  expect(datastore).to have_persisted(
56
54
  :users,
@@ -69,7 +67,7 @@ RSpec.describe "Graph persistence" do
69
67
 
70
68
  it "saves the associated object" do
71
69
  post.body = modified_post_body
72
- mapper.save(user)
70
+ user_store.save(user)
73
71
 
74
72
  expect(datastore).to have_persisted(
75
73
  :posts,
@@ -92,7 +90,7 @@ RSpec.describe "Graph persistence" do
92
90
 
93
91
  it "saves the associated object" do
94
92
  comment.body = modified_comment_body
95
- mapper.save(user)
93
+ user_store.save(user)
96
94
 
97
95
  expect(datastore).to have_persisted(
98
96
  :comments,
@@ -133,7 +131,7 @@ RSpec.describe "Graph persistence" do
133
131
  it "persists the object" do
134
132
  user.posts.push(new_post)
135
133
 
136
- mapper.save(user)
134
+ user_store.save(user)
137
135
 
138
136
  expect(datastore).to have_persisted(
139
137
  :posts,
@@ -144,6 +142,18 @@ RSpec.describe "Graph persistence" do
144
142
  )
145
143
  )
146
144
  end
145
+
146
+ context "when the collection is not loaded until the new object is persisted" do
147
+ it "is consistent with the datastore" do
148
+ user.posts.push(new_post)
149
+
150
+ user_store.save(user)
151
+
152
+ expect(user.posts.to_a.map(&:id)).to eq(
153
+ ["posts/1", "posts/2", "posts/neu"]
154
+ )
155
+ end
156
+ end
147
157
  end
148
158
 
149
159
  context "delete an object from a has many association" do
@@ -157,7 +167,7 @@ RSpec.describe "Graph persistence" do
157
167
 
158
168
  it "delete the object from the datastore on save" do
159
169
  user.posts.delete(post)
160
- mapper.save(user)
170
+ user_store.save(user)
161
171
 
162
172
  expect(datastore).not_to have_persisted(
163
173
  :posts,
@@ -182,7 +192,7 @@ RSpec.describe "Graph persistence" do
182
192
  it "deletes the 'join table' record" do
183
193
  category = post.categories.first
184
194
  post.categories.delete(category)
185
- mapper.save(user)
195
+ user_store.save(user)
186
196
 
187
197
  expect(datastore).not_to have_persisted(
188
198
  :categories_to_posts,
@@ -196,7 +206,7 @@ RSpec.describe "Graph persistence" do
196
206
  it "does not delete the object" do
197
207
  category = post.categories.first
198
208
  post.categories.delete(category)
199
- mapper.save(user)
209
+ user_store.save(user)
200
210
 
201
211
  expect(datastore).to have_persisted(
202
212
  :categories,
@@ -220,7 +230,7 @@ RSpec.describe "Graph persistence" do
220
230
 
221
231
  it "persists the change" do
222
232
  post_with_one_category.categories.push(new_category)
223
- mapper.save(user)
233
+ user_store.save(user)
224
234
 
225
235
  expect(datastore).to have_persisted(
226
236
  :categories_to_posts,
@@ -245,7 +255,7 @@ RSpec.describe "Graph persistence" do
245
255
 
246
256
  it "persists the change" do
247
257
  category.name = modified_category_name
248
- mapper.save(user)
258
+ user_store.save(user)
249
259
 
250
260
  expect(datastore).to have_persisted(
251
261
  :categories,
@@ -258,13 +268,13 @@ RSpec.describe "Graph persistence" do
258
268
  end
259
269
 
260
270
  context "node loaded as root has undefined one to many association" do
261
- let(:post_mapper) { mappers[:posts] }
262
- let(:post) { post_mapper.where(id: "posts/1").first }
271
+ let(:post_store) { object_store[:posts] }
272
+ let(:post) { post_store.where(id: "posts/1").first }
263
273
 
264
274
  it "persists the changes to the root node" do
265
275
  post.body = "modified body"
266
276
 
267
- post_mapper.save(post)
277
+ post_store.save(post)
268
278
 
269
279
  expect(datastore).to have_persisted(
270
280
  :posts,
@@ -278,7 +288,7 @@ RSpec.describe "Graph persistence" do
278
288
  it "does not overwrite unused foreign key" do
279
289
  post.body = "modified body"
280
290
 
281
- post_mapper.save(post)
291
+ post_store.save(post)
282
292
 
283
293
  expect(datastore).to have_persisted(
284
294
  :posts,
@@ -291,6 +301,52 @@ RSpec.describe "Graph persistence" do
291
301
  end
292
302
  end
293
303
 
304
+ context "when a many to one association is nil" do
305
+ context "when the object does not have a reference to its parent" do
306
+ it "populates that association with a nil" do
307
+ post = Post.new(
308
+ id: "posts/orphan",
309
+ subject: "Nils gonna getcha",
310
+ body: "",
311
+ created_at: Time.parse("2015-09-05T15:00:00+01:00"),
312
+ categories: [],
313
+ comments: [],
314
+ )
315
+
316
+ object_store[:posts].save(post)
317
+
318
+ expect(datastore).to have_persisted(
319
+ :posts,
320
+ hash_including(
321
+ id: "posts/orphan",
322
+ author_id: nil,
323
+ )
324
+ )
325
+ end
326
+ end
327
+
328
+ context "when an existing partent object reference is set to nil" do
329
+ it "populates that association with a nil" do
330
+ comment = user
331
+ .posts
332
+ .flat_map(&:comments)
333
+ .detect { |c| c.id == "comments/1" }
334
+
335
+ comment.commenter = nil
336
+
337
+ user_store.save(user)
338
+
339
+ expect(datastore).to have_persisted(
340
+ :comments,
341
+ hash_including(
342
+ id: "comments/1",
343
+ commenter_id: nil,
344
+ )
345
+ )
346
+ end
347
+ end
348
+ end
349
+
294
350
  context "when a save operation fails (some object is not persistable)" do
295
351
  let(:unpersistable_object) { ->() { } }
296
352
 
@@ -301,8 +357,8 @@ RSpec.describe "Graph persistence" do
301
357
  user.first_name = "this will be rolled back"
302
358
  user.posts.first.subject = unpersistable_object
303
359
 
304
- mapper.save(user)
305
- rescue Sequel::Error
360
+ user_store.save(user)
361
+ rescue Terrestrial::Error
306
362
  end
307
363
 
308
364
  post_change = datastore[:users].to_a.map(&:to_a).sort
@@ -1,20 +1,18 @@
1
1
  require "spec_helper"
2
2
 
3
- require "support/mapper_setup"
4
- require "support/sequel_persistence_setup"
3
+ require "support/object_store_setup"
5
4
  require "support/seed_data_setup"
6
5
  require "terrestrial"
7
6
 
8
7
  RSpec.describe "Graph traversal" do
9
- include_context "mapper setup"
10
- include_context "sequel persistence setup"
8
+ include_context "object store setup"
11
9
  include_context "seed data setup"
12
10
 
13
11
  describe "associations" do
14
- subject(:mapper) { user_mapper }
12
+ subject(:user_store) { object_store[:users] }
15
13
 
16
14
  let(:user_query) {
17
- mapper.where(id: "users/1")
15
+ user_store.where(id: "users/1")
18
16
  }
19
17
 
20
18
  let(:user) { user_query.first }
@@ -43,6 +41,16 @@ RSpec.describe "Graph traversal" do
43
41
  ).to eq("oh noes")
44
42
  end
45
43
 
44
+ context "when a many to one association foreign key is nil" do
45
+ before do
46
+ datastore[:comments].update(commenter_id: nil)
47
+ end
48
+
49
+ it "populates that association with a nil" do
50
+ expect(user.posts.flat_map(&:comments).flat_map(&:commenter).uniq).to eq([nil])
51
+ end
52
+ end
53
+
46
54
  describe "lazy loading" do
47
55
  let(:post_factory) { double(:post_factory, call: nil) }
48
56
 
@@ -1,15 +1,13 @@
1
1
  require "spec_helper"
2
- require "support/mapper_setup"
3
- require "support/sequel_persistence_setup"
2
+ require "support/object_store_setup"
4
3
  require "support/have_persisted_matcher"
5
4
 
6
5
  RSpec.describe "Persist a new graph in empty datastore" do
7
- include_context "mapper setup"
8
- include_context "sequel persistence setup"
6
+ include_context "object store setup"
9
7
 
10
8
  context "given a graph of new objects" do
11
9
  it "persists the root node" do
12
- user_mapper.save(hansel)
10
+ object_store[:users].save(hansel)
13
11
 
14
12
  expect(datastore).to have_persisted(:users, {
15
13
  id: hansel.id,
@@ -20,7 +18,7 @@ RSpec.describe "Persist a new graph in empty datastore" do
20
18
  end
21
19
 
22
20
  it "persists one to many related nodes 1 level deep" do
23
- user_mapper.save(hansel)
21
+ object_store[:users].save(hansel)
24
22
 
25
23
  expect(datastore).to have_persisted(:posts, hash_including(
26
24
  id: "posts/1",
@@ -39,7 +37,7 @@ RSpec.describe "Persist a new graph in empty datastore" do
39
37
 
40
38
  context "deep node with two foreign keys" do
41
39
  it "persists the node with both foreign keys" do
42
- user_mapper.save(hansel)
40
+ object_store[:users].save(hansel)
43
41
 
44
42
  expect(datastore).to have_persisted(:comments, {
45
43
  id: "comments/1",
@@ -51,7 +49,7 @@ RSpec.describe "Persist a new graph in empty datastore" do
51
49
  end
52
50
 
53
51
  it "persists many to many related nodes" do
54
- user_mapper.save(hansel)
52
+ object_store[:users].save(hansel)
55
53
 
56
54
  expect(datastore).to have_persisted(:categories, {
57
55
  id: "categories/1",
@@ -60,12 +58,48 @@ RSpec.describe "Persist a new graph in empty datastore" do
60
58
  end
61
59
 
62
60
  it "persists a 'join table' to faciliate many to many" do
63
- user_mapper.save(hansel)
61
+ object_store[:users].save(hansel)
64
62
 
65
63
  expect(datastore).to have_persisted(:categories_to_posts, {
66
64
  category_id: "categories/1",
67
65
  post_id: "posts/1",
68
66
  })
69
67
  end
68
+
69
+ context "when saving an object a second time" do
70
+ context "when the first time fails" do
71
+ before do
72
+ error = attempt_unpersistable_save
73
+ unless error.is_a?(Terrestrial::UpsertError)
74
+ raise "Unpersistable save did not raise an error. Expected this save to fail."
75
+ end
76
+ hansel.email = "hansel@gmail.com"
77
+ end
78
+
79
+ let(:unpersistable) { ->() {} }
80
+
81
+ it "successfully saves all attributes the second time" do
82
+ object_store[:users].save(hansel)
83
+
84
+ expect(datastore).to have_persisted(
85
+ :users,
86
+ hash_including(
87
+ id: hansel.id,
88
+ email: hansel.email,
89
+ first_name: hansel.first_name,
90
+ last_name: hansel.last_name,
91
+ )
92
+ )
93
+ end
94
+
95
+ def attempt_unpersistable_save
96
+ email = hansel.email
97
+ hansel.email = unpersistable
98
+ object_store[:users].save(hansel)
99
+ rescue Terrestrial::UpsertError => e
100
+ e
101
+ end
102
+ end
103
+ end
70
104
  end
71
105
  end
@@ -1,18 +1,16 @@
1
1
  require "spec_helper"
2
2
 
3
- require "support/mapper_setup"
4
- require "support/sequel_persistence_setup"
3
+ require "support/object_store_setup"
5
4
  require "support/seed_data_setup"
6
5
  require "terrestrial"
7
6
 
8
7
  RSpec.describe "Object identity" do
9
- include_context "mapper setup"
10
- include_context "sequel persistence setup"
8
+ include_context "object store setup"
11
9
  include_context "seed data setup"
12
10
 
13
- subject(:mapper) { mappers.fetch(:users) }
11
+ subject(:user_store) { object_store.fetch(:users) }
14
12
 
15
- let(:user) { mapper.where(id: "users/1").first }
13
+ let(:user) { user_store.where(id: "users/1").first }
16
14
  let(:post) { user.posts.first }
17
15
 
18
16
  context "when using arbitrary where query" do
@@ -46,7 +44,7 @@ RSpec.describe "Object identity" do
46
44
  end
47
45
 
48
46
  context "when eager loading" do
49
- let(:user_query) { mapper.where(id: "users/1") }
47
+ let(:user_query) { user_store.where(id: "users/1") }
50
48
 
51
49
  let(:eager_category) {
52
50
  user_query
@@ -1,18 +1,16 @@
1
1
  require "spec_helper"
2
2
 
3
- require "support/mapper_setup"
4
- require "support/sequel_persistence_setup"
3
+ require "support/object_store_setup"
5
4
  require "support/seed_data_setup"
6
5
  require "terrestrial"
7
6
  require "terrestrial/configurations/conventional_configuration"
8
7
 
9
8
  RSpec.describe "Ordered associations" do
10
- include_context "mapper setup"
11
- include_context "sequel persistence setup"
9
+ include_context "object store setup"
12
10
  include_context "seed data setup"
13
11
 
14
12
  context "one to many association ordered by `created_at DESC`" do
15
- let(:posts) { user_mapper.first.posts }
13
+ let(:posts) { object_store[:users].first.posts }
16
14
 
17
15
  before do
18
16
  configs.fetch(:users).fetch(:associations).fetch(:posts).merge!(
@@ -40,7 +38,7 @@ RSpec.describe "Ordered associations" do
40
38
  )
41
39
  end
42
40
 
43
- let(:categories) { user_mapper.first.posts.first.categories }
41
+ let(:categories) { object_store[:users].first.posts.first.categories }
44
42
 
45
43
  it "enumerates the objects in order specified in the config" do
46
44
  expect(categories.map(&:id)).to eq(
@@ -1,17 +1,15 @@
1
1
  require "spec_helper"
2
2
 
3
- require "support/mapper_setup"
4
- require "support/sequel_persistence_setup"
3
+ require "support/object_store_setup"
5
4
  require "support/seed_data_setup"
6
5
  require "terrestrial"
7
6
  require "terrestrial/configurations/conventional_configuration"
8
7
 
9
8
  RSpec.describe "Predefined subset queries" do
10
- include_context "mapper setup"
11
- include_context "sequel persistence setup"
9
+ include_context "object store setup"
12
10
  include_context "seed data setup"
13
11
 
14
- subject(:users) { user_mapper }
12
+ subject(:users) { object_store[:users] }
15
13
 
16
14
  context "on the top level mapper" do
17
15
  context "subset is defined with a block" do
@@ -41,7 +39,7 @@ RSpec.describe "Predefined subset queries" do
41
39
  configs.fetch(:posts).merge!(
42
40
  subsets: {
43
41
  body_contains: ->(dataset, search_string) {
44
- dataset.where("body like '%#{search_string}%'")
42
+ dataset.where(body: /#{search_string}/)
45
43
  },
46
44
  },
47
45
  )