sequel_mapper 0.0.1 → 0.0.3

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