sequel_mapper 0.0.1

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.
@@ -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