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