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,60 @@
1
+ require "sequel_mapper/dataset"
2
+
3
+ module SequelMapper
4
+ class ManyToOneAssociation
5
+ def initialize(mapping_name:, foreign_key:, key:, proxy_factory:)
6
+ @mapping_name = mapping_name
7
+ @foreign_key = foreign_key
8
+ @key = key
9
+ @proxy_factory = proxy_factory
10
+ end
11
+
12
+ attr_reader :mapping_name
13
+
14
+ attr_reader :foreign_key, :key, :proxy_factory
15
+ private :foreign_key, :key, :proxy_factory
16
+
17
+ def build_proxy(data_superset:, loader:, record:)
18
+ proxy_factory.call(
19
+ query: build_query(data_superset, record),
20
+ loader: loader,
21
+ preloaded_data: {
22
+ key => foreign_key_value(record),
23
+ },
24
+ )
25
+ end
26
+
27
+ def eager_superset(superset, associated_dataset)
28
+ Dataset.new(
29
+ superset.where(key => associated_dataset.select(foreign_key)).to_a
30
+ )
31
+ end
32
+
33
+ def build_query(superset, record)
34
+ superset.where(key => foreign_key_value(record))
35
+ end
36
+
37
+ def dump(parent_record, collection, &block)
38
+ collection.flat_map { |object|
39
+ block.call(mapping_name, object, _foreign_key_does_not_go_here = {})
40
+ .flat_map { |associated_record|
41
+ foreign_key_pair = {
42
+ foreign_key => associated_record.fetch(key),
43
+ }
44
+
45
+ [
46
+ associated_record,
47
+ parent_record.merge(foreign_key_pair),
48
+ ]
49
+ }
50
+ }
51
+ end
52
+ alias_method :delete, :dump
53
+
54
+ private
55
+
56
+ def foreign_key_value(record)
57
+ record.fetch(foreign_key)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,180 @@
1
+ require "sequel_mapper/graph_serializer"
2
+ require "sequel_mapper/graph_loader"
3
+
4
+ module SequelMapper
5
+ class MapperFacade
6
+ def initialize(mappings:, mapping_name:, datastore:, dataset:, identity_map:, dirty_map:)
7
+ @mappings = mappings
8
+ @mapping_name = mapping_name
9
+ @datastore = datastore
10
+ @dataset = dataset
11
+ @identity_map = identity_map
12
+ @dirty_map = dirty_map
13
+ @eager_data = {}
14
+ end
15
+
16
+ attr_reader :mappings, :mapping_name, :datastore, :dataset, :identity_map, :dirty_map
17
+ private :mappings, :mapping_name, :datastore, :dataset, :identity_map, :dirty_map
18
+
19
+ def save(graph)
20
+ record_dump = graph_serializer.call(mapping_name, graph)
21
+
22
+ object_dump_pipeline.call(record_dump)
23
+
24
+ self
25
+ end
26
+
27
+ def all
28
+ self
29
+ end
30
+
31
+ def where(query)
32
+ new_with_dataset(
33
+ dataset.where(query)
34
+ )
35
+ end
36
+
37
+ def subset(name, *params)
38
+ new_with_dataset(
39
+ mapping.subsets.execute(dataset, name, *params)
40
+ )
41
+ end
42
+
43
+ include Enumerable
44
+ def each(&block)
45
+ dataset
46
+ .map { |record|
47
+ graph_loader.call(mapping_name, record, Hash[@eager_data])
48
+ }
49
+ .each(&block)
50
+ end
51
+
52
+ def eager_load(association_name_map)
53
+ @eager_data = eager_load_the_things(mapping, dataset, association_name_map)
54
+
55
+ self
56
+ end
57
+
58
+ def delete(object)
59
+ object_dump_pipeline.call(
60
+ graph_serializer.call(mapping_name, object)
61
+ .take(1)
62
+ .map { |record|
63
+ DeletedRecord.new(record.namespace, record.identity)
64
+ }
65
+ )
66
+ end
67
+
68
+ private
69
+
70
+ def eager_load_the_things(mapping, parent_dataset, association_name_map)
71
+ association_name_map
72
+ .flat_map { |name, deeper_association_names|
73
+ association = mapping.associations.fetch(name)
74
+ association_mapping = mappings.fetch(association.mapping_name)
75
+ association_namespace = association_mapping.namespace
76
+ association_dataset = get_eager_dataset(association, association_namespace, parent_dataset)
77
+
78
+ [
79
+ [[mapping.name, name] , association_dataset]
80
+ ] + eager_load_the_things(association_mapping, association_dataset, deeper_association_names)
81
+ }
82
+ end
83
+
84
+ def get_eager_dataset(association, association_namespace, parent_dataset)
85
+ association.eager_superset(
86
+ datastore[association_namespace],
87
+ parent_dataset,
88
+ )
89
+ end
90
+
91
+ def new_with_dataset(new_dataset)
92
+ self.class.new(
93
+ dataset: new_dataset,
94
+ mappings: mappings,
95
+ mapping_name: mapping_name,
96
+ datastore: datastore,
97
+ identity_map: identity_map,
98
+ dirty_map: dirty_map,
99
+ )
100
+ end
101
+
102
+ def graph_serializer
103
+ GraphSerializer.new(mappings: mappings)
104
+ end
105
+
106
+ def graph_loader
107
+ GraphLoader.new(
108
+ datasets: datastore,
109
+ mappings: mappings,
110
+ object_load_pipeline: object_load_pipeline,
111
+ )
112
+ end
113
+
114
+ def object_load_pipeline
115
+ ->(mapping, record, other_attrs = {}) {
116
+ [
117
+ record_factory(mapping),
118
+ dirty_map.method(:load),
119
+ ->(r) { identity_map.call(r, mapping.factory.call(r.merge(other_attrs))) },
120
+ ].reduce(record) { |agg, operation|
121
+ operation.call(agg)
122
+ }
123
+ }
124
+ end
125
+
126
+ def object_dump_pipeline
127
+ ->(records) {
128
+ [
129
+ :uniq.to_proc,
130
+ ->(rs) { rs.select { |r| dirty_map.dirty?(r) } },
131
+ ->(rs) {
132
+ rs.each { |r|
133
+ r.if_upsert(&method(:upsert_record))
134
+ .if_delete(&method(:delete_record))
135
+ }
136
+ },
137
+ ].reduce(records) { |agg, operation|
138
+ operation.call(agg)
139
+ }
140
+ }
141
+ end
142
+
143
+ def record_factory(mapping)
144
+ ->(record_hash) {
145
+ identity = Hash[
146
+ mapping.primary_key.map { |field|
147
+ [field, record_hash.fetch(field)]
148
+ }
149
+ ]
150
+
151
+ SequelMapper::UpsertedRecord.new(
152
+ mapping.namespace,
153
+ identity,
154
+ record_hash,
155
+ )
156
+ }
157
+ end
158
+
159
+ def mapping
160
+ mappings.fetch(mapping_name)
161
+ end
162
+
163
+ def upsert_record(record)
164
+ # TODO I doubt this is really more performant but fewer queries register :)
165
+ row_count = datastore[record.namespace]
166
+ .where(record.identity)
167
+ .update(record.to_h)
168
+
169
+ if row_count < 1
170
+ row_count = datastore[record.namespace].insert(record.to_h)
171
+ end
172
+
173
+ row_count
174
+ end
175
+
176
+ def delete_record(record)
177
+ datastore[record.namespace].where(record.identity).delete
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,51 @@
1
+ require "sequel_mapper/dataset"
2
+
3
+ module SequelMapper
4
+ class OneToManyAssociation
5
+ def initialize(mapping_name:, foreign_key:, key:, order:, proxy_factory:)
6
+ @mapping_name = mapping_name
7
+ @foreign_key = foreign_key
8
+ @key = key
9
+ @order = order
10
+ @proxy_factory = proxy_factory
11
+ end
12
+
13
+ attr_reader :mapping_name
14
+
15
+ attr_reader :foreign_key, :key, :order, :proxy_factory
16
+ private :foreign_key, :key, :order, :proxy_factory
17
+
18
+ def build_proxy(data_superset:, loader:, record:)
19
+ proxy_factory.call(
20
+ query: build_query(data_superset, record),
21
+ loader: loader,
22
+ mapping_name: mapping_name,
23
+ )
24
+ end
25
+
26
+ def dump(parent_record, collection, &block)
27
+ foreign_key_pair = {
28
+ foreign_key => parent_record.fetch(key),
29
+ }
30
+
31
+ collection.flat_map { |associated_object|
32
+ block.call(mapping_name, associated_object, foreign_key_pair)
33
+ }
34
+ end
35
+ alias_method :delete, :dump
36
+
37
+ def eager_superset(superset, associated_dataset)
38
+ Dataset.new(
39
+ superset.where(
40
+ foreign_key => associated_dataset.select(key)
41
+ ).to_a
42
+ )
43
+ end
44
+
45
+ def build_query(superset, record)
46
+ order.apply(
47
+ superset.where(foreign_key => record.fetch(key))
48
+ )
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,27 @@
1
+ require "sequel_mapper/identity_map"
2
+ require "sequel_mapper/dirty_map"
3
+ require "sequel_mapper/mapper_facade"
4
+ require "sequel_mapper/configurations/conventional_configuration"
5
+
6
+ module SequelMapper
7
+ module PublicConveniencies
8
+ def config(database_connection)
9
+ Configurations::ConventionalConfiguration.new(database_connection)
10
+ end
11
+
12
+ def mapper(config:, name:, datastore:)
13
+ dataset = datastore[config.fetch(name).namespace]
14
+ identity_map = IdentityMap.new({})
15
+ dirty_map = DirtyMap.new({})
16
+
17
+ MapperFacade.new(
18
+ mappings: config,
19
+ mapping_name: name,
20
+ datastore: datastore,
21
+ dataset: dataset,
22
+ identity_map: identity_map,
23
+ dirty_map: dirty_map,
24
+ )
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ module SequelMapper
2
+ class QueryOrder
3
+ def initialize(fields:, direction:)
4
+ @fields = fields
5
+ @direction_function = get_direction_function(direction.to_s.upcase)
6
+ end
7
+
8
+ attr_reader :fields, :direction_function
9
+
10
+ def apply(dataset)
11
+ if fields.any?
12
+ apply_direction(dataset.order(fields))
13
+ else
14
+ dataset
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def apply_direction(dataset)
21
+ direction_function.call(dataset)
22
+ end
23
+
24
+ # TODO: Consider a nicer API for this and push this into SequelAdapter
25
+ def get_direction_function(direction)
26
+ {
27
+ "ASC" => ->(x){x},
28
+ "DESC" => :reverse.to_proc,
29
+ }.fetch(direction) { raise "Unsupported sort option #{direction}. Choose one of [ASC, DESC]." }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,70 @@
1
+ require "sequel_mapper/short_inspection_string"
2
+
3
+ module SequelMapper
4
+ class QueryableLazyDatasetLoader
5
+ include ShortInspectionString
6
+ include Enumerable
7
+
8
+ def initialize(database_enum, loader, queries)
9
+ @database_enum = database_enum
10
+ @loader = loader
11
+ @queries = queries
12
+ @loaded = false
13
+ end
14
+
15
+ attr_reader :database_enum, :loader, :queries
16
+ private :database_enum, :loader, :queries
17
+
18
+ def where(criteria)
19
+ self.class.new(database_enum.where(criteria), loader, queries)
20
+ end
21
+
22
+ def subset(name, *params)
23
+ self.class.new(
24
+ queries.execute(database_enum, name, *params),
25
+ loader,
26
+ queries,
27
+ )
28
+ end
29
+
30
+ def each(&block)
31
+ enum.each(&block)
32
+ end
33
+
34
+ def each_loaded(&block)
35
+ loaded_objects.each(&block)
36
+ end
37
+
38
+ private
39
+
40
+ def enum
41
+ @enum ||= Enumerator.new { |yielder|
42
+ loaded_objects.each do |obj|
43
+ yielder.yield(obj)
44
+ end
45
+
46
+ loop do
47
+ object_enum.next.tap { |obj|
48
+ loaded_objects.push(obj)
49
+ yielder.yield(obj)
50
+ }
51
+ end
52
+ }
53
+ end
54
+
55
+ def object_enum
56
+ @object_enum ||= database_enum.lazy.map(&loader)
57
+ end
58
+
59
+ def loaded_objects
60
+ @loaded_objects ||= []
61
+ end
62
+
63
+ def inspectable_properties
64
+ [
65
+ :database_enum,
66
+ :loaded,
67
+ ]
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,35 @@
1
+ module SequelMapper
2
+ class RelationMapping
3
+ def initialize(name:, namespace:, fields:, primary_key:, factory:, serializer:, associations:, subsets:)
4
+ @name = name
5
+ @namespace = namespace
6
+ @fields = fields
7
+ @primary_key = primary_key
8
+ @factory = factory
9
+ @serializer = serializer
10
+ @associations = associations
11
+ @subsets = subsets
12
+ end
13
+
14
+ attr_reader :name, :namespace, :fields, :primary_key, :factory, :serializer, :associations, :subsets
15
+
16
+ def add_association(name, new_association)
17
+ @associations = associations.merge(name => new_association)
18
+ end
19
+
20
+ private
21
+
22
+ def new_with_associations(new_associations)
23
+ self.class.new(
24
+ name: name,
25
+ namespace: namespace,
26
+ fields: fields,
27
+ primary_key: primary_key,
28
+ factory: factory,
29
+ serializer: serializer,
30
+ associations: new_associations,
31
+ subsets: subsets,
32
+ )
33
+ end
34
+ end
35
+ end