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