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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03a33801e7bd4bd56cd4b55d3295a97ce3fe96e5fe489641b5c2e5118618c023
4
- data.tar.gz: b778857dd526e7b80b286b95be11d4ef6b738c68aab74e9394b57aee4148f1a6
3
+ metadata.gz: d63b815252ba55f7501178013917e4cb7c48cf64f34ff1501dab8a6d84b3d7c4
4
+ data.tar.gz: 8465dbaee0cd16da98fcaaeb9a20b92185060ded5371220ae04194610e9c2a58
5
5
  SHA512:
6
- metadata.gz: 2642d76755c1c45ddd25cae9e53acbefe4f5083771dd751c9e58f367e580fda33c586d5cb026d220c45f635816db2b81a4f1f07e00a820403cac81279445ac17
7
- data.tar.gz: df227a8b82651322a83b3fad613b44d746b89bb68e7b604ac3474b4f430c5fb1633c7b977b2c1b54538d8679d309a0442037ea016c0278da2163df808735d7a2
6
+ metadata.gz: cbac3bdc3566aaf3ac39702e8485661b3460ddf25506a52aa46c2f386345d47d868cce197f1b5dec3f1adff7e14081d772d2650cd65c889a48291ab1aabf8125
7
+ data.tar.gz: 155101c5bc0bf1d791ccbad29b95d8f10556a32caa16c4ded2c6cfef51b3b834c2759e15d273830dc5f99221bd7b969022b66879886afe29cb7b8c7d8ace094b
@@ -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,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.
@@ -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.run_query(query_name:, in_format:, args: {})
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 = 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'")
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
- sql = if (custom_args = DEFAULT_ARGS[query_name].merge(args)) != {}
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 = connection.exec(sql)
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;
@@ -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.1"
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,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
@@ -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,56 @@ 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
+ 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.5.1
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-01-04 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
@@ -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