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
data/spec/querying_spec.rb
CHANGED
@@ -1,19 +1,17 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
|
-
require "support/
|
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 "
|
10
|
-
include_context "sequel persistence setup"
|
8
|
+
include_context "object store setup"
|
11
9
|
include_context "seed data setup"
|
12
10
|
|
13
|
-
subject(:
|
11
|
+
subject(:user_store) { object_store[:users] }
|
14
12
|
|
15
13
|
let(:user) {
|
16
|
-
|
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/
|
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/
|
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/
|
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 "
|
9
|
-
include_context "
|
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(:
|
14
|
-
let(:user_query) {
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 "
|
131
|
-
context "
|
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
|
-
|
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
|
-
|
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/
|
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 "
|
15
|
+
RSpec.shared_context "object store setup" do
|
16
16
|
include_context "object graph setup"
|
17
17
|
|
18
|
-
let(:
|
19
|
-
Terrestrial.
|
18
|
+
let(:object_store) {
|
19
|
+
Terrestrial.object_store(mappings: mappings, datastore: datastore)
|
20
20
|
}
|
21
21
|
|
22
|
-
let(:
|
22
|
+
let(:user_store) { object_store[:users] }
|
23
23
|
|
24
24
|
let(:mappings) {
|
25
25
|
Hash[
|
@@ -2,42 +2,66 @@ require "sequel"
|
|
2
2
|
|
3
3
|
module Terrestrial
|
4
4
|
module SequelTestSupport
|
5
|
-
def
|
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.
|
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(
|
40
|
-
|
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
|
-
|
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
|