sequel_mapper 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CODE_OF_CONDUCT.md +28 -0
  4. data/Gemfile.lock +32 -2
  5. data/MissingFeatures.md +64 -0
  6. data/README.md +141 -72
  7. data/Rakefile +29 -0
  8. data/TODO.md +16 -11
  9. data/features/env.rb +57 -0
  10. data/features/example.feature +121 -0
  11. data/features/step_definitions/example_steps.rb +46 -0
  12. data/lib/sequel_mapper.rb +6 -2
  13. data/lib/sequel_mapper/abstract_record.rb +53 -0
  14. data/lib/sequel_mapper/association_loaders.rb +52 -0
  15. data/lib/sequel_mapper/collection_mutability_proxy.rb +77 -0
  16. data/lib/sequel_mapper/configurations/conventional_association_configuration.rb +187 -0
  17. data/lib/sequel_mapper/configurations/conventional_configuration.rb +269 -0
  18. data/lib/sequel_mapper/dataset.rb +37 -0
  19. data/lib/sequel_mapper/deleted_record.rb +16 -0
  20. data/lib/sequel_mapper/dirty_map.rb +31 -0
  21. data/lib/sequel_mapper/graph_loader.rb +48 -0
  22. data/lib/sequel_mapper/graph_serializer.rb +107 -0
  23. data/lib/sequel_mapper/identity_map.rb +22 -0
  24. data/lib/sequel_mapper/lazy_object_proxy.rb +51 -0
  25. data/lib/sequel_mapper/many_to_many_association.rb +181 -0
  26. data/lib/sequel_mapper/many_to_one_association.rb +60 -0
  27. data/lib/sequel_mapper/mapper_facade.rb +180 -0
  28. data/lib/sequel_mapper/one_to_many_association.rb +51 -0
  29. data/lib/sequel_mapper/public_conveniencies.rb +27 -0
  30. data/lib/sequel_mapper/query_order.rb +32 -0
  31. data/lib/sequel_mapper/queryable_lazy_dataset_loader.rb +70 -0
  32. data/lib/sequel_mapper/relation_mapping.rb +35 -0
  33. data/lib/sequel_mapper/serializer.rb +18 -0
  34. data/lib/sequel_mapper/short_inspection_string.rb +18 -0
  35. data/lib/sequel_mapper/subset_queries_proxy.rb +11 -0
  36. data/lib/sequel_mapper/upserted_record.rb +15 -0
  37. data/lib/sequel_mapper/version.rb +1 -1
  38. data/sequel_mapper.gemspec +3 -0
  39. data/spec/config_override_spec.rb +167 -0
  40. data/spec/custom_serializers_spec.rb +77 -0
  41. data/spec/deletion_spec.rb +104 -0
  42. data/spec/graph_persistence_spec.rb +83 -88
  43. data/spec/graph_traversal_spec.rb +32 -31
  44. data/spec/new_graph_persistence_spec.rb +69 -0
  45. data/spec/object_identity_spec.rb +70 -0
  46. data/spec/ordered_association_spec.rb +46 -16
  47. data/spec/persistence_efficiency_spec.rb +186 -0
  48. data/spec/predefined_queries_spec.rb +73 -0
  49. data/spec/proxying_spec.rb +25 -19
  50. data/spec/querying_spec.rb +24 -27
  51. data/spec/readme_examples_spec.rb +35 -0
  52. data/spec/sequel_mapper/abstract_record_spec.rb +179 -0
  53. data/spec/sequel_mapper/{association_proxy_spec.rb → collection_mutability_proxy_spec.rb} +6 -6
  54. data/spec/sequel_mapper/deleted_record_spec.rb +59 -0
  55. data/spec/sequel_mapper/lazy_object_proxy_spec.rb +140 -0
  56. data/spec/sequel_mapper/public_conveniencies_spec.rb +49 -0
  57. data/spec/sequel_mapper/queryable_lazy_dataset_loader_spec.rb +103 -0
  58. data/spec/sequel_mapper/upserted_record_spec.rb +59 -0
  59. data/spec/spec_helper.rb +7 -10
  60. data/spec/support/blog_schema.rb +29 -0
  61. data/spec/support/have_persisted_matcher.rb +19 -0
  62. data/spec/support/mapper_setup.rb +234 -0
  63. data/spec/support/mock_sequel.rb +0 -1
  64. data/spec/support/object_graph_setup.rb +106 -0
  65. data/spec/support/seed_data_setup.rb +122 -0
  66. data/spec/support/sequel_persistence_setup.rb +19 -0
  67. data/spec/support/sequel_test_support.rb +159 -0
  68. metadata +121 -15
  69. data/lib/sequel_mapper/association_proxy.rb +0 -54
  70. data/lib/sequel_mapper/belongs_to_association_proxy.rb +0 -27
  71. data/lib/sequel_mapper/graph.rb +0 -174
  72. data/lib/sequel_mapper/queryable_association_proxy.rb +0 -23
  73. data/spec/sequel_mapper/belongs_to_association_proxy_spec.rb +0 -65
  74. data/spec/support/graph_fixture.rb +0 -331
  75. data/spec/support/query_counter.rb +0 -29
