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,19 +1,17 @@
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 "Querying" 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) { user_mapper }
11
+ subject(:user_store) { object_store[:users] }
14
12
 
15
13
  let(:user) {
16
- mapper.where(id: "users/1").first
14
+ user_store.where(id: "users/1").first
17
15
  }
18
16
 
19
17
  let(:query_criteria) {
@@ -31,12 +29,6 @@ RSpec.describe "Querying" do
31
29
  expect(filtered_posts.map(&:id)).to eq(["posts/2"])
32
30
  end
33
31
 
34
- it "delegates the query to the datastore, performs two additiona reads" do
35
- expect {
36
- filtered_posts.map(&:id)
37
- }.to change { query_counter.read_count }.by(2)
38
- end
39
-
40
32
  it "returns another collection" do
41
33
  expect(filtered_posts).not_to be(user.posts)
42
34
  end
@@ -1,20 +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
  require "spec_helper"
9
8
 
10
- require "support/mapper_setup"
11
- require "support/sequel_persistence_setup"
9
+ require "support/object_store_setup"
12
10
  require "support/seed_data_setup"
13
11
  require "terrestrial"
14
12
 
15
13
  RSpec.describe "README examples" do
16
- include_context "sequel persistence setup"
17
-
18
14
  readme_contents = File.read("README.md")
19
15
 
20
16
  code_samples = readme_contents
@@ -29,6 +25,7 @@ RSpec.describe "README examples" do
29
25
  rescue => e
30
26
  File.open("./example#{i}.rb", "w") { |f| f.puts(code_sample) }
31
27
  binding.pry if ENV["DEBUG"]
28
+ raise e
32
29
  end
33
30
  end
34
31
  end
@@ -1,19 +1,86 @@
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
- RSpec.describe "Graph persistence efficiency" do
9
- include_context "mapper setup"
10
- include_context "sequel persistence setup"
7
+ RSpec.describe "Sequel query efficiency", backend: "sequel" do
8
+ include_context "object store setup"
11
9
  include_context "seed data setup"
12
10
 
13
- let(:mapper) { user_mapper }
14
- let(:user_query) { mapper.where(id: "users/1") }
11
+ let(:user_store) { object_store[:users] }
12
+ let(:user_query) { user_store.where(id: "users/1") }
15
13
  let(:user) { user_query.first }
16
14
 
15
+ context "when loading the root node" do
16
+ it "only performs one read" do
17
+ expect {
18
+ user
19
+ }.to change { query_counter.read_count }.by(1)
20
+ end
21
+ end
22
+
23
+ context "when traversing associations (lazily)" do
24
+ let(:user) { object_store[:users].where(id: "users/1").first }
25
+ let(:posts) { user.posts }
26
+
27
+ context "when getting a reference to an association proxy" do
28
+ before { user }
29
+
30
+ it "does no additional reads" do
31
+ expect{
32
+ user.posts
33
+ }.to change { query_counter.read_count }.by(0)
34
+ end
35
+ end
36
+
37
+ context "when iteratiing over a has many association" do
38
+ before { posts }
39
+
40
+ it "does a single additional read for the association collection" do
41
+ expect {
42
+ user.posts.each { |x| x }
43
+ }.to change { query_counter.read_count }.by(1)
44
+ end
45
+
46
+ context "when doing this more than once" do
47
+ before do
48
+ posts.each { |x| x }
49
+ end
50
+
51
+ it "performs no additional reads" do
52
+ expect {
53
+ user.posts.each { |x| x }
54
+ }.not_to change { query_counter.read_count }
55
+ end
56
+ end
57
+ end
58
+
59
+ context "when getting a reference to a many to many association" do
60
+ before { post }
61
+
62
+ let(:post) { user.posts.first }
63
+
64
+ it "does no additional reads" do
65
+ expect {
66
+ post.categories
67
+ }.to change { query_counter.read_count }.by(0)
68
+ end
69
+ end
70
+
71
+ context "when iterating over a many to many association" do
72
+ let(:category_count) { 3 }
73
+
74
+ it "does 1 read" do
75
+ post = user.posts.first
76
+
77
+ expect {
78
+ post.categories.each { |x| x }
79
+ }.to change { query_counter.read_count }.by(1)
80
+ end
81
+ end
82
+ end
83
+
17
84
  context "when modifying the root node" do
18
85
  let(:modified_email) { "modified@example.com" }
19
86
 
@@ -24,14 +91,15 @@ RSpec.describe "Graph persistence efficiency" do
24
91
 
25
92
  it "performs 1 update" do
26
93
  expect {
27
- mapper.save(user)
94
+ user_store.save(user)
28
95
  }.to change { query_counter.update_count }.by(1)
29
96
  end
30
97
 
31
98
  it "sends only the updated fields to the datastore" do
32
- mapper.save(user)
99
+ user_store.save(user)
33
100
  update_sql = query_counter.updates.last
34
101
 
102
+ # TODO: SQL parser?
35
103
  expect(update_sql).to eq(
36
104
  %{UPDATE "users" SET "email" = '#{modified_email}' WHERE ("id" = '#{user.id}')}
37
105
  )
@@ -48,19 +116,19 @@ RSpec.describe "Graph persistence efficiency" do
48
116
 
49
117
  it "performs 1 update" do
50
118
  expect {
51
- mapper.save(user)
119
+ user_store.save(user)
52
120
  }.to change { query_counter.update_count }.by(1)
53
121
  end
54
122
 
55
123
  it "performs 0 deletes" do
56
124
  expect {
57
- mapper.save(user)
125
+ user_store.save(user)
58
126
  }.to change { query_counter.delete_count }.by(0)
59
127
  end
60
128
 
61
129
  it "performs 0 additional reads" do
62
130
  expect {
63
- mapper.save(user)
131
+ user_store.save(user)
64
132
  }.to change { query_counter.read_count }.by(0)
65
133
  end
66
134
  end
@@ -77,7 +145,7 @@ RSpec.describe "Graph persistence efficiency" do
77
145
 
78
146
  it "performs 1 write" do
79
147
  expect {
80
- mapper.save(user)
148
+ user_store.save(user)
81
149
  }.to change { query_counter.update_count }.by(1)
82
150
  end
83
151
  end
@@ -91,7 +159,7 @@ RSpec.describe "Graph persistence efficiency" do
91
159
 
92
160
  it "performs 1 update" do
93
161
  expect {
94
- mapper.save(user)
162
+ user_store.save(user)
95
163
  }.to change { query_counter.update_count }.by(1)
96
164
  end
97
165
  end
@@ -106,7 +174,7 @@ RSpec.describe "Graph persistence efficiency" do
106
174
 
107
175
  it "performs 2 updates" do
108
176
  expect {
109
- mapper.save(user)
177
+ user_store.save(user)
110
178
  }.to change { query_counter.update_count }.by(2)
111
179
  end
112
180
  end
@@ -122,16 +190,16 @@ RSpec.describe "Graph persistence efficiency" do
122
190
 
123
191
  it "performs 1 write" do
124
192
  expect {
125
- mapper.save(user)
193
+ user_store.save(user)
126
194
  }.to change { query_counter.update_count }.by(1)
127
195
  end
128
196
  end
129
197
 
130
- context "eager loading" do
131
- context "on root node" do
198
+ context "when traversing assocations (eagerly)" do
199
+ context "laoding `#all` from the object store with one assocation" do
132
200
  it "performs 1 read per table rather than n + 1" do
133
201
  expect {
134
- mapper.eager_load(:posts => []).all.map { |user|
202
+ user_store.eager_load(:posts => []).all.map { |user|
135
203
  [user.id, user.posts.map(&:id)]
136
204
  }
137
205
  }.to change { query_counter.read_count }.by(2)
@@ -218,6 +286,20 @@ RSpec.describe "Graph persistence efficiency" do
218
286
  end
219
287
  end
220
288
 
289
+ context "when double saving a new object" do
290
+ context "when the first time is successful" do
291
+ before do
292
+ object_store[:users].save(hansel)
293
+ end
294
+
295
+ it "does not double write to the database" do
296
+ expect {
297
+ object_store[:users].save(hansel)
298
+ }.not_to change { query_counter.write_count }
299
+ end
300
+ end
301
+ end
302
+
221
303
  after do |example|
222
304
  query_counter.show_queries if example.exception
223
305
  end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "pry"
2
2
  require "support/sequel_test_support"
3
+ require "support/memory_adapter_test_support"
3
4
  require "support/blog_schema"
4
5
 
5
6
  RSpec.configure do |config|
@@ -29,8 +30,29 @@ RSpec.configure do |config|
29
30
 
30
31
  # Kernel.srand config.seed
31
32
 
33
+ adapter_support = case ENV.fetch("ADAPTER", "sequel")
34
+ when "memory"
35
+ Terrestrial::MemoryAdapterTestSupport
36
+ when "sequel"
37
+ Terrestrial::SequelTestSupport
38
+ else
39
+ raise "Adapter `#{ENV["ADAPTER"]}` not found"
40
+ end
41
+
42
+ def schema
43
+ BLOG_SCHEMA
44
+ end
45
+
46
+ RSpec.shared_context "adapter setup" do
47
+ let(:datastore) { adapter_support.build_datastore(schema) }
48
+ let(:query_counter) { adapter_support.query_counter }
49
+ end
50
+
51
+ config.include_context "adapter setup"
52
+
32
53
  config.before(:suite) do
33
- Terrestrial::SequelTestSupport.drop_tables
34
- Terrestrial::SequelTestSupport.create_tables(BLOG_SCHEMA)
54
+ adapter_support.before_suite(schema)
35
55
  end
56
+
57
+ config.filter_run_excluding(backend: adapter_support.excluded_adapters)
36
58
  end
@@ -0,0 +1,21 @@
1
+ require "terrestrial/adapters/memory_adapter"
2
+
3
+ module Terrestrial
4
+ module MemoryAdapterTestSupport
5
+ module_function def build_datastore(schema, raw_storage = {})
6
+ Adapters::MemoryAdapter.build_from_schema(schema.fetch(:tables), raw_storage)
7
+ end
8
+
9
+ module_function def excluded_adapters
10
+ "sequel"
11
+ end
12
+
13
+ module_function def before_suite(_schema)
14
+ # NOOP
15
+ end
16
+
17
+ module_function def query_counter
18
+ # NOOP
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,5 @@
1
1
  require "terrestrial"
2
- require "terrestrial/mapper_facade"
2
+ require "terrestrial/relational_store"
3
3
  require "terrestrial/relation_mapping"
4
4
  require "terrestrial/lazy_collection"
5
5
  require "terrestrial/collection_mutability_proxy"
@@ -12,14 +12,14 @@ require "terrestrial/many_to_many_association"
12
12
  require "terrestrial/subset_queries_proxy"
13
13
  require "support/object_graph_setup"
14
14
 
15
- RSpec.shared_context "mapper setup" do
15
+ RSpec.shared_context "object store setup" do
16
16
  include_context "object graph setup"
17
17
 
18
- let(:mappers) {
19
- Terrestrial.mappers(mappings: mappings, datastore: datastore)
18
+ let(:object_store) {
19
+ Terrestrial.object_store(mappings: mappings, datastore: datastore)
20
20
  }
21
21
 
22
- let(:user_mapper) { mappers[:users] }
22
+ let(:user_store) { object_store[:users] }
23
23
 
24
24
  let(:mappings) {
25
25
  Hash[
@@ -3,7 +3,9 @@ RSpec.shared_context "seed data setup" do
3
3
  include_context "object graph setup"
4
4
 
5
5
  before {
6
- insert_records(datastore, seeded_records)
6
+ seeded_records.each do |(namespace, record)|
7
+ datastore[namespace].insert(record)
8
+ end
7
9
  }
8
10
 
9
11
  let(:seeded_records) {
@@ -2,42 +2,66 @@ require "sequel"
2
2
 
3
3
  module Terrestrial
4
4
  module SequelTestSupport
5
- def create_database
5
+ module_function def build_datastore(_schema)
6
+ db_connection.tap { |db|
7
+ # This test is using the database so we better clean it out first
8
+ truncate_tables
9
+
10
+ # The query_counter will let us make assertions about how efficiently
11
+ # the database is being used
12
+ reset_query_counter
13
+ db.loggers << query_counter
14
+ }
15
+ end
16
+
17
+ module_function def query_counter
18
+ @@query_counter ||= QueryCounter.new
19
+ end
20
+
21
+ module_function def before_suite(schema)
22
+ drop_tables
23
+ create_tables(schema.fetch(:tables))
24
+ add_foreign_keys(schema.fetch(:foreign_keys))
25
+ end
26
+
27
+ module_function def excluded_adapters
28
+ "memory"
29
+ end
30
+
31
+ module_function def reset_query_counter
32
+ @@query_counter = nil
33
+ end
34
+
35
+ module_function def create_database
6
36
  `psql postgres --command "CREATE DATABASE $PGDATABASE;"`
7
37
  end
8
- module_function :create_database
9
38
 
10
- def drop_database
39
+ module_function def drop_database
11
40
  `psql postgres --command "DROP DATABASE $PGDATABASE;"`
12
41
  end
13
- module_function :drop_database
14
42
 
15
- def drop_tables(tables = db_connection.tables)
43
+ module_function def drop_tables(tables = db_connection.tables)
16
44
  tables.each do |table_name|
17
45
  db_connection.drop_table(table_name, cascade: true)
18
46
  end
19
47
  end
20
- module_function :drop_tables
21
48
 
22
- def truncate_tables(tables = db_connection.tables)
49
+ module_function def truncate_tables(tables = db_connection.tables)
23
50
  tables.each do |table_name|
24
51
  db_connection[table_name].truncate(cascade: true)
25
52
  end
26
53
  end
27
- module_function :truncate_tables
28
54
 
29
- def db_connection
30
- Sequel.default_timezone = :utc
31
- @db_connection ||= Sequel.postgres(
55
+ module_function def db_connection
56
+ @@db_connection ||= Sequel.postgres(
32
57
  host: ENV.fetch("PGHOST"),
33
58
  user: ENV.fetch("PGUSER"),
34
59
  database: ENV.fetch("PGDATABASE"),
35
- )
60
+ ).tap { Sequel.default_timezone = :utc }
36
61
  end
37
- module_function :db_connection
38
62
 
39
- def create_tables(schema)
40
- schema.fetch(:tables).each do |table_name, fields|
63
+ module_function def create_tables(tables)
64
+ tables.each do |table_name, fields|
41
65
  db_connection.create_table(table_name) do
42
66
  fields.each do |field|
43
67
  type = field.fetch(:type)
@@ -49,20 +73,15 @@ module Terrestrial
49
73
  end
50
74
  end
51
75
 
52
- schema.fetch(:foreign_keys).each do |(table, fk_col, foreign_table, key_col)|
76
+ tables.keys
77
+ end
78
+
79
+ module_function def add_foreign_keys(foreign_keys)
80
+ foreign_keys.each do |(table, fk_col, foreign_table, key_col)|
53
81
  db_connection.alter_table(table) do
54
82
  add_foreign_key([fk_col], foreign_table, key: key_col, deferrable: false, on_delete: :set_null)
55
83
  end
56
84
  end
57
-
58
- schema.fetch(:tables).keys
59
- end
60
- module_function :create_tables
61
-
62
- def insert_records(datastore, records)
63
- records.each { |(namespace, record)|
64
- datastore[namespace].insert(record)
65
- }
66
85
  end
67
86
 
68
87
  class QueryCounter
@@ -88,16 +107,30 @@ module Terrestrial
88
107
  }
89
108
  end
90
109
 
110
+ def write_count
111
+ insert_count + update_count
112
+ end
113
+
91
114
  def update_count
92
115
  updates.count
93
116
  end
94
117
 
118
+ def insert_count
119
+ inserts.count
120
+ end
121
+
95
122
  def updates
96
123
  @info
97
124
  .map { |query| query.gsub(/\A\([0-9\.]+s\) /, "") }
98
125
  .select { |query| query.start_with?("UPDATE") }
99
126
  end
100
127
 
128
+ def inserts
129
+ @info
130
+ .map { |query| query.gsub(/\A\([0-9\.]+s\) /, "") }
131
+ .select { |query| query.start_with?("INSERT") }
132
+ end
133
+
101
134
  def show_queries
102
135
  puts @info.join("\n")
103
136
  end