ruby-pg-extras 5.6.13 → 5.6.15
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 +4 -4
- data/.github/workflows/ci.yml +0 -12
- data/README.md +51 -8
- data/Rakefile +1 -1
- data/docker-compose.yml.sample +0 -9
- data/lib/ruby-pg-extras.rb +33 -3
- data/lib/ruby_pg_extras/detect_fk_column.rb +16 -7
- data/lib/ruby_pg_extras/ignore_list.rb +47 -0
- data/lib/ruby_pg_extras/missing_fk_constraints.rb +51 -22
- data/lib/ruby_pg_extras/missing_fk_indexes.rb +7 -14
- data/lib/ruby_pg_extras/queries/analyze_progress.sql +27 -0
- data/lib/ruby_pg_extras/queries/foreign_keys.sql +19 -0
- data/lib/ruby_pg_extras/queries/table_schemas.sql +5 -0
- data/lib/ruby_pg_extras/queries/vacuum_io_stats.sql +31 -0
- data/lib/ruby_pg_extras/queries/vacuum_io_stats_legacy.sql +5 -0
- data/lib/ruby_pg_extras/queries/vacuum_progress.sql +26 -0
- data/lib/ruby_pg_extras/queries/vacuum_progress_17.sql +29 -0
- data/lib/ruby_pg_extras/queries/vacuum_stats.sql +37 -7
- data/lib/ruby_pg_extras/version.rb +1 -1
- data/spec/detect_fk_column_spec.rb +36 -0
- data/spec/ignore_list_spec.rb +60 -0
- data/spec/missing_fk_constraints_spec.rb +41 -11
- data/spec/missing_fk_indexes_spec.rb +7 -11
- data/spec/smoke_spec.rb +2 -2
- data/spec/spec_helper.rb +34 -6
- metadata +12 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e20ed34b0677aa7e50c14ca2d5a511d4854f0eb34a1ad7e04a4f94fe2349895b
|
|
4
|
+
data.tar.gz: ef35f2a431195ba26a0271808315b54985254df379412ffb4eef9dc4ff81f7e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6145da151dc40da7d0f79a3a2c9ff3d059dd9b320e44b751f6d5a9d0b00d395a78aff3c8bd1bf51453aa65ccb7cf12953ec73e54b4a350d7ee34ec61b5f661a0
|
|
7
|
+
data.tar.gz: 72861cc4517a977e39266f2617094db82be77ba31f932831785540c3bc3ae078e1deccbc32b5b025343bbbe2e2c2d25529e24dad170ea9e8969795dfe3ee64de
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -15,13 +15,6 @@ jobs:
|
|
|
15
15
|
ruby-version: ['3.4', '3.3', '3.2', '3.1', '3.0', '2.7']
|
|
16
16
|
steps:
|
|
17
17
|
- uses: actions/checkout@v4
|
|
18
|
-
- name: Run PostgreSQL 12
|
|
19
|
-
run: |
|
|
20
|
-
docker run --env POSTGRES_USER=postgres \
|
|
21
|
-
--env POSTGRES_DB=ruby-pg-extras-test \
|
|
22
|
-
--env POSTGRES_PASSWORD=secret \
|
|
23
|
-
-d -p 5432:5432 postgres:12.20-alpine \
|
|
24
|
-
postgres -c shared_preload_libraries=pg_stat_statements
|
|
25
18
|
- name: Run PostgreSQL 13
|
|
26
19
|
run: |
|
|
27
20
|
docker run --env POSTGRES_USER=postgres \
|
|
@@ -71,11 +64,6 @@ jobs:
|
|
|
71
64
|
bundle config set --local path 'vendor/bundle'
|
|
72
65
|
bundle install
|
|
73
66
|
sleep 5
|
|
74
|
-
- name: Run tests for PG 12
|
|
75
|
-
env:
|
|
76
|
-
PG_VERSION: 12
|
|
77
|
-
run: |
|
|
78
|
-
bundle exec rspec spec/
|
|
79
67
|
- name: Run tests for PG 13
|
|
80
68
|
env:
|
|
81
69
|
PG_VERSION: 13
|
data/README.md
CHANGED
|
@@ -647,17 +647,60 @@ This command displays an estimation of table "bloat" – space allocated to a re
|
|
|
647
647
|
|
|
648
648
|
RubyPgExtras.vacuum_stats
|
|
649
649
|
|
|
650
|
-
schema | table |
|
|
651
|
-
|
|
652
|
-
public | log_table |
|
|
653
|
-
public | data_table |
|
|
654
|
-
public | other_table |
|
|
655
|
-
public | queue_table | | 2013-04-26 17:39 | 12 | 8,228 | 52 | yes
|
|
656
|
-
public | picnic_table | | | 13 | 0 | 53 |
|
|
650
|
+
schema | table | last_manual_vacuum | manual_vacuum_count | last_autovacuum | autovacuum_count | rowcount | dead_rowcount | dead_tup_autovacuum_threshold | n_ins_since_vacuum | insert_autovacuum_threshold | expect_autovacuum
|
|
651
|
+
--------+-----------------------+--------------------+---------------------+------------------+------------------+----------------+----------------+-------------------------------+--------------------+-----------------------------+-------------------
|
|
652
|
+
public | log_table | | 0 | 2013-04-26 17:37 | 5 | 18,030 | 0 | 3,656 | 0 | 3,606 |
|
|
653
|
+
public | data_table | | 0 | 2013-04-26 13:09 | 3 | 79 | 28 | 66 | 10 | 16 | yes (dead_tuples)
|
|
654
|
+
public | other_table | | 0 | 2013-04-26 11:41 | 4 | 41 | 47 | 58 | 2,000 | 1,008 | yes (dead_tuples & inserts)
|
|
657
655
|
(truncated results for brevity)
|
|
658
656
|
```
|
|
659
657
|
|
|
660
|
-
This command displays statistics related to vacuum operations for each table, including an estimation of dead rows,
|
|
658
|
+
This command displays statistics related to vacuum operations for each table, including last manual vacuum and autovacuum timestamps and counters, an estimation of dead rows, dead-tuple-based autovacuum threshold, number of rows inserted since the last VACUUM (`n_ins_since_vacuum`) and the insert-based autovacuum threshold introduced in PostgreSQL 13 ([PostgreSQL autovacuum configuration](https://www.postgresql.org/docs/current/runtime-config-vacuum.html#RUNTIME-CONFIG-AUTOVACUUM)). It helps determine if current autovacuum thresholds (both dead-tuple and insert-based) are appropriate, and whether an automatic vacuum is expected to be triggered soon.
|
|
659
|
+
|
|
660
|
+
### `vacuum_progress`
|
|
661
|
+
|
|
662
|
+
```ruby
|
|
663
|
+
|
|
664
|
+
RubyPgExtras.vacuum_progress
|
|
665
|
+
|
|
666
|
+
database | schema | table | pid | phase | heap_blks_total | heap_blks_scanned | heap_blks_vacuumed | index_vacuum_count
|
|
667
|
+
----------+--------+----------+-------+---------------------+-----------------+-------------------+--------------------+--------------------
|
|
668
|
+
app_db | public | users | 12345 | scanning heap | 125000 | 32000 | 0 | 0
|
|
669
|
+
app_db | public | orders | 12346 | vacuuming indexes | 80000 | 80000 | 75000 | 3
|
|
670
|
+
(truncated results for brevity)
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
This command shows the current progress of `VACUUM` / autovacuum operations by reading `pg_stat_progress_vacuum` ([VACUUM progress reporting docs](https://www.postgresql.org/docs/current/progress-reporting.html#VACUUM-PROGRESS-REPORTING)). It can be used to see which tables are being vacuumed right now, how far each operation has progressed, and how many index vacuum cycles have been performed.
|
|
674
|
+
|
|
675
|
+
### `analyze_progress`
|
|
676
|
+
|
|
677
|
+
```ruby
|
|
678
|
+
|
|
679
|
+
RubyPgExtras.analyze_progress
|
|
680
|
+
|
|
681
|
+
database | schema | table | pid | phase | sample_blks_total | sample_blks_scanned | ext_stats_total | ext_stats_computed
|
|
682
|
+
----------+--------+----------+-------+----------------------+-------------------+---------------------+-----------------+--------------------
|
|
683
|
+
app_db | public | users | 22345 | acquiring sample rows| 5000 | 1200 | 2 | 0
|
|
684
|
+
app_db | public | orders | 22346 | computing statistics | 8000 | 8000 | 1 | 1
|
|
685
|
+
(truncated results for brevity)
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
This command displays the current progress of `ANALYZE` and auto-analyze operations using `pg_stat_progress_analyze` ([ANALYZE progress reporting docs](https://www.postgresql.org/docs/current/progress-reporting.html#ANALYZE-PROGRESS-REPORTING)). It helps understand how far statistics collection has progressed for each active analyze and whether extended statistics are being computed.
|
|
689
|
+
|
|
690
|
+
### `vacuum_io_stats`
|
|
691
|
+
|
|
692
|
+
```ruby
|
|
693
|
+
|
|
694
|
+
RubyPgExtras.vacuum_io_stats
|
|
695
|
+
|
|
696
|
+
backend_type | object | context | reads | writes | writebacks | extends | evictions | reuses | fsyncs | stats_reset
|
|
697
|
+
--------------------+----------+----------+---------+---------+-----------+---------+-----------+---------+--------+-------------------------------
|
|
698
|
+
autovacuum worker | relation | vacuum | 5824251 | 3028684 | 0 | 0 | 2588 | 5821460 | 0 | 2025-01-10 11:50:27.583875+00
|
|
699
|
+
autovacuum launcher| relation | autovacuum| 16306 | 2494 | 0 | 2915 | 17785 | 0 | 0 | 2025-01-10 11:50:27.583875+00
|
|
700
|
+
(truncated results for brevity)
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
This command surfaces cumulative I/O statistics for autovacuum-related VACUUM activity, based on the `pg_stat_io` view introduced in PostgreSQL 16 ([pg_stat_io documentation](https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-IO-VIEW)). It shows how many blocks autovacuum workers have read and written, how many buffer evictions and ring-buffer reuses occurred, and when the statistics were last reset; this is useful for determining whether autovacuum is responsible for I/O spikes, as described in the pganalyze article on `pg_stat_io` ([Tracking cumulative I/O activity by autovacuum and manual VACUUMs](https://pganalyze.com/blog/pg-stat-io#tracking-cumulative-io-activity-by-autovacuum-and-manual-vacuums)). On PostgreSQL versions below 16 this method returns a single informational row indicating that the feature is unavailable.
|
|
661
704
|
|
|
662
705
|
### `kill_all`
|
|
663
706
|
|
data/Rakefile
CHANGED
|
@@ -5,5 +5,5 @@ RSpec::Core::RakeTask.new(:spec)
|
|
|
5
5
|
|
|
6
6
|
desc "Test all PG versions"
|
|
7
7
|
task :test_all do
|
|
8
|
-
system("PG_VERSION=
|
|
8
|
+
system("PG_VERSION=13 bundle exec rspec spec/ && PG_VERSION=14 bundle exec rspec spec/ && PG_VERSION=15 bundle exec rspec spec/ && PG_VERSION=16 bundle exec rspec spec/ && PG_VERSION=17 bundle exec rspec spec/")
|
|
9
9
|
end
|
data/docker-compose.yml.sample
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
1
|
services:
|
|
2
|
-
postgres12:
|
|
3
|
-
image: postgres:12.20-alpine
|
|
4
|
-
command: postgres -c shared_preload_libraries=pg_stat_statements
|
|
5
|
-
environment:
|
|
6
|
-
POSTGRES_USER: postgres
|
|
7
|
-
POSTGRES_DB: ruby-pg-extras-test
|
|
8
|
-
POSTGRES_PASSWORD: secret
|
|
9
|
-
ports:
|
|
10
|
-
- '5432:5432'
|
|
11
2
|
postgres13:
|
|
12
3
|
image: postgres:13.16-alpine
|
|
13
4
|
command: postgres -c shared_preload_libraries=pg_stat_statements
|
data/lib/ruby-pg-extras.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "ruby_pg_extras/size_parser"
|
|
|
7
7
|
require "ruby_pg_extras/diagnose_data"
|
|
8
8
|
require "ruby_pg_extras/diagnose_print"
|
|
9
9
|
require "ruby_pg_extras/detect_fk_column"
|
|
10
|
+
require "ruby_pg_extras/ignore_list"
|
|
10
11
|
require "ruby_pg_extras/missing_fk_indexes"
|
|
11
12
|
require "ruby_pg_extras/missing_fk_constraints"
|
|
12
13
|
require "ruby_pg_extras/index_info"
|
|
@@ -26,10 +27,13 @@ module RubyPgExtras
|
|
|
26
27
|
long_running_queries mandelbrot outliers
|
|
27
28
|
records_rank seq_scans table_index_scans table_indexes_size
|
|
28
29
|
table_size total_index_size total_table_size
|
|
29
|
-
unused_indexes duplicate_indexes vacuum_stats
|
|
30
|
+
unused_indexes duplicate_indexes vacuum_stats vacuum_progress vacuum_io_stats
|
|
31
|
+
analyze_progress
|
|
32
|
+
kill_all kill_pid
|
|
30
33
|
pg_stat_statements_reset buffercache_stats
|
|
31
34
|
buffercache_usage ssl_used connections
|
|
32
|
-
table_schema
|
|
35
|
+
table_schema table_schemas
|
|
36
|
+
table_foreign_keys foreign_keys
|
|
33
37
|
)
|
|
34
38
|
|
|
35
39
|
DEFAULT_SCHEMA = ENV["PG_EXTRAS_SCHEMA"] || "public"
|
|
@@ -49,6 +53,11 @@ module RubyPgExtras
|
|
|
49
53
|
outliers: { limit: 10 },
|
|
50
54
|
outliers_legacy: { limit: 10 },
|
|
51
55
|
outliers_17: { limit: 10 },
|
|
56
|
+
vacuum_progress: {},
|
|
57
|
+
vacuum_progress_17: {},
|
|
58
|
+
vacuum_io_stats: {},
|
|
59
|
+
vacuum_io_stats_legacy: {},
|
|
60
|
+
analyze_progress: {},
|
|
52
61
|
buffercache_stats: { limit: 10 },
|
|
53
62
|
buffercache_usage: { limit: 20 },
|
|
54
63
|
unused_indexes: { max_scans: 50, schema: DEFAULT_SCHEMA },
|
|
@@ -57,12 +66,14 @@ module RubyPgExtras
|
|
|
57
66
|
index_cache_hit: { schema: DEFAULT_SCHEMA },
|
|
58
67
|
table_cache_hit: { schema: DEFAULT_SCHEMA },
|
|
59
68
|
table_size: { schema: DEFAULT_SCHEMA },
|
|
69
|
+
table_schemas: { schema: DEFAULT_SCHEMA },
|
|
60
70
|
index_scans: { schema: DEFAULT_SCHEMA },
|
|
61
71
|
cache_hit: { schema: DEFAULT_SCHEMA },
|
|
62
72
|
seq_scans: { schema: DEFAULT_SCHEMA },
|
|
63
73
|
table_index_scans: { schema: DEFAULT_SCHEMA },
|
|
64
74
|
records_rank: { schema: DEFAULT_SCHEMA },
|
|
65
75
|
tables: { schema: DEFAULT_SCHEMA },
|
|
76
|
+
foreign_keys: { schema: DEFAULT_SCHEMA },
|
|
66
77
|
kill_pid: { pid: 0 },
|
|
67
78
|
})
|
|
68
79
|
|
|
@@ -89,6 +100,25 @@ module RubyPgExtras
|
|
|
89
100
|
end
|
|
90
101
|
end
|
|
91
102
|
|
|
103
|
+
# vacuum_progress uses pg_stat_progress_vacuum only and does not depend on pg_stat_statements,
|
|
104
|
+
# so we switch it based on the server_version_num instead of the pg_stat_statements version.
|
|
105
|
+
if query_name == :vacuum_progress
|
|
106
|
+
server_version_num = conn.send(exec_method, "SHOW server_version_num").to_a[0].values[0].to_i
|
|
107
|
+
if server_version_num >= 170000
|
|
108
|
+
query_name = :vacuum_progress_17
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# vacuum_io_stats relies on pg_stat_io which is available starting from PostgreSQL 16.
|
|
113
|
+
# For older versions we fall back to vacuum_io_stats_legacy which just indicates
|
|
114
|
+
# that this feature is not available on the current server.
|
|
115
|
+
if query_name == :vacuum_io_stats
|
|
116
|
+
server_version_num = conn.send(exec_method, "SHOW server_version_num").to_a[0].values[0].to_i
|
|
117
|
+
if server_version_num < 160000
|
|
118
|
+
query_name = :vacuum_io_stats_legacy
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
92
122
|
REQUIRED_ARGS.fetch(query_name) { [] }.each do |arg_name|
|
|
93
123
|
if args[arg_name].nil?
|
|
94
124
|
raise ArgumentError, "'#{arg_name}' is required"
|
|
@@ -166,7 +196,7 @@ module RubyPgExtras
|
|
|
166
196
|
end
|
|
167
197
|
|
|
168
198
|
def self.missing_fk_constraints(args: {}, in_format: :display_table)
|
|
169
|
-
RubyPgExtras::MissingFkConstraints.call(args[:table_name])
|
|
199
|
+
RubyPgExtras::MissingFkConstraints.call(args[:table_name], ignore_list: args[:ignore_list])
|
|
170
200
|
end
|
|
171
201
|
|
|
172
202
|
def self.display_result(result, title:, in_format:)
|
|
@@ -34,16 +34,25 @@ module RubyPgExtras
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def call(column_name, tables)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
# Heuristic: Rails-style foreign keys are usually named `<table_singular>_id`.
|
|
38
|
+
# We accept underscores in the prefix (e.g. `account_user_id` -> `account_users`).
|
|
39
|
+
match = /\A(?<table_singular>.+)_id\z/i.match(column_name.to_s)
|
|
40
|
+
return false unless match
|
|
41
|
+
|
|
42
|
+
table_singular = match[:table_singular]
|
|
43
|
+
return false if table_singular.empty?
|
|
44
|
+
|
|
45
|
+
tables.include?(pluralize(table_singular))
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
def pluralize(word)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
# Table names from Postgres are typically lowercase. Normalize before applying rules.
|
|
50
|
+
word = word.to_s.downcase
|
|
51
|
+
|
|
52
|
+
return word if UNCOUNTABLE.include?(word)
|
|
53
|
+
return IRREGULAR.fetch(word) if IRREGULAR.key?(word)
|
|
54
|
+
# If the word is already an irregular plural (e.g. "people"), keep it as-is.
|
|
55
|
+
return word if IRREGULAR.value?(word)
|
|
47
56
|
|
|
48
57
|
PLURAL_RULES.reverse.each do |(rule, replacement)|
|
|
49
58
|
return word.gsub(rule, replacement) if word.match?(rule)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyPgExtras
|
|
4
|
+
# Parses and matches ignore patterns like:
|
|
5
|
+
# - "*" (ignore everything)
|
|
6
|
+
# - "posts.*" (ignore all columns on a table)
|
|
7
|
+
# - "category_id" (ignore this column name on all tables)
|
|
8
|
+
# - "posts.topic_id" (ignore a specific table+column)
|
|
9
|
+
class IgnoreList
|
|
10
|
+
def initialize(ignore_list)
|
|
11
|
+
@rules = normalize(ignore_list)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def ignored?(table:, column_name:)
|
|
15
|
+
@rules.any? do |rule|
|
|
16
|
+
next true if rule == "*"
|
|
17
|
+
next true if rule == "#{table}.*"
|
|
18
|
+
next true if rule == column_name
|
|
19
|
+
next true if rule == "#{table}.#{column_name}"
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def normalize(ignore_list)
|
|
27
|
+
entries =
|
|
28
|
+
case ignore_list
|
|
29
|
+
when nil
|
|
30
|
+
[]
|
|
31
|
+
when String
|
|
32
|
+
ignore_list.split(",")
|
|
33
|
+
when Array
|
|
34
|
+
ignore_list
|
|
35
|
+
else
|
|
36
|
+
raise ArgumentError, "ignore_list must be a String or Array"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
entries
|
|
40
|
+
.map { |v| v.to_s.strip }
|
|
41
|
+
.reject(&:empty?)
|
|
42
|
+
.uniq
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
|
|
@@ -2,39 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyPgExtras
|
|
4
4
|
class MissingFkConstraints
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
# ignore_list: array (or comma-separated string) of entries like:
|
|
6
|
+
# - "posts.category_id" (ignore a specific table+column)
|
|
7
|
+
# - "category_id" (ignore this column name for all tables)
|
|
8
|
+
# - "posts.*" (ignore all columns on a table)
|
|
9
|
+
# - "*" (ignore everything)
|
|
10
|
+
def self.call(table_name, ignore_list: nil)
|
|
11
|
+
new.call(table_name, ignore_list: ignore_list)
|
|
7
12
|
end
|
|
8
13
|
|
|
9
|
-
def call(table_name)
|
|
10
|
-
|
|
14
|
+
def call(table_name, ignore_list: nil)
|
|
15
|
+
ignore_list_matcher = IgnoreList.new(ignore_list)
|
|
16
|
+
|
|
17
|
+
tables =
|
|
18
|
+
if table_name
|
|
11
19
|
[table_name]
|
|
12
20
|
else
|
|
13
21
|
all_tables
|
|
14
22
|
end
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
schemas_by_table = query_module
|
|
25
|
+
.table_schemas(in_format: :hash)
|
|
26
|
+
.group_by { |row| row.fetch("table_name") }
|
|
19
27
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
fk_columns_by_table = query_module
|
|
29
|
+
.foreign_keys(in_format: :hash)
|
|
30
|
+
.group_by { |row| row.fetch("table_name") }
|
|
31
|
+
.transform_values { |rows| rows.map { |row| row.fetch("column_name") } }
|
|
32
|
+
|
|
33
|
+
tables.each_with_object([]) do |table, agg|
|
|
34
|
+
schema = schemas_by_table.fetch(table, [])
|
|
35
|
+
fk_columns_for_table = fk_columns_by_table.fetch(table, [])
|
|
36
|
+
schema_column_names = schema.map { |row| row.fetch("column_name") }
|
|
37
|
+
|
|
38
|
+
candidate_fk_columns = schema.filter_map do |row|
|
|
39
|
+
column_name = row.fetch("column_name")
|
|
40
|
+
|
|
41
|
+
# Skip columns explicitly excluded via ignore list.
|
|
42
|
+
next if ignore_list_matcher.ignored?(table: table, column_name: column_name)
|
|
25
43
|
|
|
26
|
-
|
|
27
|
-
if
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
# Skip columns that already have a foreign key constraint on this table.
|
|
45
|
+
next if fk_columns_for_table.include?(column_name)
|
|
46
|
+
|
|
47
|
+
# Skip columns that don't look like an FK candidate based on naming conventions.
|
|
48
|
+
next unless DetectFkColumn.call(column_name, all_tables)
|
|
49
|
+
|
|
50
|
+
# Rails polymorphic associations use <name>_id + <name>_type and can't have FK constraints.
|
|
51
|
+
candidate_prefix = column_name.delete_suffix("_id")
|
|
52
|
+
polymorphic_type_column = "#{candidate_prefix}_type"
|
|
53
|
+
# Skip polymorphic associations (cannot be expressed as a real FK constraint).
|
|
54
|
+
next if schema_column_names.include?(polymorphic_type_column)
|
|
55
|
+
|
|
56
|
+
column_name
|
|
35
57
|
end
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
candidate_fk_columns.each do |column_name|
|
|
60
|
+
agg.push(
|
|
61
|
+
{
|
|
62
|
+
table: table,
|
|
63
|
+
column_name: column_name,
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
end
|
|
38
67
|
end
|
|
39
68
|
end
|
|
40
69
|
|
|
@@ -7,25 +7,22 @@ module RubyPgExtras
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def call(table_name)
|
|
10
|
+
indexes_info = query_module.indexes(in_format: :hash)
|
|
11
|
+
foreign_keys = query_module.foreign_keys(in_format: :hash)
|
|
12
|
+
|
|
10
13
|
tables = if table_name
|
|
11
14
|
[table_name]
|
|
12
15
|
else
|
|
13
|
-
|
|
16
|
+
foreign_keys.map { |row| row.fetch("table_name") }.uniq
|
|
14
17
|
end
|
|
15
18
|
|
|
16
|
-
indexes_info = query_module.indexes(in_format: :hash)
|
|
17
|
-
|
|
18
19
|
tables.reduce([]) do |agg, table|
|
|
19
20
|
index_info = indexes_info.select { |row| row.fetch("tablename") == table }
|
|
20
|
-
|
|
21
|
+
table_fks = foreign_keys.select { |row| row.fetch("table_name") == table }
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
row.fetch("column_name")
|
|
25
|
-
end
|
|
26
|
-
end
|
|
23
|
+
table_fks.each do |fk|
|
|
24
|
+
column_name = fk.fetch("column_name")
|
|
27
25
|
|
|
28
|
-
fk_columns.each do |column_name|
|
|
29
26
|
if index_info.none? { |row| row.fetch("columns").split(",").first == column_name }
|
|
30
27
|
agg.push(
|
|
31
28
|
{
|
|
@@ -42,10 +39,6 @@ module RubyPgExtras
|
|
|
42
39
|
|
|
43
40
|
private
|
|
44
41
|
|
|
45
|
-
def all_tables
|
|
46
|
-
@_all_tables ||= query_module.table_size(in_format: :hash).map { |row| row.fetch("name") }
|
|
47
|
-
end
|
|
48
|
-
|
|
49
42
|
def query_module
|
|
50
43
|
RubyPgExtras
|
|
51
44
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* Current ANALYZE progress as reported by pg_stat_progress_analyze */
|
|
2
|
+
|
|
3
|
+
SELECT
|
|
4
|
+
a.datname AS database,
|
|
5
|
+
n.nspname AS schema,
|
|
6
|
+
c.relname AS table,
|
|
7
|
+
p.pid,
|
|
8
|
+
p.phase,
|
|
9
|
+
p.sample_blks_total,
|
|
10
|
+
p.sample_blks_scanned,
|
|
11
|
+
p.ext_stats_total,
|
|
12
|
+
p.ext_stats_computed,
|
|
13
|
+
p.child_tables_total,
|
|
14
|
+
p.child_tables_done,
|
|
15
|
+
p.current_child_table_relid
|
|
16
|
+
FROM
|
|
17
|
+
pg_stat_progress_analyze p
|
|
18
|
+
LEFT JOIN pg_class c ON p.relid = c.oid
|
|
19
|
+
LEFT JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
20
|
+
LEFT JOIN pg_stat_activity a ON p.pid = a.pid
|
|
21
|
+
ORDER BY
|
|
22
|
+
a.datname,
|
|
23
|
+
n.nspname,
|
|
24
|
+
c.relname,
|
|
25
|
+
p.pid;
|
|
26
|
+
|
|
27
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* Foreign keys info for all tables */
|
|
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
|
+
JOIN
|
|
16
|
+
pg_namespace AS n ON n.oid = c.connamespace
|
|
17
|
+
WHERE
|
|
18
|
+
c.contype = 'f'
|
|
19
|
+
AND n.nspname = '%{schema}';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* I/O statistics for autovacuum backends from pg_stat_io (PostgreSQL 16+) */
|
|
2
|
+
|
|
3
|
+
SELECT
|
|
4
|
+
backend_type,
|
|
5
|
+
object,
|
|
6
|
+
context,
|
|
7
|
+
reads,
|
|
8
|
+
read_time,
|
|
9
|
+
writes,
|
|
10
|
+
write_time,
|
|
11
|
+
writebacks,
|
|
12
|
+
writeback_time,
|
|
13
|
+
extends,
|
|
14
|
+
extend_time,
|
|
15
|
+
fsyncs,
|
|
16
|
+
fsync_time,
|
|
17
|
+
reuses,
|
|
18
|
+
evictions,
|
|
19
|
+
stats_reset
|
|
20
|
+
FROM
|
|
21
|
+
pg_stat_io
|
|
22
|
+
WHERE
|
|
23
|
+
backend_type IN ('autovacuum worker', 'autovacuum launcher')
|
|
24
|
+
AND object = 'relation'
|
|
25
|
+
AND context IN ('vacuum', 'autovacuum')
|
|
26
|
+
ORDER BY
|
|
27
|
+
backend_type,
|
|
28
|
+
context,
|
|
29
|
+
object;
|
|
30
|
+
|
|
31
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* Current VACUUM progress as reported by pg_stat_progress_vacuum */
|
|
2
|
+
|
|
3
|
+
SELECT
|
|
4
|
+
a.datname AS database,
|
|
5
|
+
n.nspname AS schema,
|
|
6
|
+
c.relname AS table,
|
|
7
|
+
p.pid,
|
|
8
|
+
p.phase,
|
|
9
|
+
p.heap_blks_total,
|
|
10
|
+
p.heap_blks_scanned,
|
|
11
|
+
p.heap_blks_vacuumed,
|
|
12
|
+
p.index_vacuum_count,
|
|
13
|
+
p.max_dead_tuples,
|
|
14
|
+
p.num_dead_tuples
|
|
15
|
+
FROM
|
|
16
|
+
pg_stat_progress_vacuum p
|
|
17
|
+
LEFT JOIN pg_class c ON p.relid = c.oid
|
|
18
|
+
LEFT JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
19
|
+
LEFT JOIN pg_stat_activity a ON p.pid = a.pid
|
|
20
|
+
ORDER BY
|
|
21
|
+
a.datname,
|
|
22
|
+
n.nspname,
|
|
23
|
+
c.relname,
|
|
24
|
+
p.pid;
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* Current VACUUM progress as reported by pg_stat_progress_vacuum */
|
|
2
|
+
|
|
3
|
+
SELECT
|
|
4
|
+
a.datname AS database,
|
|
5
|
+
n.nspname AS schema,
|
|
6
|
+
c.relname AS table,
|
|
7
|
+
p.pid,
|
|
8
|
+
p.phase,
|
|
9
|
+
p.heap_blks_total,
|
|
10
|
+
p.heap_blks_scanned,
|
|
11
|
+
p.heap_blks_vacuumed,
|
|
12
|
+
p.index_vacuum_count,
|
|
13
|
+
p.indexes_total,
|
|
14
|
+
p.indexes_processed,
|
|
15
|
+
p.num_dead_item_ids,
|
|
16
|
+
p.dead_tuple_bytes,
|
|
17
|
+
p.max_dead_tuple_bytes
|
|
18
|
+
FROM
|
|
19
|
+
pg_stat_progress_vacuum p
|
|
20
|
+
LEFT JOIN pg_class c ON p.relid = c.oid
|
|
21
|
+
LEFT JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
22
|
+
LEFT JOIN pg_stat_activity a ON p.pid = a.pid
|
|
23
|
+
ORDER BY
|
|
24
|
+
a.datname,
|
|
25
|
+
n.nspname,
|
|
26
|
+
c.relname,
|
|
27
|
+
p.pid;
|
|
28
|
+
|
|
29
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Dead rows and whether an automatic vacuum is expected to be triggered */
|
|
1
|
+
/* Dead rows, new inserts since last VACUUM and whether an automatic vacuum is expected to be triggered */
|
|
2
2
|
|
|
3
3
|
WITH table_opts AS (
|
|
4
4
|
SELECT
|
|
@@ -17,22 +17,52 @@ WITH table_opts AS (
|
|
|
17
17
|
WHEN relopts LIKE '%autovacuum_vacuum_scale_factor%'
|
|
18
18
|
THEN substring(relopts, '.*autovacuum_vacuum_scale_factor=([0-9.]+).*')::real
|
|
19
19
|
ELSE current_setting('autovacuum_vacuum_scale_factor')::real
|
|
20
|
-
END AS autovacuum_vacuum_scale_factor
|
|
20
|
+
END AS autovacuum_vacuum_scale_factor,
|
|
21
|
+
CASE
|
|
22
|
+
WHEN relopts LIKE '%autovacuum_vacuum_insert_threshold%'
|
|
23
|
+
THEN substring(relopts, '.*autovacuum_vacuum_insert_threshold=([0-9.]+).*')::integer
|
|
24
|
+
ELSE current_setting('autovacuum_vacuum_insert_threshold')::integer
|
|
25
|
+
END AS autovacuum_vacuum_insert_threshold,
|
|
26
|
+
CASE
|
|
27
|
+
WHEN relopts LIKE '%autovacuum_vacuum_insert_scale_factor%'
|
|
28
|
+
THEN substring(relopts, '.*autovacuum_vacuum_insert_scale_factor=([0-9.]+).*')::real
|
|
29
|
+
ELSE current_setting('autovacuum_vacuum_insert_scale_factor')::real
|
|
30
|
+
END AS autovacuum_vacuum_insert_scale_factor
|
|
21
31
|
FROM
|
|
22
32
|
table_opts
|
|
23
33
|
)
|
|
24
34
|
SELECT
|
|
25
35
|
vacuum_settings.nspname AS schema,
|
|
26
36
|
vacuum_settings.relname AS table,
|
|
27
|
-
to_char(psut.last_vacuum, 'YYYY-MM-DD HH24:MI') AS
|
|
37
|
+
to_char(psut.last_vacuum, 'YYYY-MM-DD HH24:MI') AS last_manual_vacuum,
|
|
38
|
+
to_char(psut.vacuum_count, '9G999G999G999') AS manual_vacuum_count,
|
|
28
39
|
to_char(psut.last_autovacuum, 'YYYY-MM-DD HH24:MI') AS last_autovacuum,
|
|
40
|
+
to_char(psut.autovacuum_count, '9G999G999G999') AS autovacuum_count,
|
|
29
41
|
to_char(pg_class.reltuples, '9G999G999G999') AS rowcount,
|
|
30
42
|
to_char(psut.n_dead_tup, '9G999G999G999') AS dead_rowcount,
|
|
31
|
-
to_char(
|
|
32
|
-
|
|
43
|
+
to_char(
|
|
44
|
+
autovacuum_vacuum_threshold
|
|
45
|
+
+ (autovacuum_vacuum_scale_factor::numeric * pg_class.reltuples),
|
|
46
|
+
'9G999G999G999'
|
|
47
|
+
) AS dead_tup_autovacuum_threshold,
|
|
48
|
+
to_char(psut.n_ins_since_vacuum, '9G999G999G999') AS n_ins_since_vacuum,
|
|
49
|
+
to_char(
|
|
50
|
+
autovacuum_vacuum_insert_threshold
|
|
51
|
+
+ (autovacuum_vacuum_insert_scale_factor::numeric * pg_class.reltuples),
|
|
52
|
+
'9G999G999G999'
|
|
53
|
+
) AS insert_autovacuum_threshold,
|
|
33
54
|
CASE
|
|
34
|
-
WHEN
|
|
35
|
-
|
|
55
|
+
WHEN psut.n_dead_tup >= autovacuum_vacuum_threshold
|
|
56
|
+
+ (autovacuum_vacuum_scale_factor::numeric * pg_class.reltuples)
|
|
57
|
+
AND psut.n_ins_since_vacuum >= autovacuum_vacuum_insert_threshold
|
|
58
|
+
+ (autovacuum_vacuum_insert_scale_factor::numeric * pg_class.reltuples)
|
|
59
|
+
THEN 'yes (dead_tuples & inserts)'
|
|
60
|
+
WHEN psut.n_dead_tup >= autovacuum_vacuum_threshold
|
|
61
|
+
+ (autovacuum_vacuum_scale_factor::numeric * pg_class.reltuples)
|
|
62
|
+
THEN 'yes (dead_tuples)'
|
|
63
|
+
WHEN psut.n_ins_since_vacuum >= autovacuum_vacuum_insert_threshold
|
|
64
|
+
+ (autovacuum_vacuum_insert_scale_factor::numeric * pg_class.reltuples)
|
|
65
|
+
THEN 'yes (inserts)'
|
|
36
66
|
END AS expect_autovacuum
|
|
37
67
|
FROM
|
|
38
68
|
pg_stat_user_tables psut INNER JOIN pg_class ON psut.relid = pg_class.oid
|
|
@@ -34,5 +34,41 @@ describe RubyPgExtras::DetectFkColumn do
|
|
|
34
34
|
expect(result).to eq(true)
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
|
+
|
|
38
|
+
context "matching table with underscored prefix" do
|
|
39
|
+
let(:column_name) { "account_user_id" }
|
|
40
|
+
let(:tables) { ["account_users", "users"] }
|
|
41
|
+
|
|
42
|
+
it "returns true" do
|
|
43
|
+
expect(result).to eq(true)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
context "matching table with uncountable noun" do
|
|
48
|
+
let(:column_name) { "fish_id" }
|
|
49
|
+
let(:tables) { ["fish", "users"] }
|
|
50
|
+
|
|
51
|
+
it "returns true" do
|
|
52
|
+
expect(result).to eq(true)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
context "matching table with mixed-case column name" do
|
|
57
|
+
let(:column_name) { "User_id" }
|
|
58
|
+
let(:tables) { ["users", "posts"] }
|
|
59
|
+
|
|
60
|
+
it "returns true" do
|
|
61
|
+
expect(result).to eq(true)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
context "matching table with irregular plural already pluralized" do
|
|
66
|
+
let(:column_name) { "people_id" }
|
|
67
|
+
let(:tables) { ["people", "users"] }
|
|
68
|
+
|
|
69
|
+
it "returns true" do
|
|
70
|
+
expect(result).to eq(true)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
37
73
|
end
|
|
38
74
|
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "ruby-pg-extras"
|
|
5
|
+
|
|
6
|
+
describe RubyPgExtras::IgnoreList do
|
|
7
|
+
describe "#ignored?" do
|
|
8
|
+
it "returns false when ignore_list is nil" do
|
|
9
|
+
list = described_class.new(nil)
|
|
10
|
+
expect(list.ignored?(table: "posts", column_name: "topic_id")).to eq(false)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "supports '*' to ignore everything" do
|
|
14
|
+
list = described_class.new(["*"])
|
|
15
|
+
expect(list.ignored?(table: "posts", column_name: "topic_id")).to eq(true)
|
|
16
|
+
expect(list.ignored?(table: "users", column_name: "customer_id")).to eq(true)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "supports 'table.*' to ignore all columns for a table" do
|
|
20
|
+
list = described_class.new(["posts.*"])
|
|
21
|
+
expect(list.ignored?(table: "posts", column_name: "topic_id")).to eq(true)
|
|
22
|
+
expect(list.ignored?(table: "posts", column_name: "user_id")).to eq(true)
|
|
23
|
+
expect(list.ignored?(table: "users", column_name: "topic_id")).to eq(false)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "supports 'column' to ignore a column name globally" do
|
|
27
|
+
list = described_class.new(["topic_id"])
|
|
28
|
+
expect(list.ignored?(table: "posts", column_name: "topic_id")).to eq(true)
|
|
29
|
+
expect(list.ignored?(table: "users", column_name: "topic_id")).to eq(true)
|
|
30
|
+
expect(list.ignored?(table: "posts", column_name: "user_id")).to eq(false)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "supports 'table.column' to ignore a specific table+column" do
|
|
34
|
+
list = described_class.new(["posts.topic_id"])
|
|
35
|
+
expect(list.ignored?(table: "posts", column_name: "topic_id")).to eq(true)
|
|
36
|
+
expect(list.ignored?(table: "users", column_name: "topic_id")).to eq(false)
|
|
37
|
+
expect(list.ignored?(table: "posts", column_name: "user_id")).to eq(false)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "accepts ignore_list as a comma-separated string" do
|
|
41
|
+
list = described_class.new("posts.topic_id, customer_id")
|
|
42
|
+
expect(list.ignored?(table: "posts", column_name: "topic_id")).to eq(true)
|
|
43
|
+
expect(list.ignored?(table: "users", column_name: "customer_id")).to eq(true)
|
|
44
|
+
expect(list.ignored?(table: "users", column_name: "topic_id")).to eq(false)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "strips whitespace, drops empty entries and de-duplicates" do
|
|
48
|
+
list = described_class.new([" posts.topic_id ", "", "posts.topic_id", " "])
|
|
49
|
+
expect(list.ignored?(table: "posts", column_name: "topic_id")).to eq(true)
|
|
50
|
+
expect(list.ignored?(table: "users", column_name: "topic_id")).to eq(false)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "raises ArgumentError for unsupported ignore_list types" do
|
|
54
|
+
expect { described_class.new(123) }.to raise_error(ArgumentError)
|
|
55
|
+
expect { described_class.new({}) }.to raise_error(ArgumentError)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
|
|
@@ -6,21 +6,51 @@ require "ruby-pg-extras"
|
|
|
6
6
|
describe "#missing_fk_constraints" do
|
|
7
7
|
it "detects missing foreign keys for all tables" do
|
|
8
8
|
result = RubyPgExtras.missing_fk_constraints(in_format: :hash)
|
|
9
|
-
expect(result
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
expect(result[1]).to eq({
|
|
14
|
-
table: "posts", column_name: "topic_id",
|
|
15
|
-
})
|
|
9
|
+
expect(result).to match_array([
|
|
10
|
+
{ table: "users", column_name: "customer_id" },
|
|
11
|
+
{ table: "posts", column_name: "category_id" },
|
|
12
|
+
])
|
|
16
13
|
end
|
|
17
14
|
|
|
18
15
|
it "detects missing foreign_keys for a specific table" do
|
|
19
16
|
result = RubyPgExtras.missing_fk_constraints(args: { table_name: "posts" }, in_format: :hash)
|
|
20
17
|
|
|
21
|
-
expect(result
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
expect(result).to eq([
|
|
19
|
+
{ table: "posts", column_name: "category_id" },
|
|
20
|
+
])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "does not report columns that already have foreign key constraints" do
|
|
24
|
+
result = RubyPgExtras.missing_fk_constraints(args: { table_name: "users" }, in_format: :hash)
|
|
25
|
+
expect(result).to eq([
|
|
26
|
+
{ table: "users", column_name: "customer_id" },
|
|
27
|
+
])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "does not report polymorphic associations (<name>_id with <name>_type)" do
|
|
31
|
+
result = RubyPgExtras.missing_fk_constraints(args: { table_name: "events" }, in_format: :hash)
|
|
32
|
+
expect(result).to eq([])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "supports ignoring a specific table+column via args" do
|
|
36
|
+
result = RubyPgExtras.missing_fk_constraints(
|
|
37
|
+
args: { ignore_list: ["posts.category_id"] },
|
|
38
|
+
in_format: :hash
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
expect(result).to eq([
|
|
42
|
+
{ table: "users", column_name: "customer_id" },
|
|
43
|
+
])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "supports ignoring a column name globally via args" do
|
|
47
|
+
result = RubyPgExtras.missing_fk_constraints(
|
|
48
|
+
args: { ignore_list: ["customer_id"] },
|
|
49
|
+
in_format: :hash
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
expect(result).to eq([
|
|
53
|
+
{ table: "posts", column_name: "category_id" },
|
|
54
|
+
])
|
|
25
55
|
end
|
|
26
56
|
end
|
|
@@ -6,20 +6,16 @@ require "ruby-pg-extras"
|
|
|
6
6
|
describe "missing_fk_indexes" do
|
|
7
7
|
it "detects missing indexes for all tables" do
|
|
8
8
|
result = RubyPgExtras.missing_fk_indexes(in_format: :hash)
|
|
9
|
-
expect(result
|
|
10
|
-
|
|
11
|
-
table: "
|
|
12
|
-
|
|
13
|
-
expect(result[1]).to eq({
|
|
14
|
-
table: "posts", column_name: "topic_id",
|
|
15
|
-
})
|
|
9
|
+
expect(result).to match_array([
|
|
10
|
+
{ table: "users", column_name: "company_id" },
|
|
11
|
+
{ table: "posts", column_name: "topic_id" },
|
|
12
|
+
])
|
|
16
13
|
end
|
|
17
14
|
|
|
18
15
|
it "detects missing indexes for specific table" do
|
|
19
16
|
result = RubyPgExtras.missing_fk_indexes(args: { table_name: "posts" }, in_format: :hash)
|
|
20
|
-
expect(result
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
})
|
|
17
|
+
expect(result).to eq([
|
|
18
|
+
{ table: "posts", column_name: "topic_id" },
|
|
19
|
+
])
|
|
24
20
|
end
|
|
25
21
|
end
|
data/spec/smoke_spec.rb
CHANGED
|
@@ -33,7 +33,7 @@ describe RubyPgExtras do
|
|
|
33
33
|
describe "table_foreign_keys" do
|
|
34
34
|
it "returns a correct fk info" do
|
|
35
35
|
result = RubyPgExtras.table_foreign_keys(args: { table_name: :posts }, in_format: :hash)
|
|
36
|
-
expect(result.size).to eq(
|
|
36
|
+
expect(result.size).to eq(2)
|
|
37
37
|
expect(result[0].keys).to eq(["table_name", "constraint_name", "column_name", "foreign_table_name", "foreign_column_name"])
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -47,7 +47,7 @@ describe RubyPgExtras do
|
|
|
47
47
|
describe "table_schema" do
|
|
48
48
|
it "returns a correct schema" do
|
|
49
49
|
result = RubyPgExtras.table_schema(args: { table_name: :users }, in_format: :hash)
|
|
50
|
-
expect(result.size).to eq(
|
|
50
|
+
expect(result.size).to eq(4)
|
|
51
51
|
expect(result[0].keys).to eq(["column_name", "data_type", "is_nullable", "column_default"])
|
|
52
52
|
end
|
|
53
53
|
|
data/spec/spec_helper.rb
CHANGED
|
@@ -7,7 +7,6 @@ require_relative "../lib/ruby-pg-extras"
|
|
|
7
7
|
pg_version = ENV["PG_VERSION"]
|
|
8
8
|
|
|
9
9
|
PG_PORTS = {
|
|
10
|
-
"12" => "5432",
|
|
11
10
|
"13" => "5433",
|
|
12
11
|
"14" => "5434",
|
|
13
12
|
"15" => "5435",
|
|
@@ -22,36 +21,65 @@ ENV["DATABASE_URL"] ||= "postgresql://postgres:secret@localhost:#{port}/ruby-pg-
|
|
|
22
21
|
RSpec.configure do |config|
|
|
23
22
|
config.before(:suite) do
|
|
24
23
|
DB_SCHEMA = <<-SQL
|
|
24
|
+
DROP TABLE IF EXISTS categories;
|
|
25
|
+
DROP TABLE IF EXISTS customers;
|
|
26
|
+
DROP TABLE IF EXISTS events;
|
|
27
|
+
DROP TABLE IF EXISTS subjects;
|
|
25
28
|
DROP TABLE IF EXISTS posts;
|
|
26
29
|
DROP TABLE IF EXISTS users;
|
|
27
30
|
DROP TABLE IF EXISTS topics;
|
|
28
31
|
DROP TABLE IF EXISTS companies;
|
|
29
32
|
|
|
33
|
+
CREATE TABLE companies (
|
|
34
|
+
id SERIAL PRIMARY KEY,
|
|
35
|
+
name VARCHAR(255)
|
|
36
|
+
);
|
|
37
|
+
|
|
30
38
|
CREATE TABLE users (
|
|
31
39
|
id SERIAL PRIMARY KEY,
|
|
32
40
|
email VARCHAR(255),
|
|
33
|
-
company_id INTEGER
|
|
41
|
+
company_id INTEGER,
|
|
42
|
+
customer_id INTEGER,
|
|
43
|
+
CONSTRAINT fk_users_company FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE topics (
|
|
47
|
+
id SERIAL PRIMARY KEY,
|
|
48
|
+
title VARCHAR(255)
|
|
34
49
|
);
|
|
35
50
|
|
|
36
51
|
CREATE TABLE posts (
|
|
37
52
|
id SERIAL PRIMARY KEY,
|
|
38
53
|
user_id INTEGER NOT NULL,
|
|
39
54
|
topic_id INTEGER,
|
|
55
|
+
category_id INTEGER,
|
|
40
56
|
external_id INTEGER,
|
|
41
57
|
title VARCHAR(255),
|
|
42
|
-
CONSTRAINT fk_posts_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
58
|
+
CONSTRAINT fk_posts_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
59
|
+
CONSTRAINT fk_posts_topic FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE CASCADE
|
|
43
60
|
);
|
|
44
61
|
|
|
45
|
-
CREATE TABLE
|
|
62
|
+
CREATE TABLE customers (
|
|
46
63
|
id SERIAL PRIMARY KEY,
|
|
47
|
-
|
|
64
|
+
name VARCHAR(255)
|
|
48
65
|
);
|
|
49
66
|
|
|
50
|
-
CREATE TABLE
|
|
67
|
+
CREATE TABLE categories (
|
|
68
|
+
id SERIAL PRIMARY KEY,
|
|
69
|
+
name VARCHAR(255)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
CREATE TABLE subjects (
|
|
51
73
|
id SERIAL PRIMARY KEY,
|
|
52
74
|
name VARCHAR(255)
|
|
53
75
|
);
|
|
54
76
|
|
|
77
|
+
CREATE TABLE events (
|
|
78
|
+
id SERIAL PRIMARY KEY,
|
|
79
|
+
subject_id INTEGER,
|
|
80
|
+
subject_type VARCHAR(255)
|
|
81
|
+
);
|
|
82
|
+
|
|
55
83
|
CREATE INDEX index_posts_on_user_id ON posts(user_id, topic_id);
|
|
56
84
|
SQL
|
|
57
85
|
|
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.
|
|
4
|
+
version: 5.6.15
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- pawurb
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-12-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: pg
|
|
@@ -114,12 +114,14 @@ files:
|
|
|
114
114
|
- lib/ruby_pg_extras/detect_fk_column.rb
|
|
115
115
|
- lib/ruby_pg_extras/diagnose_data.rb
|
|
116
116
|
- lib/ruby_pg_extras/diagnose_print.rb
|
|
117
|
+
- lib/ruby_pg_extras/ignore_list.rb
|
|
117
118
|
- lib/ruby_pg_extras/index_info.rb
|
|
118
119
|
- lib/ruby_pg_extras/index_info_print.rb
|
|
119
120
|
- lib/ruby_pg_extras/missing_fk_constraints.rb
|
|
120
121
|
- lib/ruby_pg_extras/missing_fk_indexes.rb
|
|
121
122
|
- lib/ruby_pg_extras/queries/add_extensions.sql
|
|
122
123
|
- lib/ruby_pg_extras/queries/all_locks.sql
|
|
124
|
+
- lib/ruby_pg_extras/queries/analyze_progress.sql
|
|
123
125
|
- lib/ruby_pg_extras/queries/bloat.sql
|
|
124
126
|
- lib/ruby_pg_extras/queries/blocking.sql
|
|
125
127
|
- lib/ruby_pg_extras/queries/buffercache_stats.sql
|
|
@@ -132,6 +134,7 @@ files:
|
|
|
132
134
|
- lib/ruby_pg_extras/queries/db_settings.sql
|
|
133
135
|
- lib/ruby_pg_extras/queries/duplicate_indexes.sql
|
|
134
136
|
- lib/ruby_pg_extras/queries/extensions.sql
|
|
137
|
+
- lib/ruby_pg_extras/queries/foreign_keys.sql
|
|
135
138
|
- lib/ruby_pg_extras/queries/index_cache_hit.sql
|
|
136
139
|
- lib/ruby_pg_extras/queries/index_scans.sql
|
|
137
140
|
- lib/ruby_pg_extras/queries/index_size.sql
|
|
@@ -155,11 +158,16 @@ files:
|
|
|
155
158
|
- lib/ruby_pg_extras/queries/table_index_scans.sql
|
|
156
159
|
- lib/ruby_pg_extras/queries/table_indexes_size.sql
|
|
157
160
|
- lib/ruby_pg_extras/queries/table_schema.sql
|
|
161
|
+
- lib/ruby_pg_extras/queries/table_schemas.sql
|
|
158
162
|
- lib/ruby_pg_extras/queries/table_size.sql
|
|
159
163
|
- lib/ruby_pg_extras/queries/tables.sql
|
|
160
164
|
- lib/ruby_pg_extras/queries/total_index_size.sql
|
|
161
165
|
- lib/ruby_pg_extras/queries/total_table_size.sql
|
|
162
166
|
- lib/ruby_pg_extras/queries/unused_indexes.sql
|
|
167
|
+
- lib/ruby_pg_extras/queries/vacuum_io_stats.sql
|
|
168
|
+
- lib/ruby_pg_extras/queries/vacuum_io_stats_legacy.sql
|
|
169
|
+
- lib/ruby_pg_extras/queries/vacuum_progress.sql
|
|
170
|
+
- lib/ruby_pg_extras/queries/vacuum_progress_17.sql
|
|
163
171
|
- lib/ruby_pg_extras/queries/vacuum_stats.sql
|
|
164
172
|
- lib/ruby_pg_extras/size_parser.rb
|
|
165
173
|
- lib/ruby_pg_extras/table_info.rb
|
|
@@ -170,6 +178,7 @@ files:
|
|
|
170
178
|
- spec/detect_fk_column_spec.rb
|
|
171
179
|
- spec/diagnose_data_spec.rb
|
|
172
180
|
- spec/diagnose_print_spec.rb
|
|
181
|
+
- spec/ignore_list_spec.rb
|
|
173
182
|
- spec/index_info_spec.rb
|
|
174
183
|
- spec/missing_fk_constraints_spec.rb
|
|
175
184
|
- spec/missing_fk_indexes_spec.rb
|
|
@@ -205,6 +214,7 @@ test_files:
|
|
|
205
214
|
- spec/detect_fk_column_spec.rb
|
|
206
215
|
- spec/diagnose_data_spec.rb
|
|
207
216
|
- spec/diagnose_print_spec.rb
|
|
217
|
+
- spec/ignore_list_spec.rb
|
|
208
218
|
- spec/index_info_spec.rb
|
|
209
219
|
- spec/missing_fk_constraints_spec.rb
|
|
210
220
|
- spec/missing_fk_indexes_spec.rb
|