@@ -1,45 +1,46 @@
1
1
  require "spec_helper"
2
2
 
3
+ require "support/mapper_setup"
4
+ require "support/sequel_persistence_setup"
5
+ require "support/seed_data_setup"
3
6
  require "sequel_mapper"
4
- require "support/graph_fixture"
5
7
 
6
8
  RSpec.describe "Graph traversal" do
7
- include SequelMapper::GraphFixture
8
-
9
- describe "assocaitions" do
10
- subject(:graph) {
11
- SequelMapper::Graph.new(
12
- top_level_namespace: :users,
13
- datastore: datastore,
14
- relation_mappings: relation_mappings,
15
- )
16
- }
9
+ include_context "mapper setup"
10
+ include_context "sequel persistence setup"
11
+ include_context "seed data setup"
12
+
13
+ describe "associations" do
14
+ subject(:mapper) { user_mapper }
17
15
 
18
16
  let(:user_query) {
19
- graph.where(id: "user/1")
17
+ mapper.where(id: "users/1")
20
18
  }
21
19
 
20
+ let(:user) { user_query.first }
21
+
22
22
  it "finds data via the storage adapter" do
23
- expect(user_query.count).to be 1
23
+ expect(user_query.count).to eq(1)
24
24
  end
25
25
 
26
26
  it "maps the raw data from the store into domain objects" do
27
- expect(user_query.first.id).to eq("user/1")
28
- expect(user_query.first.first_name).to eq("Stephen")
27
+ expect(user_query.first.id).to eq("users/1")
28
+ expect(user_query.first.first_name).to eq("Hansel")
29
29
  end
30
30
 
31
31
  it "handles has_many associations" do
32
- expect(user_query.first.posts.first.subject)
33
- .to eq("Object mapping")
32
+ post = user.posts.first
33
+
34
+ expect(post.subject).to eq("Biscuits")
34
35
  end
35
36
 
36
37
  it "handles nested has_many associations" do
37
38
  expect(
38
- user_query.first
39
+ user
39
40
  .posts.first
40
41
  .comments.first
41
42
  .body
42
- ).to eq("Trololol")
43
+ ).to eq("oh noes")
43
44
  end
44
45
 
45
46
  describe "lazy loading" do
@@ -53,33 +54,33 @@ RSpec.describe "Graph traversal" do
53
54
  end
54
55
 
55
56
  it "maps belongs to assocations" do
56
- expect(user_query.first.posts.first.author.id)
57
- .to eq("user/1")
57
+ post = user.posts.first
58
+ comment = post.comments.first
59
+
60
+ expect(comment.commenter.id).to eq("users/1")
58
61
  end
59
62
 
60
63
  describe "identity map" do
61
64
  it "always returns (a proxy of) the same object for a given id" do
62
- expect(user_query.first.posts.first.author.__getobj__)
63
- .to be(user_query.first)
65
+ post = user.posts.first
66
+ comment = post.comments.first
67
+
68
+ expect(comment.commenter.__getobj__)
69
+ .to be(user)
64
70
  end
65
71
  end
66
72
 
67
73
  it "maps deeply nested belongs to assocations" do
68
74
  expect(user_query.first.posts.first.comments.first.commenter.id)
69
- .to eq("user/2")
75
+ .to eq("users/1")
70
76
  end
71
77
 
72
78
  it "maps has many to many associations as has many through" do
73
79
  expect(user_query.first.posts.first.categories.map(&:id))
74
- .to match_array(["category/1", "category/2"])
80
+ .to match_array(["categories/1", "categories/2"])
75
81
 
76
82
  expect(user_query.first.posts.first.categories.to_a.last.posts.map(&:id))
