activerecord-analyze 0.7.1 → 0.10.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/.circleci/config.yml +1 -1
- data/README.md +46 -9
- data/lib/activerecord-analyze/main.rb +4 -51
- data/lib/activerecord-analyze/version.rb +1 -1
- data/lib/activerecord-analyze.rb +80 -0
- data/spec/analyze_sql_spec.rb +83 -0
- data/spec/main_spec.rb +10 -1
- data/spec/spec_helper.rb +12 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 309228bac640cf76afc26a1c224e0e88427e4941e7ab5b5427e9bc969a031b6b
|
4
|
+
data.tar.gz: a71f6053f7562b8e50e86701f7fa37876a732576463c5e0a9d19d85a14615233
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 13f73a221903084b66760dd62bede7d93559c87bb92750c3675843ac97137b99f7f42a61010697e2368527e5c80a27d2cb8352c9bad570f154a1be2b468a81fc
|
7
|
+
data.tar.gz: 38338c77c2829e621529b1fc5c7a7b045fa0d58cf1f12f5e6b19dafbb64af4ad135ea839bf9e3a0292ad6a2e46796c932c6d4a960083e8ea8f274f3ba3c75d88
|
data/.circleci/config.yml
CHANGED
@@ -16,7 +16,7 @@ jobs:
|
|
16
16
|
- run: gem update --system
|
17
17
|
- run: gem install bundler
|
18
18
|
- run: bundle install --path vendor/bundle
|
19
|
-
- run: sudo apt-get update
|
19
|
+
- run: sudo apt-get update --allow-releaseinfo-change
|
20
20
|
- run: sudo apt install postgresql-client-11
|
21
21
|
- run: dockerize -wait tcp://localhost:5432 -timeout 1m
|
22
22
|
- run:
|
data/README.md
CHANGED
@@ -23,21 +23,21 @@ gem 'activerecord-analyze'
|
|
23
23
|
The `analyze` method supports the following EXPLAIN query options ([PostgreSQL docs reference](https://www.postgresql.org/docs/12/sql-explain.html)):
|
24
24
|
|
25
25
|
```
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
26
|
+
buffers: [ boolean ]
|
27
|
+
verbose: [ boolean ]
|
28
|
+
costs: [ boolean ]
|
29
|
+
settings: [ boolean ]
|
30
|
+
timing: [ boolean ]
|
31
|
+
summary: [ boolean ]
|
32
|
+
format: { :text | :json | :xml | :yaml | :pretty_json }
|
33
33
|
```
|
34
34
|
|
35
35
|
You can execute it like that:
|
36
36
|
|
37
37
|
```ruby
|
38
38
|
|
39
|
-
User.all.analyze(
|
40
|
-
format: :
|
39
|
+
puts User.all.analyze(
|
40
|
+
format: :pretty_json, # :pretty_json format option generates a formatted JSON output
|
41
41
|
verbose: true,
|
42
42
|
costs: true,
|
43
43
|
settings: true,
|
@@ -109,6 +109,43 @@ User.all.analyze(analyze: false)
|
|
109
109
|
|
110
110
|
```
|
111
111
|
|
112
|
+
### Analyzing raw SQL queries
|
113
|
+
|
114
|
+
You can also use a raw SQL query string to generate an EXPLAIN ANALYZE output:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
query = "SELECT * FROM users WHERE email = 'email@example.com'"
|
118
|
+
|
119
|
+
puts ActiveRecordAnalyze.analyze_sql(query, { format: :json})
|
120
|
+
|
121
|
+
# [
|
122
|
+
# {
|
123
|
+
# "Plan": {
|
124
|
+
# "Node Type": "Seq Scan",
|
125
|
+
# "Parallel Aware": false,
|
126
|
+
# "Relation Name": "users",
|
127
|
+
# "Alias": "users",
|
128
|
+
# "Startup Cost": 0.00,
|
129
|
+
# "Total Cost": 18.75,
|
130
|
+
# "Plan Rows": 4,
|
131
|
+
# "Plan Width": 88,
|
132
|
+
# "Actual Startup Time": 0.010,
|
133
|
+
# "Actual Total Time": 0.018,
|
134
|
+
# "Actual Rows": 0,
|
135
|
+
# "Actual Loops": 1,
|
136
|
+
# "Filter": "((email)::text = 'email@example.com'::text)",
|
137
|
+
# "Rows Removed by Filter": 0
|
138
|
+
# },
|
139
|
+
# "Planning Time": 0.052,
|
140
|
+
# "Triggers": [
|
141
|
+
# ],
|
142
|
+
# "Execution Time": 0.062
|
143
|
+
# }
|
144
|
+
# ]
|
145
|
+
```
|
146
|
+
|
147
|
+
This feature is helpful in analyzing SQL queries extracted from the logs.
|
148
|
+
|
112
149
|
### Disclaimer
|
113
150
|
|
114
151
|
It is a bit experimental and can break with new Rails release.
|
@@ -3,56 +3,7 @@ module ActiveRecord
|
|
3
3
|
module PostgreSQL
|
4
4
|
module DatabaseStatements
|
5
5
|
def analyze(arel, binds = [], opts = {})
|
6
|
-
|
7
|
-
case fmt
|
8
|
-
when :json
|
9
|
-
"FORMAT JSON,"
|
10
|
-
when :hash
|
11
|
-
"FORMAT JSON,"
|
12
|
-
when :yaml
|
13
|
-
"FORMAT YAML,"
|
14
|
-
when :text
|
15
|
-
"FORMAT TEXT,"
|
16
|
-
when :xml
|
17
|
-
"FORMAT XML,"
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
verbose_sql = if opts[:verbose] == true
|
22
|
-
", VERBOSE"
|
23
|
-
end
|
24
|
-
|
25
|
-
costs_sql = if opts[:costs] == true
|
26
|
-
", COSTS"
|
27
|
-
end
|
28
|
-
|
29
|
-
settings_sql = if opts[:settings] == true
|
30
|
-
", SETTINGS"
|
31
|
-
end
|
32
|
-
|
33
|
-
buffers_sql = if opts[:buffers] == true
|
34
|
-
", BUFFERS"
|
35
|
-
end
|
36
|
-
|
37
|
-
timing_sql = if opts[:timing] == true
|
38
|
-
", TIMING"
|
39
|
-
end
|
40
|
-
|
41
|
-
summary_sql = if opts[:summary] == true
|
42
|
-
", SUMMARY"
|
43
|
-
end
|
44
|
-
|
45
|
-
analyze_sql = if opts[:analyze] == false
|
46
|
-
""
|
47
|
-
else
|
48
|
-
"ANALYZE"
|
49
|
-
end
|
50
|
-
|
51
|
-
opts_sql = "(#{format_sql} #{analyze_sql}#{verbose_sql}#{costs_sql}#{settings_sql}#{buffers_sql}#{timing_sql}#{summary_sql})"
|
52
|
-
.strip.gsub(/\s+/, " ")
|
53
|
-
.gsub(/\(\s?\s?\s?,/, "(")
|
54
|
-
.gsub(/\s,\s/, " ")
|
55
|
-
.gsub(/\(\s?\)/, "")
|
6
|
+
opts_sql = ActiveRecordAnalyze.build_prefix(opts)
|
56
7
|
|
57
8
|
sql = "EXPLAIN #{opts_sql} #{to_sql(arel, binds)}"
|
58
9
|
PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN #{opts_sql}".strip, binds))
|
@@ -66,7 +17,7 @@ module ActiveRecord
|
|
66
17
|
class Relation
|
67
18
|
def analyze(opts = {})
|
68
19
|
res = exec_analyze(collecting_queries_for_explain { exec_queries }, opts)
|
69
|
-
if [:json, :hash].include?(opts[:format])
|
20
|
+
if [:json, :hash, :pretty_json].include?(opts[:format])
|
70
21
|
start = res.index("[\n")
|
71
22
|
finish = res.rindex("]")
|
72
23
|
raw_json = res.slice(start, finish - start + 1)
|
@@ -75,6 +26,8 @@ module ActiveRecord
|
|
75
26
|
JSON.parse(raw_json).to_json
|
76
27
|
elsif opts[:format] == :hash
|
77
28
|
JSON.parse(raw_json)
|
29
|
+
elsif opts[:format] == :pretty_json
|
30
|
+
JSON.pretty_generate(JSON.parse(raw_json))
|
78
31
|
end
|
79
32
|
else
|
80
33
|
res
|
data/lib/activerecord-analyze.rb
CHANGED
@@ -1,4 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'activerecord-analyze/main'
|
2
4
|
|
3
5
|
module ActiveRecordAnalyze
|
6
|
+
def self.analyze_sql(raw_sql, opts = {})
|
7
|
+
prefix = "EXPLAIN #{build_prefix(opts)}"
|
8
|
+
|
9
|
+
result = ActiveRecord::Base.connection.execute("#{prefix} #{raw_sql}").to_a
|
10
|
+
|
11
|
+
|
12
|
+
if [:json, :hash, :pretty_json].include?(opts[:format])
|
13
|
+
raw_json = result[0].fetch("QUERY PLAN")
|
14
|
+
if opts[:format] == :json
|
15
|
+
raw_json
|
16
|
+
elsif opts[:format] == :hash
|
17
|
+
JSON.parse(raw_json)
|
18
|
+
elsif opts[:format] == :pretty_json
|
19
|
+
JSON.pretty_generate(JSON.parse(raw_json))
|
20
|
+
end
|
21
|
+
else
|
22
|
+
result.map do |el|
|
23
|
+
el.fetch("QUERY PLAN")
|
24
|
+
end.join("\n")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.build_prefix(opts = {})
|
29
|
+
format_sql = if fmt = opts[:format].presence
|
30
|
+
case fmt
|
31
|
+
when :json
|
32
|
+
"FORMAT JSON, "
|
33
|
+
when :hash
|
34
|
+
"FORMAT JSON, "
|
35
|
+
when :pretty_json
|
36
|
+
"FORMAT JSON, "
|
37
|
+
when :yaml
|
38
|
+
"FORMAT YAML, "
|
39
|
+
when :text
|
40
|
+
"FORMAT TEXT, "
|
41
|
+
when :xml
|
42
|
+
"FORMAT XML, "
|
43
|
+
else
|
44
|
+
""
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
verbose_sql = if opts[:verbose] == true
|
49
|
+
", VERBOSE"
|
50
|
+
end
|
51
|
+
|
52
|
+
costs_sql = if opts[:costs] == true
|
53
|
+
", COSTS"
|
54
|
+
end
|
55
|
+
|
56
|
+
settings_sql = if opts[:settings] == true
|
57
|
+
", SETTINGS"
|
58
|
+
end
|
59
|
+
|
60
|
+
buffers_sql = if opts[:buffers] == true
|
61
|
+
", BUFFERS"
|
62
|
+
end
|
63
|
+
|
64
|
+
timing_sql = if opts[:timing] == true
|
65
|
+
", TIMING"
|
66
|
+
end
|
67
|
+
|
68
|
+
summary_sql = if opts[:summary] == true
|
69
|
+
", SUMMARY"
|
70
|
+
end
|
71
|
+
|
72
|
+
analyze_sql = if opts[:analyze] == false
|
73
|
+
""
|
74
|
+
else
|
75
|
+
"ANALYZE"
|
76
|
+
end
|
77
|
+
|
78
|
+
opts_sql = "(#{format_sql}#{analyze_sql}#{verbose_sql}#{costs_sql}#{settings_sql}#{buffers_sql}#{timing_sql}#{summary_sql})"
|
79
|
+
.strip.gsub(/\s+/, " ")
|
80
|
+
.gsub(/\(\s?\s?\s?,/, "(")
|
81
|
+
.gsub(/\s,\s/, " ")
|
82
|
+
.gsub(/\(\s?\)/, "")
|
83
|
+
end
|
4
84
|
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe "ActiveRecord analyze" do
|
6
|
+
let(:raw_sql) do
|
7
|
+
"SELECT * FROM users WHERE email = 'email@example.com'"
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:result) do
|
11
|
+
ActiveRecordAnalyze.analyze_sql(raw_sql, opts)
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "default opts" do
|
15
|
+
let(:opts) do
|
16
|
+
{}
|
17
|
+
end
|
18
|
+
|
19
|
+
it "works" do
|
20
|
+
expect do
|
21
|
+
result
|
22
|
+
end.not_to raise_error
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "format json" do
|
27
|
+
let(:opts) do
|
28
|
+
{ format: :json }
|
29
|
+
end
|
30
|
+
|
31
|
+
it "works" do
|
32
|
+
puts result
|
33
|
+
expect(JSON.parse(result)[0].keys.sort).to eq [
|
34
|
+
"Execution Time", "Plan", "Planning Time", "Triggers"
|
35
|
+
]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "format hash" do
|
40
|
+
let(:opts) do
|
41
|
+
{ format: :hash }
|
42
|
+
end
|
43
|
+
|
44
|
+
it "works" do
|
45
|
+
expect(result[0].keys.sort).to eq [
|
46
|
+
"Execution Time", "Plan", "Planning Time", "Triggers"
|
47
|
+
]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "format pretty" do
|
52
|
+
let(:opts) do
|
53
|
+
{ format: :pretty_json }
|
54
|
+
end
|
55
|
+
|
56
|
+
it "works" do
|
57
|
+
expect(JSON.parse(result)[0].keys.sort).to eq [
|
58
|
+
"Execution Time", "Plan", "Planning Time", "Triggers"
|
59
|
+
]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "supports options" do
|
64
|
+
let(:raw_sql) do
|
65
|
+
"SELECT \"users\".* FROM \"users\" WHERE \"users\".\"email\" IS NOT NULL LIMIT 10"
|
66
|
+
end
|
67
|
+
|
68
|
+
let(:opts) do
|
69
|
+
{
|
70
|
+
format: :hash,
|
71
|
+
costs: true,
|
72
|
+
timing: true,
|
73
|
+
summary: true
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
it "works" do
|
78
|
+
expect(result[0].keys.sort).to eq [
|
79
|
+
"Execution Time", "Plan", "Planning Time", "Triggers"
|
80
|
+
]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/spec/main_spec.rb
CHANGED
@@ -15,7 +15,7 @@ describe "ActiveRecord analyze" do
|
|
15
15
|
ActiveRecord::Migrator.new(:up, [CreateUsers.new], @schema_migration).migrate
|
16
16
|
end
|
17
17
|
|
18
|
-
describe "
|
18
|
+
describe "default opts" do
|
19
19
|
it "works" do
|
20
20
|
expect do
|
21
21
|
User.all.analyze
|
@@ -41,6 +41,15 @@ describe "ActiveRecord analyze" do
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
+
describe "format pretty" do
|
45
|
+
it "works" do
|
46
|
+
result = User.all.analyze(format: :pretty_json)
|
47
|
+
expect(JSON.parse(result)[0].keys.sort).to eq [
|
48
|
+
"Execution Time", "Plan", "Planning Time", "Triggers"
|
49
|
+
]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
44
53
|
describe "supports options" do
|
45
54
|
it "works" do
|
46
55
|
result = User.all.limit(10).where.not(email: nil).analyze(
|
data/spec/spec_helper.rb
CHANGED
@@ -3,6 +3,18 @@
|
|
3
3
|
require 'rubygems'
|
4
4
|
require "active_record/railtie"
|
5
5
|
require 'bundler/setup'
|
6
|
+
require "migrations/create_users_migration.rb"
|
6
7
|
require_relative '../lib/activerecord-analyze'
|
7
8
|
|
8
9
|
ENV["DATABASE_URL"] ||= "postgresql://postgres:secret@localhost:5432/activerecord-analyze-test"
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.before(:suite) do
|
13
|
+
ActiveRecord::Base.establish_connection(
|
14
|
+
ENV.fetch("DATABASE_URL")
|
15
|
+
)
|
16
|
+
|
17
|
+
@schema_migration = ActiveRecord::Base.connection.schema_migration
|
18
|
+
ActiveRecord::Migrator.new(:up, [CreateUsers.new], @schema_migration).migrate
|
19
|
+
end
|
20
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-analyze
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- pawurb
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-12-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -86,6 +86,7 @@ files:
|
|
86
86
|
- lib/activerecord-analyze/main.rb
|
87
87
|
- lib/activerecord-analyze/version.rb
|
88
88
|
- query-plan.png
|
89
|
+
- spec/analyze_sql_spec.rb
|
89
90
|
- spec/main_spec.rb
|
90
91
|
- spec/migrations/create_users_migration.rb
|
91
92
|
- spec/spec_helper.rb
|
@@ -108,11 +109,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
108
109
|
- !ruby/object:Gem::Version
|
109
110
|
version: '0'
|
110
111
|
requirements: []
|
111
|
-
rubygems_version: 3.
|
112
|
+
rubygems_version: 3.0.3
|
112
113
|
signing_key:
|
113
114
|
specification_version: 4
|
114
115
|
summary: Add EXPLAIN ANALYZE to Active Record query objects
|
115
116
|
test_files:
|
117
|
+
- spec/analyze_sql_spec.rb
|
116
118
|
- spec/main_spec.rb
|
117
119
|
- spec/migrations/create_users_migration.rb
|
118
120
|
- spec/spec_helper.rb
|