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