n_1_finder 0.0.2
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 +7 -0
- data/Readme.md +74 -0
- data/lib/n_1_finder.rb +111 -0
- data/lib/n_1_finder/adapters/active_record_adapter.rb +49 -0
- data/lib/n_1_finder/adapters/base_adapter.rb +46 -0
- data/lib/n_1_finder/adapters/null_adapter.rb +16 -0
- data/lib/n_1_finder/adapters/sequel_adapter.rb +33 -0
- data/lib/n_1_finder/logger.rb +56 -0
- data/lib/n_1_finder/middleware.rb +13 -0
- data/lib/n_1_finder/n_1_query.rb +38 -0
- data/lib/n_1_finder/query.rb +57 -0
- data/lib/n_1_finder/storage.rb +23 -0
- data/lib/n_1_finder/version.rb +4 -0
- data/spec/n_1_finder/adapters/active_record_adapter_spec.rb +37 -0
- data/spec/n_1_finder/adapters/null_adapter_spec.rb +5 -0
- data/spec/n_1_finder/adapters/sequel_adapter_spec.rb +5 -0
- data/spec/n_1_finder/features/active_record_mysql_spec.rb +12 -0
- data/spec/n_1_finder/features/active_record_postgres_spec.rb +12 -0
- data/spec/n_1_finder/features/active_record_sqlite_spec.rb +12 -0
- data/spec/n_1_finder/features/sequel_mysql_spec.rb +12 -0
- data/spec/n_1_finder/features/sequel_postgres_spec.rb +13 -0
- data/spec/n_1_finder/features/sequel_sqlite_spec.rb +12 -0
- data/spec/n_1_finder/logger_spec.rb +40 -0
- data/spec/n_1_finder/middleware_spec.rb +15 -0
- data/spec/n_1_finder/n_1_query_spec.rb +28 -0
- data/spec/n_1_finder/query_spec.rb +81 -0
- data/spec/n_1_finder/storage_spec.rb +23 -0
- data/spec/n_1_finder_spec.rb +67 -0
- data/spec/secrets.yml.example +9 -0
- data/spec/shared_examples/adapter.rb +37 -0
- data/spec/spec_helper.rb +61 -0
- data/spec/support/n_1_helpers.rb +56 -0
- data/spec/support/orm_helpers.rb +89 -0
- data/spec/support/test_active_record_migration.rb +46 -0
- data/spec/support/test_active_record_models.rb +56 -0
- data/spec/support/test_no_connection_models.rb +57 -0
- data/spec/support/test_sequel_migration.rb +39 -0
- data/spec/support/test_sequel_models.rb +65 -0
- metadata +81 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
RSpec.shared_examples 'adapter' do
|
2
|
+
let(:service) { described_class.new(nil) }
|
3
|
+
|
4
|
+
describe '#exec' do
|
5
|
+
before do
|
6
|
+
allow(service).to receive(:set_trap)
|
7
|
+
allow(service).to receive(:remove_trap)
|
8
|
+
end
|
9
|
+
subject { service.exec(&block) }
|
10
|
+
|
11
|
+
context 'when success executed block' do
|
12
|
+
let(:block) { -> { 'OK' } }
|
13
|
+
|
14
|
+
it 'returns passed block execution result' do
|
15
|
+
expect(subject).to eq 'OK'
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'clears after self' do
|
19
|
+
expect(service).to receive(:remove_trap)
|
20
|
+
subject
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'when block raises an error' do
|
25
|
+
let(:block) { -> { raise 'error' } }
|
26
|
+
|
27
|
+
it 'returns passed block execution result' do
|
28
|
+
expect { subject }.to raise_error 'error'
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'clears after self' do
|
32
|
+
expect(service).to receive(:remove_trap)
|
33
|
+
expect { subject }.to raise_error 'error'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'pry-nav'
|
2
|
+
require 'bundler/setup'
|
3
|
+
Bundler.setup
|
4
|
+
|
5
|
+
require 'support/orm_helpers'
|
6
|
+
require 'active_record'
|
7
|
+
require 'sequel'
|
8
|
+
require 'support/n_1_helpers.rb'
|
9
|
+
require 'support/test_active_record_models'
|
10
|
+
require 'support/test_active_record_migration'
|
11
|
+
require 'support/test_sequel_models'
|
12
|
+
require 'support/test_sequel_migration'
|
13
|
+
require 'support/test_no_connection_models'
|
14
|
+
require 'n_1_finder'
|
15
|
+
|
16
|
+
include ORMHelpers
|
17
|
+
|
18
|
+
RSpec.configure do |config|
|
19
|
+
config.expect_with :rspec do |expectations|
|
20
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
21
|
+
end
|
22
|
+
|
23
|
+
config.mock_with :rspec do |mocks|
|
24
|
+
mocks.verify_partial_doubles = true
|
25
|
+
end
|
26
|
+
|
27
|
+
config.disable_monkey_patching!
|
28
|
+
|
29
|
+
config.default_formatter = 'doc' if config.files_to_run.one?
|
30
|
+
|
31
|
+
config.around(:each, type: :active_record_mysql) do |example|
|
32
|
+
use_active_record(:mysql) { example.run }
|
33
|
+
end
|
34
|
+
|
35
|
+
config.around(:each, type: :active_record_pg) do |example|
|
36
|
+
use_active_record(:pg) { example.run }
|
37
|
+
end
|
38
|
+
|
39
|
+
config.around(:each, type: :active_record_sqlite) do |example|
|
40
|
+
use_active_record(:sqlite) { example.run }
|
41
|
+
end
|
42
|
+
|
43
|
+
config.around(:each, type: :sequel_mysql) do |example|
|
44
|
+
use_sequel(:mysql) { example.run }
|
45
|
+
end
|
46
|
+
|
47
|
+
config.around(:each, type: :sequel_pg) do |example|
|
48
|
+
use_sequel(:pg) { example.run }
|
49
|
+
end
|
50
|
+
|
51
|
+
config.around(:each, type: :sequel_sqlite) do |example|
|
52
|
+
use_sequel(:sqlite) { example.run }
|
53
|
+
end
|
54
|
+
|
55
|
+
config.around(:each, type: :no_connection) do |example|
|
56
|
+
use_no_connection { example.run }
|
57
|
+
end
|
58
|
+
|
59
|
+
config.order = :random
|
60
|
+
Kernel.srand config.seed
|
61
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module N1Helpers
|
2
|
+
def populate_database
|
3
|
+
users = create_new_users
|
4
|
+
posts = create_new_posts(users)
|
5
|
+
documents = create_new_documents(posts)
|
6
|
+
create_new_links(documents)
|
7
|
+
end
|
8
|
+
|
9
|
+
def n_1_heavy_block
|
10
|
+
proc do
|
11
|
+
users = users_class.all.to_a # 1 query
|
12
|
+
posts = users.map(&:posts).to_a.flatten # 2 queries, N+1 should be detected
|
13
|
+
documents = posts.map(&:document).to_a # 4 queries, N+1 should be detected
|
14
|
+
documents.map(&:links).map(&:to_a).flatten # 4 queries, N+1 should be detected
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def create_new_users
|
21
|
+
[
|
22
|
+
users_class.create,
|
23
|
+
users_class.create
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_new_posts(users)
|
28
|
+
user_1, user_2 = users
|
29
|
+
[
|
30
|
+
posts_class.create(user_id: user_1.id),
|
31
|
+
posts_class.create(user_id: user_1.id),
|
32
|
+
posts_class.create(user_id: user_2.id),
|
33
|
+
posts_class.create(user_id: user_2.id)
|
34
|
+
]
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_new_documents(posts)
|
38
|
+
post_1_user_1, post_2_user_1, post_1_user_2, post_2_user_2 = posts
|
39
|
+
[
|
40
|
+
documents_class.create(post_id: post_1_user_1.id),
|
41
|
+
documents_class.create(post_id: post_2_user_1.id),
|
42
|
+
documents_class.create(post_id: post_1_user_2.id),
|
43
|
+
documents_class.create(post_id: post_2_user_2.id)
|
44
|
+
]
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_new_links(documents)
|
48
|
+
document_1, document_2, document_3, document_4 = documents
|
49
|
+
[
|
50
|
+
links_class.create(document_id: document_1.id),
|
51
|
+
links_class.create(document_id: document_2.id),
|
52
|
+
links_class.create(document_id: document_3.id),
|
53
|
+
links_class.create(document_id: document_4.id)
|
54
|
+
]
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module ORMHelpers
|
4
|
+
SECRETS = YAML.load(File.open('spec/secrets.yml'))
|
5
|
+
|
6
|
+
DB_PARAMS = {
|
7
|
+
active_record: {
|
8
|
+
sqlite: {
|
9
|
+
adapter: 'sqlite3',
|
10
|
+
database: ':memory:'
|
11
|
+
},
|
12
|
+
pg: {
|
13
|
+
adapter: 'postgresql',
|
14
|
+
host: 'localhost',
|
15
|
+
username: SECRETS[:pg][:username].to_s,
|
16
|
+
password: SECRETS[:pg][:password].to_s,
|
17
|
+
database: SECRETS[:pg][:database].to_s
|
18
|
+
},
|
19
|
+
mysql: {
|
20
|
+
adapter: 'mysql',
|
21
|
+
host: 'localhost',
|
22
|
+
username: SECRETS[:mysql][:username].to_s,
|
23
|
+
password: SECRETS[:mysql][:password].to_s,
|
24
|
+
database: SECRETS[:mysql][:database].to_s
|
25
|
+
}
|
26
|
+
},
|
27
|
+
sequel: {
|
28
|
+
sqlite: {
|
29
|
+
adapter: 'sqlite',
|
30
|
+
database: ':memory:'
|
31
|
+
},
|
32
|
+
pg: {
|
33
|
+
adapter: 'postgres',
|
34
|
+
host: 'localhost',
|
35
|
+
username: SECRETS[:pg][:username].to_s,
|
36
|
+
password: SECRETS[:pg][:password].to_s,
|
37
|
+
database: SECRETS[:pg][:database].to_s
|
38
|
+
},
|
39
|
+
mysql: {
|
40
|
+
adapter: 'mysql',
|
41
|
+
host: 'localhost',
|
42
|
+
username: SECRETS[:mysql][:username].to_s,
|
43
|
+
password: SECRETS[:mysql][:password].to_s,
|
44
|
+
database: SECRETS[:mysql][:database].to_s
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}.freeze
|
48
|
+
|
49
|
+
def connect_sequel(adapter)
|
50
|
+
Sequel::Model.db = Sequel.connect(DB_PARAMS[:sequel][adapter])
|
51
|
+
TestSequelMigration.up
|
52
|
+
end
|
53
|
+
|
54
|
+
def connect_active_record(adapter)
|
55
|
+
ActiveRecord::Base.establish_connection(DB_PARAMS[:active_record][adapter])
|
56
|
+
silence_stream(STDOUT) { TestActiveRecordMigration.up }
|
57
|
+
end
|
58
|
+
|
59
|
+
def use_active_record(adapter)
|
60
|
+
N1Finder.orm = :active_record
|
61
|
+
connect_active_record(adapter)
|
62
|
+
|
63
|
+
extend TestActiveRecordModels
|
64
|
+
extend N1Helpers
|
65
|
+
|
66
|
+
ActiveRecord::Base.transaction do
|
67
|
+
yield
|
68
|
+
raise ActiveRecord::Rollback
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def use_sequel(adapter)
|
73
|
+
N1Finder.orm = :sequel
|
74
|
+
connect_sequel(adapter)
|
75
|
+
|
76
|
+
extend TestSequelModels
|
77
|
+
extend N1Helpers
|
78
|
+
|
79
|
+
Sequel::Model.db.transaction(rollback: :always) do
|
80
|
+
yield
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def use_no_connection
|
85
|
+
N1Finder.instance_variable_set(:@orm, :no_connection)
|
86
|
+
extend TestNoConnectionModels
|
87
|
+
yield
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class TestActiveRecordMigration < ActiveRecord::Migration
|
2
|
+
class << self
|
3
|
+
def up
|
4
|
+
create_users
|
5
|
+
create_posts
|
6
|
+
create_documents
|
7
|
+
create_links
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def create_users
|
13
|
+
without_exists_error { create_table :users }
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_posts
|
17
|
+
without_exists_error do
|
18
|
+
create_table :posts do |t|
|
19
|
+
t.integer :user_id
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_documents
|
25
|
+
without_exists_error do
|
26
|
+
create_table :documents do |t|
|
27
|
+
t.integer :post_id
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_links
|
33
|
+
without_exists_error do
|
34
|
+
create_table :links do |t|
|
35
|
+
t.integer :document_id
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def without_exists_error
|
41
|
+
yield
|
42
|
+
rescue ActiveRecord::StatementInvalid => error
|
43
|
+
raise error unless error.message.include?('already exists')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module TestActiveRecordModels
|
2
|
+
def users_class
|
3
|
+
@users_class ||= begin
|
4
|
+
posts = posts_class
|
5
|
+
Class.new(ActiveRecord::Base) do
|
6
|
+
self.table_name = 'users'
|
7
|
+
|
8
|
+
def self.name
|
9
|
+
'User'
|
10
|
+
end
|
11
|
+
|
12
|
+
has_many :posts, anonymous_class: posts
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def posts_class
|
18
|
+
@posts_class ||= begin
|
19
|
+
documents = documents_class
|
20
|
+
Class.new(ActiveRecord::Base) do
|
21
|
+
self.table_name = 'posts'
|
22
|
+
|
23
|
+
def self.name
|
24
|
+
'Post'
|
25
|
+
end
|
26
|
+
|
27
|
+
has_one :document, anonymous_class: documents
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def documents_class
|
33
|
+
@documents_class ||= begin
|
34
|
+
links = links_class
|
35
|
+
Class.new(ActiveRecord::Base) do
|
36
|
+
self.table_name = 'documents'
|
37
|
+
|
38
|
+
def self.name
|
39
|
+
'Document'
|
40
|
+
end
|
41
|
+
|
42
|
+
has_many :links, anonymous_class: links
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def links_class
|
48
|
+
@links_class ||= Class.new(ActiveRecord::Base) do
|
49
|
+
self.table_name = 'links'
|
50
|
+
|
51
|
+
def self.name
|
52
|
+
'Link'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module TestNoConnectionModels
|
2
|
+
module BaseNoConnectionModel
|
3
|
+
def self.included(base)
|
4
|
+
base.include InstanceMethods
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module InstanceMethods
|
9
|
+
def id
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def create(*)
|
15
|
+
new
|
16
|
+
end
|
17
|
+
|
18
|
+
def all
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def users_class
|
24
|
+
Class.new do
|
25
|
+
include BaseNoConnectionModel
|
26
|
+
|
27
|
+
def posts
|
28
|
+
[]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def posts_class
|
34
|
+
Class.new do
|
35
|
+
include BaseNoConnectionModel
|
36
|
+
|
37
|
+
def document
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def documents_class
|
43
|
+
Class.new do
|
44
|
+
include BaseNoConnectionModel
|
45
|
+
|
46
|
+
def links
|
47
|
+
[]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def links_class
|
53
|
+
Class.new do
|
54
|
+
include BaseNoConnectionModel
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class TestSequelMigration
|
2
|
+
class << self
|
3
|
+
def up
|
4
|
+
create_users
|
5
|
+
create_posts
|
6
|
+
create_documents
|
7
|
+
create_links
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def create_users
|
13
|
+
Sequel::Model.db.create_table? :users do
|
14
|
+
primary_key :id
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_posts
|
19
|
+
Sequel::Model.db.create_table? :posts do
|
20
|
+
primary_key :id
|
21
|
+
Integer :user_id
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_documents
|
26
|
+
Sequel::Model.db.create_table? :documents do
|
27
|
+
primary_key :id
|
28
|
+
Integer :post_id
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_links
|
33
|
+
Sequel::Model.db.create_table? :links do
|
34
|
+
primary_key :id
|
35
|
+
Integer :document_id
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|