ruby-pg-extras 5.5.1 → 5.6.0

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: 03a33801e7bd4bd56cd4b55d3295a97ce3fe96e5fe489641b5c2e5118618c023
4
- data.tar.gz: b778857dd526e7b80b286b95be11d4ef6b738c68aab74e9394b57aee4148f1a6
3
+ metadata.gz: 1348ba4881f0d65e98e76e5224fc830d5c124f9005a66d5b4157da7eb8f7cfd3
4
+ data.tar.gz: 83803cc3cbb81a80791b74aa8c768d6033f572d71b8074083c96172bc41f364e
5
5
  SHA512:
6
- metadata.gz: 2642d76755c1c45ddd25cae9e53acbefe4f5083771dd751c9e58f367e580fda33c586d5cb026d220c45f635816db2b81a4f1f07e00a820403cac81279445ac17
7
- data.tar.gz: df227a8b82651322a83b3fad613b44d746b89bb68e7b604ac3474b4f430c5fb1633c7b977b2c1b54538d8679d309a0442037ea016c0278da2163df808735d7a2
6
+ metadata.gz: 27e9cc6ffc9dd82e53bc1511a8338123b2d2c60fb820358955d47412026d9b77660975d32005e829085bd780adf448774f9801507edf2a9844594b0318645779
7
+ data.tar.gz: d27cd85774ca39e9837c136e006359beb3e88ddf10ca5b5c6b88ea59b3734731a0a0dfb1f97a9e905aff034e67a3cc5e8da250a0cdc6bb18fb5a6dfb2b9b7a95
@@ -12,7 +12,7 @@ jobs:
12
12
  strategy:
13
13
  fail-fast: false
14
14
  matrix:
15
- ruby-version: ['3.3', '3.2', '3.1', '3.0', '2.7', '2.6']
15
+ ruby-version: ['3.3', '3.2', '3.1', '3.0', '2.7']
16
16
  steps:
17
17
  - uses: actions/checkout@v4
18
18
  - name: Run PostgreSQL 12
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,24 @@ 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
+
165
+ ### `table_foreign_keys`
166
+
167
+ This method displays foreign key constraints for a selected table. It lists foreign key name, source and target columns, and related table name.
168
+
169
+ ```ruby
170
+ RubyPgExtras.table_foreign_keys(args: { table_name: "users" })
171
+
172
+ ```
173
+
132
174
  ### `index_info`
133
175
 
134
176
  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.
@@ -6,6 +6,8 @@ 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/missing_fk_indexes"
10
+ require "ruby_pg_extras/missing_fk_constraints"
9
11
  require "ruby_pg_extras/index_info"
10
12
  require "ruby_pg_extras/index_info_print"
11
13
  require "ruby_pg_extras/table_info"
@@ -26,10 +28,16 @@ module RubyPgExtras
26
28
  unused_indexes duplicate_indexes vacuum_stats kill_all kill_pid
27
29
  pg_stat_statements_reset buffercache_stats
28
30
  buffercache_usage ssl_used connections
31
+ table_schema table_foreign_keys
29
32
  )
30
33
 
31
34
  DEFAULT_SCHEMA = ENV["PG_EXTRAS_SCHEMA"] || "public"
32
35
 
