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 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