ruby-pg-extras 5.5.1 → 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/.github/workflows/ci.yml +1 -1
- data/README.md +56 -0
- data/lib/ruby-pg-extras.rb +38 -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 +51 -0
- data/lib/ruby_pg_extras/missing_fk_indexes.rb +53 -0
- data/lib/ruby_pg_extras/queries/table_foreign_keys.sql +17 -0
- data/lib/ruby_pg_extras/queries/table_schema.sql +5 -0
- data/lib/ruby_pg_extras/version.rb +1 -1
- data/ruby-pg-extras.gemspec +1 -0
- data/spec/detect_fk_column_spec.rb +38 -0
- data/spec/diagnose_data_spec.rb +14 -0
- data/spec/missing_fk_constraints_spec.rb +26 -0
- data/spec/missing_fk_indexes_spec.rb +25 -0
- data/spec/smoke_spec.rb +35 -1
- data/spec/spec_helper.rb +45 -15
- metadata +27 -2
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/.github/workflows/ci.yml
CHANGED
data/README.md
CHANGED
@@ -116,6 +116,30 @@ Keep reading to learn about methods that `diagnose` uses under the hood.
|
|
116
116
|
|
117
117
|
## Available methods
|
118
118
|
|
119
|
+
### `missing_fk_indexes`
|
120
|
+
|
121
|
+
This method lists columns likely to be foreign keys (i.e. name ending in `_id`) but don't have a corresponding index. It's a generally recommended to always index foreign key columns because they are used for searching relation objects.
|
122
|
+
|
123
|
+
You can add indexes on the columns returned by this query and later check if they are receiving scans using the [unused_indexes method](#unused_indexes). Please also remember that each index decreases write performance and autovacuuming overhead, so be careful when adding multiple indexes to often updated tables.
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
RubyPgExtras.missing_fk_indexes(args: { table_name: "users" })
|
127
|
+
|
128
|
+
```
|
129
|
+
|
130
|
+
`table_name` argument is optional, if omitted, the method will display missing fk indexes for all the tables.
|
131
|
+
|
132
|
+
## `missing_fk_constraints`
|
133
|
+
|
134
|
+
Similarly to the previous method, this one shows columns likely to be foreign keys (i.e. name ending in `_id`) that don't have a corresponding foreign key constraint. Foreign key constraints improve data integrity in the database by preventing relations with nonexisting objects. You can read more about the benefits of using foreign keys [in this blog post](https://pawelurbanek.com/rails-postgresql-data-integrity).
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
RubyPgExtras.missing_fk_constraints(args: { table_name: "users" })
|
138
|
+
|
139
|
+
```
|
140
|
+
|
141
|
+
`table_name` argument is optional, if omitted, method will display missing foreign keys for all the tables.
|
142
|
+
|
119
143
|
### `table_info`
|
120
144
|
|
121
145
|
This method displays metadata metrics for all or a selected table. You can use it to check the table's size, its cache hit metrics, and whether it is correctly indexed. Many sequential scans or no index scans are potential indicators of misconfigured indexes. This method aggregates data provided by other methods in an easy to analyze summary format.
|
@@ -129,6 +153,38 @@ RubyPgExtras.table_info(args: { table_name: "users" })
|
|
129
153
|
|
130
154
|
```
|
131
155
|
|
156
|
+
### `table_schema`
|
157
|
+
|
158
|
+
This method displays structure of a selected table, listing its column names, together with types, null constraints, and default values.
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
RubyPgExtras.table_schema(args: { table_name: "users" })
|
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
|
+
|
171
|
+
```
|
172
|
+
|
173
|
+
### `table_foreign_keys`
|
174
|
+
|
175
|
+
This method displays foreign key constraints for a selected table. It lists foreign key name, source and target columns, and related table name.
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
RubyPgExtras.table_foreign_keys(args: { table_name: "users" })
|
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
|
+
|
186
|
+
```
|
187
|
+
|
132
188
|
### `index_info`
|
133
189
|
|
134
190
|
This method returns summary info about database indexes. You can check index size, how often it is used and what percentage of its total size are NULL values. Like the previous method, it aggregates data from other helper methods in an easy-to-digest format.
|
data/lib/ruby-pg-extras.rb
CHANGED
@@ -6,6 +6,9 @@ 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"
|
10
|
+
require "ruby_pg_extras/missing_fk_indexes"
|
11
|
+
require "ruby_pg_extras/missing_fk_constraints"
|
9
12
|
require "ruby_pg_extras/index_info"
|
10
13
|
require "ruby_pg_extras/index_info_print"
|
11
14
|
require "ruby_pg_extras/table_info"
|
@@ -26,10 +29,16 @@ module RubyPgExtras
|
|
26
29
|
unused_indexes duplicate_indexes vacuum_stats kill_all kill_pid
|
27
30
|
pg_stat_statements_reset buffercache_stats
|
28
31
|
buffercache_usage ssl_used connections
|
32
|
+
table_schema table_foreign_keys
|
29
33
|
)
|
30
34
|
|
31
35
|
DEFAULT_SCHEMA = ENV["PG_EXTRAS_SCHEMA"] || "public"
|
32
36
|
|
37
|
+
REQUIRED_ARGS = {
|
38
|
+
table_schema: [:table_name],
|
39
|
+
table_foreign_keys: [:table_name],
|
40
|
+
}
|
41
|
+
|
33
42
|
DEFAULT_ARGS = Hash.new({}).merge({
|
34
43
|
calls: { limit: 10 },
|
35
44
|
calls_legacy: { limit: 10 },
|
@@ -66,9 +75,9 @@ module RubyPgExtras
|
|
66
75
|
end
|
67
76
|
end
|
68
77
|
|
69
|
-
def self.
|
78
|
+
def self.run_query_base(query_name:, conn:, exec_method:, in_format:, args: {})
|
70
79
|
if %i(calls outliers).include?(query_name)
|
71
|
-
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'")
|
72
81
|
.to_a[0].fetch("installed_version", nil)
|
73
82
|
if pg_stat_statements_ver != nil
|
74
83
|
if Gem::Version.new(pg_stat_statements_ver) < Gem::Version.new(NEW_PG_STAT_STATEMENTS)
|
@@ -79,12 +88,18 @@ module RubyPgExtras
|
|
79
88
|
end
|
80
89
|
end
|
81
90
|
|
82
|
-
|
91
|
+
REQUIRED_ARGS.fetch(query_name) { [] }.each do |arg_name|
|
92
|
+
if args[arg_name].nil?
|
93
|
+
raise ArgumentError, "'#{arg_name}' is required"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
sql = if (custom_args = DEFAULT_ARGS.fetch(query_name, {}).merge(args)) != {}
|
83
98
|
sql_for(query_name: query_name) % custom_args
|
84
99
|
else
|
85
100
|
sql_for(query_name: query_name)
|
86
101
|
end
|
87
|
-
result =
|
102
|
+
result = conn.send(exec_method, sql)
|
88
103
|
|
89
104
|
display_result(
|
90
105
|
result,
|
@@ -93,6 +108,16 @@ module RubyPgExtras
|
|
93
108
|
)
|
94
109
|
end
|
95
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
|
+
|
96
121
|
def self.diagnose(in_format: :display_table)
|
97
122
|
data = RubyPgExtras::DiagnoseData.call
|
98
123
|
|
@@ -135,6 +160,14 @@ module RubyPgExtras
|
|
135
160
|
end
|
136
161
|
end
|
137
162
|
|
163
|
+
def self.missing_fk_indexes(args: {}, in_format: :display_table)
|
164
|
+
RubyPgExtras::MissingFkIndexes.call(args[:table_name])
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.missing_fk_constraints(args: {}, in_format: :display_table)
|
168
|
+
RubyPgExtras::MissingFkConstraints.call(args[:table_name])
|
169
|
+
end
|
170
|
+
|
138
171
|
def self.display_result(result, title:, in_format:)
|
139
172
|
case in_format
|
140
173
|
when :array
|
@@ -153,7 +186,7 @@ module RubyPgExtras
|
|
153
186
|
puts Terminal::Table.new(
|
154
187
|
title: title,
|
155
188
|
headings: headings,
|
156
|
-
rows: result.values,
|
189
|
+
rows: (result.try(:values) || result.map(&:values)),
|
157
190
|
)
|
158
191
|
else
|
159
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",
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyPgExtras
|
4
|
+
class MissingFkConstraints
|
5
|
+
def self.call(table_name)
|
6
|
+
new.call(table_name)
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(table_name)
|
10
|
+
tables = if table_name
|
11
|
+
[table_name]
|
12
|
+
else
|
13
|
+
all_tables
|
14
|
+
end
|
15
|
+
|
16
|
+
tables.reduce([]) do |agg, table|
|
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
|
+
|
20
|
+
fk_columns = schema.filter_map do |row|
|
21
|
+
if DetectFkColumn.call(row.fetch("column_name"), all_tables)
|
22
|
+
row.fetch("column_name")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
fk_columns.each do |column_name|
|
27
|
+
if foreign_keys_info.none? { |row| row.fetch("column_name") == column_name }
|
28
|
+
agg.push(
|
29
|
+
{
|
30
|
+
table: table,
|
31
|
+
column_name: column_name,
|
32
|
+
}
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
agg
|
38
|
+
end
|
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
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyPgExtras
|
4
|
+
class MissingFkIndexes
|
5
|
+
def self.call(table_name)
|
6
|
+
new.call(table_name)
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(table_name)
|
10
|
+
tables = if table_name
|
11
|
+
[table_name]
|
12
|
+
else
|
13
|
+
all_tables
|
14
|
+
end
|
15
|
+
|
16
|
+
indexes_info = query_module.index_info(in_format: :hash)
|
17
|
+
|
18
|
+
tables.reduce([]) do |agg, table|
|
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)
|
21
|
+
|
22
|
+
fk_columns = schema.filter_map do |row|
|
23
|
+
if DetectFkColumn.call(row.fetch("column_name"), all_tables)
|
24
|
+
row.fetch("column_name")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
fk_columns.each do |column_name|
|
29
|
+
if index_info.none? { |row| row.fetch(:columns)[0] == column_name }
|
30
|
+
agg.push(
|
31
|
+
{
|
32
|
+
table: table,
|
33
|
+
column_name: column_name,
|
34
|
+
}
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
agg
|
40
|
+
end
|
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
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
/* Foreign keys info for a specific table */
|
2
|
+
|
3
|
+
SELECT
|
4
|
+
conrelid::regclass AS table_name,
|
5
|
+
conname AS constraint_name,
|
6
|
+
a.attname AS column_name,
|
7
|
+
confrelid::regclass AS foreign_table_name,
|
8
|
+
af.attname AS foreign_column_name
|
9
|
+
FROM
|
10
|
+
pg_constraint AS c
|
11
|
+
JOIN
|
12
|
+
pg_attribute AS a ON a.attnum = ANY(c.conkey) AND a.attrelid = c.conrelid
|
13
|
+
JOIN
|
14
|
+
pg_attribute AS af ON af.attnum = ANY(c.confkey) AND af.attrelid = c.confrelid
|
15
|
+
WHERE
|
16
|
+
c.contype = 'f'
|
17
|
+
AND c.conrelid = '%{table_name}'::regclass;
|
data/ruby-pg-extras.gemspec
CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
|
|
20
20
|
s.add_development_dependency "rake"
|
21
21
|
s.add_development_dependency "rspec"
|
22
22
|
s.add_development_dependency "rufo"
|
23
|
+
s.add_development_dependency "dbg-rb"
|
23
24
|
|
24
25
|
if s.respond_to?(:metadata=)
|
25
26
|
s.metadata = { "rubygems_mfa_required" => "true" }
|
@@ -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
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
require "ruby-pg-extras"
|
5
|
+
|
6
|
+
describe "#missing_fk_constraints" do
|
7
|
+
it "detects missing foreign keys for all tables" do
|
8
|
+
result = RubyPgExtras.missing_fk_constraints(in_format: :hash)
|
9
|
+
expect(result.size).to eq(2)
|
10
|
+
expect(result[0]).to eq({
|
11
|
+
table: "users", column_name: "company_id",
|
12
|
+
})
|
13
|
+
expect(result[1]).to eq({
|
14
|
+
table: "posts", column_name: "topic_id",
|
15
|
+
})
|
16
|
+
end
|
17
|
+
|
18
|
+
it "detects missing foreign_keys for a specific table" do
|
19
|
+
result = RubyPgExtras.missing_fk_constraints(args: { table_name: "posts" }, in_format: :hash)
|
20
|
+
|
21
|
+
expect(result.size).to eq(1)
|
22
|
+
expect(result[0]).to eq({
|
23
|
+
table: "posts", column_name: "topic_id",
|
24
|
+
})
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
require "ruby-pg-extras"
|
5
|
+
|
6
|
+
describe "missing_fk_indexes" do
|
7
|
+
it "detects missing indexes for all tables" do
|
8
|
+
result = RubyPgExtras.missing_fk_indexes(in_format: :hash)
|
9
|
+
expect(result.size).to eq(2)
|
10
|
+
expect(result[0]).to eq({
|
11
|
+
table: "users", column_name: "company_id",
|
12
|
+
})
|
13
|
+
expect(result[1]).to eq({
|
14
|
+
table: "posts", column_name: "topic_id",
|
15
|
+
})
|
16
|
+
end
|
17
|
+
|
18
|
+
it "detects missing indexes for specific table" do
|
19
|
+
result = RubyPgExtras.missing_fk_indexes(args: { table_name: "posts" }, in_format: :hash)
|
20
|
+
expect(result.size).to eq(1)
|
21
|
+
expect(result[0]).to eq({
|
22
|
+
table: "posts", column_name: "topic_id",
|
23
|
+
})
|
24
|
+
end
|
25
|
+
end
|
data/spec/smoke_spec.rb
CHANGED
@@ -13,7 +13,13 @@ describe RubyPgExtras do
|
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
-
|
16
|
+
SKIP_QUERIES = %i[
|
17
|
+
kill_all
|
18
|
+
table_schema
|
19
|
+
table_foreign_keys
|
20
|
+
]
|
21
|
+
|
22
|
+
RubyPgExtras::QUERIES.reject { |q| SKIP_QUERIES.include?(q) }.each do |query_name|
|
17
23
|
it "#{query_name} query can be executed" do
|
18
24
|
expect do
|
19
25
|
RubyPgExtras.run_query(
|
@@ -24,6 +30,34 @@ describe RubyPgExtras do
|
|
24
30
|
end
|
25
31
|
end
|
26
32
|
|
33
|
+
describe "table_foreign_keys" do
|
34
|
+
it "returns a correct fk info" do
|
35
|
+
result = RubyPgExtras.table_foreign_keys(args: { table_name: :posts }, in_format: :hash)
|
36
|
+
expect(result.size).to eq(1)
|
37
|
+
expect(result[0].keys).to eq(["table_name", "constraint_name", "column_name", "foreign_table_name", "foreign_column_name"])
|
38
|
+
end
|
39
|
+
|
40
|
+
it "requires table_name arg" do
|
41
|
+
expect {
|
42
|
+
RubyPgExtras.table_foreign_keys(in_format: :hash)
|
43
|
+
}.to raise_error(ArgumentError)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "table_schema" do
|
48
|
+
it "returns a correct schema" do
|
49
|
+
result = RubyPgExtras.table_schema(args: { table_name: :users }, in_format: :hash)
|
50
|
+
expect(result.size).to eq(3)
|
51
|
+
expect(result[0].keys).to eq(["column_name", "data_type", "is_nullable", "column_default"])
|
52
|
+
end
|
53
|
+
|
54
|
+
it "requires table_name arg" do
|
55
|
+
expect {
|
56
|
+
RubyPgExtras.table_schema(in_format: :hash)
|
57
|
+
}.to raise_error(ArgumentError)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
27
61
|
describe "#database_url=" do
|
28
62
|
it "setting custom database URL works" do
|
29
63
|
RubyPgExtras.database_url = ENV.fetch("DATABASE_URL")
|
data/spec/spec_helper.rb
CHANGED
@@ -6,26 +6,56 @@ require_relative "../lib/ruby-pg-extras"
|
|
6
6
|
|
7
7
|
pg_version = ENV["PG_VERSION"]
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
elsif pg_version == "17"
|
20
|
-
"5437"
|
21
|
-
else
|
22
|
-
"5432"
|
23
|
-
end
|
9
|
+
PG_PORTS = {
|
10
|
+
"12" => "5432",
|
11
|
+
"13" => "5433",
|
12
|
+
"14" => "5434",
|
13
|
+
"15" => "5435",
|
14
|
+
"16" => "5436",
|
15
|
+
"17" => "5437",
|
16
|
+
}
|
17
|
+
|
18
|
+
port = PG_PORTS.fetch(pg_version, "5432")
|
24
19
|
|
25
20
|
ENV["DATABASE_URL"] ||= "postgresql://postgres:secret@localhost:#{port}/ruby-pg-extras-test"
|
26
21
|
|
27
22
|
RSpec.configure do |config|
|
28
23
|
config.before(:suite) do
|
24
|
+
DB_SCHEMA = <<-SQL
|
25
|
+
DROP TABLE IF EXISTS posts;
|
26
|
+
DROP TABLE IF EXISTS users;
|
27
|
+
DROP TABLE IF EXISTS topics;
|
28
|
+
DROP TABLE IF EXISTS companies;
|
29
|
+
|
30
|
+
CREATE TABLE users (
|
31
|
+
id SERIAL PRIMARY KEY,
|
32
|
+
email VARCHAR(255),
|
33
|
+
company_id INTEGER
|
34
|
+
);
|
35
|
+
|
36
|
+
CREATE TABLE posts (
|
37
|
+
id SERIAL PRIMARY KEY,
|
38
|
+
user_id INTEGER NOT NULL,
|
39
|
+
topic_id INTEGER,
|
40
|
+
external_id INTEGER,
|
41
|
+
title VARCHAR(255),
|
42
|
+
CONSTRAINT fk_posts_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
43
|
+
);
|
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
|
+
|
55
|
+
CREATE INDEX index_posts_on_user_id ON posts(user_id);
|
56
|
+
SQL
|
57
|
+
|
58
|
+
RubyPgExtras.connection.exec(DB_SCHEMA)
|
29
59
|
RubyPgExtras.connection.exec("CREATE EXTENSION IF NOT EXISTS pg_stat_statements;")
|
30
60
|
RubyPgExtras.connection.exec("CREATE EXTENSION IF NOT EXISTS pg_buffercache;")
|
31
61
|
RubyPgExtras.connection.exec("CREATE EXTENSION IF NOT EXISTS sslinfo;")
|
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.
|
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-
|
11
|
+
date: 2025-02-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pg
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: dbg-rb
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
83
97
|
description: " Ruby port of Heroku PG Extras. The goal of this project is to provide
|
84
98
|
a powerful insights into PostgreSQL database for Ruby on Rails apps that are not
|
85
99
|
using the default Heroku PostgreSQL plugin. "
|
@@ -97,10 +111,13 @@ files:
|
|
97
111
|
- Rakefile
|
98
112
|
- docker-compose.yml.sample
|
99
113
|
- lib/ruby-pg-extras.rb
|
114
|
+
- lib/ruby_pg_extras/detect_fk_column.rb
|
100
115
|
- lib/ruby_pg_extras/diagnose_data.rb
|
101
116
|
- lib/ruby_pg_extras/diagnose_print.rb
|
102
117
|
- lib/ruby_pg_extras/index_info.rb
|
103
118
|
- lib/ruby_pg_extras/index_info_print.rb
|
119
|
+
- lib/ruby_pg_extras/missing_fk_constraints.rb
|
120
|
+
- lib/ruby_pg_extras/missing_fk_indexes.rb
|
104
121
|
- lib/ruby_pg_extras/queries/add_extensions.sql
|
105
122
|
- lib/ruby_pg_extras/queries/all_locks.sql
|
106
123
|
- lib/ruby_pg_extras/queries/bloat.sql
|
@@ -134,8 +151,10 @@ files:
|
|
134
151
|
- lib/ruby_pg_extras/queries/seq_scans.sql
|
135
152
|
- lib/ruby_pg_extras/queries/ssl_used.sql
|
136
153
|
- lib/ruby_pg_extras/queries/table_cache_hit.sql
|
154
|
+
- lib/ruby_pg_extras/queries/table_foreign_keys.sql
|
137
155
|
- lib/ruby_pg_extras/queries/table_index_scans.sql
|
138
156
|
- lib/ruby_pg_extras/queries/table_indexes_size.sql
|
157
|
+
- lib/ruby_pg_extras/queries/table_schema.sql
|
139
158
|
- lib/ruby_pg_extras/queries/table_size.sql
|
140
159
|
- lib/ruby_pg_extras/queries/tables.sql
|
141
160
|
- lib/ruby_pg_extras/queries/total_index_size.sql
|
@@ -148,9 +167,12 @@ files:
|
|
148
167
|
- lib/ruby_pg_extras/version.rb
|
149
168
|
- ruby-pg-extras-diagnose.png
|
150
169
|
- ruby-pg-extras.gemspec
|
170
|
+
- spec/detect_fk_column_spec.rb
|
151
171
|
- spec/diagnose_data_spec.rb
|
152
172
|
- spec/diagnose_print_spec.rb
|
153
173
|
- spec/index_info_spec.rb
|
174
|
+
- spec/missing_fk_constraints_spec.rb
|
175
|
+
- spec/missing_fk_indexes_spec.rb
|
154
176
|
- spec/size_parser_spec.rb
|
155
177
|
- spec/smoke_spec.rb
|
156
178
|
- spec/spec_helper.rb
|
@@ -180,9 +202,12 @@ signing_key:
|
|
180
202
|
specification_version: 4
|
181
203
|
summary: Ruby PostgreSQL performance database insights
|
182
204
|
test_files:
|
205
|
+
- spec/detect_fk_column_spec.rb
|
183
206
|
- spec/diagnose_data_spec.rb
|
184
207
|
- spec/diagnose_print_spec.rb
|
185
208
|
- spec/index_info_spec.rb
|
209
|
+
- spec/missing_fk_constraints_spec.rb
|
210
|
+
- spec/missing_fk_indexes_spec.rb
|
186
211
|
- spec/size_parser_spec.rb
|
187
212
|
- spec/smoke_spec.rb
|
188
213
|
- spec/spec_helper.rb
|