terrestrial 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.ruby-version +1 -1
- data/Gemfile.lock +44 -53
- data/README.md +3 -6
- data/bin/test +1 -1
- data/features/env.rb +12 -2
- data/features/example.feature +23 -26
- data/lib/terrestrial.rb +31 -0
- data/lib/terrestrial/adapters/abstract_adapter.rb +6 -0
- data/lib/terrestrial/adapters/memory_adapter.rb +82 -6
- data/lib/terrestrial/adapters/sequel_postgres_adapter.rb +191 -0
- data/lib/terrestrial/configurations/conventional_association_configuration.rb +65 -35
- data/lib/terrestrial/configurations/conventional_configuration.rb +280 -124
- data/lib/terrestrial/configurations/mapping_config_options_proxy.rb +97 -0
- data/lib/terrestrial/deleted_record.rb +12 -8
- data/lib/terrestrial/dirty_map.rb +17 -9
- data/lib/terrestrial/functional_pipeline.rb +64 -0
- data/lib/terrestrial/inspection_string.rb +6 -1
- data/lib/terrestrial/lazy_object_proxy.rb +1 -0
- data/lib/terrestrial/many_to_many_association.rb +34 -20
- data/lib/terrestrial/many_to_one_association.rb +11 -3
- data/lib/terrestrial/one_to_many_association.rb +9 -0
- data/lib/terrestrial/public_conveniencies.rb +65 -82
- data/lib/terrestrial/record.rb +106 -0
- data/lib/terrestrial/relation_mapping.rb +43 -12
- data/lib/terrestrial/relational_store.rb +33 -11
- data/lib/terrestrial/upsert_record.rb +54 -0
- data/lib/terrestrial/version.rb +1 -1
- data/spec/automatic_timestamps_spec.rb +339 -0
- data/spec/changes_api_spec.rb +81 -0
- data/spec/config_override_spec.rb +28 -19
- data/spec/custom_serializers_spec.rb +3 -2
- data/spec/database_default_fields_spec.rb +213 -0
- data/spec/database_generated_id_spec.rb +291 -0
- data/spec/database_owned_fields_and_timestamps_spec.rb +200 -0
- data/spec/deletion_spec.rb +1 -1
- data/spec/error_handling/factory_error_handling_spec.rb +1 -4
- data/spec/error_handling/serialization_error_spec.rb +1 -4
- data/spec/error_handling/upsert_error_spec.rb +7 -11
- data/spec/graph_persistence_spec.rb +52 -18
- data/spec/ordered_association_spec.rb +10 -12
- data/spec/predefined_queries_spec.rb +14 -12
- data/spec/readme_examples_spec.rb +1 -1
- data/spec/sequel_query_efficiency_spec.rb +19 -16
- data/spec/spec_helper.rb +6 -1
- data/spec/support/blog_schema.rb +7 -3
- data/spec/support/object_graph_setup.rb +30 -39
- data/spec/support/object_store_setup.rb +16 -196
- data/spec/support/seed_data_setup.rb +15 -149
- data/spec/support/seed_records.rb +141 -0
- data/spec/support/sequel_test_support.rb +46 -13
- data/spec/terrestrial/abstract_record_spec.rb +138 -106
- data/spec/terrestrial/adapters/sequel_postgres_adapter_spec.rb +138 -0
- data/spec/terrestrial/deleted_record_spec.rb +0 -27
- data/spec/terrestrial/dirty_map_spec.rb +52 -77
- data/spec/terrestrial/functional_pipeline_spec.rb +153 -0
- data/spec/terrestrial/inspection_string_spec.rb +61 -0
- data/spec/terrestrial/upsert_record_spec.rb +29 -0
- data/terrestrial.gemspec +7 -8
- metadata +43 -40
- data/MissingFeatures.md +0 -64
- data/lib/terrestrial/abstract_record.rb +0 -99
- data/lib/terrestrial/association_loaders.rb +0 -52
- data/lib/terrestrial/upserted_record.rb +0 -15
- data/spec/terrestrial/public_conveniencies_spec.rb +0 -63
- data/spec/terrestrial/upserted_record_spec.rb +0 -59
@@ -0,0 +1,81 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "support/object_store_setup"
|
4
|
+
require "support/seed_data_setup"
|
5
|
+
require "support/have_persisted_matcher"
|
6
|
+
require "terrestrial"
|
7
|
+
|
8
|
+
RSpec.describe "Changes API", backend: "sequel" do
|
9
|
+
include_context "object store setup"
|
10
|
+
include_context "seed data setup"
|
11
|
+
|
12
|
+
subject(:user_store) { object_store.fetch(:users) }
|
13
|
+
|
14
|
+
let(:user) {
|
15
|
+
user_store.where(id: "users/1").first
|
16
|
+
}
|
17
|
+
|
18
|
+
describe "#changes" do
|
19
|
+
context "when there are no changes" do
|
20
|
+
it "returns an empty change set" do
|
21
|
+
expect(user_store.changes(user)).to be_empty
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "when loading and modifying only the root node" do
|
26
|
+
let(:modified_email) { "hasel+modified@gmail.com" }
|
27
|
+
|
28
|
+
it "returns changes to only that node" do
|
29
|
+
user.email = modified_email
|
30
|
+
|
31
|
+
expect(user_store.changes(user).map(&:to_h)).to eq(
|
32
|
+
[
|
33
|
+
{
|
34
|
+
id: "users/1",
|
35
|
+
email: modified_email,
|
36
|
+
}
|
37
|
+
]
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "does not persist the changes" do
|
42
|
+
user.email = modified_email
|
43
|
+
|
44
|
+
user_store.changes(user)
|
45
|
+
|
46
|
+
expect(datastore).not_to have_persisted(
|
47
|
+
:users,
|
48
|
+
hash_including(
|
49
|
+
id: user.id,
|
50
|
+
email: modified_email,
|
51
|
+
)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#changes_sql" do
|
58
|
+
context "when there are no changes" do
|
59
|
+
it "returns an empty list" do
|
60
|
+
expect(user_store.changes_sql(user)).to be_empty
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context "when loading and modifying only the root node" do
|
65
|
+
let(:modified_email) { "hasel+modified@gmail.com" }
|
66
|
+
|
67
|
+
it "returns the upsert statement for just that change" do
|
68
|
+
user.email = modified_email
|
69
|
+
|
70
|
+
expect(user_store.changes_sql(user)).to eq(
|
71
|
+
[
|
72
|
+
"INSERT INTO \"users\" (\"email\", \"id\") VALUES " \
|
73
|
+
"('hasel+modified@gmail.com', 'users/1') ON CONFLICT (\"id\") " \
|
74
|
+
"DO UPDATE SET \"email\" = 'hasel+modified@gmail.com' " \
|
75
|
+
"RETURNING *",
|
76
|
+
]
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -12,19 +12,16 @@ RSpec.describe "Configuration override" do
|
|
12
12
|
include_context "seed data setup"
|
13
13
|
|
14
14
|
let(:object_store) {
|
15
|
-
Terrestrial.object_store(
|
15
|
+
Terrestrial.object_store(config: override_config)
|
16
16
|
}
|
17
17
|
|
18
18
|
let(:override_config) {
|
19
|
-
Terrestrial::
|
19
|
+
Terrestrial::config(datastore)
|
20
20
|
.setup_mapping(:users) { |users|
|
21
21
|
users.has_many :posts, foreign_key: :author_id
|
22
22
|
users.fields([:id, :first_name, :last_name, :email])
|
23
23
|
}
|
24
|
-
|
25
|
-
|
26
|
-
let(:user) {
|
27
|
-
object_store[:users].where(id: "users/1").first
|
24
|
+
.setup_mapping(:posts)
|
28
25
|
}
|
29
26
|
|
30
27
|
context "override the root mapper factory" do
|
@@ -38,6 +35,8 @@ RSpec.describe "Configuration override" do
|
|
38
35
|
let(:user_struct) { Struct.new(*User.members) }
|
39
36
|
|
40
37
|
it "uses the class from the override" do
|
38
|
+
user = object_store[:users].where(id: "users/1").first
|
39
|
+
|
41
40
|
expect(user.class).to be(user_struct)
|
42
41
|
end
|
43
42
|
end
|
@@ -58,11 +57,10 @@ RSpec.describe "Configuration override" do
|
|
58
57
|
post_class.method(:new)
|
59
58
|
}
|
60
59
|
|
61
|
-
let(:posts) {
|
62
|
-
user.posts
|
63
|
-
}
|
64
|
-
|
65
60
|
it "uses the specified factory" do
|
61
|
+
user = object_store[:users].where(id: "users/1").first
|
62
|
+
posts = user.posts
|
63
|
+
|
66
64
|
expect(posts.first.class).to be(post_class)
|
67
65
|
end
|
68
66
|
end
|
@@ -79,12 +77,11 @@ RSpec.describe "Configuration override" do
|
|
79
77
|
end
|
80
78
|
|
81
79
|
let(:override_config) {
|
82
|
-
Terrestrial
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
end
|
80
|
+
Terrestrial.config(datastore)
|
81
|
+
.setup_mapping(:users) do |config|
|
82
|
+
config.relation_name unconventional_table_name
|
83
|
+
config.class(OpenStruct)
|
84
|
+
end
|
88
85
|
}
|
89
86
|
|
90
87
|
let(:unconventional_table_name) {
|
@@ -131,12 +128,12 @@ RSpec.describe "Configuration override" do
|
|
131
128
|
config.class(OpenStruct)
|
132
129
|
config.relation_name strange_table_name_map.fetch(:posts)
|
133
130
|
config.belongs_to(:author, mapping_name: :users)
|
134
|
-
config.has_many_through(:categories,
|
131
|
+
config.has_many_through(:categories, through_table_name: strange_table_name_map.fetch(:categories_to_posts))
|
135
132
|
end
|
136
133
|
.setup_mapping(:categories) do |config|
|
137
134
|
config.class(OpenStruct)
|
138
135
|
config.relation_name strange_table_name_map.fetch(:categories)
|
139
|
-
config.has_many_through(:posts,
|
136
|
+
config.has_many_through(:posts, through_table_name: strange_table_name_map.fetch(:categories_to_posts))
|
140
137
|
end
|
141
138
|
end
|
142
139
|
|
@@ -150,12 +147,24 @@ RSpec.describe "Configuration override" do
|
|
150
147
|
}
|
151
148
|
|
152
149
|
it "maps data from the specified relation into a has many collection" do
|
150
|
+
user = object_store[:users].where(id: "users/1").first
|
151
|
+
|
153
152
|
expect(
|
154
153
|
user.posts.map(&:id)
|
155
154
|
).to eq(["posts/1", "posts/2"])
|
156
155
|
end
|
157
156
|
|
157
|
+
it "maps data from the specified relation into a has many through collection" do
|
158
|
+
user = object_store[:users].where(id: "users/1").first
|
159
|
+
|
160
|
+
expect(
|
161
|
+
user.posts.flat_map(&:categories).map(&:id).uniq
|
162
|
+
).to eq(["categories/1", "categories/2"])
|
163
|
+
end
|
164
|
+
|
158
165
|
it "maps data from the specified relation into a `belongs_to` field" do
|
166
|
+
user = object_store[:users].where(id: "users/1").first
|
167
|
+
|
159
168
|
expect(
|
160
169
|
user.posts.first.author.__getobj__.object_id
|
161
170
|
).to eq(user.object_id)
|
@@ -168,7 +177,7 @@ RSpec.describe "Configuration override" do
|
|
168
177
|
TypeTwoUser = Class.new(OpenStruct)
|
169
178
|
|
170
179
|
let(:override_config) {
|
171
|
-
Terrestrial
|
180
|
+
Terrestrial.config(datastore)
|
172
181
|
.setup_mapping(:t1_users) { |c|
|
173
182
|
c.class(TypeOneUser)
|
174
183
|
c.table_name(:users)
|
@@ -25,8 +25,9 @@ RSpec.describe "Config override" do
|
|
25
25
|
|
26
26
|
before do
|
27
27
|
mappings
|
28
|
-
.
|
29
|
-
|
28
|
+
.setup_mapping(:users) { |users|
|
29
|
+
users.serializer(user_serializer)
|
30
|
+
}
|
30
31
|
end
|
31
32
|
|
32
33
|
context "when saving the object" do
|
@@ -0,0 +1,213 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "support/have_persisted_matcher"
|
4
|
+
require "support/object_store_setup"
|
5
|
+
require "support/seed_data_setup"
|
6
|
+
|
7
|
+
RSpec.describe "Database default fields", backend: "sequel" do
|
8
|
+
include_context "object store setup"
|
9
|
+
|
10
|
+
before(:all) do
|
11
|
+
create_db_timestamp_tables
|
12
|
+
end
|
13
|
+
|
14
|
+
after(:all) do
|
15
|
+
drop_db_timestamp_tables
|
16
|
+
end
|
17
|
+
|
18
|
+
before do
|
19
|
+
clean_db_timestamp_tables
|
20
|
+
end
|
21
|
+
|
22
|
+
let(:user_store) {
|
23
|
+
object_store[:users]
|
24
|
+
}
|
25
|
+
|
26
|
+
let(:object_store) {
|
27
|
+
Terrestrial.object_store(config: with_db_default_fields_config)
|
28
|
+
}
|
29
|
+
|
30
|
+
let(:user_with_post) {
|
31
|
+
User.new(
|
32
|
+
id: "users/1",
|
33
|
+
first_name: "Hansel",
|
34
|
+
last_name: "Trickett",
|
35
|
+
email: "hansel@tricketts.org",
|
36
|
+
posts: [post],
|
37
|
+
)
|
38
|
+
}
|
39
|
+
|
40
|
+
let(:post) {
|
41
|
+
Post.new(
|
42
|
+
id: "posts/1",
|
43
|
+
author: nil,
|
44
|
+
subject: "Biscuits",
|
45
|
+
body: "I like them",
|
46
|
+
comments: [],
|
47
|
+
categories: [],
|
48
|
+
created_at: nil,
|
49
|
+
updated_at: nil,
|
50
|
+
)
|
51
|
+
}
|
52
|
+
|
53
|
+
let(:party_time) { Time.parse("1999-01-01 00:00:00 UTC") }
|
54
|
+
|
55
|
+
let(:with_db_default_fields_config) {
|
56
|
+
Terrestrial.config(datastore)
|
57
|
+
.setup_mapping(:users) { |users|
|
58
|
+
users.has_many(:posts, foreign_key: :author_id)
|
59
|
+
}
|
60
|
+
.setup_mapping(:posts) { |posts|
|
61
|
+
posts.relation_name(:timestamped_posts)
|
62
|
+
posts.database_default_field(:created_at)
|
63
|
+
posts.database_default_field(:updated_at)
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
context "new objects" do
|
68
|
+
context "when the object's value is nil" do
|
69
|
+
before do
|
70
|
+
post.created_at = nil
|
71
|
+
end
|
72
|
+
|
73
|
+
it "updates the object with the new default value" do
|
74
|
+
expect(post).to receive(:created_at=).with(an_instance_of(Time))
|
75
|
+
|
76
|
+
user_store.save(user_with_post)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "when the object's value has been set to something" do
|
81
|
+
before do
|
82
|
+
post.created_at = party_time
|
83
|
+
end
|
84
|
+
|
85
|
+
it "does not set a value on the object" do
|
86
|
+
expect(post).not_to receive(:created_at=).with(party_time)
|
87
|
+
|
88
|
+
user_store.save(user_with_post)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "persists the user-defined value" do
|
92
|
+
user_store.save(user_with_post)
|
93
|
+
|
94
|
+
expect(datastore).to have_persisted(
|
95
|
+
:timestamped_posts,
|
96
|
+
hash_including(
|
97
|
+
id: post.id,
|
98
|
+
created_at: party_time,
|
99
|
+
)
|
100
|
+
)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context "updating existing objects" do
|
106
|
+
before do
|
107
|
+
user_store.save(user_with_post)
|
108
|
+
post.body = "new body"
|
109
|
+
end
|
110
|
+
|
111
|
+
it "regardless, updates the object with the returned value" do
|
112
|
+
expect(post).to receive(:created_at=).with(an_instance_of(Time))
|
113
|
+
|
114
|
+
user_store.save(user_with_post)
|
115
|
+
end
|
116
|
+
|
117
|
+
context "when the value changes in the database (e.g. a trigger)" do
|
118
|
+
before do
|
119
|
+
datastore[:timestamped_posts]
|
120
|
+
.where(id: post.id)
|
121
|
+
.update("created_at" => party_time)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "regardless, updates the object with the returned value" do
|
125
|
+
expect(post).to receive(:created_at=).with(party_time)
|
126
|
+
|
127
|
+
user_store.save(user_with_post)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
context "when the object's value is modified by the application" do
|
132
|
+
it "does not modify the object" do
|
133
|
+
original_time = post.created_at
|
134
|
+
post.created_at = party_time
|
135
|
+
|
136
|
+
expect(post).not_to receive(:created_at=)
|
137
|
+
|
138
|
+
user_store.save(user_with_post)
|
139
|
+
expect(post.created_at).to eq(party_time)
|
140
|
+
end
|
141
|
+
|
142
|
+
it "persists the object's new value" do
|
143
|
+
post.created_at = party_time
|
144
|
+
|
145
|
+
user_store.save(user_with_post)
|
146
|
+
|
147
|
+
expect(datastore).to have_persisted(
|
148
|
+
:timestamped_posts,
|
149
|
+
hash_including(
|
150
|
+
id: post.id,
|
151
|
+
created_at: party_time,
|
152
|
+
)
|
153
|
+
)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def schema
|
159
|
+
{
|
160
|
+
:tables => {
|
161
|
+
:timestamped_posts => [
|
162
|
+
{
|
163
|
+
:name => :id,
|
164
|
+
:type => String,
|
165
|
+
:options => {
|
166
|
+
:primary_key => true,
|
167
|
+
}
|
168
|
+
},
|
169
|
+
{
|
170
|
+
:name => :subject,
|
171
|
+
:type => String,
|
172
|
+
},
|
173
|
+
{
|
174
|
+
:name => :body,
|
175
|
+
:type => String,
|
176
|
+
},
|
177
|
+
{
|
178
|
+
:name => :author_id,
|
179
|
+
:type => String,
|
180
|
+
},
|
181
|
+
{
|
182
|
+
:name => :created_at,
|
183
|
+
:type => DateTime,
|
184
|
+
:options => {
|
185
|
+
:default => Sequel::CURRENT_TIMESTAMP,
|
186
|
+
:null => false,
|
187
|
+
},
|
188
|
+
},
|
189
|
+
{
|
190
|
+
:name => :updated_at,
|
191
|
+
:type => DateTime,
|
192
|
+
:options => {
|
193
|
+
:default => Sequel::CURRENT_TIMESTAMP,
|
194
|
+
:null => false,
|
195
|
+
},
|
196
|
+
},
|
197
|
+
],
|
198
|
+
},
|
199
|
+
}
|
200
|
+
end
|
201
|
+
|
202
|
+
def create_db_timestamp_tables
|
203
|
+
Terrestrial::SequelTestSupport.create_tables(schema.fetch(:tables))
|
204
|
+
end
|
205
|
+
|
206
|
+
def clean_db_timestamp_tables
|
207
|
+
Terrestrial::SequelTestSupport.clean_tables(schema.fetch(:tables).keys)
|
208
|
+
end
|
209
|
+
|
210
|
+
def drop_db_timestamp_tables
|
211
|
+
Terrestrial::SequelTestSupport.drop_tables(schema.fetch(:tables).keys)
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,291 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "support/have_persisted_matcher"
|
4
|
+
require "support/object_store_setup"
|
5
|
+
require "support/seed_data_setup"
|
6
|
+
|
7
|
+
RSpec.describe "Database generated IDs", backend: "sequel" do
|
8
|
+
include_context "object store setup"
|
9
|
+
|
10
|
+
before(:all) do
|
11
|
+
create_serial_id_tables
|
12
|
+
end
|
13
|
+
|
14
|
+
after(:all) do
|
15
|
+
drop_serial_id_tables
|
16
|
+
end
|
17
|
+
|
18
|
+
before(:each) do
|
19
|
+
clean_serial_id_tables
|
20
|
+
end
|
21
|
+
|
22
|
+
let(:user_store) {
|
23
|
+
object_store[:users]
|
24
|
+
}
|
25
|
+
|
26
|
+
let(:object_store) {
|
27
|
+
Terrestrial.object_store(config: serial_id_config)
|
28
|
+
}
|
29
|
+
|
30
|
+
let(:user) {
|
31
|
+
User.new(user_attrs)
|
32
|
+
}
|
33
|
+
|
34
|
+
let(:user_attrs) {
|
35
|
+
{
|
36
|
+
id: nil,
|
37
|
+
first_name: "Hansel",
|
38
|
+
last_name: "Trickett",
|
39
|
+
email: "hansel@tricketts.org",
|
40
|
+
posts: [],
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
let(:serial_id_config) {
|
45
|
+
Terrestrial.config(datastore)
|
46
|
+
.setup_mapping(:users) { |users|
|
47
|
+
users.use_database_id
|
48
|
+
users.relation_name(:serial_id_users)
|
49
|
+
users.has_many(:posts, foreign_key: :author_id)
|
50
|
+
}
|
51
|
+
.setup_mapping(:posts) { |posts|
|
52
|
+
posts.use_database_id
|
53
|
+
posts.relation_name(:serial_id_posts)
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
it "persists the root node" do
|
58
|
+
expected_sequence_id = get_next_sequence_value("serial_id_users")
|
59
|
+
|
60
|
+
user_store.save(user)
|
61
|
+
|
62
|
+
expect(datastore).to have_persisted(
|
63
|
+
:serial_id_users,
|
64
|
+
hash_including(
|
65
|
+
id: expected_sequence_id,
|
66
|
+
first_name: hansel.first_name,
|
67
|
+
last_name: hansel.last_name,
|
68
|
+
email: hansel.email,
|
69
|
+
)
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "updates the object with serial database ID" do
|
74
|
+
expected_sequence_id = get_next_sequence_value("serial_id_users")
|
75
|
+
|
76
|
+
user_store.save(user)
|
77
|
+
|
78
|
+
expect(user.id).to eq(expected_sequence_id)
|
79
|
+
end
|
80
|
+
|
81
|
+
context "when persisting two associated objects" do
|
82
|
+
before { user.posts.push(post) }
|
83
|
+
|
84
|
+
let(:post) { Post.new(post_attrs) }
|
85
|
+
|
86
|
+
let(:post_attrs) {
|
87
|
+
{
|
88
|
+
id: nil,
|
89
|
+
author: nil,
|
90
|
+
subject: "Biscuits",
|
91
|
+
body: "I like them",
|
92
|
+
comments: [],
|
93
|
+
categories: [],
|
94
|
+
created_at: Time.parse("2015-09-05T15:00:00+01:00"),
|
95
|
+
updated_at: Time.parse("2015-09-05T15:00:00+01:00"),
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
it "persists both objects" do
|
100
|
+
expected_user_sequence_id = get_next_sequence_value("serial_id_users")
|
101
|
+
expected_post_sequence_id = get_next_sequence_value("serial_id_posts")
|
102
|
+
|
103
|
+
user_store.save(user)
|
104
|
+
|
105
|
+
expect(datastore).to have_persisted(
|
106
|
+
:serial_id_users,
|
107
|
+
hash_including(
|
108
|
+
id: expected_user_sequence_id,
|
109
|
+
first_name: hansel.first_name,
|
110
|
+
last_name: hansel.last_name,
|
111
|
+
email: hansel.email,
|
112
|
+
)
|
113
|
+
)
|
114
|
+
|
115
|
+
expect(datastore).to have_persisted(
|
116
|
+
:serial_id_posts,
|
117
|
+
hash_including(
|
118
|
+
id: expected_post_sequence_id,
|
119
|
+
subject: "Biscuits",
|
120
|
+
body: "I like them",
|
121
|
+
created_at: Time.parse("2015-09-05T15:00:00+01:00"),
|
122
|
+
)
|
123
|
+
)
|
124
|
+
end
|
125
|
+
|
126
|
+
it "writes the foreign key" do
|
127
|
+
user_store.save(user)
|
128
|
+
|
129
|
+
expect(datastore[:serial_id_posts].first.fetch(:author_id)).to eq(user.id)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context "after an initial successful save of the object graph" do
|
134
|
+
before do
|
135
|
+
user_store.save(user)
|
136
|
+
end
|
137
|
+
|
138
|
+
context "when saving again without modifications" do
|
139
|
+
it "does not perform any more database writes" do
|
140
|
+
expect {
|
141
|
+
user_store.save(user)
|
142
|
+
}.not_to change { query_counter.write_count }
|
143
|
+
end
|
144
|
+
|
145
|
+
it "does not produce any change records" do
|
146
|
+
expect(user_store.changes(user)).to be_empty
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
context "when updating an existing record" do
|
152
|
+
before do
|
153
|
+
user_store.save(user)
|
154
|
+
end
|
155
|
+
|
156
|
+
it "performs an update" do
|
157
|
+
new_email = "hansel+alternate@gmail.com"
|
158
|
+
user.email = new_email
|
159
|
+
|
160
|
+
user_store.save(user)
|
161
|
+
|
162
|
+
expect(datastore).to have_persisted(
|
163
|
+
:serial_id_users,
|
164
|
+
hash_including(
|
165
|
+
id: user.id,
|
166
|
+
email: new_email,
|
167
|
+
)
|
168
|
+
)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context "when the user id must be set by an unconventional method" do
|
173
|
+
before do
|
174
|
+
change_objects_id_setter_method(user)
|
175
|
+
end
|
176
|
+
|
177
|
+
let(:serial_id_config) {
|
178
|
+
Terrestrial.config(datastore)
|
179
|
+
.setup_mapping(:users) { |users|
|
180
|
+
users.use_database_id { |object, new_id| object.unusual_id_setter(new_id) }
|
181
|
+
users.relation_name(:serial_id_users)
|
182
|
+
}
|
183
|
+
}
|
184
|
+
|
185
|
+
it "calls the user-defined config block which should update the ID" do
|
186
|
+
next_id = get_next_sequence_value(:serial_id_users)
|
187
|
+
expect(user).to receive(:unusual_id_setter).with(next_id)
|
188
|
+
expect(user).not_to receive(:id=)
|
189
|
+
|
190
|
+
user_store.save(user)
|
191
|
+
end
|
192
|
+
|
193
|
+
def change_objects_id_setter_method(user)
|
194
|
+
def user.id=(*args)
|
195
|
+
raise "This method should not be called"
|
196
|
+
end
|
197
|
+
|
198
|
+
def user.unusual_id_setter(value)
|
199
|
+
@id = value
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def serial_id_schema
|
205
|
+
{
|
206
|
+
:tables => {
|
207
|
+
:serial_id_users => [
|
208
|
+
{
|
209
|
+
:name => :id,
|
210
|
+
:type => Integer,
|
211
|
+
:options => {
|
212
|
+
:primary_key => true,
|
213
|
+
:serial => true,
|
214
|
+
},
|
215
|
+
},
|
216
|
+
{
|
217
|
+
:name => :first_name,
|
218
|
+
:type => String,
|
219
|
+
},
|
220
|
+
{
|
221
|
+
:name => :last_name,
|
222
|
+
:type => String,
|
223
|
+
},
|
224
|
+
{
|
225
|
+
:name => :email,
|
226
|
+
:type => String,
|
227
|
+
},
|
228
|
+
],
|
229
|
+
:serial_id_posts => [
|
230
|
+
{
|
231
|
+
:name => :id,
|
232
|
+
:type => Integer,
|
233
|
+
:options => {
|
234
|
+
:primary_key => true,
|
235
|
+
:serial => true,
|
236
|
+
},
|
237
|
+
},
|
238
|
+
{
|
239
|
+
:name => :subject,
|
240
|
+
:type => String,
|
241
|
+
},
|
242
|
+
{
|
243
|
+
:name => :body,
|
244
|
+
:type => String,
|
245
|
+
},
|
246
|
+
{
|
247
|
+
:name => :author_id,
|
248
|
+
:type => Integer,
|
249
|
+
},
|
250
|
+
{
|
251
|
+
:name => :created_at,
|
252
|
+
:type => DateTime,
|
253
|
+
},
|
254
|
+
],
|
255
|
+
},
|
256
|
+
:foreign_keys => [
|
257
|
+
[
|
258
|
+
:posts,
|
259
|
+
:author_id,
|
260
|
+
:users,
|
261
|
+
:id,
|
262
|
+
],
|
263
|
+
],
|
264
|
+
}
|
265
|
+
end
|
266
|
+
|
267
|
+
def create_serial_id_tables
|
268
|
+
Terrestrial::SequelTestSupport.create_tables(serial_id_schema.fetch(:tables))
|
269
|
+
end
|
270
|
+
|
271
|
+
def drop_serial_id_tables
|
272
|
+
Terrestrial::SequelTestSupport.drop_tables(serial_id_schema.fetch(:tables).keys)
|
273
|
+
end
|
274
|
+
|
275
|
+
def clean_serial_id_tables
|
276
|
+
Terrestrial::SequelTestSupport.clean_tables(serial_id_schema.fetch(:tables).keys)
|
277
|
+
end
|
278
|
+
|
279
|
+
def get_next_sequence_value(table_name)
|
280
|
+
datastore["select currval(pg_get_serial_sequence('#{table_name}', 'id'))"]
|
281
|
+
.to_a
|
282
|
+
.fetch(0)
|
283
|
+
.fetch(:currval) + 1
|
284
|
+
rescue Sequel::DatabaseError => e
|
285
|
+
if /PG::ObjectNotInPrerequisiteState/.match?(e.message)
|
286
|
+
1
|
287
|
+
else
|
288
|
+
raise e
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|