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
@@ -0,0 +1,37 @@
1
+ module SequelMapper
2
+ class Dataset
3
+ def initialize(records)
4
+ @records = records
5
+ end
6
+
7
+ attr_reader :records
8
+ private :records
9
+
10
+ include Enumerable
11
+
12
+ def each(&block)
13
+ records.each(&block)
14
+ self
15
+ end
16
+
17
+ def where(criteria)
18
+ new(
19
+ records.select { |row|
20
+ criteria.all? { |k, v|
21
+ row.fetch(k, :nope) == v
22
+ }
23
+ }
24
+ )
25
+ end
26
+
27
+ def select(field)
28
+ map { |data| data.fetch(field) }
29
+ end
30
+
31
+ private
32
+
33
+ def new(records)
34
+ self.class.new(records)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,16 @@
1
+ require "sequel_mapper/abstract_record"
2
+
3
+ module SequelMapper
4
+ class DeletedRecord < AbstractRecord
5
+ def if_delete(&block)
6
+ block.call(self)
7
+ self
8
+ end
9
+
10
+ protected
11
+
12
+ def operation
13
+ :delete
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ module SequelMapper
2
+ class DirtyMap
3
+ def initialize(storage)
4
+ @storage = storage
5
+ end
6
+
7
+ attr_reader :storage
8
+ private :storage
9
+
10
+ def load(record)
11
+ storage.store(hash_key(record), deep_clone(record))
12
+ record
13
+ end
14
+
15
+ def dirty?(record)
16
+ record_as_loaded = storage.fetch(hash_key(record), :not_found)
17
+
18
+ record != record_as_loaded
19
+ end
20
+
21
+ private
22
+
23
+ def hash_key(record)
24
+ deep_clone([record.namespace, record.identity])
25
+ end
26
+
27
+ def deep_clone(record)
28
+ Marshal.load(Marshal.dump(record))
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,48 @@
1
+ module SequelMapper
2
+ class GraphLoader
3
+ def initialize(datasets:, mappings:, object_load_pipeline:)
4
+ @datasets = datasets
5
+ @mappings = mappings
6
+ @object_load_pipeline = object_load_pipeline
7
+ end
8
+
9
+ attr_reader :datasets, :mappings, :object_load_pipeline
10
+
11
+ def call(mapping_name, record, eager_data = {})
12
+ mapping = mappings.fetch(mapping_name)
13
+
14
+ load_record(mapping, record, eager_data)
15
+ end
16
+
17
+ private
18
+
19
+ def load_record(mapping, record, eager_data)
20
+ associations = load_associations(mapping, record, eager_data)
21
+
22
+ object_load_pipeline.call(mapping, record, Hash[associations])
23
+ end
24
+
25
+ def load_associations(mapping, record, eager_data)
26
+ mapping.associations.map { |name, association|
27
+ data_superset = eager_data.fetch([mapping.name, name]) {
28
+ datasets[mappings.fetch(association.mapping_name).namespace]
29
+ }
30
+
31
+ [
32
+ name,
33
+ association.build_proxy(
34
+ record: record,
35
+ data_superset: data_superset,
36
+ loader: ->(associated_record, join_records = []) {
37
+ join_records.map { |jr|
38
+ join_mapping = mappings.fetch(association.join_mapping_name)
39
+ object_load_pipeline.call(join_mapping, jr)
40
+ }
41
+ call(association.mapping_name, associated_record, eager_data)
42
+ },
43
+ )
44
+ ]
45
+ }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,107 @@
1
+ require "sequel_mapper/upserted_record"
2
+ require "sequel_mapper/deleted_record"
3
+
4
+ module SequelMapper
5
+ class GraphSerializer
6
+ def initialize(mappings:)
7
+ @mappings = mappings
8
+ @count = 0
9
+ @encountered_records = Set.new
10
+ end
11
+
12
+ attr_reader :mappings, :encountered_records
13
+ private :mappings, :encountered_records
14
+
15
+ def call(mapping_name, object, foreign_key = {})
16
+ # TODO may need some attention :)
17
+ mapping = mappings.fetch(mapping_name)
18
+ serializer = mapping.serializer
19
+ namespace = mapping.namespace
20
+ primary_key = mapping.primary_key
21
+ fields = mapping.fields
22
+ associations_map = mapping.associations
23
+
24
+ serialized_record = serializer.call(object)
25
+
26
+ current_record = UpsertedRecord.new(
27
+ namespace,
28
+ record_identity(primary_key, serialized_record),
29
+ serialized_record
30
+ .select { |k, _v| fields.include?(k) }
31
+ .merge(foreign_key)
32
+ )
33
+
34
+ if encountered_records.include?(current_record.identity)
35
+ return [current_record]
36
+ else
37
+ encountered_records.add(current_record.identity)
38
+ end
39
+
40
+ [current_record] + associations_map
41
+ .map { |name, association|
42
+ [serialized_record.fetch(name), association]
43
+ }
44
+ .map { |collection, association|
45
+ [nodes(collection), deleted_nodes(collection), association]
46
+ }
47
+ .map { |nodes, deleted_nodes, association|
48
+ assoc_mapping = mappings.fetch(association.mapping_name)
49
+
50
+ association.dump(current_record, nodes) { |assoc_mapping_name, assoc_object, foreign_key|
51
+ call(assoc_mapping_name, assoc_object, foreign_key)
52
+ } +
53
+ association.delete(current_record, deleted_nodes) { |assoc_mapping_name, assoc_object, foreign_key|
54
+ delete(assoc_mapping_name, assoc_object, foreign_key)
55
+ }
56
+ }
57
+ .flatten(1)
58
+ end
59
+
60
+ private
61
+
62
+ def delete(mapping_name, object, _foreign_key)
63
+ # TODO copypasta ¯\_(ツ)_/¯
64
+ mapping = mappings.fetch(mapping_name)
65
+ primary_key = mapping.primary_key
66
+ serializer = mapping.serializer
67
+ namespace = mapping.namespace
68
+
69
+ serialized_record = serializer.call(object)
70
+
71
+ [
72
+ DeletedRecord.new(
73
+ namespace,
74
+ record_identity(primary_key, serialized_record),
75
+ )
76
+ ]
77
+ end
78
+
79
+ def nodes(collection)
80
+ if collection.respond_to?(:each_loaded)
81
+ collection.each_loaded
82
+ elsif collection.is_a?(Struct)
83
+ [collection]
84
+ elsif collection.respond_to?(:each)
85
+ collection.each
86
+ else
87
+ collection
88
+ end
89
+ end
90
+
91
+ def deleted_nodes(collection)
92
+ if collection.respond_to?(:each_deleted)
93
+ collection.each_deleted
94
+ else
95
+ []
96
+ end
97
+ end
98
+
99
+ def record_identity(primary_key, record)
100
+ Hash[
101
+ primary_key.map { |field|
102
+ [field, record.fetch(field)]
103
+ }
104
+ ]
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,22 @@
1
+ module SequelMapper
2
+ class IdentityMap
3
+ def initialize(storage)
4
+ @storage = storage
5
+ end
6
+
7
+ attr_reader :storage
8
+ private :storage
9
+
10
+ def call(record, object)
11
+ storage.fetch(hash_key(record)) {
12
+ storage.store(hash_key(record), object)
13
+ }
14
+ end
15
+
16
+ private
17
+
18
+ def hash_key(record)
19
+ [record.namespace, record.identity]
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,51 @@
1
+ module SequelMapper
2
+ class LazyObjectProxy
3
+ include ShortInspectionString
4
+
5
+ def initialize(object_loader, known_fields)
6
+ @object_loader = object_loader
7
+ @known_fields = known_fields
8
+ @lazy_object = nil
9
+ end
10
+
11
+ attr_reader :object_loader, :known_fields
12
+ private :object_loader, :known_fields
13
+
14
+ def method_missing(method_id, *args, &block)
15
+ if args.empty? && known_fields.include?(method_id)
16
+ known_fields.fetch(method_id)
17
+ else
18
+ lazy_object.public_send(method_id, *args, &block)
19
+ end
20
+ end
21
+
22
+ def loaded?
23
+ !!@lazy_object
24
+ end
25
+
26
+ def __getobj__
27
+ lazy_object
28
+ end
29
+
30
+ def each_loaded(&block)
31
+ [self].select(&:loaded?).each(&block)
32
+ end
33
+
34
+ private
35
+
36
+ def respond_to_missing?(method_id, _include_private = false)
37
+ known_fields.include?(method_id) || lazy_object.respond_to?(method_id)
38
+ end
39
+
40
+ def lazy_object
41
+ @lazy_object ||= object_loader.call
42
+ end
43
+
44
+ def inspectable_properties
45
+ [
46
+ :known_fields,
47
+ :lazy_object,
48
+ ]
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,181 @@
1
+ require "sequel_mapper/dataset"
2
+
3
+ module SequelMapper
4
+ class ManyToManyAssociation
5
+ def initialize(mapping_name:, foreign_key:, key:, proxy_factory:, association_foreign_key:, association_key:, join_mapping_name:, join_dataset:, order:)
6
+ @mapping_name = mapping_name
7
+ @foreign_key = foreign_key
8
+ @key = key
9
+ @proxy_factory = proxy_factory
10
+ @association_foreign_key = association_foreign_key
11
+ @association_key = association_key
12
+ @join_mapping_name = join_mapping_name
13
+ @join_dataset = join_dataset
14
+ @order = order
15
+ end
16
+
17
+ attr_reader :mapping_name, :join_mapping_name
18
+
19
+ attr_reader :foreign_key, :key, :proxy_factory, :association_key, :association_foreign_key, :join_dataset, :order
20
+ private :foreign_key, :key, :proxy_factory, :association_key, :association_foreign_key, :join_dataset, :order
21
+
22
+ def build_proxy(data_superset:, loader:, record:)
23
+ proxy_factory.call(
24
+ query: build_query(data_superset, record),
25
+ loader: ->(record_list) {
26
+ record = record_list.first
27
+ join_records = record_list.last
28
+
29
+ loader.call(record, join_records)
30
+ },
31
+ mapping_name: mapping_name,
32
+ )
33
+ end
34
+
35
+ def eager_superset(superset, associated_dataset)
36
+ # TODO: All these keys can be confusing, write some focused tests.
37
+ eager_join_dataset = Dataset.new(
38
+ join_dataset
39
+ .where(foreign_key => associated_dataset.select(association_key))
40
+ .to_a
41
+ )
42
+
43
+ eager_dataset = superset
44
+ .where(key => eager_join_dataset.select(association_foreign_key))
45
+ .to_a
46
+
47
+ JoinedDataset.new(eager_dataset, eager_join_dataset)
48
+ end
49
+
50
+ def build_query(superset, parent_record)
51
+ order
52
+ .apply(
53
+ superset.join(join_mapping_name, association_foreign_key => key)
54
+ .where(foreign_key => foreign_key_value(parent_record))
55
+ )
56
+ .lazy.map { |record|
57
+ [record, [foreign_keys(parent_record, record)]]
58
+ }
59
+ end
60
+
61
+ class JoinedDataset < Dataset
62
+ def initialize(records, join_records)
63
+ @records = records
64
+ @join_records = join_records
65
+ end
66
+
67
+ def join(_relation_name, _conditions)
68
+ # TODO: This works for the current test suite but is probably too
69
+ # simplistic. Perhaps if the dataset was aware of its join conditions
70
+ # it would be able to intellegently skip joining or delegate
71
+ self
72
+ end
73
+
74
+ def where(criteria)
75
+ self.class.new(
76
+ *decompose_set(
77
+ find_like_sequel(criteria)
78
+ )
79
+ )
80
+ end
81
+
82
+ private
83
+
84
+ def decompose_set(set)
85
+ set.map(&:to_pair).transpose.+([ [], [] ]).take(2)
86
+ end
87
+
88
+ def find_like_sequel(criteria)
89
+ joined_records
90
+ .select { |record|
91
+ criteria.all? { |k, v|
92
+ record.fetch(k, :nope) == v
93
+ }
94
+ }
95
+ end
96
+
97
+ def joined_records
98
+ # TODO: there will inevitably nearly always be a mismatch between the
99
+ # number of records and unique join records. This zip/transpose
100
+ # approach may be too simplistic.
101
+ @joined_records ||= records
102
+ .zip(@join_records)
103
+ .map { |record, join_record|
104
+ JoinedRecord.new(record, join_record)
105
+ }
106
+ end
107
+
108
+ class JoinedRecord
109
+ def initialize(record, join_record)
110
+ @record = record
111
+ @join_record = join_record
112
+ end
113
+
114
+ attr_reader :record, :join_record
115
+ private :record, :join_record
116
+
117
+ def to_pair
118
+ [record, join_record]
119
+ end
120
+
121
+ def to_h
122
+ @record
123
+ end
124
+
125
+ def fetch(key, default = NO_DEFAULT, &block)
126
+ args = [key, default].reject { |a| a == NO_DEFAULT }
127
+
128
+ @record.fetch(key) {
129
+ @join_record.fetch(*args, &block)
130
+ }
131
+ end
132
+
133
+ NO_DEFAULT = Module.new
134
+ end
135
+ end
136
+
137
+ def dump(parent_record, collection, &block)
138
+ flat_list_of_records_and_join_records(parent_record, collection, &block)
139
+ end
140
+
141
+ def delete(parent_record, collection, &block)
142
+ flat_list_of_just_join_records(parent_record, collection, &block)
143
+ end
144
+
145
+ private
146
+
147
+ def flat_list_of_records_and_join_records(parent_record, collection, &block)
148
+ record_join_record_pairs(parent_record, collection, &block).flatten(1)
149
+ end
150
+
151
+ def flat_list_of_just_join_records(parent_record, collection, &block)
152
+ record_join_record_pairs(parent_record, collection, &block)
153
+ .map { |(_records, join_records)| join_records }
154
+ .flatten(1)
155
+ end
156
+
157
+ def record_join_record_pairs(parent_record, collection, &block)
158
+ (collection || []).map { |associated_object|
159
+ records = block.call(mapping_name, associated_object, _no_foreign_key = {})
160
+
161
+ join_records = records.take(1).flat_map { |record|
162
+ fks = foreign_keys(parent_record, record)
163
+ block.call(join_mapping_name, fks, fks)
164
+ }
165
+
166
+ records + join_records
167
+ }
168
+ end
169
+
170
+ def foreign_keys(parent_record, record)
171
+ {
172
+ foreign_key => foreign_key_value(parent_record),
173
+ association_foreign_key => record.fetch(association_key),
174
+ }
175
+ end
176
+
177
+ def foreign_key_value(record)
178
+ record.fetch(key)
179
+ end
180
+ end
181
+ end