praxis-mapper 3.4.0 → 4.0
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/.travis.yml +2 -1
- data/CHANGELOG.md +20 -1
- data/lib/praxis-mapper.rb +7 -0
- data/lib/praxis-mapper/connection_factories/sequel.rb +66 -0
- data/lib/praxis-mapper/connection_factories/simple.rb +27 -0
- data/lib/praxis-mapper/connection_manager.rb +29 -50
- data/lib/praxis-mapper/identity_map.rb +37 -13
- data/lib/praxis-mapper/identity_map_extensions/persistence.rb +83 -0
- data/lib/praxis-mapper/query/base.rb +1 -1
- data/lib/praxis-mapper/query/sequel.rb +11 -0
- data/lib/praxis-mapper/sequel_compat.rb +99 -0
- data/lib/praxis-mapper/version.rb +1 -1
- data/praxis-mapper.gemspec +2 -0
- data/spec/factories/all.rb +32 -0
- data/spec/praxis-mapper/connection_factories/sequel_spec.rb +67 -0
- data/spec/praxis-mapper/connection_factories/simple_spec.rb +29 -0
- data/spec/praxis-mapper/connection_manager_spec.rb +49 -71
- data/spec/praxis-mapper/identity_map_extensions/persistence_spec.rb +122 -0
- data/spec/praxis-mapper/sequel_compat_spec.rb +106 -0
- data/spec/spec_helper.rb +19 -4
- data/spec/support/spec_sequel_models.rb +110 -0
- metadata +46 -2
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Praxis::Mapper
|
4
|
+
module SequelCompat
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
attr_accessor :_resource
|
9
|
+
attr_accessor :_query
|
10
|
+
attr_accessor :identity_map
|
11
|
+
|
12
|
+
@repository_name = :default
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
|
17
|
+
def identities
|
18
|
+
[primary_key]
|
19
|
+
end
|
20
|
+
|
21
|
+
def finalized?
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
def associations
|
26
|
+
orig = self.association_reflections.clone
|
27
|
+
|
28
|
+
orig.each do |k,v|
|
29
|
+
v[:model] = v.associated_class
|
30
|
+
if v.respond_to?(:primary_key)
|
31
|
+
v[:primary_key] = v.primary_key
|
32
|
+
else
|
33
|
+
# FIXME: figure out exactly what to do here.
|
34
|
+
# not super critical, as we can't track these associations
|
35
|
+
# directly, but it would be nice to traverse these
|
36
|
+
# properly.
|
37
|
+
v[:primary_key] = :unsupported
|
38
|
+
end
|
39
|
+
end
|
40
|
+
orig
|
41
|
+
end
|
42
|
+
|
43
|
+
def repository_name(name=nil)
|
44
|
+
return @repository_name if name.nil?
|
45
|
+
|
46
|
+
@repository_name = name
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
def _load_associated_objects(opts, dynamic_opts=OPTS)
|
53
|
+
return super if self.identity_map.nil?
|
54
|
+
target = opts.associated_class
|
55
|
+
key = opts[:key]
|
56
|
+
|
57
|
+
case opts[:type]
|
58
|
+
when :many_to_one
|
59
|
+
val = if key.kind_of?(Array)
|
60
|
+
@values.values_at(*key)
|
61
|
+
else
|
62
|
+
@values[key]
|
63
|
+
end
|
64
|
+
return nil if val.nil?
|
65
|
+
self.identity_map.get(target, target.primary_key => val)
|
66
|
+
when :one_to_many
|
67
|
+
self.identity_map.all(target, key => [pk] )
|
68
|
+
when :many_to_many
|
69
|
+
# OPTIMIZE: cache this result
|
70
|
+
join_model = opts[:join_model].constantize
|
71
|
+
|
72
|
+
left_key = opts[:left_key]
|
73
|
+
right_key = opts[:right_key]
|
74
|
+
|
75
|
+
right_values = self.identity_map.
|
76
|
+
all(join_model, left_key => Array(values[primary_key])).
|
77
|
+
collect(&right_key)
|
78
|
+
|
79
|
+
self.identity_map.all(target, target.primary_key => right_values )
|
80
|
+
else
|
81
|
+
raise "#{opts[:type]} is not currently supported"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
def identities
|
87
|
+
self.class.identities.each_with_object(Hash.new) do |identity, hash|
|
88
|
+
case identity
|
89
|
+
when Symbol
|
90
|
+
hash[identity] = values[identity].freeze
|
91
|
+
else
|
92
|
+
hash[identity] = values.values_at(*identity).collect(&:freeze)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
data/praxis-mapper.gemspec
CHANGED
@@ -34,4 +34,6 @@ Gem::Specification.new do |spec|
|
|
34
34
|
spec.add_development_dependency(%q<pry-byebug>, ["~> 1"])
|
35
35
|
spec.add_development_dependency(%q<pry-stack_explorer>, ["~> 0"])
|
36
36
|
spec.add_development_dependency(%q<fuubar>, ["~> 1"])
|
37
|
+
spec.add_development_dependency('sqlite3')
|
38
|
+
spec.add_development_dependency('factory_girl')
|
37
39
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
|
3
|
+
to_create { |i| i.save }
|
4
|
+
|
5
|
+
factory :user, class: UserModel, aliases: [:author] do
|
6
|
+
name { /[:name:]/.gen }
|
7
|
+
email { /[:email:]/.gen }
|
8
|
+
end
|
9
|
+
|
10
|
+
factory :post, class: PostModel do
|
11
|
+
title { /\w+/.gen }
|
12
|
+
body { /\w+/.gen }
|
13
|
+
author
|
14
|
+
end
|
15
|
+
|
16
|
+
factory :comment, class: CommentModel do
|
17
|
+
author
|
18
|
+
post
|
19
|
+
end
|
20
|
+
|
21
|
+
factory :composite, class: CompositeIdSequelModel do
|
22
|
+
id { /\w+/.gen }
|
23
|
+
type { /\w+/.gen }
|
24
|
+
|
25
|
+
name { /\w+/.gen }
|
26
|
+
end
|
27
|
+
|
28
|
+
factory :other, class: OtherSequelModel do
|
29
|
+
composite
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Praxis::Mapper::ConnectionFactories::Sequel do
|
4
|
+
|
5
|
+
let(:database) { Sequel.mock}
|
6
|
+
let(:thread) { Thread.current }
|
7
|
+
let(:connection_manager) { double('Praxis::Mapper::ConnectionManager', thread: thread) }
|
8
|
+
|
9
|
+
subject(:factory) { described_class.new(connection:database) }
|
10
|
+
|
11
|
+
context 'checkout' do
|
12
|
+
it 'returns the connection' do
|
13
|
+
factory.checkout(connection_manager).should be database
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'allocates a connection to the thread of the manager' do
|
17
|
+
database.pool.allocated.should_not have_key(thread)
|
18
|
+
factory.checkout(connection_manager)
|
19
|
+
database.pool.allocated.should have_key(thread)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'does not acquire a new connection if the thread already has one' do
|
23
|
+
connection = double('connection')
|
24
|
+
database.pool.allocated[thread] = connection
|
25
|
+
|
26
|
+
factory.checkout(connection_manager)
|
27
|
+
database.pool.allocated[thread].should be connection
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'release' do
|
33
|
+
before do
|
34
|
+
factory.checkout(connection_manager)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'releases the connection' do
|
38
|
+
database.pool.allocated.should have_key(thread)
|
39
|
+
factory.release(connection_manager, database)
|
40
|
+
database.pool.allocated.should_not have_key(thread)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'across multiple threads' do
|
45
|
+
before do
|
46
|
+
factory.checkout(connection_manager)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'acquires a connection in a separate thread' do
|
50
|
+
database.pool.allocated.should have_key(thread)
|
51
|
+
|
52
|
+
thread_2 = Thread.new do
|
53
|
+
connection_manager_2 = Praxis::Mapper::ConnectionManager.new
|
54
|
+
database.pool.allocated.should_not have_key(Thread.current)
|
55
|
+
database.pool.allocated.should have_key(thread)
|
56
|
+
|
57
|
+
factory.checkout(connection_manager_2)
|
58
|
+
database.pool.allocated.should have_key(Thread.current)
|
59
|
+
database.pool.allocated.should have_key(thread)
|
60
|
+
end
|
61
|
+
|
62
|
+
thread_2.join
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Praxis::Mapper::ConnectionFactories::Simple do
|
4
|
+
|
5
|
+
let(:connection) { double("connection") }
|
6
|
+
|
7
|
+
let(:connection_manager) { double('Praxis::Mapper::ConnectionManager') }
|
8
|
+
|
9
|
+
context 'with a raw connection' do
|
10
|
+
subject(:factory) { described_class.new(connection:connection) }
|
11
|
+
|
12
|
+
it 'returns the connection on checkout' do
|
13
|
+
factory.checkout(connection_manager).should be connection
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'with a proc' do
|
19
|
+
let(:block) { Proc.new { connection } }
|
20
|
+
|
21
|
+
subject(:factory) { described_class.new(&block) }
|
22
|
+
|
23
|
+
it 'calls the block on checkout' do
|
24
|
+
block.should_receive(:call).and_call_original
|
25
|
+
factory.checkout(connection_manager).should be connection
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -1,117 +1,95 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
2
|
|
3
3
|
class DummyFactory
|
4
|
+
attr_reader :opts
|
4
5
|
def initialize(opts)
|
5
6
|
@opts = opts
|
6
7
|
end
|
7
8
|
|
8
|
-
def
|
9
|
-
@opts
|
9
|
+
def checkout(connection_manager)
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
13
|
-
end
|
14
|
-
|
15
|
-
def release(connection)
|
12
|
+
def release(connection_manager, connection)
|
16
13
|
end
|
17
14
|
|
18
15
|
end
|
19
16
|
|
20
|
-
|
21
17
|
describe Praxis::Mapper::ConnectionManager do
|
22
18
|
let(:mock_connection) { double("connection") }
|
23
19
|
|
24
20
|
let(:default_hash) { Hash.new }
|
25
21
|
let(:factory_opts) { {:foo => "bar"} }
|
26
|
-
let(:config) { {:dummy => {:connection_factory => "DummyFactory", :connection_opts => factory_opts}} }
|
27
22
|
|
23
|
+
let(:dummy_connection) { double("dummy connection")}
|
28
24
|
let(:dummy_factory_mock) { double("dummy_factory")}
|
29
25
|
|
30
|
-
subject { Praxis::Mapper::ConnectionManager }
|
31
|
-
|
26
|
+
subject(:connection_manager) { Praxis::Mapper::ConnectionManager }
|
27
|
+
|
32
28
|
before do
|
33
|
-
|
29
|
+
opts = factory_opts
|
30
|
+
block = Proc.new { mock_connection }
|
31
|
+
|
32
|
+
Praxis::Mapper::ConnectionManager.setup do
|
33
|
+
repository :foo, &block
|
34
|
+
repository :bar, :factory => "DummyFactory", :opts => opts
|
35
|
+
end
|
34
36
|
end
|
35
37
|
|
36
|
-
it '
|
37
|
-
|
38
|
+
it 'supports proc-based repostories' do
|
39
|
+
subject.repository(:foo)[:factory].should be_kind_of(Praxis::Mapper::ConnectionFactories::Simple)
|
40
|
+
end
|
38
41
|
|
39
|
-
|
40
|
-
repository[:
|
42
|
+
it 'supports config-based repositories' do
|
43
|
+
subject.repository(:bar)[:factory].should be_kind_of(DummyFactory)
|
44
|
+
subject.repository(:bar)[:factory].opts.should == factory_opts
|
41
45
|
end
|
42
46
|
|
43
|
-
context
|
44
|
-
let(:dummy_connection) { double("dummy connection")}
|
45
|
-
|
46
|
-
before do
|
47
|
-
opts = factory_opts
|
48
|
-
|
49
|
-
block = Proc.new { mock_connection }
|
50
|
-
Praxis::Mapper::ConnectionManager.setup do
|
51
|
-
repository :foo, &block
|
52
|
-
repository :bar, :connection_factory => "DummyFactory", :connection_opts => opts
|
53
|
-
end
|
54
|
-
end
|
47
|
+
context 'getting connections' do
|
55
48
|
|
56
|
-
|
57
|
-
subject.repository(:foo)[:connection_factory].should be_kind_of(Proc)
|
58
|
-
end
|
49
|
+
subject { Praxis::Mapper::ConnectionManager.new }
|
59
50
|
|
60
|
-
it '
|
61
|
-
subject.
|
62
|
-
subject.repository(:bar)[:connection_factory].opts.should == factory_opts
|
51
|
+
it 'gets connections from proc-based repositories' do
|
52
|
+
subject.checkout(:foo).should == mock_connection
|
63
53
|
end
|
64
54
|
|
65
|
-
|
66
|
-
|
67
|
-
subject
|
68
|
-
|
69
|
-
it 'gets connections from proc-based repositories' do
|
70
|
-
subject.checkout(:foo).should == mock_connection
|
71
|
-
end
|
55
|
+
it 'gets connections from config-based repositories' do
|
56
|
+
DummyFactory.any_instance.should_receive(:checkout).with(subject).and_return(dummy_connection)
|
57
|
+
subject.checkout(:bar).should == dummy_connection
|
58
|
+
end
|
72
59
|
|
73
|
-
|
74
|
-
DummyFactory.any_instance.should_receive(:checkout).and_return(dummy_connection)
|
75
|
-
subject.checkout(:bar).should == dummy_connection
|
76
|
-
end
|
60
|
+
end
|
77
61
|
|
78
|
-
|
62
|
+
context 'releasing connections' do
|
63
|
+
subject { Praxis::Mapper::ConnectionManager.new }
|
79
64
|
|
80
|
-
|
81
|
-
subject
|
65
|
+
it 'releases connections from config-based repositories' do
|
66
|
+
DummyFactory.any_instance.should_receive(:checkout).with(subject).exactly(2).times.and_return(dummy_connection)
|
67
|
+
DummyFactory.any_instance.should_receive(:release).with(subject, dummy_connection).and_return(true)
|
82
68
|
|
83
|
-
|
69
|
+
subject.checkout(:bar)
|
70
|
+
subject.checkout(:bar)
|
84
71
|
|
85
|
-
|
86
|
-
DummyFactory.any_instance.should_receive(:release).with(dummy_connection).and_return(true)
|
72
|
+
subject.release(:bar)
|
87
73
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
subject.release(:bar)
|
92
|
-
|
93
|
-
subject.checkout(:bar)
|
94
|
-
end
|
74
|
+
subject.checkout(:bar)
|
75
|
+
end
|
95
76
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
77
|
+
it 'releases connections from proc-based repositories' do
|
78
|
+
subject.checkout(:foo)
|
79
|
+
subject.release(:foo)
|
80
|
+
end
|
100
81
|
|
101
|
-
|
102
|
-
|
103
|
-
|
82
|
+
it 'releases all connections' do
|
83
|
+
DummyFactory.any_instance.should_receive(:checkout).with(subject).exactly(1).times.and_return(dummy_connection)
|
84
|
+
DummyFactory.any_instance.should_receive(:release).with(subject, dummy_connection).and_return(true)
|
104
85
|
|
105
|
-
|
86
|
+
subject.checkout(:bar)
|
106
87
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
subject.release
|
111
|
-
end
|
88
|
+
subject.should_not_receive(:release_one).with(:foo).and_call_original
|
89
|
+
subject.should_receive(:release_one).with(:bar).and_call_original
|
112
90
|
|
91
|
+
subject.release
|
113
92
|
end
|
114
93
|
|
115
|
-
|
116
94
|
end
|
117
95
|
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Praxis::Mapper::IdentityMapExtensions::Persistence do
|
4
|
+
|
5
|
+
let(:user) { UserModel.create }
|
6
|
+
let(:post) { PostModel.create(title: 'title', author: user) }
|
7
|
+
|
8
|
+
before do
|
9
|
+
identity_map.add_record(post)
|
10
|
+
identity_map.add_record(user)
|
11
|
+
identity_map.reindex!(PostModel, :author_id)
|
12
|
+
end
|
13
|
+
|
14
|
+
subject(:identity_map) { Praxis::Mapper::IdentityMap.new }
|
15
|
+
|
16
|
+
context '#attach(record)' do
|
17
|
+
context 'for a record that is missing an identity' do
|
18
|
+
let(:post) { PostModel.new(title: 'title') }
|
19
|
+
|
20
|
+
it 'saves the record before adding it ' do
|
21
|
+
post.should_receive(:save).and_call_original
|
22
|
+
identity_map.should_receive(:add_record).with(post).and_call_original
|
23
|
+
|
24
|
+
identity_map.attach(post)
|
25
|
+
|
26
|
+
identity_map.get(PostModel, id: post.id).should be post
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'for a record that is not missing an identity' do
|
31
|
+
let(:post) { PostModel.create(title: 'title') }
|
32
|
+
|
33
|
+
it 'does not save the record before adding it' do
|
34
|
+
post.should_not_receive(:save).and_call_original
|
35
|
+
identity_map.should_receive(:add_record).with(post).and_call_original
|
36
|
+
|
37
|
+
identity_map.attach(post)
|
38
|
+
|
39
|
+
identity_map.get(PostModel, id: post.id).should be post
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context '#flush!(object=nil)' do
|
45
|
+
|
46
|
+
context 'with a single record' do
|
47
|
+
it 'saves the changes' do
|
48
|
+
post.title = 'something else'
|
49
|
+
|
50
|
+
identity_map.flush!(post)
|
51
|
+
post.modified?.should be false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'for an entire model class' do
|
56
|
+
it 'flushes every changed record' do
|
57
|
+
post.title = 'something else'
|
58
|
+
|
59
|
+
identity_map.flush!(PostModel)
|
60
|
+
post.modified?.should be false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'with no object' do
|
65
|
+
it 'flushes every class' do
|
66
|
+
identity_map.should_receive(:flush!).with(no_args).and_call_original
|
67
|
+
identity_map.should_receive(:flush!).with(PostModel)
|
68
|
+
identity_map.should_receive(:flush!).with(UserModel)
|
69
|
+
|
70
|
+
|
71
|
+
identity_map.flush!
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context '#remove(record)' do
|
77
|
+
it 'detaches and deletes the record' do
|
78
|
+
identity_map.should_receive(:detach).with(post).and_call_original
|
79
|
+
post.should_receive(:delete)
|
80
|
+
|
81
|
+
identity_map.remove(post)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context '#detach(record)' do
|
86
|
+
|
87
|
+
it 'unsets record identity_map and deindexes the record' do
|
88
|
+
identity_map.should_receive(:deindex).with(post)
|
89
|
+
identity_map.detach(post)
|
90
|
+
post.identity_map.should be nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context '#deindex(record)' do
|
95
|
+
let!(:original_id) { post.id }
|
96
|
+
|
97
|
+
before do
|
98
|
+
post.id = 100
|
99
|
+
|
100
|
+
# build secondary index and ensure it's populated correctly
|
101
|
+
identity_map.all(PostModel, title: [post.title]).should include post
|
102
|
+
|
103
|
+
identity_map.deindex(post)
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'cleans up all-rows index' do
|
107
|
+
identity_map.all(PostModel).should be_empty
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'cleans up identity indexes' do
|
111
|
+
identity_map.all(PostModel, id: [original_id]).should be_empty
|
112
|
+
identity_map.all(PostModel, id: [post.id]).should be_empty
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'cleans up secondary indexes' do
|
116
|
+
identity_map.all(PostModel, title: [post.title]).should_not include post
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
end
|