activerecord-analyze 0.7.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2021239ed9352a040106ce8c1de50503e785fccd9a3093928eeaf5dfa79a0dc
4
- data.tar.gz: 8ba00244e759dc1f31091ec4196aded3887c9007902faf867431ea2ef9ae93dc
3
+ metadata.gz: 309228bac640cf76afc26a1c224e0e88427e4941e7ab5b5427e9bc969a031b6b
4
+ data.tar.gz: a71f6053f7562b8e50e86701f7fa37876a732576463c5e0a9d19d85a14615233
5
5
  SHA512:
6
- metadata.gz: ad2874f8422dc7299b8b7b3036fbec8e90f29780862ea3dd192629894905aafede52731314cc569afb6c9ce31f61a379b31ac308af0c304fc9e72bea583e58a7
7
- data.tar.gz: 114236b8af394e3f7fad06549e3bc99fa1814e815dd93282c7d3abfc0a0535ee568d90c0360abab021c83a7f1b38d2bfd0bfec31f043720893ab321776fe83fe
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
- BUFFERS [ boolean ]
27
- VERBOSE [ boolean ]
28
- COSTS [ boolean ]
29
- SETTINGS [ boolean ]
30
- TIMING [ boolean ]
31
- SUMMARY [ boolean ]
32
- FORMAT { TEXT | XML | JSON | YAML }
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: :json,
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
- format_sql = if fmt = opts[:format].presence
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
@@ -1,3 +1,3 @@
1
1
  module ActiveRecordAnalyze
2
- VERSION = "0.7.1"
2
+ VERSION = "0.10.0"
3
3
  end
@@ -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 "no opts" do
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.7.1
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-04-09 00:00:00.000000000 Z
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.1.4
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