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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae9c796ca782d5f4e317bbf7ac272a448ba66fdbc4aa20d1469222504c503d5a
4
- data.tar.gz: b3fa452496b4a996ca0567e2d3e62432ca6f482970677b67cf9a91e3c47633a5
3
+ metadata.gz: e20ed34b0677aa7e50c14ca2d5a511d4854f0eb34a1ad7e04a4f94fe2349895b
4
+ data.tar.gz: ef35f2a431195ba26a0271808315b54985254df379412ffb4eef9dc4ff81f7e8
5
5
  SHA512:
6
- metadata.gz: 8a705da09c1cbadac19b53bfe4f28e94b78ba998819fe535ae6f5fca9834c5cd5140a4acf974babad7aedf2678b24ce7041163f176510e5ae6dc89b2622bd48a
7
- data.tar.gz: 46071f90e959c491b88abbab7fad356c9a83e165cf9f171a8f4163f3c7ac16f66ada34e629556ec4506c0bdca020b526e443275345d111d8e3211aeee4029e78
6
+ metadata.gz: 6145da151dc40da7d0f79a3a2c9ff3d059dd9b320e44b751f6d5a9d0b00d395a78aff3c8bd1bf51453aa65ccb7cf12953ec73e54b4a350d7ee34ec61b5f661a0
7
+ data.tar.gz: 72861cc4517a977e39266f2617094db82be77ba31f932831785540c3bc3ae078e1deccbc32b5b025343bbbe2e2c2d25529e24dad170ea9e8969795dfe3ee64de
@@ -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 | last_vacuum | last_autovacuum | rowcount | dead_rowcount | autovacuum_threshold | expect_autovacuum
651
- --------+-----------------------+-------------+------------------+----------------+----------------+----------------------+-------------------
652
- public | log_table | | 2013-04-26 17:37 | 18,030 | 0 | 3,656 |
653
- public | data_table | | 2013-04-26 13:09 | 79 | 28 | 66 |
654
- public | other_table | | 2013-04-26 11:41 | 41 | 47 | 58 |
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, last autovacuum and the current autovacuum threshold. This command can be useful when determining if current vacuum thresholds require adjustments, and to determine when the table was last vacuumed.
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=12 bundle exec rspec spec/ && 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/")
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
@@ -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
@@ -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 kill_all kill_pid
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 table_foreign_keys
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
- return false unless column_name =~ /_id$/
38
- table_name = column_name.split("_").first
39
- table_name = pluralize(table_name)
40
- tables.include?(table_name)
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
- return word if UNCOUNTABLE.include?(word.downcase)
45
- return IRREGULAR[word] if IRREGULAR.key?(word)
46
- return IRREGULAR.invert[word] if IRREGULAR.value?(word)
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
- def self.call(table_name)
6
- new.call(table_name)
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
- tables = if table_name
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
- 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)
24
+ schemas_by_table = query_module
25
+ .table_schemas(in_format: :hash)
26
+ .group_by { |row| row.fetch("table_name") }
19
27
 
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
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
- 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
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
- agg
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
- all_tables
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
- schema = query_module.table_schema(args: { table_name: table }, in_format: :hash)
21
+ table_fks = foreign_keys.select { |row| row.fetch("table_name") == table }
21
22
 
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
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,5 @@
1
+ /* Column names and types for all tables */
2
+
3
+ SELECT table_name, column_name, data_type, is_nullable, column_default
4
+ FROM information_schema.columns
5
+ WHERE table_schema = '%{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,5 @@
1
+ /* I/O statistics for autovacuum backends are only available via pg_stat_io (PostgreSQL 16+). */
2
+
3
+ SELECT 'Upgrade to PostgreSQL 16 or newer to use this feature' AS "feature not available";
4
+
5
+
@@ -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 last_vacuum,
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(autovacuum_vacuum_threshold
32
- + (autovacuum_vacuum_scale_factor::numeric * pg_class.reltuples), '9G999G999G999') AS autovacuum_threshold,
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 autovacuum_vacuum_threshold + (autovacuum_vacuum_scale_factor::numeric * pg_class.reltuples) < psut.n_dead_tup
35
- THEN 'yes'
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyPgExtras
4
- VERSION = "5.6.13"
4
+ VERSION = "5.6.15"
5
5
  end
@@ -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.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
- })
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.size).to eq(1)
22
- expect(result[0]).to eq({
23
- table: "posts", column_name: "topic_id",
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.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
- })
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.size).to eq(1)
21
- expect(result[0]).to eq({
22
- table: "posts", column_name: "topic_id",
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(1)
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(3)
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 topics (
62
+ CREATE TABLE customers (
46
63
  id SERIAL PRIMARY KEY,
47
- title VARCHAR(255)
64
+ name VARCHAR(255)
48
65
  );
49
66
 
50
- CREATE TABLE companies (
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.13
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-09-07 00:00:00.000000000 Z
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