ruby-pg-extras 5.6.0 → 5.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +14 -0
- data/lib/ruby-pg-extras.rb +16 -5
- data/lib/ruby_pg_extras/detect_fk_column.rb +54 -0
- data/lib/ruby_pg_extras/diagnose_data.rb +42 -0
- data/lib/ruby_pg_extras/missing_fk_constraints.rb +17 -5
- data/lib/ruby_pg_extras/missing_fk_indexes.rb +18 -4
- data/lib/ruby_pg_extras/version.rb +1 -1
- data/spec/detect_fk_column_spec.rb +38 -0
- data/spec/diagnose_data_spec.rb +14 -0
- data/spec/spec_helper.rb +13 -0
- metadata +7 -4
- /data/spec/{missing_fk_constraints.rb → missing_fk_constraints_spec.rb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d63b815252ba55f7501178013917e4cb7c48cf64f34ff1501dab8a6d84b3d7c4
|
4
|
+
data.tar.gz: 8465dbaee0cd16da98fcaaeb9a20b92185060ded5371220ae04194610e9c2a58
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cbac3bdc3566aaf3ac39702e8485661b3460ddf25506a52aa46c2f386345d47d868cce197f1b5dec3f1adff7e14081d772d2650cd65c889a48291ab1aabf8125
|
7
|
+
data.tar.gz: 155101c5bc0bf1d791ccbad29b95d8f10556a32caa16c4ded2c6cfef51b3b834c2759e15d273830dc5f99221bd7b969022b66879886afe29cb7b8c7d8ace094b
|
data/README.md
CHANGED
@@ -160,6 +160,14 @@ This method displays structure of a selected table, listing its column names, to
|
|
160
160
|
```ruby
|
161
161
|
RubyPgExtras.table_schema(args: { table_name: "users" })
|
162
162
|
|
163
|
+
+-----------------------------+-----------------------------+-------------+-----------------------------------+
|
164
|
+
| column_name | data_type | is_nullable | column_default |
|
165
|
+
+-----------------------------+-----------------------------+-------------+-----------------------------------+
|
166
|
+
| id | bigint | NO | nextval('users_id_seq'::regclass) |
|
167
|
+
| team_id | integer | NO | |
|
168
|
+
| slack_id | character varying | NO | |
|
169
|
+
| pseudonym | character varying | YES | |
|
170
|
+
|
163
171
|
```
|
164
172
|
|
165
173
|
### `table_foreign_keys`
|
@@ -169,6 +177,12 @@ This method displays foreign key constraints for a selected table. It lists fore
|
|
169
177
|
```ruby
|
170
178
|
RubyPgExtras.table_foreign_keys(args: { table_name: "users" })
|
171
179
|
|
180
|
+
+------------+---------------------+-------------+--------------------+---------------------+
|
181
|
+
| table_name | constraint_name | column_name | foreign_table_name | foreign_column_name |
|
182
|
+
+------------+---------------------+-------------+--------------------+---------------------+
|
183
|
+
| users | fk_rails_b2bbf87303 | team_id | teams | id |
|
184
|
+
+------------+---------------------+-------------+--------------------+---------------------+
|
185
|
+
|
172
186
|
```
|
173
187
|
|
174
188
|
### `index_info`
|
data/lib/ruby-pg-extras.rb
CHANGED
@@ -6,6 +6,7 @@ require "pg"
|
|
6
6
|
require "ruby_pg_extras/size_parser"
|
7
7
|
require "ruby_pg_extras/diagnose_data"
|
8
8
|
require "ruby_pg_extras/diagnose_print"
|
9
|
+
require "ruby_pg_extras/detect_fk_column"
|
9
10
|
require "ruby_pg_extras/missing_fk_indexes"
|
10
11
|
require "ruby_pg_extras/missing_fk_constraints"
|
11
12
|
require "ruby_pg_extras/index_info"
|
@@ -74,9 +75,9 @@ module RubyPgExtras
|
|
74
75
|
end
|
75
76
|
end
|
76
77
|
|
77
|
-
def self.
|
78
|
+
def self.run_query_base(query_name:, conn:, exec_method:, in_format:, args: {})
|
78
79
|
if %i(calls outliers).include?(query_name)
|
79
|
-
pg_stat_statements_ver =
|
80
|
+
pg_stat_statements_ver = conn.send(exec_method, "select installed_version from pg_available_extensions where name='pg_stat_statements'")
|
80
81
|
.to_a[0].fetch("installed_version", nil)
|
81
82
|
if pg_stat_statements_ver != nil
|
82
83
|
if Gem::Version.new(pg_stat_statements_ver) < Gem::Version.new(NEW_PG_STAT_STATEMENTS)
|
@@ -98,7 +99,7 @@ module RubyPgExtras
|
|
98
99
|
else
|
99
100
|
sql_for(query_name: query_name)
|
100
101
|
end
|
101
|
-
result =
|
102
|
+
result = conn.send(exec_method, sql)
|
102
103
|
|
103
104
|
display_result(
|
104
105
|
result,
|
@@ -107,6 +108,16 @@ module RubyPgExtras
|
|
107
108
|
)
|
108
109
|
end
|
109
110
|
|
111
|
+
def self.run_query(query_name:, in_format:, args: {})
|
112
|
+
run_query_base(
|
113
|
+
query_name: query_name,
|
114
|
+
conn: connection,
|
115
|
+
exec_method: :exec,
|
116
|
+
in_format: in_format,
|
117
|
+
args: args,
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
110
121
|
def self.diagnose(in_format: :display_table)
|
111
122
|
data = RubyPgExtras::DiagnoseData.call
|
112
123
|
|
@@ -154,7 +165,7 @@ module RubyPgExtras
|
|
154
165
|
end
|
155
166
|
|
156
167
|
def self.missing_fk_constraints(args: {}, in_format: :display_table)
|
157
|
-
RubyPgExtras::
|
168
|
+
RubyPgExtras::MissingFkConstraints.call(args[:table_name])
|
158
169
|
end
|
159
170
|
|
160
171
|
def self.display_result(result, title:, in_format:)
|
@@ -175,7 +186,7 @@ module RubyPgExtras
|
|
175
186
|
puts Terminal::Table.new(
|
176
187
|
title: title,
|
177
188
|
headings: headings,
|
178
|
-
rows: result.values,
|
189
|
+
rows: (result.try(:values) || result.map(&:values)),
|
179
190
|
)
|
180
191
|
else
|
181
192
|
raise "Invalid in_format option"
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyPgExtras
|
4
|
+
class DetectFkColumn
|
5
|
+
PLURAL_RULES = [
|
6
|
+
[/s$/i, "s"],
|
7
|
+
[/^(ax|test)is$/i, '\1es'],
|
8
|
+
[/(octop|vir)us$/i, '\1i'],
|
9
|
+
[/(alias|status)$/i, '\1es'],
|
10
|
+
[/(bu)s$/i, '\1ses'],
|
11
|
+
[/(buffal|tomat)o$/i, '\1oes'],
|
12
|
+
[/([ti])um$/i, '\1a'],
|
13
|
+
[/sis$/i, "ses"],
|
14
|
+
[/(?:([^f])fe|([lr])f)$/i, '\1\2ves'],
|
15
|
+
[/([^aeiouy]|qu)y$/i, '\1ies'],
|
16
|
+
[/(x|ch|ss|sh)$/i, '\1es'],
|
17
|
+
[/(matr|vert|ind)(?:ix|ex)$/i, '\1ices'],
|
18
|
+
[/^(m|l)ouse$/i, '\1ice'],
|
19
|
+
[/^(ox)$/i, '\1en'],
|
20
|
+
[/(quiz)$/i, '\1zes'],
|
21
|
+
]
|
22
|
+
IRREGULAR = {
|
23
|
+
"person" => "people",
|
24
|
+
"man" => "men",
|
25
|
+
"child" => "children",
|
26
|
+
"sex" => "sexes",
|
27
|
+
"move" => "moves",
|
28
|
+
"zombie" => "zombies",
|
29
|
+
}
|
30
|
+
UNCOUNTABLE = %w(equipment information rice money species series fish sheep jeans police)
|
31
|
+
|
32
|
+
def self.call(column_name, tables)
|
33
|
+
new.call(column_name, tables)
|
34
|
+
end
|
35
|
+
|
36
|
+
def call(column_name, tables)
|
37
|
+
return false unless column_name =~ /_id$/
|
38
|
+
table_name = column_name.split("_").first
|
39
|
+
table_name = pluralize(table_name)
|
40
|
+
tables.include?(table_name)
|
41
|
+
end
|
42
|
+
|
43
|
+
def pluralize(word)
|
44
|
+
return word if UNCOUNTABLE.include?(word.downcase)
|
45
|
+
return IRREGULAR[word] if IRREGULAR.key?(word)
|
46
|
+
return IRREGULAR.invert[word] if IRREGULAR.value?(word)
|
47
|
+
|
48
|
+
PLURAL_RULES.reverse.each do |(rule, replacement)|
|
49
|
+
return word.gsub(rule, replacement) if word.match?(rule)
|
50
|
+
end
|
51
|
+
word + "s"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -23,6 +23,8 @@ module RubyPgExtras
|
|
23
23
|
:null_indexes,
|
24
24
|
:bloat,
|
25
25
|
:duplicate_indexes,
|
26
|
+
:missing_fk_indexes,
|
27
|
+
:missing_fk_constraints,
|
26
28
|
].yield_self do |checks|
|
27
29
|
extensions_data = query_module.extensions(in_format: :hash)
|
28
30
|
|
@@ -55,6 +57,46 @@ module RubyPgExtras
|
|
55
57
|
RubyPgExtras
|
56
58
|
end
|
57
59
|
|
60
|
+
def missing_fk_indexes
|
61
|
+
missing = query_module.missing_fk_indexes(in_format: :hash)
|
62
|
+
|
63
|
+
if missing.count == 0
|
64
|
+
return {
|
65
|
+
ok: true,
|
66
|
+
message: "No missing foreign key indexes detected.",
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
missing_text = missing.map do |el|
|
71
|
+
"#{el.fetch(:table)}.#{el.fetch(:column_name)}"
|
72
|
+
end.join(",\n")
|
73
|
+
|
74
|
+
{
|
75
|
+
ok: false,
|
76
|
+
message: "Missing foreign key indexes detected: #{missing_text}.",
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
def missing_fk_constraints
|
81
|
+
missing = query_module.missing_fk_constraints(in_format: :hash)
|
82
|
+
|
83
|
+
if missing.count == 0
|
84
|
+
return {
|
85
|
+
ok: true,
|
86
|
+
message: "No missing foreign key constraints detected.",
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
missing_text = missing.map do |el|
|
91
|
+
"#{el.fetch(:table)}.#{el.fetch(:column_name)}"
|
92
|
+
end.join(",\n")
|
93
|
+
|
94
|
+
{
|
95
|
+
ok: false,
|
96
|
+
message: "Missing foreign key constraints detected: #{missing_text}.",
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
58
100
|
def table_cache_hit
|
59
101
|
min_expected = ENV.fetch(
|
60
102
|
"PG_EXTRAS_TABLE_CACHE_HIT_MIN_EXPECTED",
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module RubyPgExtras
|
4
|
-
class
|
4
|
+
class MissingFkConstraints
|
5
5
|
def self.call(table_name)
|
6
6
|
new.call(table_name)
|
7
7
|
end
|
@@ -10,15 +10,17 @@ module RubyPgExtras
|
|
10
10
|
tables = if table_name
|
11
11
|
[table_name]
|
12
12
|
else
|
13
|
-
|
13
|
+
all_tables
|
14
14
|
end
|
15
15
|
|
16
16
|
tables.reduce([]) do |agg, table|
|
17
|
-
foreign_keys_info =
|
18
|
-
schema =
|
17
|
+
foreign_keys_info = query_module.table_foreign_keys(args: { table_name: table }, in_format: :hash)
|
18
|
+
schema = query_module.table_schema(args: { table_name: table }, in_format: :hash)
|
19
19
|
|
20
20
|
fk_columns = schema.filter_map do |row|
|
21
|
-
|
21
|
+
if DetectFkColumn.call(row.fetch("column_name"), all_tables)
|
22
|
+
row.fetch("column_name")
|
23
|
+
end
|
22
24
|
end
|
23
25
|
|
24
26
|
fk_columns.each do |column_name|
|
@@ -35,5 +37,15 @@ module RubyPgExtras
|
|
35
37
|
agg
|
36
38
|
end
|
37
39
|
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def all_tables
|
44
|
+
@_all_tables ||= query_module.table_size(in_format: :hash).map { |row| row.fetch("name") }
|
45
|
+
end
|
46
|
+
|
47
|
+
def query_module
|
48
|
+
RubyPgExtras
|
49
|
+
end
|
38
50
|
end
|
39
51
|
end
|
@@ -10,15 +10,19 @@ module RubyPgExtras
|
|
10
10
|
tables = if table_name
|
11
11
|
[table_name]
|
12
12
|
else
|
13
|
-
|
13
|
+
all_tables
|
14
14
|
end
|
15
15
|
|
16
|
+
indexes_info = query_module.index_info(in_format: :hash)
|
17
|
+
|
16
18
|
tables.reduce([]) do |agg, table|
|
17
|
-
index_info =
|
18
|
-
schema =
|
19
|
+
index_info = indexes_info.select { |row| row.fetch(:table_name) == table }
|
20
|
+
schema = query_module.table_schema(args: { table_name: table }, in_format: :hash)
|
19
21
|
|
20
22
|
fk_columns = schema.filter_map do |row|
|
21
|
-
|
23
|
+
if DetectFkColumn.call(row.fetch("column_name"), all_tables)
|
24
|
+
row.fetch("column_name")
|
25
|
+
end
|
22
26
|
end
|
23
27
|
|
24
28
|
fk_columns.each do |column_name|
|
@@ -35,5 +39,15 @@ module RubyPgExtras
|
|
35
39
|
agg
|
36
40
|
end
|
37
41
|
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def all_tables
|
46
|
+
@_all_tables ||= query_module.table_size(in_format: :hash).map { |row| row.fetch("name") }
|
47
|
+
end
|
48
|
+
|
49
|
+
def query_module
|
50
|
+
RubyPgExtras
|
51
|
+
end
|
38
52
|
end
|
39
53
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe RubyPgExtras::DetectFkColumn do
|
6
|
+
subject(:result) do
|
7
|
+
RubyPgExtras::DetectFkColumn.call(column_name, tables)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "call" do
|
11
|
+
context "no matching table" do
|
12
|
+
let(:column_name) { "company_id" }
|
13
|
+
let(:tables) { ["users", "posts"] }
|
14
|
+
|
15
|
+
it "returns false" do
|
16
|
+
expect(result).to eq(false)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context "matching table" do
|
21
|
+
let(:column_name) { "user_id" }
|
22
|
+
let(:tables) { ["users", "posts"] }
|
23
|
+
|
24
|
+
it "returns true" do
|
25
|
+
expect(result).to eq(true)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "matching table" do
|
30
|
+
let(:column_name) { "octopus_id" }
|
31
|
+
let(:tables) { ["users", "octopi"] }
|
32
|
+
|
33
|
+
it "returns true" do
|
34
|
+
expect(result).to eq(true)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/spec/diagnose_data_spec.rb
CHANGED
@@ -45,6 +45,20 @@ describe RubyPgExtras::DiagnoseData do
|
|
45
45
|
{ "query" => "SELECT * FROM users WHERE users.age > 20 AND users.height > 160", "exec_time" => "154:39:26.431466", "prop_exec_time" => "72.2%", "ncalls" => "34,211,877", "sync_io_time" => "00:34:19.784318" },
|
46
46
|
]
|
47
47
|
}
|
48
|
+
|
49
|
+
expect(RubyPgExtras).to receive(:missing_fk_constraints) {
|
50
|
+
[
|
51
|
+
{ table: "users", column_name: "company_id" },
|
52
|
+
{ table: "posts", column_name: "topic_id" },
|
53
|
+
]
|
54
|
+
}
|
55
|
+
|
56
|
+
expect(RubyPgExtras).to receive(:missing_fk_indexes) {
|
57
|
+
[
|
58
|
+
{ table: "users", column_name: "company_id" },
|
59
|
+
{ table: "posts", column_name: "topic_id" },
|
60
|
+
]
|
61
|
+
}
|
48
62
|
end
|
49
63
|
|
50
64
|
it "works" do
|
data/spec/spec_helper.rb
CHANGED
@@ -24,6 +24,8 @@ RSpec.configure do |config|
|
|
24
24
|
DB_SCHEMA = <<-SQL
|
25
25
|
DROP TABLE IF EXISTS posts;
|
26
26
|
DROP TABLE IF EXISTS users;
|
27
|
+
DROP TABLE IF EXISTS topics;
|
28
|
+
DROP TABLE IF EXISTS companies;
|
27
29
|
|
28
30
|
CREATE TABLE users (
|
29
31
|
id SERIAL PRIMARY KEY,
|
@@ -35,10 +37,21 @@ CREATE TABLE posts (
|
|
35
37
|
id SERIAL PRIMARY KEY,
|
36
38
|
user_id INTEGER NOT NULL,
|
37
39
|
topic_id INTEGER,
|
40
|
+
external_id INTEGER,
|
38
41
|
title VARCHAR(255),
|
39
42
|
CONSTRAINT fk_posts_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
40
43
|
);
|
41
44
|
|
45
|
+
CREATE TABLE topics (
|
46
|
+
id SERIAL PRIMARY KEY,
|
47
|
+
title VARCHAR(255)
|
48
|
+
);
|
49
|
+
|
50
|
+
CREATE TABLE companies (
|
51
|
+
id SERIAL PRIMARY KEY,
|
52
|
+
name VARCHAR(255)
|
53
|
+
);
|
54
|
+
|
42
55
|
CREATE INDEX index_posts_on_user_id ON posts(user_id);
|
43
56
|
SQL
|
44
57
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-pg-extras
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.6.
|
4
|
+
version: 5.6.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- pawurb
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-02-
|
11
|
+
date: 2025-02-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pg
|
@@ -111,6 +111,7 @@ files:
|
|
111
111
|
- Rakefile
|
112
112
|
- docker-compose.yml.sample
|
113
113
|
- lib/ruby-pg-extras.rb
|
114
|
+
- lib/ruby_pg_extras/detect_fk_column.rb
|
114
115
|
- lib/ruby_pg_extras/diagnose_data.rb
|
115
116
|
- lib/ruby_pg_extras/diagnose_print.rb
|
116
117
|
- lib/ruby_pg_extras/index_info.rb
|
@@ -166,10 +167,11 @@ files:
|
|
166
167
|
- lib/ruby_pg_extras/version.rb
|
167
168
|
- ruby-pg-extras-diagnose.png
|
168
169
|
- ruby-pg-extras.gemspec
|
170
|
+
- spec/detect_fk_column_spec.rb
|
169
171
|
- spec/diagnose_data_spec.rb
|
170
172
|
- spec/diagnose_print_spec.rb
|
171
173
|
- spec/index_info_spec.rb
|
172
|
-
- spec/
|
174
|
+
- spec/missing_fk_constraints_spec.rb
|
173
175
|
- spec/missing_fk_indexes_spec.rb
|
174
176
|
- spec/size_parser_spec.rb
|
175
177
|
- spec/smoke_spec.rb
|
@@ -200,10 +202,11 @@ signing_key:
|
|
200
202
|
specification_version: 4
|
201
203
|
summary: Ruby PostgreSQL performance database insights
|
202
204
|
test_files:
|
205
|
+
- spec/detect_fk_column_spec.rb
|
203
206
|
- spec/diagnose_data_spec.rb
|
204
207
|
- spec/diagnose_print_spec.rb
|
205
208
|
- spec/index_info_spec.rb
|
206
|
-
- spec/
|
209
|
+
- spec/missing_fk_constraints_spec.rb
|
207
210
|
- spec/missing_fk_indexes_spec.rb
|
208
211
|
- spec/size_parser_spec.rb
|
209
212
|
- spec/smoke_spec.rb
|
File without changes
|