terrestrial 0.3.0 → 0.5.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 +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
|