ruby-pg-extras 5.6.0 → 5.6.1
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/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
|