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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/Readme.md +74 -0
  3. data/lib/n_1_finder.rb +111 -0
  4. data/lib/n_1_finder/adapters/active_record_adapter.rb +49 -0
  5. data/lib/n_1_finder/adapters/base_adapter.rb +46 -0
  6. data/lib/n_1_finder/adapters/null_adapter.rb +16 -0
  7. data/lib/n_1_finder/adapters/sequel_adapter.rb +33 -0
  8. data/lib/n_1_finder/logger.rb +56 -0
  9. data/lib/n_1_finder/middleware.rb +13 -0
  10. data/lib/n_1_finder/n_1_query.rb +38 -0
  11. data/lib/n_1_finder/query.rb +57 -0
  12. data/lib/n_1_finder/storage.rb +23 -0
  13. data/lib/n_1_finder/version.rb +4 -0
  14. data/spec/n_1_finder/adapters/active_record_adapter_spec.rb +37 -0
  15. data/spec/n_1_finder/adapters/null_adapter_spec.rb +5 -0
  16. data/spec/n_1_finder/adapters/sequel_adapter_spec.rb +5 -0
  17. data/spec/n_1_finder/features/active_record_mysql_spec.rb +12 -0
  18. data/spec/n_1_finder/features/active_record_postgres_spec.rb +12 -0
  19. data/spec/n_1_finder/features/active_record_sqlite_spec.rb +12 -0
  20. data/spec/n_1_finder/features/sequel_mysql_spec.rb +12 -0
  21. data/spec/n_1_finder/features/sequel_postgres_spec.rb +13 -0
  22. data/spec/n_1_finder/features/sequel_sqlite_spec.rb +12 -0
  23. data/spec/n_1_finder/logger_spec.rb +40 -0
  24. data/spec/n_1_finder/middleware_spec.rb +15 -0
  25. data/spec/n_1_finder/n_1_query_spec.rb +28 -0
  26. data/spec/n_1_finder/query_spec.rb +81 -0
  27. data/spec/n_1_finder/storage_spec.rb +23 -0
  28. data/spec/n_1_finder_spec.rb +67 -0
  29. data/spec/secrets.yml.example +9 -0
  30. data/spec/shared_examples/adapter.rb +37 -0
  31. data/spec/spec_helper.rb +61 -0
  32. data/spec/support/n_1_helpers.rb +56 -0
  33. data/spec/support/orm_helpers.rb +89 -0
  34. data/spec/support/test_active_record_migration.rb +46 -0
  35. data/spec/support/test_active_record_models.rb +56 -0
  36. data/spec/support/test_no_connection_models.rb +57 -0
  37. data/spec/support/test_sequel_migration.rb +39 -0
  38. data/spec/support/test_sequel_models.rb +65 -0
  39. metadata +81 -0
@@ -0,0 +1,9 @@
1
+ :pg:
2
+ :username: XXX
3
+ :password: XXX
4
+ :database: XXX
5
+
6
+ :mysql:
7
+ :username: XXX
8
+ :password: XXX
9
+ :database: XXX
@@ -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
@@ -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