sequel_mapper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,29 @@
1
+ require "spec_helper"
2
+
3
+ require "sequel_mapper"
4
+ require "support/graph_fixture"
5
+
6
+ RSpec.describe "Ordered associations" do
7
+ include SequelMapper::GraphFixture
8
+
9
+ context "of type `has_many`" do
10
+ subject(:graph) {
11
+ SequelMapper::Graph.new(
12
+ top_level_namespace: :users,
13
+ datastore: datastore,
14
+ relation_mappings: relation_mappings,
15
+ )
16
+ }
17
+
18
+ let(:user) {
19
+ graph.where(id: "user/1").first
20
+ }
21
+
22
+ it "enumerates the objects in order specified in the config" do
23
+ user.toots.to_a
24
+
25
+ expect(user.toots.map(&:id).to_a)
26
+ .to eq(user.toots.to_a.sort_by { |t| t.tooted_at }.map(&:id).reverse)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,82 @@
1
+ require "spec_helper"
2
+
3
+ require "sequel_mapper"
4
+ require "support/graph_fixture"
5
+
6
+ RSpec.describe "Proxying associations" do
7
+ include SequelMapper::GraphFixture
8
+
9
+ context "of type `has_many`" do
10
+ subject(:graph) {
11
+ SequelMapper::Graph.new(
12
+ top_level_namespace: :users,
13
+ datastore: datastore,
14
+ relation_mappings: relation_mappings,
15
+ )
16
+ }
17
+
18
+ let(:user) {
19
+ graph.where(id: "user/1").first
20
+ }
21
+
22
+ let(:posts) { user.posts }
23
+
24
+ def identity
25
+ ->(x){x}
26
+ end
27
+
28
+ describe "limiting datastore reads" do
29
+ context "when loading the root node" do
30
+ it "only performs one read" do
31
+ user
32
+
33
+ expect(query_counter.read_count).to eq(1)
34
+ end
35
+ end
36
+
37
+ context "when getting a reference to an association proxy" do
38
+ before { user }
39
+
40
+ it "does no additional reads" do
41
+ expect{
42
+ user.posts
43
+ }.to change { query_counter.read_count }.by(0)
44
+ end
45
+ end
46
+
47
+ context "when iteratiing over a has many association" do
48
+ before { posts }
49
+
50
+ it "does a single additional read for the assocation collection" do
51
+ expect {
52
+ user.posts.map(&identity)
53
+ }.to change { query_counter.read_count }.by(1)
54
+ end
55
+ end
56
+
57
+ context "when getting a reference to a many to many assocation" do
58
+ before { post }
59
+
60
+ let(:post) { user.posts.first }
61
+
62
+ it "does no additional reads" do
63
+ expect {
64
+ post.categories
65
+ }.to change { query_counter.read_count }.by(0)
66
+ end
67
+ end
68
+
69
+ context "when iterating over a many to many assocation" do
70
+ let(:category_count) { 3 }
71
+
72
+ it "does 1 read" do
73
+ post = user.posts.first
74
+
75
+ expect {
76
+ post.categories.map(&:name).to_a
77
+ }.to change { query_counter.read_count }.by(1)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,51 @@
1
+ require "spec_helper"
2
+
3
+ require "sequel_mapper"
4
+ require "support/graph_fixture"
5
+
6
+ RSpec.describe "Querying" do
7
+ include SequelMapper::GraphFixture
8
+
9
+ subject(:graph) {
10
+ SequelMapper::Graph.new(
11
+ top_level_namespace: :users,
12
+ datastore: datastore,
13
+ relation_mappings: relation_mappings,
14
+ )
15
+ }
16
+
17
+ let(:user) {
18
+ graph.where(id: "user/1").first
19
+ }
20
+
21
+ let(:query_criteria) {
22
+ {
23
+ body: "Lazy load all the things!",
24
+ }
25
+ }
26
+
27
+ describe "arbitrary where query" do
28
+ it "returns a filtered version of the association" do
29
+ expect(
30
+ user.posts
31
+ .where(query_criteria)
32
+ .map(&:id)
33
+ ).to eq(["post/2"])
34
+ end
35
+
36
+ it "sends the query directly to the datastore" do
37
+ expect {
38
+ user.posts
39
+ .where(query_criteria)
40
+ .map(&:id)
41
+ }.to change { query_counter.read_count }.by(2)
42
+
43
+ # TODO: this is a quick hack to assert that no superfluous records where
44
+ # loaded. Figure out a better way to check efficiency
45
+ expect(graph.send(:identity_map).values.map(&:id)).to match_array([
46
+ "user/1",
47
+ "post/2",
48
+ ])
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,95 @@
1
+ require "spec_helper"
2
+
3
+ require "sequel_mapper/association_proxy"
4
+
5
+ RSpec.describe SequelMapper::AssociationProxy do
6
+ let(:proxy) {
7
+ SequelMapper::AssociationProxy.new(lazy_enum)
8
+ }
9
+
10
+ let(:lazy_enum) { data_set.each.lazy }
11
+ let(:data_set) { (0..9) }
12
+
13
+ def id
14
+ ->(x) { x }
15
+ end
16
+
17
+ it "is Enumerable" do
18
+ expect(proxy).to be_a(Enumerable)
19
+ end
20
+
21
+ describe "#to_a" do
22
+ it "is equivalent to the original enumeration" do
23
+ expect(proxy.map(&id)).to eq(data_set.to_a)
24
+ end
25
+ end
26
+
27
+ describe "#each" do
28
+ context "when called with a block" do
29
+ it "returns self" do
30
+ expect(proxy.each(&id)).to eq(proxy)
31
+ end
32
+
33
+ it "yields each element to the block" do
34
+ yielded = []
35
+
36
+ proxy.each do |element|
37
+ yielded.push(element)
38
+ end
39
+
40
+ expect(yielded).to eq(data_set.to_a)
41
+ end
42
+
43
+ context "when calling each more than once" do
44
+ before do
45
+ proxy.each { |x| nil }
46
+ proxy.each { |x| nil }
47
+ end
48
+
49
+ it "rewinds the enumeration on each call" do
50
+ expect(proxy.map(&id)).to eq(data_set.to_a)
51
+ end
52
+ end
53
+ end
54
+
55
+ context "when called without a block" do
56
+ it "returns an enumerator" do
57
+ expect(proxy.each).to be_a(Enumerator)
58
+ end
59
+ end
60
+ end
61
+
62
+ describe "#remove" do
63
+ it "returns self" do
64
+ expect(proxy.remove(3)).to be(proxy)
65
+ end
66
+
67
+ context "after removing a element from the enumeration" do
68
+ before do
69
+ proxy.remove(3)
70
+ end
71
+
72
+ it "skips that element in the enumeration" do
73
+ expect(proxy.map(&id)).to eq([0,1,2,4,5,6,7,8,9])
74
+ end
75
+ end
76
+ end
77
+
78
+ describe "#push" do
79
+ context "after pushing another element into the enumeration" do
80
+ before do
81
+ proxy.push(new_value)
82
+ end
83
+
84
+ let(:new_value) { double(:new_value) }
85
+
86
+ it "does not alter the other elements" do
87
+ expect(proxy.map(&id)[0..-2]).to eq([0,1,2,3,4,5,6,7,8,9])
88
+ end
89
+
90
+ it "appends the element to the enumeration" do
91
+ expect(proxy.map(&id).last).to eq(new_value)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,65 @@
1
+ require "spec_helper"
2
+
3
+ require "sequel_mapper/belongs_to_association_proxy"
4
+
5
+ RSpec.describe BelongsToAssociationProxy do
6
+ subject(:proxy) { BelongsToAssociationProxy.new(object_loader) }
7
+
8
+ let(:object_loader) { double(:object_loader, call: proxied_object) }
9
+ let(:proxied_object) { double(:proxied_object, name: name) }
10
+ let(:name) { double(:name) }
11
+
12
+ describe "#__getobj__" do
13
+ it "loads the object" do
14
+ proxy.__getobj__
15
+
16
+ expect(object_loader).to have_received(:call)
17
+ end
18
+
19
+ it "returns the proxied object" do
20
+ expect(proxy.__getobj__).to be(proxied_object)
21
+ end
22
+ end
23
+
24
+ context "when no method is called on it" do
25
+ it "does not call the loader" do
26
+ proxy
27
+
28
+ expect(object_loader).not_to have_received(:call)
29
+ end
30
+ end
31
+
32
+ context "when a missing method is called on the proxy" do
33
+ it "is a true decorator" do
34
+ expect(proxied_object).to receive(:arbitrary_message)
35
+
36
+ proxy.arbitrary_message
37
+ end
38
+
39
+ it "loads the object" do
40
+ proxy.name
41
+
42
+ expect(object_loader).to have_received(:call)
43
+ end
44
+
45
+ it "returns delegates the message to the object" do
46
+ args = [ double, double ]
47
+ proxy.name(*args)
48
+
49
+ expect(proxied_object).to have_received(:name).with(*args)
50
+ end
51
+
52
+ it "returns the objects return value" do
53
+ expect(proxy.name).to eq(name)
54
+ end
55
+
56
+ context "when calling a method twice" do
57
+ it "loads the object once" do
58
+ proxy.name
59
+ proxy.name
60
+
61
+ expect(object_loader).to have_received(:call)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,39 @@
1
+ require "pry"
2
+ require "sequel"
3
+ require "logger"
4
+
5
+ `psql postgres --command "CREATE DATABASE $PGDATABASE;"`
6
+
7
+ DB = Sequel.postgres(
8
+ host: ENV.fetch("PGHOST"),
9
+ user: ENV.fetch("PGUSER"),
10
+ database: ENV.fetch("PGDATABASE"),
11
+ )
12
+
13
+ RSpec.configure do |config|
14
+ config.expect_with :rspec do |expectations|
15
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
16
+ end
17
+
18
+ config.mock_with :rspec do |mocks|
19
+ mocks.verify_partial_doubles = true
20
+ end
21
+
22
+ config.filter_run :focus
23
+ config.run_all_when_everything_filtered = true
24
+
25
+ config.disable_monkey_patching!
26
+
27
+ # TODO: get everything running without warnings
28
+ config.warnings = false
29
+
30
+ if config.files_to_run.one?
31
+ config.default_formatter = 'doc'
32
+ end
33
+
34
+ # config.profile_examples = 10
35
+
36
+ # config.order = :random
37
+
38
+ # Kernel.srand config.seed
39
+ end
@@ -0,0 +1,331 @@
1
+ require "support/mock_sequel"
2
+ require "sequel_mapper/struct_factory"
3
+ require "support/query_counter"
4
+
5
+ module SequelMapper
6
+ module GraphFixture
7
+
8
+ # A little hack so these let blocks from an RSpec example don't have
9
+ # to change
10
+ def self.let(name, &block)
11
+ define_method(name) {
12
+ instance_variable_get("@#{name}") or
13
+ instance_variable_set("@#{name}", instance_eval(&block))
14
+ }
15
+ end
16
+
17
+ User = Struct.new(:id, :first_name, :last_name, :email, :posts, :toots)
18
+ Post = Struct.new(:id, :author, :subject, :body, :comments, :categories)
19
+ Comment = Struct.new(:id, :post, :commenter, :body)
20
+ Category = Struct.new(:id, :name, :posts)
21
+ Toot = Struct.new(:id, :tooter, :body, :tooted_at)
22
+
23
+ let(:query_counter) {
24
+ QueryCounter.new
25
+ }
26
+
27
+ let(:datastore) {
28
+ DB.tap { |db|
29
+ load_fixture_data(db)
30
+ db.loggers << query_counter
31
+ }
32
+ }
33
+
34
+ def load_fixture_data(datastore)
35
+ tables.each do |table, rows|
36
+
37
+ datastore.drop_table?(table)
38
+
39
+ datastore.create_table(table) do
40
+ rows.first.keys.each do |column|
41
+ String column
42
+ end
43
+ end
44
+
45
+ rows.each do |row|
46
+ datastore[table].insert(row)
47
+ end
48
+ end
49
+ end
50
+
51
+ let(:tables) {
52
+ {
53
+ users: [
54
+ user_1_data,
55
+ user_2_data,
56
+ user_3_data,
57
+ ],
58
+ posts: [
59
+ post_1_data,
60
+ post_2_data,
61
+ ],
62
+ comments: [
63
+ comment_1_data,
64
+ comment_2_data,
65
+ ],
66
+ categories: [
67
+ category_1_data,
68
+ category_2_data,
69
+ ],
70
+ categories_to_posts: [
71
+ {
72
+ post_id: post_1_data.fetch(:id),
73
+ category_id: category_1_data.fetch(:id),
74
+ },
75
+ {
76
+ post_id: post_1_data.fetch(:id),
77
+ category_id: category_2_data.fetch(:id),
78
+ },
79
+ {
80
+ post_id: post_2_data.fetch(:id),
81
+ category_id: category_2_data.fetch(:id),
82
+ },
83
+ ],
84
+ toots: [
85
+ # Toot ordering is inconsistent for scope testing.
86
+ toot_2_data,
87
+ toot_1_data,
88
+ toot_3_data,
89
+ ],
90
+ }
91
+ }
92
+
93
+ let(:relation_mappings) {
94
+ {
95
+ users: {
96
+ columns: [
97
+ :id,
98
+ :first_name,
99
+ :last_name,
100
+ :email,
101
+ ],
102
+ factory: user_factory,
103
+ has_many: {
104
+ posts: {
105
+ relation_name: :posts,
106
+ foreign_key: :author_id,
107
+ },
108
+ toots: {
109
+ relation_name: :toots,
110
+ foreign_key: :tooter_id,
111
+ order_by: {
112
+ columns: [:tooted_at],
113
+ direction: :desc,
114
+ },
115
+ },
116
+ },
117
+ # TODO: maybe combine associations like this
118
+ # has_many_through: {
119
+ # categories_posted_in: {
120
+ # through_association: [ :posts, :categories ]
121
+ # }
122
+ # }
123
+ },
124
+ posts: {
125
+ columns: [
126
+ :id,
127
+ :author_id,
128
+ :subject,
129
+ :body,
130
+ ],
131
+ factory: post_factory,
132
+ has_many: {
133
+ comments: {
134
+ relation_name: :comments,
135
+ foreign_key: :post_id,
136
+ },
137
+ },
138
+ has_many_through: {
139
+ categories: {
140
+ through_relation_name: :categories_to_posts,
141
+ relation_name: :categories,
142
+ foreign_key: :post_id,
143
+ association_foreign_key: :category_id,
144
+ }
145
+ },
146
+ belongs_to: {
147
+ author: {
148
+ relation_name: :users,
149
+ foreign_key: :author_id,
150
+ },
151
+ },
152
+ },
153
+ comments: {
154
+ columns: [
155
+ :id,
156
+ :post_id,
157
+ :commenter_id,
158
+ :body,
159
+ ],
160
+ factory: comment_factory,
161
+ belongs_to: {
162
+ commenter: {
163
+ relation_name: :users,
164
+ foreign_key: :commenter_id,
165
+ },
166
+ },
167
+ },
168
+ categories: {
169
+ columns: [
170
+ :id,
171
+ :name,
172
+ ],
173
+ factory: category_factory,
174
+ has_many_through: {
175
+ posts: {
176
+ through_relation_name: :categories_to_posts,
177
+ relation_name: :posts,
178
+ foreign_key: :category_id,
179
+ association_foreign_key: :post_id,
180
+ }
181
+ },
182
+ },
183
+ toots: {
184
+ columns: [
185
+ :id,
186
+ :tooter_id,
187
+ :body,
188
+ :tooted_at,
189
+ ],
190
+ factory: toot_factory,
191
+ belongs_to: {
192
+ tooter: {
193
+ relation_name: :users,
194
+ foreign_key: :tooter_id,
195
+ },
196
+ },
197
+ },
198
+ }
199
+ }
200
+
201
+ let(:user_factory){
202
+ SequelMapper::StructFactory.new(User)
203
+ }
204
+
205
+ let(:post_factory){
206
+ SequelMapper::StructFactory.new(Post)
207
+ }
208
+
209
+ let(:comment_factory){
210
+ SequelMapper::StructFactory.new(Comment)
211
+ }
212
+
213
+ let(:category_factory){
214
+ SequelMapper::StructFactory.new(Category)
215
+ }
216
+
217
+ let(:toot_factory){
218
+ SequelMapper::StructFactory.new(Toot)
219
+ }
220
+
221
+ let(:user_1_data) {
222
+ {
223
+ id: "user/1",
224
+ first_name: "Stephen",
225
+ last_name: "Best",
226
+ email: "bestie@gmail.com",
227
+ }
228
+ }
229
+
230
+ let(:user_2_data) {
231
+ {
232
+ id: "user/2",
233
+ first_name: "Hansel",
234
+ last_name: "Trickett",
235
+ email: "hansel@gmail.com",
236
+ }
237
+ }
238
+
239
+ let(:user_3_data) {
240
+ {
241
+ id: "user/3",
242
+ first_name: "Jasper",
243
+ last_name: "Trickett",
244
+ email: "jasper@gmail.com",
245
+ }
246
+ }
247
+
248
+ let(:post_1_data) {
249
+ {
250
+ id: "post/1",
251
+ author_id: "user/1",
252
+ subject: "Object mapping",
253
+ body: "It is often tricky",
254
+ }
255
+ }
256
+
257
+ let(:post_2_data) {
258
+ {
259
+ id: "post/2",
260
+ author_id: "user/1",
261
+ subject: "Object mapping part 2",
262
+ body: "Lazy load all the things!",
263
+ }
264
+ }
265
+
266
+ let(:comment_1_data) {
267
+ {
268
+ id: "comment/1",
269
+ post_id: "post/1",
270
+ commenter_id: "user/2",
271
+ body: "Trololol",
272
+ }
273
+ }
274
+
275
+ let(:comment_2_data) {
276
+ {
277
+ id: "comment/2",
278
+ post_id: "post/1",
279
+ commenter_id: "user/1",
280
+ body: "You are so LOL",
281
+ }
282
+ }
283
+
284
+ let(:category_1_data) {
285
+ {
286
+ id: "category/1",
287
+ name: "good",
288
+ }
289
+ }
290
+
291
+ let(:category_2_data) {
292
+ {
293
+ id: "category/2",
294
+ name: "bad",
295
+ }
296
+ }
297
+
298
+ let(:category_3_data) {
299
+ {
300
+ id: "category/3",
301
+ name: "ugly",
302
+ }
303
+ }
304
+
305
+ let(:toot_1_data) {
306
+ {
307
+ id: "toot/1",
308
+ tooter_id: "user/1",
309
+ body: "Armistice toots",
310
+ tooted_at: Time.parse("2014-11-11 11:11:00 UTC").iso8601,
311
+ }
312
+ }
313
+ let(:toot_2_data) {
314
+ {
315
+ id: "toot/2",
316
+ tooter_id: "user/1",
317
+ body: "Tooting every second",
318
+ tooted_at: Time.parse("2014-11-11 11:11:01 UTC").iso8601,
319
+ }
320
+ }
321
+
322
+ let(:toot_3_data) {
323
+ {
324
+ id: "toot/3",
325
+ tooter_id: "user/1",
326
+ body: "Join me in a minutes' toots",
327
+ tooted_at: Time.parse("2014-11-11 11:11:02 UTC").iso8601,
328
+ }
329
+ }
330
+ end
331
+ end