ruby-pg-extras 5.6.0 → 5.6.2

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: 57c2a84fde86ad8d8f2edba0c3bb0036b525df7471013341338291b1e8c1f8e9
4
+ data.tar.gz: bc5624774204781bd1e970f118957aea5107e555c6f5698f3d32f7dfe5f3dc8e
5
5
  SHA512:
6
- metadata.gz: 27e9cc6ffc9dd82e53bc1511a8338123b2d2c60fb820358955d47412026d9b77660975d32005e829085bd780adf448774f9801507edf2a9844594b0318645779
7
- data.tar.gz: d27cd85774ca39e9837c136e006359beb3e88ddf10ca5b5c6b88ea59b3734731a0a0dfb1f97a9e905aff034e67a3cc5e8da250a0cdc6bb18fb5a6dfb2b9b7a95
6
+ metadata.gz: bbe16634243b05f414ca9eee58b30b939a3a7bed2bbadead9782aa39951bb2b4dc13e71cd5b3e38a0698b03fb330b43427a807f1b221e4beb62128e3dc2e6a62
7
+ data.tar.gz: 6ea5ef05e992a7f8f9dcb986b3ff983a888584bea0468711ce7e40a4c43c4485d205fbd1bb71dd50591f37e5036406829f7ae24f3ec5b5390da2e75bca7eccb7
data/README.md CHANGED
@@ -118,27 +118,45 @@ Keep reading to learn about methods that `diagnose` uses under the hood.
118
118
 
119
119
  ### `missing_fk_indexes`
120
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.
121
+ This method lists columns likely to be foreign keys (i.e. column name ending in `_id` and related table exists) which don't have an index. It's recommended to always index foreign key columns because they are used for searching relation objects.
122
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.
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 remember that each index decreases write performance and autovacuuming overhead, so be careful when adding multiple indexes to often updated tables.
124
124
 
125
125
  ```ruby
126
126
  RubyPgExtras.missing_fk_indexes(args: { table_name: "users" })
127
127
 
128
+ +---------------------------------+
129
+ | Missing foreign key indexes |
130
+ +-------------------+-------------+
131
+ | table | column_name |
132
+ +-------------------+-------------+
133
+ | feedbacks | team_id |
134
+ | votes | user_id |
135
+ +-------------------+-------------+
136
+
128
137
  ```
129
138
 
130
139
  `table_name` argument is optional, if omitted, the method will display missing fk indexes for all the tables.
131
140
 
132
141
  ## `missing_fk_constraints`
133
142
 
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).
143
+ Similarly to the previous method, this one shows columns likely to be foreign keys 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
144
 
136
145
  ```ruby
137
146
  RubyPgExtras.missing_fk_constraints(args: { table_name: "users" })
138
147
 
148
+ +---------------------------------+
149
+ | Missing foreign key constraints |
150
+ +-------------------+-------------+
151
+ | table | column_name |
152
+ +-------------------+-------------+
153
+ | feedbacks | team_id |
154
+ | votes | user_id |
155
+ +-------------------+-------------+
156
+
139
157
  ```
140
158
 
141
- `table_name` argument is optional, if omitted, method will display missing foreign keys for all the tables.
159
+ `table_name` argument is optional, if omitted, method will display missing fk constraints for all the tables.
142
160
 
143
161
  ### `table_info`
144
162
 
@@ -160,6 +178,14 @@ This method displays structure of a selected table, listing its column names, to
160
178
  ```ruby
161
179
  RubyPgExtras.table_schema(args: { table_name: "users" })
162
180
 
181
+ +-----------------------------+-----------------------------+-------------+-----------------------------------+
182
+ | column_name | data_type | is_nullable | column_default |
183
+ +-----------------------------+-----------------------------+-------------+-----------------------------------+
184
+ | id | bigint | NO | nextval('users_id_seq'::regclass) |
185
+ | team_id | integer | NO | |
186
+ | slack_id | character varying | NO | |
187
+ | pseudonym | character varying | YES | |
188
+
163
189
  ```
164
190
 
165
191
  ### `table_foreign_keys`
@@ -169,6 +195,12 @@ This method displays foreign key constraints for a selected table. It lists fore
169
195
  ```ruby
170
196
  RubyPgExtras.table_foreign_keys(args: { table_name: "users" })
171
197
 
198
+ +------------+---------------------+-------------+--------------------+---------------------+
199
+ | table_name | constraint_name | column_name | foreign_table_name | foreign_column_name |
200
+ +------------+---------------------+-------------+--------------------+---------------------+
201
+ | users | fk_rails_b2bbf87303 | team_id | teams | id |
202
+ +------------+---------------------+-------------+--------------------+---------------------+
203
+
172
204
  ```
173
205
 
174
206
  ### `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,19 +10,23 @@ 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.indexes(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("tablename") == 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|
25
- if index_info.none? { |row| row.fetch(:columns)[0] == column_name }
29
+ if index_info.none? { |row| row.fetch("columns").split(",").first == column_name }
26
30
  agg.push(
27
31
  {
28
32
  table: table,
@@ -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.2"
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,11 +37,22 @@ 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
 
42
- CREATE INDEX index_posts_on_user_id ON posts(user_id);
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, topic_id);
43
56
  SQL
44
57
 
45
58
  RubyPgExtras.connection.exec(DB_SCHEMA)
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.2
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-11 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