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