77
- .to match_array(["post/1", "post/2"])
78
- end
79
-
80
- xit "combines has many through associations" do
81
- expect(user_query.first.categories_posted_in.map(&:id))
82
- .to match_array(["category/1", "category/2"])
83
+ .to match_array(["posts/1", "posts/2"])
83
84
  end
84
85
  end
85
86
  end
@@ -0,0 +1,69 @@
1
+ require "spec_helper"
2
+ require "support/mapper_setup"
3
+ require "support/sequel_persistence_setup"
4
+ require "support/have_persisted_matcher"
5
+
6
+ RSpec.describe "Persist a new graph in empty datastore" do
7
+ include_context "mapper setup"
8
+ include_context "sequel persistence setup"
9
+
10
+ context "given a graph of new objects" do
11
+ it "persists the root node" do
12
+ user_mapper.save(hansel)
13
+
14
+ expect(datastore).to have_persisted(:users, {
15
+ id: hansel.id,
16
+ first_name: hansel.first_name,
17
+ last_name: hansel.last_name,
18
+ email: hansel.email,
19
+ })
20
+ end
21
+
22
+ it "persists one to many related nodes 1 level deep" do
23
+ user_mapper.save(hansel)
24
+
25
+ expect(datastore).to have_persisted(:posts, hash_including(
26
+ id: "posts/1",
27
+ subject: "Biscuits",
28
+ body: "I like them",
29
+ author_id: "users/1",
30
+ ))
31
+
32
+ expect(datastore).to have_persisted(:posts, hash_including(
33
+ id: "posts/2",
34
+ subject: "Sleeping",
35
+ body: "I do it three times purrr day",
36
+ author_id: "users/1",
37
+ ))
38
+ end
39
+
40
+ it "persists one to many related nodes 2 levels deep" do
41
+ user_mapper.save(hansel)
42
+
43
+ expect(datastore).to have_persisted(:comments, {
44
+ id: "comments/1",
45
+ body: "oh noes",
46
+ post_id: "posts/1",
47
+ commenter_id: "users/1",
48
+ })
49
+ end
50
+
51
+ it "persists many to many related nodes" do
52
+ user_mapper.save(hansel)
53
+
54
+ expect(datastore).to have_persisted(:categories, {
55
+ id: "categories/1",
56
+ name: "Cat biscuits",
57
+ })
58
+ end
59
+
60
+ it "persists a 'join table' to faciliate many to many" do
61
+ user_mapper.save(hansel)
62
+
63
+ expect(datastore).to have_persisted(:categories_to_posts, {
64
+ category_id: "categories/1",
65
+ post_id: "posts/1",
66
+ })
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,70 @@
1
+ require "spec_helper"
2
+
3
+ require "support/mapper_setup"
4
+ require "support/sequel_persistence_setup"
5
+ require "support/seed_data_setup"
6
+ require "sequel_mapper"
7
+
8
+ RSpec.describe "Object identity" do
9
+ include_context "mapper setup"
10
+ include_context "sequel persistence setup"
11
+ include_context "seed data setup"
12
+
13
+ subject(:mapper) { mappers.fetch(:users) }
14
+
15
+ let(:user) { mapper.where(id: "users/1").first }
16
+ let(:post) { user.posts.first }
17
+
18
+ context "when using arbitrary where query" do
19
+ it "returns the same object for a row's primary key" do
20
+ expect(
21
+ user.posts.where(id: post.id).first
22
+ ).to be(post)
23
+ end
24
+ end
25
+
26
+ context "when traversing deep into the graph" do
27
+ context "via has many through" do
28
+ it "returns the same object for a row's primary key" do
29
+ expect(
30
+ user.posts.first.categories.first.posts
31
+ .find { |cat_post| cat_post.id == post.id }
32
+ ).to be(post)
33
+ end
34
+ end
35
+
36
+ context "via a belongs to" do
37
+ it "returns the same object for a row's primary once loaded" do
38
+ # TODO: Add another method to avoid using #__getobj__
39
+ expect(
40
+ user.posts.first.comments
41
+ .find { |comment| comment.commenter.id == user.id }
42
+ .commenter
43
+ .__getobj__
44
+ ).to be(user)
45
+ end
46
+ end
47
+
48
+ context "when eager loading" do
49
+ let(:user_query) { mapper.where(id: "users/1") }
50
+
51
+ let(:eager_category) {
52
+ user_query
53
+ .eager_load(:posts => { :categories => { :posts => [] }})
54
+ .first
55
+ .posts
56
+ .first
57
+ .categories
58
+ .first
59
+ }
60
+
61
+ it "returns the same object for a row's primary once loaded" do
62
+ expect(
63
+ eager_category
64
+ .posts
65
+ .find { |cat_post| cat_post.id == post.id }
66
+ ).to be(post)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,29 +1,59 @@
1
1
  require "spec_helper"
