active_record_json_explain 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: edb1f284ede6aaa6bcb2b0e879125cbc55ec4deacad65968c2aa7821fc315e24
4
+ data.tar.gz: e6684d5f22de628426a19350988aede20352da0b933133ad1c973ea9d2e2deac
5
+ SHA512:
6
+ metadata.gz: 757c8c11dd1663b8aeefe9bc3195d94f2b34701f9cf6616928d075b0ce1b2f3b08e476c21fccf062de39332cffff6b36f418fd38766ffcd7c65567ca71029ea5
7
+ data.tar.gz: 6e68e87df83116b0b5a19adc140d790a3c1fded56a0796e9fbe26b8feb90f7637de7b923830b4b9e4d4a5668d921436db59f348769b01cbffaa9da6ca307a927
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /vendor
10
+ .DS_Store
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,33 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.7.0
3
+ Exclude:
4
+ - './active_record_json_explain.gemspec'
5
+ - 'vendor/**/*'
6
+
7
+ Metrics/ClassLength:
8
+ Max: 120
9
+
10
+ Metrics/BlockLength:
11
+ Exclude:
12
+ - 'spec/**/*'
13
+
14
+ Style/Documentation:
15
+ Enabled: false
16
+
17
+ Style/ParallelAssignment:
18
+ Enabled: false
19
+
20
+ Style/HashEachMethods:
21
+ Enabled: true
22
+
23
+ Style/HashTransformKeys:
24
+ Enabled: true
25
+
26
+ Style/HashTransformValues:
27
+ Enabled: true
28
+
29
+ Layout/LineLength:
30
+ Max: 100
31
+
32
+ Layout/EmptyLineAfterGuardClause:
33
+ Enabled: false
@@ -0,0 +1,20 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ services:
5
+ - mysql
6
+ - postgresql
7
+ rvm:
8
+ - 2.7.0
9
+ - 2.6.5
10
+ os:
11
+ - linux
12
+ before_install:
13
+ - gem install bundler -v 2.1.2
14
+ gemfile:
15
+ - gemfiles/active_record5.gemfile
16
+ - gemfiles/active_record6.gemfile
17
+ before_script:
18
+ - psql -c "CREATE USER sample WITH PASSWORD 'password';" -U postgres
19
+ - psql -c "CREATE DATABASE sample;" -U postgres
20
+ - bin/db_setup
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at 17684091+Madogiwa0124@users.noreply.github.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in active_record_scope_analyzer.gemspec
6
+ gemspec
7
+
8
+ gem 'mysql2'
9
+ gem 'pg'
10
+ gem 'rake', '~> 12.0'
11
+ gem 'rspec', '~> 3.0'
12
+ gem 'rubocop'
@@ -0,0 +1,76 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ active_record_json_explain (0.1.0)
5
+ activerecord (>= 5.2)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (6.0.2.2)
11
+ activesupport (= 6.0.2.2)
12
+ activerecord (6.0.2.2)
13
+ activemodel (= 6.0.2.2)
14
+ activesupport (= 6.0.2.2)
15
+ activesupport (6.0.2.2)
16
+ concurrent-ruby (~> 1.0, >= 1.0.2)
17
+ i18n (>= 0.7, < 2)
18
+ minitest (~> 5.1)
19
+ tzinfo (~> 1.1)
20
+ zeitwerk (~> 2.2)
21
+ ast (2.4.0)
22
+ concurrent-ruby (1.1.6)
23
+ diff-lcs (1.3)
24
+ i18n (1.8.2)
25
+ concurrent-ruby (~> 1.0)
26
+ jaro_winkler (1.5.4)
27
+ minitest (5.14.0)
28
+ mysql2 (0.5.3)
29
+ parallel (1.19.1)
30
+ parser (2.7.0.5)
31
+ ast (~> 2.4.0)
32
+ pg (1.2.3)
33
+ rainbow (3.0.0)
34
+ rake (12.3.3)
35
+ rexml (3.2.4)
36
+ rspec (3.9.0)
37
+ rspec-core (~> 3.9.0)
38
+ rspec-expectations (~> 3.9.0)
39
+ rspec-mocks (~> 3.9.0)
40
+ rspec-core (3.9.1)
41
+ rspec-support (~> 3.9.1)
42
+ rspec-expectations (3.9.1)
43
+ diff-lcs (>= 1.2.0, < 2.0)
44
+ rspec-support (~> 3.9.0)
45
+ rspec-mocks (3.9.1)
46
+ diff-lcs (>= 1.2.0, < 2.0)
47
+ rspec-support (~> 3.9.0)
48
+ rspec-support (3.9.2)
49
+ rubocop (0.80.1)
50
+ jaro_winkler (~> 1.5.1)
51
+ parallel (~> 1.10)
52
+ parser (>= 2.7.0.1)
53
+ rainbow (>= 2.2.2, < 4.0)
54
+ rexml
55
+ ruby-progressbar (~> 1.7)
56
+ unicode-display_width (>= 1.4.0, < 1.7)
57
+ ruby-progressbar (1.10.1)
58
+ thread_safe (0.3.6)
59
+ tzinfo (1.2.6)
60
+ thread_safe (~> 0.1)
61
+ unicode-display_width (1.6.1)
62
+ zeitwerk (2.3.0)
63
+
64
+ PLATFORMS
65
+ ruby
66
+
67
+ DEPENDENCIES
68
+ active_record_json_explain!
69
+ mysql2
70
+ pg
71
+ rake (~> 12.0)
72
+ rspec (~> 3.0)
73
+ rubocop
74
+
75
+ BUNDLED WITH
76
+ 2.1.2
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Madogiwa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,187 @@
1
+ # ActiveRecordJsonExplain
2
+ [![Build Status](https://travis-ci.com/Madogiwa0124/active_record_json_explain.svg?branch=master)](https://travis-ci.com/Madogiwa0124/active_record_json_explain)
3
+
4
+ This gem extends `ActiveRecord::Relation#explain` to make it possible to get EXPLAIN in JSON format.(Only supported MySQL and Postgresql)
5
+
6
+ Supports ActiveRecord 5 latest and 6 latest.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'active_record_json_explain', require: false
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle install
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install active_record_json_explain
23
+
24
+ ## Usage
25
+
26
+ If you use this gem, you can get the result in JSON format by `explain(json: true)`.
27
+
28
+ ``` ruby
29
+ # NOTE:
30
+ # if use postgresql adapter, Rails run `require 'activerecord/lib/active_record/connection_adapters/postgresql/database_statement'` when establish_connection runs.
31
+ # So run `require 'active_record_json_explain'` after `establish_connection`.
32
+ require 'active_record_json_explain'
33
+
34
+ class Sample < ActiveRecord::Base
35
+ scope :with_title, -> { where(title: 'hoge') }
36
+ end
37
+
38
+ # --- MySql ---
39
+ Sample.with_title.explain
40
+ => EXPLAIN for: SELECT `samples`.* FROM `samples` WHERE `samples`.`title` = 'hoge'
41
+ +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
42
+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
43
+ +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
44
+ | 1 | SIMPLE | samples | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.0 | Using where |
45
+ +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
46
+ 1 row in set (0.00 sec)
47
+
48
+ Sample.with_title.explain(json: true)
49
+ => EXPLAIN for: SELECT `samples`.* FROM `samples` WHERE `samples`.`title` = 'hoge'
50
+ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
51
+ | EXPLAIN |
52
+ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
53
+ | {
54
+ "query_block": {
55
+ "select_id": 1,
56
+ "cost_info": {
57
+ "query_cost": "0.35"
58
+ },
59
+ "table": {
60
+ "table_name": "samples",
61
+ "access_type": "ALL",
62
+ "rows_examined_per_scan": 1,
63
+ "rows_produced_per_join": 1,
64
+ "filtered": "100.00",
65
+ "cost_info": {
66
+ "read_cost": "0.25",
67
+ "eval_cost": "0.10",
68
+ "prefix_cost": "0.35",
69
+ "data_read_per_join": "1K"
70
+ },
71
+ "used_columns": [
72
+ "id",
73
+ "category",
74
+ "title",
75
+ "body"
76
+ ],
77
+ "attached_condition": "(`sample`.`samples`.`title` = 'hoge')"
78
+ }
79
+ }
80
+ } |
81
+ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
82
+ 1 row in set (0.00 sec)
83
+
84
+ #--- Postgresql ---
85
+ Sample.with_title.explain
86
+ => EXPLAIN for: SELECT "samples".* FROM "samples" WHERE "samples"."title" = $1 [["title", "hoge"]]
87
+ QUERY PLAN
88
+ ----------------------------------------------------------
89
+ Seq Scan on samples (cost=0.00..11.62 rows=1 width=556)
90
+ Filter: ((title)::text = 'hoge'::text)
91
+ (2 rows)
92
+
93
+ Sample.with_title.explain(json: true)
94
+ => EXPLAIN for: SELECT "samples".* FROM "samples" WHERE "samples"."title" = $1 [["title", "hoge"]]
95
+ QUERY PLAN
96
+ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
97
+ [
98
+ {
99
+ "Plan": {
100
+ "Node Type": "Seq Scan",
101
+ "Parallel Aware": false,
102
+ "Relation Name": "samples",
103
+ "Alias": "samples",
104
+ "Startup Cost": 0.00,
105
+ "Total Cost": 11.62,
106
+ "Plan Rows": 1,
107
+ "Plan Width": 556,
108
+ "Filter": "((title)::text = 'hoge'::text)"
109
+ }
110
+ }
111
+ ]
112
+ (1 row)
113
+ ```
114
+
115
+ ## Development
116
+
117
+ Use docker-compose to build mysql and postgres containers for development.
118
+
119
+ ```
120
+ $ docker-compose up -d
121
+ Creating active_record_json_explain_mysql_1 ... done
122
+ Creating active_record_json_explain_postgres_1 ... done
123
+
124
+ $ docker-compose ps
125
+ Name Command State Ports
126
+ -----------------------------------------------------------------------------------------------------------------
127
+ active_record_json_explain_mysql_1 docker-entrypoint.sh mysqld Up 0.0.0.0:3306->3306/tcp, 33060/tcp
128
+ active_record_json_explain_postgres_1 docker-entrypoint.sh postgres Up 0.0.0.0:5432->5432/tcp
129
+ ```
130
+
131
+ run `bin/db_setup` and create sample database and table for MySQL, Postgresql.
132
+
133
+ ```
134
+ $ bin/db_setup
135
+ === START DB SETUP ====
136
+ = MySQL START =
137
+ +--------------------+
138
+ | Database |
139
+ +--------------------+
140
+ | information_schema |
141
+ | mysql |
142
+ | performance_schema |
143
+ | root |
144
+ | sample |
145
+ | sys |
146
+ +--------------------+
147
+ +------------------+
148
+ | Tables_in_sample |
149
+ +------------------+
150
+ | samples |
151
+ +------------------+
152
+ = MySQL END =
153
+ = Postgresql START =
154
+ CREATE TABLE
155
+ List of relations
156
+ Schema | Name | Type | Owner
157
+ --------+---------+-------+--------
158
+ public | samples | table | sample
159
+ (1 row)
160
+
161
+ = Postgresql END =
162
+ === END DB SETUP ====
163
+ ```
164
+
165
+ Run require sample model and gem code.
166
+
167
+ ``` ruby
168
+ irb(main):001:0> require_relative './spec/sample/mysql/model/sample'
169
+ => true
170
+ irb(main):002:0> require 'active_record_json_explain'
171
+ => true
172
+ irb(main):003:0> Mysql::Sample.with_title.explain(json: true)
173
+ => EXPLAIN for: SELECT `samples`.* FROM `samples` WHERE `samples`.`title` = 'hoge'
174
+ ```
175
+
176
+ ## Contributing
177
+
178
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Madogiwa0124/active_record_json_explain. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/Madogiwa0124/active_record_json_explain/blob/master/CODE_OF_CONDUCT.md).
179
+
180
+
181
+ ## License
182
+
183
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
184
+
185
+ ## Code of Conduct
186
+
187
+ Everyone interacting in the ActiveRecordJsonExplain project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/Madogiwa0124/active_record_json_explain/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/active_record_json_explain/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'active_record_json_explain'
7
+ spec.version = ActiveRecordJsonExplain::VERSION
8
+ spec.authors = ['Madogiwa']
9
+ spec.email = ['madogiwa0124@gmail.com']
10
+
11
+ spec.summary = 'Possible to get EXPLAIN in JSON format.'
12
+ spec.description = 'This gem extends `ActiveRecord::Relation#explain` to make it possible to get EXPLAIN in JSON format.'
13
+ spec.homepage = 'https://github.com/Madogiwa0124/active_record_json_explain'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = spec.homepage
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+ spec.bindir = 'exe'
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ['lib']
28
+ spec.add_runtime_dependency 'activerecord', '>= 5.2'
29
+ end
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'active_record_json_explain'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bash
2
+
3
+ echo "=== START DB SETUP ===="
4
+
5
+ echo "= MySQL START ="
6
+ mysql -u root -h 0.0.0.0 < 'spec/sample/mysql/db/migrate/0000_create_sample_schema.sql'
7
+ mysql -u root -h 0.0.0.0 -e 'SHOW DATABASES;'
8
+ mysql -u root -h 0.0.0.0 -e 'USE sample; SHOW TABLES;'
9
+ echo "= MySQL END ="
10
+
11
+ echo "= Postgresql START ="
12
+ PGPASSWORD=password psql -U sample -h 0.0.0.0 -f 'spec/sample/postgresql/db/migrate/0000_create_sample_schema.sql'
13
+ PGPASSWORD=password psql -U sample -h 0.0.0.0 -c '\d'
14
+ echo "= Postgresql END ="
15
+
16
+ echo "=== END DB SETUP ===="
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,14 @@
1
+ mysql:
2
+ image: mysql
3
+ ports:
4
+ - 3306:3306
5
+ environment:
6
+ - MYSQL_DATABASE=root
7
+ - MYSQL_ALLOW_EMPTY_PASSWORD=yes
8
+ postgres:
9
+ image: postgres
10
+ ports:
11
+ - 5432:5432
12
+ environment:
13
+ - POSTGRES_USER=sample
14
+ - POSTGRES_PASSWORD=password
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in active_record_scope_analyzer.gemspec
6
+ gemspec :path => '../'
7
+
8
+ gem 'activerecord', '~> 5.2'
9
+ gem 'mysql2'
10
+ gem 'pg'
11
+ gem 'rake', '~> 12.0'
12
+ gem 'rspec', '~> 3.0'
13
+ gem 'rubocop'
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in active_record_scope_analyzer.gemspec
6
+ gemspec :path => '../'
7
+
8
+ gem 'activerecord', '~> 6.0'
9
+ gem 'mysql2'
10
+ gem 'pg'
11
+ gem 'rake', '~> 12.0'
12
+ gem 'rspec', '~> 3.0'
13
+ gem 'rubocop'
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record_json_explain/version'
4
+
5
+ module ActiveRecordJsonExplain
6
+ class Error < StandardError; end
7
+ # Your code goes here...
8
+ end
9
+
10
+ # rubocop:disable all
11
+ # Usage
12
+ # ``` ruby
13
+ # # NOTE:
14
+ # # if use postgresql adapter, Rails run `require 'activerecord/lib/active_record/connection_adapters/postgresql/database_statement'` when establish_connection runs.
15
+ # # So run `require 'active_record_scope_analyzer/json_explain'` after `establish_connection`.
16
+ # require 'active_record_scope_analyzer/json_explain'
17
+ # class Sample < ActiveRecord::Base
18
+ # scope :with_title, -> { where(title: 'hoge') }
19
+ # end
20
+ #
21
+ # --- MySql ---
22
+ # Sample.with_title.explain
23
+ # => EXPLAIN for: SELECT `samples`.* FROM `samples` WHERE `samples`.`title` = 'hoge'
24
+ # +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
25
+ # | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
26
+ # +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
27
+ # | 1 | SIMPLE | samples | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.0 | Using where |
28
+ # +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
29
+ # 1 row in set (0.00 sec)
30
+ #
31
+ # Sample.with_title.explain(json: true)
32
+ # => EXPLAIN for: SELECT `samples`.* FROM `samples` WHERE `samples`.`title` = 'hoge'
33
+ # +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
34
+ # | EXPLAIN |
35
+ # +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
36
+ # | {
37
+ # "query_block": {
38
+ # "select_id": 1,
39
+ # "cost_info": {
40
+ # "query_cost": "0.35"
41
+ # },
42
+ # "table": {
43
+ # "table_name": "samples",
44
+ # "access_type": "ALL",
45
+ # "rows_examined_per_scan": 1,
46
+ # "rows_produced_per_join": 1,
47
+ # "filtered": "100.00",
48
+ # "cost_info": {
49
+ # "read_cost": "0.25",
50
+ # "eval_cost": "0.10",
51
+ # "prefix_cost": "0.35",
52
+ # "data_read_per_join": "1K"
53
+ # },
54
+ # "used_columns": [
55
+ # "id",
56
+ # "category",
57
+ # "title",
58
+ # "body"
59
+ # ],
60
+ # "attached_condition": "(`sample`.`samples`.`title` = 'hoge')"
61
+ # }
62
+ # }
63
+ # } |
64
+ # +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
65
+ # 1 row in set (0.00 sec)
66
+ #
67
+ # --- Postgresql ---
68
+ # Sample.with_title.explain
69
+ # => EXPLAIN for: SELECT "samples".* FROM "samples" WHERE "samples"."title" = $1 [["title", "hoge"]]
70
+ # QUERY PLAN
71
+ # ----------------------------------------------------------
72
+ # Seq Scan on samples (cost=0.00..11.62 rows=1 width=556)
73
+ # Filter: ((title)::text = 'hoge'::text)
74
+ # (2 rows)
75
+ #
76
+ # Sample.with_title.explain(json: true)
77
+ # => EXPLAIN for: SELECT "samples".* FROM "samples" WHERE "samples"."title" = $1 [["title", "hoge"]]
78
+ # QUERY PLAN
79
+ # ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
80
+ # [
81
+ # {
82
+ # "Plan": {
83
+ # "Node Type": "Seq Scan",
84
+ # "Parallel Aware": false,
85
+ # "Relation Name": "samples",
86
+ # "Alias": "samples",
87
+ # "Startup Cost": 0.00,
88
+ # "Total Cost": 11.62,
89
+ # "Plan Rows": 1,
90
+ # "Plan Width": 556,
91
+ # "Filter": "((title)::text = 'hoge'::text)"
92
+ # }
93
+ # }
94
+ # ]
95
+ # (1 row)
96
+ # ```
97
+ # rubocop:enable all
98
+
99
+ # NOTE
100
+ # Depending on the timing, it may be overwritten by ActiveRecord after `require` is executed and the monkey patch may not work,
101
+ # so it is loaded.
102
+ load 'active_record_json_explain/activerecord/monkey_patches/json_explain.rb'
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_record'
5
+
6
+ # NOTE:
7
+ # This monkey patch add argument(json) for ActiveRecord::Relation#explain.
8
+ # Argument json is default false. Therefore, it does not affect the default behavior.
9
+ # Only MySql and Postgresql are supported.
10
+
11
+ # rubocop:disable all
12
+ module ActiveRecord
13
+ class Relation
14
+ # NOTE: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation.rb#L239-L241
15
+ def explain(json: false) # NOTE: add arg json
16
+ exec_explain(collecting_queries_for_explain { exec_queries }, json: json) # NOTE: add arg json
17
+ end
18
+ end
19
+
20
+ module Explain
21
+ # NOTE: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/explain.rb#L19-L36
22
+ def exec_explain(queries, json: false) # NOTE: add arg json
23
+ str = queries.map do |sql, binds|
24
+ msg = +"EXPLAIN for: #{sql}"
25
+ unless binds.empty?
26
+ msg << " "
27
+ msg << binds.map { |attr| render_bind(attr) }.inspect
28
+ end
29
+ msg << "\n"
30
+ msg << connection.explain(sql, binds, json: json) # NOTE: add arg json
31
+ end.join("\n")
32
+
33
+ # Overriding inspect to be more human readable, especially in the console.
34
+ def str.inspect
35
+ self
36
+ end
37
+
38
+ str
39
+ end
40
+ end
41
+
42
+ module ConnectionAdapters
43
+ module PostgreSQL
44
+ module DatabaseStatements
45
+ # NOTE: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb#L31-L38
46
+ def explain(arel, binds = [], json: false) # NOTE: add arg json
47
+ format_option = "(FORMAT JSON)" if json # NOTE: get format option
48
+ sql = "EXPLAIN #{format_option} #{to_sql(arel, binds)}" # NOTE: set format option
49
+ PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", binds))
50
+ end
51
+ end
52
+ end
53
+
54
+ module MySQL
55
+ module DatabaseStatements
56
+ # NOTE: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb#L7-L10
57
+ def explain(arel, binds = [], json: false) # NOTE: add arg json
58
+ format_option = "FORMAT=JSON" if json # NOTE: get format option
59
+ sql = "EXPLAIN #{format_option} #{to_sql(arel, binds)}" # NOTE: set format option
60
+ start = Concurrent.monotonic_time
61
+ result = exec_query(sql, "EXPLAIN", binds)
62
+ elapsed = Concurrent.monotonic_time - start
63
+
64
+ MySQL::ExplainPrettyPrinter.new.pp(result, elapsed)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ # rubocop:enable all
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordJsonExplain
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_record_json_explain
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Madogiwa
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-03-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '5.2'
27
+ description: This gem extends `ActiveRecord::Relation#explain` to make it possible
28
+ to get EXPLAIN in JSON format.
29
+ email:
30
+ - madogiwa0124@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".gitignore"
36
+ - ".rspec"
37
+ - ".rubocop.yml"
38
+ - ".travis.yml"
39
+ - CODE_OF_CONDUCT.md
40
+ - Gemfile
41
+ - Gemfile.lock
42
+ - LICENSE.txt
43
+ - README.md
44
+ - Rakefile
45
+ - active_record_json_explain.gemspec
46
+ - bin/console
47
+ - bin/db_setup
48
+ - bin/setup
49
+ - docker-compose.yml
50
+ - gemfiles/active_record5.gemfile
51
+ - gemfiles/active_record6.gemfile
52
+ - lib/active_record_json_explain.rb
53
+ - lib/active_record_json_explain/activerecord/monkey_patches/json_explain.rb
54
+ - lib/active_record_json_explain/version.rb
55
+ homepage: https://github.com/Madogiwa0124/active_record_json_explain
56
+ licenses:
57
+ - MIT
58
+ metadata:
59
+ homepage_uri: https://github.com/Madogiwa0124/active_record_json_explain
60
+ source_code_uri: https://github.com/Madogiwa0124/active_record_json_explain
61
+ post_install_message:
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 2.3.0
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubygems_version: 3.1.2
77
+ signing_key:
78
+ specification_version: 4
79
+ summary: Possible to get EXPLAIN in JSON format.
80
+ test_files: []