36
+ REQUIRED_ARGS = {
37
+ table_schema: [:table_name],
38
+ table_foreign_keys: [:table_name],
39
+ }
40
+
33
41
  DEFAULT_ARGS = Hash.new({}).merge({
34
42
  calls: { limit: 10 },
35
43
  calls_legacy: { limit: 10 },
@@ -79,7 +87,13 @@ module RubyPgExtras
79
87
  end
80
88
  end
81
89
 
82
- sql = if (custom_args = DEFAULT_ARGS[query_name].merge(args)) != {}
90
+ REQUIRED_ARGS.fetch(query_name) { [] }.each do |arg_name|
91
+ if args[arg_name].nil?
92
+ raise ArgumentError, "'#{arg_name}' is required"
93
+ end
94
+ end
95
+
96
+ sql = if (custom_args = DEFAULT_ARGS.fetch(query_name, {}).merge(args)) != {}
83
97
  sql_for(query_name: query_name) % custom_args
84
98
  else
85
99
  sql_for(query_name: query_name)
@@ -135,6 +149,14 @@ module RubyPgExtras
135
149
  end
136
150
  end
137
151
 
152
+ def self.missing_fk_indexes(args: {}, in_format: :display_table)
153
+ RubyPgExtras::MissingFkIndexes.call(args[:table_name])
154
+ end
155
+
156
+ def self.missing_fk_constraints(args: {}, in_format: :display_table)
157
+ RubyPgExtras::MissingFkContraints.call(args[:table_name])
158
+ end
159
+
138
160
  def self.display_result(result, title:, in_format:)
139
161
  case in_format
140
162
  when :array
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyPgExtras
4
+ class MissingFkContraints
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
+ RubyPgExtras.table_size(in_format: :hash).map { |row| row.fetch("name") }
14
+ end
15
+
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)
19
+
20
+ fk_columns = schema.filter_map do |row|
21
+ row.fetch("column_name") if row.fetch("column_name") =~ /_id$/
22
+ end
23
+
24
+ fk_columns.each do |column_name|
25
+ if foreign_keys_info.none? { |row| row.fetch("column_name") == column_name }
26
+ agg.push(
27
+ {
28
+ table: table,
29
+ column_name: column_name,
30
+ }
31
+ )
32
+ end
33
+ end
34
+
35
+ agg
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,39 @@
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
+ RubyPgExtras.table_size(in_format: :hash).map { |row| row.fetch("name") }
14
+ end
15
+
16
+ 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
+
20
+ fk_columns = schema.filter_map do |row|
21
+ row.fetch("column_name") if row.fetch("column_name") =~ /_id$/
22
+ end
23
+
24
+ fk_columns.each do |column_name|
25
+ if index_info.none? { |row| row.fetch(:columns)[0] == column_name }
26
+ agg.push(
27
+ {
28
+ table: table,
29
+ column_name: column_name,
30
+ }
31
+ )
32
+ end
33
+ end
34
+
35
+ agg
36
+ end
37
+ end
38
+ end
39
+ 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;
@@ -0,0 +1,5 @@
1
+ /* Table column names and types */
2
+
3
+ SELECT column_name, data_type, is_nullable, column_default
4
+ FROM information_schema.columns
5
+ WHERE table_name = '%{table_name}';
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyPgExtras
4
- VERSION = "5.5.1"
4
+ VERSION = "5.6.0"
5
5
  end
@@ -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,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
- RubyPgExtras::QUERIES.reject { |q| q == :kill_all }.each do |query_name|
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,43 @@ require_relative "../lib/ruby-pg-extras"
6
6
 
7
7
  pg_version = ENV["PG_VERSION"]
8
8
 
9
- port = if pg_version == "12"
10
- "5432"
11
- elsif pg_version == "13"
12
- "5433"
13
- elsif pg_version == "14"
14
- "5434"
15
- elsif pg_version == "15"
16
- "5435"
17
- elsif pg_version == "16"
18
- "5436"
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
+
28
+ CREATE TABLE users (
29
+ id SERIAL PRIMARY KEY,
30
+ email VARCHAR(255),
31
+ company_id INTEGER
32
+ );
33
+
34
+ CREATE TABLE posts (
35
+ id SERIAL PRIMARY KEY,
36
+ user_id INTEGER NOT NULL,
37
+ topic_id INTEGER,
38
+ title VARCHAR(255),
39
+ CONSTRAINT fk_posts_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
40
+ );
41
+
42
+ CREATE INDEX index_posts_on_user_id ON posts(user_id);
43
+ SQL
44
+
45
+ RubyPgExtras.connection.exec(DB_SCHEMA)
29
46
  RubyPgExtras.connection.exec("CREATE EXTENSION IF NOT EXISTS pg_stat_statements;")
30
47
  RubyPgExtras.connection.exec("CREATE EXTENSION IF NOT EXISTS pg_buffercache;")
31
48
  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.5.1
4
+ version: 5.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - pawurb
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-04 00:00:00.000000000 Z
11
+ date: 2025-02-01 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. "
@@ -101,6 +115,8 @@ files:
101
115
  - lib/ruby_pg_extras/diagnose_print.rb
102
116
  - lib/ruby_pg_extras/index_info.rb
103
117
  - lib/ruby_pg_extras/index_info_print.rb
118
+ - lib/ruby_pg_extras/missing_fk_constraints.rb
119
+ - lib/ruby_pg_extras/missing_fk_indexes.rb
104
120
  - lib/ruby_pg_extras/queries/add_extensions.sql
105
121
  - lib/ruby_pg_extras/queries/all_locks.sql
106
122
  - lib/ruby_pg_extras/queries/bloat.sql
@@ -134,8 +150,10 @@ files:
134
150
  - lib/ruby_pg_extras/queries/seq_scans.sql
135
151
  - lib/ruby_pg_extras/queries/ssl_used.sql
136
152
  - lib/ruby_pg_extras/queries/table_cache_hit.sql
153
+ - lib/ruby_pg_extras/queries/table_foreign_keys.sql
137
154
  - lib/ruby_pg_extras/queries/table_index_scans.sql
138
155
  - lib/ruby_pg_extras/queries/table_indexes_size.sql
156
+ - lib/ruby_pg_extras/queries/table_schema.sql
139
157
  - lib/ruby_pg_extras/queries/table_size.sql
140
158
  - lib/ruby_pg_extras/queries/tables.sql
141
159
  - lib/ruby_pg_extras/queries/total_index_size.sql
@@ -151,6 +169,8 @@ files:
151
169
  - spec/diagnose_data_spec.rb
152
170
  - spec/diagnose_print_spec.rb
153
171
  - spec/index_info_spec.rb
172
+ - spec/missing_fk_constraints.rb
173
+ - spec/missing_fk_indexes_spec.rb
154
174
  - spec/size_parser_spec.rb
155
175
  - spec/smoke_spec.rb
156
176
  - spec/spec_helper.rb
@@ -183,6 +203,8 @@ test_files:
183
203
  - spec/diagnose_data_spec.rb
184
204
  - spec/diagnose_print_spec.rb
185
205
  - spec/index_info_spec.rb
206
+ - spec/missing_fk_constraints.rb
207
+ - spec/missing_fk_indexes_spec.rb
186
208
  - spec/size_parser_spec.rb
187
209
  - spec/smoke_spec.rb
188
210
  - spec/spec_helper.rb