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
data/TODO.md CHANGED
@@ -3,16 +3,8 @@
3
3
  In no particular order
4
4
 
5
5
  ## General
6
- * Refactor, methods too, big objects missing
7
-
8
- ## Persistence
9
- * Efficient saving
10
- - Part one, if it wasn't loaded it wasn't modified, check identity map
11
- - Part two, dirty tracking
12
-
13
- ## Configuration
14
- * Automatic config generation based on schema, foreign keys etc
15
- * Config to take either a classes or callable factory
6
+ * Refactor, methods too big, objects missing
7
+ * Name things better
16
8
 
17
9
  ## Querying
18
10
  * Querying API, what would a repository with some arbitrary queries look like?
@@ -24,10 +16,23 @@ In no particular order
24
16
  Sequel's `#where` with block [querying API](http://sequel.jeremyevans.net/rdoc/files/doc/cheat_sheet_rdoc.html#label-AND%2FOR%2FNOT)
25
17
 
26
18
  ## Associations
27
- * Eager loading
28
19
  * Read only associations
29
20
  - Loaded objects would be immutable
30
21
  - Collection proxy would have no #push or #remove
31
22
  - Skipped when dumping
32
23
  * Associations defined with a join
33
24
  * Composable associations
25
+
26
+ # Hopefully done
27
+
28
+ ## Persistence
29
+ * Efficient saving
30
+ - Part one, if it wasn't loaded it wasn't modified, check identity map
31
+ - Part two, dirty tracking
32
+
33
+ ## Associations
34
+ * Eager loading
35
+
36
+ ## Configuration
37
+ * Automatic config generation based on schema, foreign keys etc
38
+ * Config to take either a classes or callable factory
@@ -0,0 +1,57 @@
1
+ require "pry"
2
+ require "sequel"
3
+ require "sequel_mapper"
4
+ require_relative "../spec/support/sequel_test_support"
5
+
6
+ module ExampleRunnerSupport
7
+ def example_eval_concat(code_strings)
8
+ example_eval(code_strings.join("\n"))
9
+ end
10
+
11
+ def example_eval(code_string)
12
+ example_module.module_eval(code_string)
13
+ rescue Object => e
14
+ binding.pry if ENV["DEBUG"]
15
+ raise e
16
+ end
17
+
18
+ def example_exec(&block)
19
+ example_exec.module_eval(&block)
20
+ end
21
+
22
+ def example_module
23
+ @example_module ||= Module.new
24
+ end
25
+
26
+ def normalise_inspection_string(string)
27
+ string
28
+ .strip
29
+ .gsub(/[\n\s]+/, " ")
30
+ .gsub(/\:[0-9a-f]{12}/, ":<<object id removed>>")
31
+ end
32
+
33
+ def parse_schema_table(string)
34
+ string.each_line.drop(2).map { |line|
35
+ name, type = line.split("|").map(&:strip)
36
+ {
37
+ name: name,
38
+ type: type,
39
+ }
40
+ }
41
+ end
42
+ end
43
+
44
+ module DatabaseSupport
45
+ def create_table(name, schema)
46
+ SequelMapper::SequelTestSupport.create_tables(
47
+ name => schema,
48
+ )
49
+ end
50
+ end
51
+
52
+ Before do
53
+ SequelMapper::SequelTestSupport.drop_tables
54
+ end
55
+
56
+ World(ExampleRunnerSupport)
57
+ World(DatabaseSupport)
@@ -0,0 +1,121 @@
1
+ Feature: Basic setup
2
+
3
+ Scenario: Setup with conventional configuration
4
+ Given the domain objects are defined
5
+ """
6
+ User = Struct.new(:id, :first_name, :last_name, :email, :posts)
7
+ Post = Struct.new(:id, :author, :subject, :body, :created_at, :categories)
8
+ Category = Struct.new(:id, :name, :posts)
9
+ """
10
+ And a conventionally similar database schema for table "users"
11
+ """
12
+ Column | Type
13
+ ------------ +---------
14
+ id | text
15
+ first_name | text
16
+ last_name | text
17
+ email | text
18
+ """
19
+ And a conventionally similar database schema for table "posts"
20
+ """
21
+ Column | Type
22
+ -------------+---------
23
+ id | text
24
+ author_id | text
25
+ subject | text
26
+ body | text
27
+ created_at | DateTime
28
+ """
29
+ And a conventionally similar database schema for table "categories"
30
+ """
31
+ Column | Type
32
+ -------------+---------
33
+ id | text
34
+ name | text
35
+ """
36
+ And a conventionally similar database schema for table "categories_to_posts"
37
+ """
38
+ Column | Type
39
+ -------------+---------
40
+ post_id | text
41
+ category_id | text
42
+ """
43
+ And a database connection is established
44
+ """
45
+ DB = Sequel.postgres(
46
+ host: ENV.fetch("PGHOST"),
47
+ user: ENV.fetch("PGUSER"),
48
+ database: ENV.fetch("PGDATABASE"),
49
+ )
50
+ """
51
+ And the associations are defined in the mapper configuration
52
+ """
53
+ USER_MAPPER_CONFIG = SequelMapper.config(DB)
54
+ .setup_mapping(:users) { |users|
55
+ users.class(User)
56
+ users.has_many(:posts, foreign_key: :author_id)
57
+ }
58
+ .setup_mapping(:posts) { |posts|
59
+ posts.class(Post)
60
+ posts.belongs_to(:author, mapping_name: :users)
61
+ posts.has_many_through(:categories)
62
+ }
63
+ .setup_mapping(:categories) { |categories|
64
+ categories.class(Category)
65
+ categories.has_many_through(:posts)
66
+ }
67
+ """
68
+ And a mapper is instantiated
69
+ """
70
+ USER_MAPPER = SequelMapper.mapper(
71
+ datastore: DB,
72
+ config: USER_MAPPER_CONFIG,
73
+ name: :users,
74
+ )
75
+ """
76
+ When a new graph of objects are created
77
+ """
78
+ user = User.new(
79
+ "2f0f791c-47cf-4a00-8676-e582075bcd65",
80
+ "Hansel",
81
+ "Trickett",
82
+ "hansel@tricketts.org",
83
+ [],
84
+ )
85
+
86
+ user.posts << Post.new(
87
+ "9b75fe2b-d694-4b90-9137-6201d426dda2",
88
+ user,
89
+ "Things that I like",
90
+ "I like fish and scratching",
91
+ Time.parse("2015-10-03 21:00:00 UTC"),
92
+ [],
93
+ )
94
+ """
95
+ And the new graph is saved
96
+ """
97
+ USER_MAPPER.save(user)
98
+ """
99
+ And the following query is executed
100
+ """
101
+ user = USER_MAPPER.where(id: "2f0f791c-47cf-4a00-8676-e582075bcd65").first
102
+ """
103
+ Then the persisted user object is returned with lazy associations
104
+ """
105
+ #<struct User id="2f0f791c-47cf-4a00-8676-e582075bcd65",
106
+ first_name="Hansel",
107
+ last_name="Trickett",
108
+ email="hansel@tricketts.org",
109
+ posts=#<SequelMapper::CollectionMutabilityProxy:7fa4817aa148
110
+ >>
111
+ """
112
+ And the user's posts will be loaded once the association proxy receives an Enumerable message
113
+ """
114
+ [#<struct Post id="9b75fe2b-d694-4b90-9137-6201d426dda2",
115
+ author=#<SequelMapper::LazyObjectProxy:7fec5ac2a5f8 known_fields={:id=>"2f0f791c-47cf-4a00-8676-e582075bcd65"} lazy_object=nil>,
116
+ subject="Things that I like",
117
+ body="I like fish and scratching",
118
+ created_at=2015-10-03 21:00:00 UTC,
119
+ categories=#<SequelMapper::CollectionMutabilityProxy:7fec5ac296f8
120
+ >>]
121
+ """
@@ -0,0 +1,46 @@
1
+ Given(/^the domain objects are defined$/) do |code_sample|
2
+ Object.module_eval(code_sample)
3
+ end
4
+
5
+ Given(/^a database connection is established$/) do |code_sample|
6
+ example_eval(code_sample)
7
+ end
8
+
9
+ Given(/^the associations are defined in the mapper configuration$/) do |code_sample|
10
+ example_eval(code_sample)
11
+ end
12
+
13
+ Given(/^a mapper is instantiated$/) do |code_sample|
14
+ example_eval(code_sample)
15
+ end
16
+
17
+ Given(/^a conventionally similar database schema for table "(.*?)"$/) do |table_name, schema_table|
18
+ create_table(table_name, parse_schema_table(schema_table))
19
+ end
20
+
21
+ When(/^a new graph of objects are created$/) do |code_sample|
22
+ @objects_to_be_saved_sample = code_sample
23
+ end
24
+
25
+ When(/^the new graph is saved$/) do |save_objects_code|
26
+ example_eval(
27
+ [@objects_to_be_saved_sample, save_objects_code].join("\n")
28
+ )
29
+ end
30
+
31
+ When(/^the following query is executed$/) do |code_sample|
32
+ @query = code_sample
33
+ @result = example_eval(code_sample)
34
+ end
35
+
36
+ Then(/^the persisted user object is returned with lazy associations$/) do |expected_inspection_string|
37
+ expect(normalise_inspection_string(@result.inspect))
38
+ .to eq(normalise_inspection_string(expected_inspection_string))
39
+ end
40
+
41
+ Then(/^the user's posts will be loaded once the association proxy receives an Enumerable message$/) do |expected_inspection_string|
42
+ posts = @result.posts.to_a
43
+
44
+ expect(normalise_inspection_string(posts.inspect))
45
+ .to eq(normalise_inspection_string(expected_inspection_string))
46
+ end
@@ -1,4 +1,8 @@
1
+ require "logger"
2
+ require "sequel_mapper/public_conveniencies"
3
+
1
4
  module SequelMapper
2
- end
5
+ extend PublicConveniencies
3
6
 
4
- require "sequel_mapper/graph"
7
+ LOGGER = Logger.new(STDERR)
8
+ end
@@ -0,0 +1,53 @@
1
+ require "forwardable"
2
+
3
+ module SequelMapper
4
+ class AbstractRecord
5
+ extend Forwardable
6
+
7
+ def initialize(namespace, identity, raw_data = {})
8
+ @namespace = namespace
9
+ @identity = identity
10
+ @raw_data = raw_data
11
+ end
12
+
13
+ attr_reader :namespace, :identity
14
+
15
+ attr_reader :raw_data
16
+ private :raw_data
17
+
18
+ def_delegators :to_h, :fetch
19
+
20
+ def if_upsert(&block)
21
+ self
22
+ end
23
+
24
+ def if_delete(&block)
25
+ self
26
+ end
27
+
28
+ def merge(more_data)
29
+ new_with_raw_data(raw_data.merge(more_data))
30
+ end
31
+
32
+ def to_h
33
+ raw_data.merge(identity)
34
+ end
35
+
36
+ def ==(other)
37
+ self.class === other &&
38
+ [operation, to_h] == [other.operation, other.to_h]
39
+ end
40
+
41
+ protected
42
+
43
+ def operation
44
+ raise NotImplementedError
45
+ end
46
+
47
+ private
48
+
49
+ def new_with_raw_data(new_raw_data)
50
+ self.class.new(namespace, identity, new_raw_data)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,52 @@
1
+ module SequelMapper
2
+ module AssociationLoaders
3
+ class OneToMany
4
+ def initialize(type:, mapping_name:, foreign_key:, key:, proxy_factory:)
5
+ @type = type
6
+ @mapping_name = mapping_name
7
+ @foreign_key = foreign_key
8
+ @key = key
9
+ @proxy_factory = proxy_factory
10
+ @eager_loads = {}
11
+ end
12
+
13
+ attr_reader :type, :mapping_name, :foreign_key, :key, :proxy_factory
14
+ private :type, :mapping_name, :foreign_key, :key, :proxy_factory
15
+
16
+ def fetch(*args, &block)
17
+ {
18
+ key: key,
19
+ foreign_key: foreign_key,
20
+ type: type,
21
+ mapping_name: mapping_name,
22
+ }.fetch(*args, &block)
23
+ end
24
+
25
+ def call(mappings, record, &object_pipeline)
26
+ mapping = mappings.fetch(mapping_name)
27
+
28
+ proxy_factory.call(
29
+ query: query(mapping, record),
30
+ loader: object_pipeline.call(mapping),
31
+ association_loader: self,
32
+ )
33
+ end
34
+
35
+ def query(mapping, record)
36
+ foreign_key_value = record.fetch(key)
37
+
38
+ ->(datastore) {
39
+ @eager_loads.fetch(record) {
40
+ datastore[mapping.namespace]
41
+ .where(foreign_key => foreign_key_value)
42
+ }
43
+ }
44
+ end
45
+
46
+ def eager_load(dataset, association_name)
47
+ datastore[mapping.namespace]
48
+ .where(foreign_key => dataset.select(key))
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,77 @@
1
+ require "forwardable"
2
+ require "sequel_mapper/short_inspection_string"
3
+
4
+ module SequelMapper
5
+ class CollectionMutabilityProxy
6
+ include ShortInspectionString
7
+
8
+ def initialize(collection)
9
+ @collection = collection
10
+ @added_nodes = []
11
+ @deleted_nodes = []
12
+ end
13
+
14
+ attr_reader :collection, :deleted_nodes, :added_nodes
15
+ private :collection, :deleted_nodes, :added_nodes
16
+
17
+ extend Forwardable
18
+ def_delegators :collection, :where, :subset
19
+
20
+ def each_loaded(&block)
21
+ loaded_enum.each(&block)
22
+ end
23
+
24
+ def each_deleted(&block)
25
+ @deleted_nodes.each(&block)
26
+ end
27
+
28
+ include Enumerable
29
+ def each(&block)
30
+ if block
31
+ enum.each(&block)
32
+ self
33
+ else
34
+ enum
35
+ end
36
+ end
37
+
38
+ def delete(node)
39
+ @deleted_nodes.push(node)
40
+ self
41
+ end
42
+
43
+ def push(node)
44
+ @added_nodes.push(node)
45
+ end
46
+
47
+ private
48
+
49
+ def loaded_enum
50
+ Enumerator.new do |yielder|
51
+ collection.each_loaded do |element|
52
+ yielder.yield(element) unless deleted?(element)
53
+ end
54
+
55
+ added_nodes.each do |node|
56
+ yielder.yield(node)
57
+ end
58
+ end
59
+ end
60
+
61
+ def enum
62
+ Enumerator.new do |yielder|
63
+ collection.each do |element|
64
+ yielder.yield(element) unless deleted?(element)
65
+ end
66
+
67
+ added_nodes.each do |node|
68
+ yielder.yield(node)
69
+ end
70
+ end
71
+ end
72
+
73
+ def deleted?(node)
74
+ @deleted_nodes.include?(node)
75
+ end
76
+ end
77
+ end