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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/CODE_OF_CONDUCT.md +28 -0
- data/Gemfile.lock +32 -2
- data/MissingFeatures.md +64 -0
- data/README.md +141 -72
- data/Rakefile +29 -0
- data/TODO.md +16 -11
- data/features/env.rb +57 -0
- data/features/example.feature +121 -0
- data/features/step_definitions/example_steps.rb +46 -0
- data/lib/sequel_mapper.rb +6 -2
- data/lib/sequel_mapper/abstract_record.rb +53 -0
- data/lib/sequel_mapper/association_loaders.rb +52 -0
- data/lib/sequel_mapper/collection_mutability_proxy.rb +77 -0
- data/lib/sequel_mapper/configurations/conventional_association_configuration.rb +187 -0
- data/lib/sequel_mapper/configurations/conventional_configuration.rb +269 -0
- data/lib/sequel_mapper/dataset.rb +37 -0
- data/lib/sequel_mapper/deleted_record.rb +16 -0
- data/lib/sequel_mapper/dirty_map.rb +31 -0
- data/lib/sequel_mapper/graph_loader.rb +48 -0
- data/lib/sequel_mapper/graph_serializer.rb +107 -0
- data/lib/sequel_mapper/identity_map.rb +22 -0
- data/lib/sequel_mapper/lazy_object_proxy.rb +51 -0
- data/lib/sequel_mapper/many_to_many_association.rb +181 -0
- data/lib/sequel_mapper/many_to_one_association.rb +60 -0
- data/lib/sequel_mapper/mapper_facade.rb +180 -0
- data/lib/sequel_mapper/one_to_many_association.rb +51 -0
- data/lib/sequel_mapper/public_conveniencies.rb +27 -0
- data/lib/sequel_mapper/query_order.rb +32 -0
- data/lib/sequel_mapper/queryable_lazy_dataset_loader.rb +70 -0
- data/lib/sequel_mapper/relation_mapping.rb +35 -0
- data/lib/sequel_mapper/serializer.rb +18 -0
- data/lib/sequel_mapper/short_inspection_string.rb +18 -0
- data/lib/sequel_mapper/subset_queries_proxy.rb +11 -0
- data/lib/sequel_mapper/upserted_record.rb +15 -0
- data/lib/sequel_mapper/version.rb +1 -1
- data/sequel_mapper.gemspec +3 -0
- data/spec/config_override_spec.rb +167 -0
- data/spec/custom_serializers_spec.rb +77 -0
- data/spec/deletion_spec.rb +104 -0
- data/spec/graph_persistence_spec.rb +83 -88
- data/spec/graph_traversal_spec.rb +32 -31
- data/spec/new_graph_persistence_spec.rb +69 -0
- data/spec/object_identity_spec.rb +70 -0
- data/spec/ordered_association_spec.rb +46 -16
- data/spec/persistence_efficiency_spec.rb +186 -0
- data/spec/predefined_queries_spec.rb +73 -0
- data/spec/proxying_spec.rb +25 -19
- data/spec/querying_spec.rb +24 -27
- data/spec/readme_examples_spec.rb +35 -0
- data/spec/sequel_mapper/abstract_record_spec.rb +179 -0
- data/spec/sequel_mapper/{association_proxy_spec.rb → collection_mutability_proxy_spec.rb} +6 -6
- data/spec/sequel_mapper/deleted_record_spec.rb +59 -0
- data/spec/sequel_mapper/lazy_object_proxy_spec.rb +140 -0
- data/spec/sequel_mapper/public_conveniencies_spec.rb +49 -0
- data/spec/sequel_mapper/queryable_lazy_dataset_loader_spec.rb +103 -0
- data/spec/sequel_mapper/upserted_record_spec.rb +59 -0
- data/spec/spec_helper.rb +7 -10
- data/spec/support/blog_schema.rb +29 -0
- data/spec/support/have_persisted_matcher.rb +19 -0
- data/spec/support/mapper_setup.rb +234 -0
- data/spec/support/mock_sequel.rb +0 -1
- data/spec/support/object_graph_setup.rb +106 -0
- data/spec/support/seed_data_setup.rb +122 -0
- data/spec/support/sequel_persistence_setup.rb +19 -0
- data/spec/support/sequel_test_support.rb +159 -0
- metadata +121 -15
- data/lib/sequel_mapper/association_proxy.rb +0 -54
- data/lib/sequel_mapper/belongs_to_association_proxy.rb +0 -27
- data/lib/sequel_mapper/graph.rb +0 -174
- data/lib/sequel_mapper/queryable_association_proxy.rb +0 -23
- data/spec/sequel_mapper/belongs_to_association_proxy_spec.rb +0 -65
- data/spec/support/graph_fixture.rb +0 -331
- 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
|
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
|
data/features/env.rb
ADDED
@@ -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
|
data/lib/sequel_mapper.rb
CHANGED
@@ -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
|