2
2
 
3
+ require "support/mapper_setup"
4
+ require "support/sequel_persistence_setup"
5
+ require "support/seed_data_setup"
3
6
  require "sequel_mapper"
4
- require "support/graph_fixture"
7
+ require "sequel_mapper/configurations/conventional_configuration"
5
8
 
6
9
  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
- }
10
+ include_context "mapper setup"
11
+ include_context "sequel persistence setup"
12
+ include_context "seed data setup"
13
+
14
+ subject(:user_mapper) {
15
+ SequelMapper.mapper(
16
+ config: mapper_config,
17
+ name: :users,
18
+ datastore: datastore,
19
+ )
20
+ }
21
+
17
22
 
18
- let(:user) {
19
- graph.where(id: "user/1").first
23
+ context "one to many association ordered by `created_at DESC`" do
24
+ let(:posts) { user_mapper.first.posts }
25
+
26
+ let(:mapper_config) {
27
+ SequelMapper::Configurations::ConventionalConfiguration.new(datastore)
28
+ .setup_mapping(:users) { |users|
29
+ users.has_many(:posts, foreign_key: :author_id, order_fields: [:created_at], order_direction: "DESC")
30
+ }
20
31
  }
21
32
 
22
33
  it "enumerates the objects in order specified in the config" do
23
- user.toots.to_a
34
+ expect(posts.map(&:id)).to eq(
35
+ posts.to_a.sort_by(&:created_at).reverse.map(&:id)
36
+ )
37
+ end
38
+ end
39
+
40
+ context "many to many associatin ordered by reverse alphabetical name" do
41
+ let(:mapper_config) {
42
+ SequelMapper::Configurations::ConventionalConfiguration.new(datastore)
43
+ .setup_mapping(:users) { |users|
44
+ users.has_many(:posts, foreign_key: :author_id)
45
+ }
46
+ .setup_mapping(:posts) { |posts|
47
+ posts.has_many_through(:categories, order_fields: [:name], order_direction: "DESC")
48
+ }
49
+ }
24
50
 
25
- expect(user.toots.map(&:id).to_a)
26
- .to eq(user.toots.to_a.sort_by { |t| t.tooted_at }.map(&:id).reverse)
51
+ let(:categories) { user_mapper.first.posts.first.categories }
52
+
53
+ it "enumerates the objects in order specified in the config" do
54
+ expect(categories.map(&:id)).to eq(
55
+ categories.to_a.sort_by(&:name).reverse.map(&:id)
56
+ )
27
57
  end
28
58
  end
29
59
  end
@@ -0,0 +1,186 @@
1
+ require "spec_helper"
2
+
3
+ require "support/mapper_setup"
4
+ require "support/sequel_persistence_setup"
5
+ require "support/seed_data_setup"
6
+ require "sequel_mapper"
7
+
8
+ RSpec.describe "Graph persistence efficiency" do
9
+ include_context "mapper setup"
10
+ include_context "sequel persistence setup"
11
+ include_context "seed data setup"
12
+
13
+ let(:mapper) { user_mapper }
14
+ let(:user_query) { mapper.where(id: "users/1") }
15
+ let(:user) { user_query.first }
16
+
17
+ context "when modifying the root node" do
18
+ let(:modified_email) { "modified@example.com" }
19
+
20
+ context "and only the root node" do
21
+ before do
22
+ user.email = modified_email
23
+ end
24
+
25
+ it "performs 1 update" do
26
+ expect {
27
+ mapper.save(user)
28
+ }.to change { query_counter.update_count }.by(1)
29
+ end
30
+ end
31
+ end
32
+
33
+ context "when modifying a directly associated (has many) object" do
34
+ let(:modified_post_subject) { "modified post subject" }
35
+
36
+ before do
37
+ user.posts.first.subject = modified_post_subject
38
+ end
39
+
40
+ it "performs 1 update" do
41
+ expect {
42
+ mapper.save(user)
43
+ }.to change { query_counter.update_count }.by(1)
44
+ end
45
+
46
+ it "performs 0 deletes" do
47
+ expect {
48
+ mapper.save(user)
49
+ }.to change { query_counter.delete_count }.by(0)
50
+ end
51
+
52
+ it "performs 0 additional reads" do
53
+ expect {
54
+ mapper.save(user)
55
+ }.to change { query_counter.read_count }.by(0)
56
+ end
57
+ end
58
+
59
+ context "when loading many nodes of the graph" do
60
+ let(:post) {
61
+ user.posts.first
62
+ }
63
+
64
+ context "and modifying an intermediate node" do
65
+ before do
66
+ post.subject = "MODIFIED"
67
+ end
68
+
69
+ it "performs 1 write" do
70
+ expect {
71
+ mapper.save(user)
72
+ }.to change { query_counter.update_count }.by(1)
73
+ end
74
+ end
75
+
76
+ context "and modifying a leaf node" do
77
+ let(:comment) { post.comments.first }
78
+
79
+ before do
80
+ comment.body = "UPDATED!"
81
+ end
82
+
83
+ it "performs 1 update" do
84
+ expect {
85
+ mapper.save(user)
86
+ }.to change { query_counter.update_count }.by(1)
87
+ end
88
+ end
89
+
90
+ context "and modifying both a leaf and intermediate node" do
91
+ let(:comment) { post.comments.first }
92
+
93
+ before do
94
+ comment.body = "UPDATED!"
95
+ post.subject = "MODIFIED"
96
+ end
97
+
98
+ it "performs 2 updates" do
99
+ expect {
100
+ mapper.save(user)
101
+ }.to change { query_counter.update_count }.by(2)
102
+ end
103
+ end
104
+ end
105
+
106
+ context "when modifying a many to many association" do
107
+ let(:post) { user.posts.first }
108
+ let(:category) { post.categories.first }
109
+
110
+ before do
111
+ category.name = "UPDATED"
112
+ end
113
+
114
+ it "performs 1 write" do
115
+ expect {
116
+ mapper.save(user)
117
+ }.to change { query_counter.update_count }.by(1)
118
+ end
119
+ end
120
+
121
+ context "eager loading" do
122
+ context "on root node" do
123
+ it "performs 1 read per table rather than n + 1" do
124
+ expect {
125
+ mapper.eager_load(:posts => []).all.map { |user|
126
+ [user.id, user.posts.map(&:id)]
127
+ }
128
+ }.to change { query_counter.read_count }.by(2)
129
+ end
130
+ end
131
+
132
+ # mapper.eager_load([:posts, [:comments, [:author]]])
133
+
134
+ context "with nested has many" do
135
+ it "performs 1 read per table rather than n + 1" do
136
+ expect {
137
+ user_query
138
+ .eager_load(:posts => { :comments => [] })
139
+ .first
140
+ .posts
141
+ .map { |post| post.comments.map(&:id) }
142
+ }.to change { query_counter.read_count }.by(3)
143
+ end
144
+ end
145
+
146
+ context "with has many and belongs to" do
147
+ it "performs 1 read per table rather than n + 1" do
148
+ expect {
149
+ user_query
150
+ .eager_load(:posts => { :comments => { :commenter => [] }})
151
+ .flat_map { |u| u.posts.to_a }
152
+ .flat_map { |p| p.comments.to_a }
153
+ .flat_map { |c| c.commenter.id }
154
+ }.to change { query_counter.read_count }.by(4)
155
+ end
156
+ end
157
+
158
+ context "for has many to has many through" do
159
+ it "performs 1 read per table (including join table) rather than n + 1" do
160
+ expect {
161
+ user_query
162
+ .eager_load(:posts => { :categories => [] })
163
+ .flat_map { |u| u.posts.to_a }
164
+ .flat_map { |p| p.categories.to_a }
165
+ .flat_map { |c| c.id }
166
+ }.to change { query_counter.read_count }.by(4)
167
+ end
168
+ end
169
+
170
+ context "for has many through to has many" do
171
+ it "performs 1 read per table (includiing join table) rather than n + 1" do
172
+ expect {
173
+ user_query
174
+ .eager_load(:posts => { :categories => { :posts => [] }})
175
+ .flat_map { |u| u.posts.to_a }
176
+ .flat_map { |p| p.categories.to_a }
177
+ .flat_map { |c| c.posts.to_a }
178
+ }.to change { query_counter.read_count }.by(6)
179
+ end
180
+ end
181
+ end
182
+
183
+ after do |example|
184
+ query_counter.show_queries if example.exception
185
+ end
186
+ end