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,174 +0,0 @@
1
- require "sequel_mapper/association_proxy"
2
- require "sequel_mapper/belongs_to_association_proxy"
3
- require "sequel_mapper/queryable_association_proxy"
4
-
5
- module SequelMapper
6
- class Graph
7
- def initialize(datastore:, top_level_namespace:, relation_mappings:)
8
- @top_level_namespace = top_level_namespace
9
- @datastore = datastore
10
- @relation_mappings = relation_mappings
11
- end
12
-
13
- attr_reader :top_level_namespace, :datastore, :relation_mappings
14
- private :top_level_namespace, :datastore, :relation_mappings
15
-
16
- def where(criteria)
17
- datastore[top_level_namespace]
18
- .where(criteria)
19
- .map { |row|
20
- load(
21
- relation_mappings.fetch(top_level_namespace),
22
- row,
23
- )
24
- }
25
- end
26
-
27
- def save(graph_root)
28
- @persisted_objects = []
29
- dump(top_level_namespace, graph_root)
30
- end
31
-
32
- private
33
-
34
- def identity_map
35
- @identity_map ||= {}
36
- end
37
-
38
- def dump(relation_name, object)
39
- return if @persisted_objects.include?(object)
40
- @persisted_objects.push(object)
41
-
42
- relation = relation_mappings.fetch(relation_name)
43
-
44
- row = object.to_h.select { |field_name, _v|
45
- relation.fetch(:columns).include?(field_name)
46
- }
47
-
48
- relation.fetch(:belongs_to, []).each do |assoc_name, assoc_config|
49
- row[assoc_config.fetch(:foreign_key)] = object.public_send(assoc_name).id
50
- end
51
-
52
- relation.fetch(:has_many, []).each do |assoc_name, assoc_config|
53
- object.public_send(assoc_name).each do |assoc_object|
54
- dump(assoc_config.fetch(:relation_name), assoc_object)
55
- end
56
-
57
- next unless object.public_send(assoc_name).respond_to?(:removed_nodes)
58
- object.public_send(assoc_name).removed_nodes.each do |removed_node|
59
- datastore[assoc_config.fetch(:relation_name)]
60
- .where(id: removed_node.id)
61
- .delete
62
- end
63
- end
64
-
65
- relation.fetch(:has_many_through, []).each do |assoc_name, assoc_config|
66
- object.public_send(assoc_name).each do |assoc_object|
67
- dump(assoc_config.fetch(:relation_name), assoc_object)
68
- end
69
-
70
- next unless object.public_send(assoc_name).respond_to?(:added_nodes)
71
- object.public_send(assoc_name).added_nodes.each do |added_node|
72
- datastore[assoc_config.fetch(:through_relation_name)]
73
- .insert(
74
- assoc_config.fetch(:foreign_key) => object.id,
75
- assoc_config.fetch(:association_foreign_key) => added_node.id,
76
- )
77
- end
78
-
79
- object.public_send(assoc_name).removed_nodes.each do |removed_node|
80
- datastore[assoc_config.fetch(:through_relation_name)]
81
- .where(assoc_config.fetch(:association_foreign_key) => removed_node.id)
82
- .delete
83
- end
84
- end
85
-
86
- existing = datastore[relation_name]
87
- .where(id: object.id)
88
-
89
- if existing.empty?
90
- datastore[relation_name].insert(row)
91
- else
92
- existing.update(row)
93
- end
94
- end
95
-
96
- def load(relation, row)
97
- previously_loaded_object = identity_map.fetch(row.fetch(:id), false)
98
- return previously_loaded_object if previously_loaded_object
99
-
100
- # puts "****************LOADING #{row.fetch(:id)}"
101
-
102
- has_many_associations = Hash[
103
- relation.fetch(:has_many, []).map { |assoc_name, assoc|
104
- data_enum = datastore[assoc.fetch(:relation_name)]
105
- .where(assoc.fetch(:foreign_key) => row.fetch(:id))
106
-
107
- if assoc.fetch(:order_by, false)
108
- data_enum = data_enum.order(assoc.fetch(:order_by, {}).fetch(:columns, []))
109
-
110
- if assoc.fetch(:order_by).fetch(:direction, :asc) == :desc
111
- data_enum = data_enum.reverse
112
- end
113
- end
114
-
115
- [
116
- assoc_name,
117
- AssociationProxy.new(
118
- QueryableAssociationProxy.new(
119
- data_enum,
120
- ->(row) {
121
- load(relation_mappings.fetch(assoc.fetch(:relation_name)), row)
122
- },
123
- )
124
- )
125
- ]
126
- }
127
- ]
128
-
129
- belongs_to_associations = Hash[
130
- relation.fetch(:belongs_to, []).map { |assoc_name, assoc|
131
- [
132
- assoc_name,
133
- BelongsToAssociationProxy.new(
134
- datastore[assoc.fetch(:relation_name)]
135
- .where(:id => row.fetch(assoc.fetch(:foreign_key)))
136
- .lazy
137
- .map { |row|
138
- load(relation_mappings.fetch(assoc.fetch(:relation_name)), row)
139
- }
140
- .public_method(:first)
141
- )
142
- ]
143
- }
144
- ]
145
-
146
- has_many_through_assocations = Hash[
147
- relation.fetch(:has_many_through, []).map { |assoc_name, assoc|
148
- [
149
- assoc_name,
150
- AssociationProxy.new(
151
- QueryableAssociationProxy.new(
152
- datastore[assoc.fetch(:relation_name)]
153
- .join(assoc.fetch(:through_relation_name), assoc.fetch(:association_foreign_key) => :id)
154
- .where(assoc.fetch(:foreign_key) => row.fetch(:id)),
155
- ->(row) {
156
- load(relation_mappings.fetch(assoc.fetch(:relation_name)), row)
157
- },
158
- )
159
- )
160
- ]
161
- }
162
- ]
163
-
164
- relation.fetch(:factory).call(
165
- row
166
- .merge(has_many_associations)
167
- .merge(has_many_through_assocations)
168
- .merge(belongs_to_associations)
169
- ).tap { |object|
170
- identity_map.store(row.fetch(:id), object)
171
- }
172
- end
173
- end
174
- end
@@ -1,23 +0,0 @@
1
- class QueryableAssociationProxy
2
- def initialize(database_enum, loader)
3
- @database_enum = database_enum
4
- @loader = loader
5
- end
6
-
7
- attr_reader :database_enum, :loader
8
- private :database_enum, :loader
9
-
10
- extend Forwardable
11
- def_delegators :database_enum, :where
12
-
13
- def where(criteria)
14
- @database_enum = database_enum.where(criteria)
15
- self
16
- end
17
-
18
- def each(&block)
19
- database_enum
20
- .map(&loader)
21
- .each(&block)
22
- end
23
- end
@@ -1,65 +0,0 @@
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
@@ -1,331 +0,0 @@
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