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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1348ba4881f0d65e98e76e5224fc830d5c124f9005a66d5b4157da7eb8f7cfd3
4
- data.tar.gz: 83803cc3cbb81a80791b74aa8c768d6033f572d71b8074083c96172bc41f364e
3
+ metadata.gz: d63b815252ba55f7501178013917e4cb7c48cf64f34ff1501dab8a6d84b3d7c4
4
+ data.tar.gz: 8465dbaee0cd16da98fcaaeb9a20b92185060ded5371220ae04194610e9c2a58
5
5
  SHA512:
6
- metadata.gz: 27e9cc6ffc9dd82e53bc1511a8338123b2d2c60fb820358955d47412026d9b77660975d32005e829085bd780adf448774f9801507edf2a9844594b0318645779
7
- data.tar.gz: d27cd85774ca39e9837c136e006359beb3e88ddf10ca5b5c6b88ea59b3734731a0a0dfb1f97a9e905aff034e67a3cc5e8da250a0cdc6bb18fb5a6dfb2b9b7a95
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`
@@ -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.run_query(query_name:, in_format:, args: {})
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 = RubyPgExtras.connection.exec("select installed_version from pg_available_extensions where name='pg_stat_statements'")
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 = connection.exec(sql)
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::MissingFkContraints.call(args[:table_name])
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 MissingFkContraints
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
- RubyPgExtras.table_size(in_format: :hash).map { |row| row.fetch("name") }
13
+ all_tables
14
14
  end
15
15
 
16
16
  tables.reduce([]) do |agg, table|
17
- foreign_keys_info = RubyPgExtras.table_foreign_keys(args: { table_name: table }, in_format: :hash)
18
- schema = RubyPgExtras.table_schema(args: { table_name: table }, in_format: :hash)
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
- row.fetch("column_name") if row.fetch("column_name") =~ /_id$/
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
- RubyPgExtras.table_size(in_format: :hash).map { |row| row.fetch("name") }
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 = RubyPgExtras.index_info(args: { table_name: table }, in_format: :hash)
18
- schema = RubyPgExtras.table_schema(args: { table_name: table }, in_format: :hash)
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
- row.fetch("column_name") if row.fetch("column_name") =~ /_id$/
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyPgExtras
4
- VERSION = "5.6.0"
4
+ VERSION = "5.6.1"
5
5
  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
@@ -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.0
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-01 00:00:00.000000000 Z
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/missing_fk_constraints.rb
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/missing_fk_constraints.rb
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