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