rails-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: 60cca18682745f6e881da4c4e856b05682709e5678478e4bd490af6645d26d39
4
- data.tar.gz: 3850f7f49166b8d9e22f884145877d7c3d3796bedb8ce6daa44b6e1d19ed30db
3
+ metadata.gz: e1b176294ac8dfee0602dac9a7719b685cbe2b4303bf96421853a87a1f7fdd1b
4
+ data.tar.gz: 2a9208ba3675bda0ff32fa6da55cf68f8bcfa70b1ecdf51d3456d505acaf56a9
5
5
  SHA512:
6
- metadata.gz: 93125c50d81473b96e221f57e11675fbb66c48280a7378a9bc2d7ba96e42250323aa4f2abb92dc6c0ea0fc17603b2d43768e2e25c6a5e871fc45a48e259ee213
7
- data.tar.gz: 46967984fe94e997316c673a95606eb19703400c10625334574c66473f1bb2a522355da3ef1a9252eebdd06f259f49d1e698b000a9221ff41166236acde26c9f
6
+ metadata.gz: 6fde9b532cc4afeb2581405cb11f51d965de7d5a68f7dd89feddf2646a9d1c2281b354a8f7a67d74378758f8f1cacf6658dabac8dc8d9b30f2b387231fb23dc9
7
+ data.tar.gz: 341f88edb6018e20d42994f9520961a0f5fc0c0a82118d4ceff76b626ef86188d7f9253b92b964048036555ab819b56eead63943cfa3f1e364871c051f4c9d5b
@@ -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=rails-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 \
@@ -57,6 +50,13 @@ jobs:
57
50
  --env POSTGRES_PASSWORD=secret \
58
51
  -d -p 5437:5432 postgres:17.0-alpine \
59
52
  postgres -c shared_preload_libraries=pg_stat_statements
53
+ - name: Run PostgreSQL 18
54
+ run: |
55
+ docker run --env POSTGRES_USER=postgres \
56
+ --env POSTGRES_DB=rails-pg-extras-test \
57
+ --env POSTGRES_PASSWORD=secret \
58
+ -d -p 5438:5432 postgres:18.1-alpine \
59
+ postgres -c shared_preload_libraries=pg_stat_statements
60
60
  sleep 5
61
61
  - name: Set up Ruby ${{ matrix.ruby-version }}
62
62
  uses: ruby/setup-ruby@v1
@@ -71,11 +71,6 @@ jobs:
71
71
  bundle config set --local path 'vendor/bundle'
72
72
  bundle install
73
73
  sleep 10
74
- - name: Run tests for PG 12
75
- env:
76
- PG_VERSION: 12
77
- run: |
78
- bundle exec rspec spec/
79
74
  - name: Run tests for PG 13
80
75
  env:
81
76
  PG_VERSION: 13
@@ -101,4 +96,9 @@ jobs:
101
96
  PG_VERSION: 17
102
97
  run: |
103
98
  bundle exec rspec spec/
99
+ - name: Run tests for PG 18
100
+ env:
101
+ PG_VERSION: 18
102
+ run: |
103
+ bundle exec rspec spec/
104
104
 
data/README.md CHANGED
@@ -158,6 +158,22 @@ RailsPgExtras.configure do |config|
158
158
  end
159
159
  ```
160
160
 
161
+ You can also configure a default ignore list for the heuristic missing foreign key constraints checker. This helps skip columns that you know should not be considered foreign keys.
162
+
163
+ ```ruby
164
+ RailsPgExtras.configure do |config|
165
+ # Accepts an Array or a comma-separated String of entries like:
166
+ # - "posts.category_id" (ignore a specific table+column)
167
+ # - "category_id" (ignore this column name for all tables)
168
+ # - "posts.*" (ignore all columns on a table)
169
+ # - "*" (ignore everything)
170
+ config.missing_fk_constraints_ignore_list = ["posts.category_id", "category_id"]
171
+
172
+ # Or as a comma-separated string:
173
+ # config.missing_fk_constraints_ignore_list = "posts.category_id, category_id"
174
+ end
175
+ ```
176
+
161
177
  ## Available methods
162
178
 
163
179
  ### `measure_queries`
@@ -262,6 +278,26 @@ RailsPgExtras.missing_fk_constraints(args: { table_name: "users" })
262
278
 
263
279
  `table_name` argument is optional, if omitted, method will display missing fk constraints for all the tables.
264
280
 
281
+ You can optionally pass an `ignore_list` to skip known false positives detected by the heuristic checker. It accepts an Array or a comma-separated String. Entries can be:
282
+ - "posts.category_id" to ignore a specific table+column
283
+ - "category_id" to ignore a column name for all tables
284
+ - "posts.*" to ignore all columns on a specific table
285
+ - "*" to ignore everything
286
+
287
+ Examples:
288
+
289
+ ```ruby
290
+ # Per-call ignore list
291
+ RailsPgExtras.missing_fk_constraints(args: {
292
+ ignore_list: ["posts.category_id", "legacy_id", "temp_tables.*"]
293
+ })
294
+
295
+ # As a comma-separated string
296
+ RailsPgExtras.missing_fk_constraints(args: {
297
+ ignore_list: "posts.category_id, legacy_id, temp_tables.*"
298
+ })
299
+ ```
300
+
265
301
  ### `table_schema`
266
302
 
267
303
  This method displays structure of a selected table, listing its column names, together with types, null constraints, and default values.
@@ -3,6 +3,8 @@ require "rails_pg_extras/version"
3
3
 
4
4
  module RailsPgExtras::Web
5
5
  class ApplicationController < ActionController::Base
6
+ around_action :with_selected_database
7
+ before_action :load_available_databases
6
8
  def self.get_user
7
9
  Rails.application.try(:credentials).try(:pg_extras).try(:user) || ENV["RAILS_PG_EXTRAS_USER"]
8
10
  end
@@ -31,5 +33,36 @@ module RailsPgExtras::Web
31
33
  raise "Missing credentials for rails-pg-extras dashboard! If you want to enable public dashboard please set RAILS_PG_EXTRAS_PUBLIC_DASHBOARD=true"
32
34
  end
33
35
  end
36
+
37
+ private
38
+
39
+ def with_selected_database
40
+ Thread.current[:rails_pg_extras_db_key] = params[:db_key].presence
41
+ yield
42
+ ensure
43
+ Thread.current[:rails_pg_extras_db_key] = nil
44
+ end
45
+
46
+ def load_available_databases
47
+ @available_databases = fetch_available_database_names
48
+ @current_db_key = params[:db_key].presence || default_db_key
49
+ end
50
+
51
+ def fetch_available_database_names
52
+
53
+ ActiveRecord::Base.configurations
54
+ .configs_for(env_name: Rails.env)
55
+ .filter_map { |conf| (conf.respond_to?(:name) && conf.name) || (conf.respond_to?(:spec_name) && conf.spec_name) }
56
+ .uniq
57
+ .sort
58
+
59
+ rescue
60
+ []
61
+ end
62
+
63
+ def default_db_key
64
+ # Prefer primary if available, else first available
65
+ @available_databases.include?("primary") ? "primary" : @available_databases.first
66
+ end
34
67
  end
35
68
  end
@@ -39,7 +39,7 @@ module RailsPgExtras::Web
39
39
  def unavailable_extensions
40
40
  return @unavailable_extensions if defined?(@unavailable_extensions)
41
41
 
42
- enabled_extensions = ActiveRecord::Base.connection.extensions.lazy
42
+ enabled_extensions = RailsPgExtras.connection.extensions.lazy
43
43
  @unavailable_extensions = REQUIRED_EXTENSIONS.delete_if { |ext| enabled_extensions.grep(/^([^.]+\.)?#{ext}$/).any? }
44
44
  end
45
45
  end
@@ -21,6 +21,10 @@
21
21
  </div>
22
22
  <% end %>
23
23
 
24
+ <div class="mb-4">
25
+ <%= render "rails_pg_extras/web/shared/database_selector" %>
26
+ </div>
27
+
24
28
  <%= yield %>
25
29
  </body>
26
30
  </html>
@@ -2,24 +2,32 @@
2
2
  <div class='pg-extras-help pb-4'>
3
3
  (<a href="https://pawelurbanek.com/postgresql-fix-performance"
4
4
  class="text-blue-600 hover:text-blue-800 hover:underline" target='_blank'>Tutorial</a>)
5
- </div>
6
- <div class="overflow-x-auto">
7
- <table class="w-full border border-gray-400 shadow-md rounded-lg overflow-hidden">
8
- <thead>
9
- <tr class="bg-gray-200 text-gray-900 text-left uppercase text-sm font-bold">
10
- <th class="px-4 py-3 border">Ok</th>
11
- <th class="px-4 py-3 border">Check Name</th>
12
- <th class="px-4 py-3 border">Message</th>
13
- </tr>
14
- </thead>
15
- <tbody>
16
- <% RailsPgExtras.diagnose(in_format: :hash).each do |diagnosis| %>
17
- <tr class="<%= diagnosis[:ok] ? "bg-green-100 text-green-900" : "bg-red-100 text-red-900" %> hover:bg-gray-300 transition">
18
- <td class="px-4 py-3 border font-bold"><%= diagnosis[:ok] ? "YES" : "NO" %></td>
19
- <td class="px-4 py-3 border font-semibold"><%= diagnosis[:check_name] %></td>
20
- <td class="px-4 py-3 border"><%= diagnosis[:message] %></td>
5
+ </div>
6
+ <% begin %>
7
+ <% diagnoses = RailsPgExtras.diagnose(in_format: :hash) %>
8
+ <div class="overflow-x-auto">
9
+ <table class="w-full border border-gray-400 shadow-md rounded-lg overflow-hidden">
10
+ <thead>
11
+ <tr class="bg-gray-200 text-gray-900 text-left uppercase text-sm font-bold">
12
+ <th class="px-4 py-3 border">Ok</th>
13
+ <th class="px-4 py-3 border">Check Name</th>
14
+ <th class="px-4 py-3 border">Message</th>
21
15
  </tr>
22
- <% end %>
23
- </tbody>
24
- </table>
25
- </div>
16
+ </thead>
17
+ <tbody>
18
+ <% diagnoses.each do |diagnosis| %>
19
+ <tr class="<%= diagnosis[:ok] ? "bg-green-100 text-green-900" : "bg-red-100 text-red-900" %> hover:bg-gray-300 transition">
20
+ <td class="px-4 py-3 border font-bold"><%= diagnosis[:ok] ? "YES" : "NO" %></td>
21
+ <td class="px-4 py-3 border font-semibold"><%= diagnosis[:check_name] %></td>
22
+ <td class="px-4 py-3 border"><%= diagnosis[:message] %></td>
23
+ </tr>
24
+ <% end %>
25
+ </tbody>
26
+ </table>
27
+ </div>
28
+ <% rescue => e %>
29
+ <div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4" role="alert">
30
+ <p class="font-bold">Diagnose failed</p>
31
+ <p><%= e.message %></p>
32
+ </div>
33
+ <% end %>
@@ -0,0 +1,16 @@
1
+ <% if @available_databases.present? && @available_databases.size > 1 %>
2
+ <%= form_tag queries_path, id: "db_selector", method: :get, class: "mb-4 flex items-center gap-2" do %>
3
+ <label for="db_key" class="font-semibold">Database:</label>
4
+ <%= select_tag :db_key,
5
+ options_for_select(@available_databases, @current_db_key),
6
+ class: "border p-2 font-bold" %>
7
+ <%# Preserve current query selection if present %>
8
+ <%= hidden_field_tag :query_name, params[:query_name] if params[:query_name].present? %>
9
+ <% end %>
10
+
11
+ <%= javascript_tag nonce: true do -%>
12
+ document.getElementById('db_selector').addEventListener('change', (e) => {
13
+ e.target.form.submit()
14
+ })
15
+ <% end -%>
16
+ <% end %>
@@ -1,7 +1,9 @@
1
- <%= form_tag queries_path, id: "queries", method: :get do |f| %>
2
- <%= select_tag :query_name, options_for_select(@all_queries, params[:query_name]),
3
- { prompt: "diagnose", class: "border p-2 font-bold", autofocus: true } %>
4
- <% end %>
1
+ <%= form_tag queries_path, id: "queries", method: :get do |f| %>
2
+ <%= select_tag :query_name, options_for_select(@all_queries, params[:query_name]),
3
+ { prompt: "diagnose", class: "border p-2 font-bold", autofocus: true } %>
4
+ <%# Preserve selected database across query changes %>
5
+ <%= hidden_field_tag :db_key, params[:db_key] if params[:db_key].present? %>
6
+ <% end %>
5
7
 
6
8
  <%= javascript_tag nonce: true do -%>
7
9
  document.getElementById('queries').addEventListener('change', (e) => {
@@ -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: rails-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
@@ -45,7 +36,7 @@ services:
45
36
  ports:
46
37
  - '5436:5432'
47
38
  postgres17:
48
- image: postgres:17.0-alpine
39
+ image: postgres:17.7-alpine
49
40
  command: postgres -c shared_preload_libraries=pg_stat_statements
50
41
  environment:
51
42
  POSTGRES_USER: postgres
@@ -53,4 +44,13 @@ services:
53
44
  POSTGRES_PASSWORD: secret
54
45
  ports:
55
46
  - '5437:5432'
47
+ postgres18:
48
+ image: postgres:18.1-alpine
49
+ command: postgres -c shared_preload_libraries=pg_stat_statements
50
+ environment:
51
+ POSTGRES_USER: postgres
52
+ POSTGRES_DB: rails-pg-extras-test
53
+ POSTGRES_PASSWORD: secret
54
+ ports:
55
+ - '5438:5432'
56
56
 
@@ -139,7 +139,9 @@ module RailsPgExtras
139
139
  end
140
140
 
141
141
  def self.missing_fk_constraints(args: {}, in_format: :display_table)
142
- result = RailsPgExtras::MissingFkConstraints.call(args[:table_name])
142
+ ignore_list = args[:ignore_list]
143
+ ignore_list ||= RailsPgExtras.configuration.missing_fk_constraints_ignore_list
144
+ result = RailsPgExtras::MissingFkConstraints.call(args[:table_name], ignore_list: ignore_list)
143
145
  RubyPgExtras.display_result(result, title: "Missing foreign key constraints", in_format: in_format)
144
146
  end
145
147
 
@@ -148,10 +150,50 @@ module RailsPgExtras
148
150
  end
149
151
 
150
152
  def self.connection
153
+ # Priority:
154
+ # 1) Per-request selected db (thread-local), if present -> use named configuration or URL without altering global Base
155
+ # 2) Explicit URL via setter or ENV override
156
+ # 3) Default ActiveRecord::Base connection
157
+ selected_db_key = Thread.current[:rails_pg_extras_db_key]
151
158
  db_url = @@database_url || ENV["RAILS_PG_EXTRAS_DATABASE_URL"]
152
159
 
153
- if db_url.present?
154
- connector = ActiveRecord::Base.establish_connection(db_url)
160
+ if selected_db_key.present?
161
+ const_name = selected_db_key.classify
162
+ # Use an isolated abstract class to avoid changing the global connection
163
+ thread_classes = (Thread.current[:rails_pg_extras_ar_classes] ||= {})
164
+ ar_class = (thread_classes[selected_db_key] ||= begin
165
+ if const_defined?(const_name, false)
166
+ const_get(const_name, false)
167
+ else
168
+ klass = Class.new(ActiveRecord::Base)
169
+ klass.abstract_class = true
170
+ const_set(const_name, klass)
171
+ end
172
+ end)
173
+
174
+ connector = ar_class.establish_connection(selected_db_key.to_sym)
175
+
176
+ if connector.respond_to?(:connection)
177
+ connector.connection
178
+ elsif connector.respond_to?(:lease_connection)
179
+ connector.lease_connection
180
+ else
181
+ raise "Unsupported connector: #{connector.class}"
182
+ end
183
+ elsif db_url.present?
184
+ # Use an isolated abstract class to avoid changing the global connection
185
+ thread_classes = (Thread.current[:rails_pg_extras_ar_classes] ||= {})
186
+ ar_class = (thread_classes[:database_url] ||= begin
187
+ if const_defined?(:PgExtrasURLConn, false)
188
+ const_get(:PgExtrasURLConn, false)
189
+ else
190
+ klass = Class.new(ActiveRecord::Base)
191
+ klass.abstract_class = true
192
+ const_set(:PgExtrasURLConn, klass)
193
+ end
194
+ end)
195
+
196
+ connector = ar_class.establish_connection(db_url)
155
197
 
156
198
  if connector.respond_to?(:connection)
157
199
  connector.connection
@@ -4,14 +4,16 @@ require "rails_pg_extras/web"
4
4
 
5
5
  module RailsPgExtras
6
6
  class Configuration
7
- DEFAULT_CONFIG = { enabled_web_actions: Web::ACTIONS - [:kill_all, :kill_pid], public_dashboard: ENV["RAILS_PG_EXTRAS_PUBLIC_DASHBOARD"] == "true" }
7
+ DEFAULT_CONFIG = { enabled_web_actions: Web::ACTIONS - [:kill_all, :kill_pid], public_dashboard: ENV["RAILS_PG_EXTRAS_PUBLIC_DASHBOARD"] == "true", missing_fk_constraints_ignore_list: [] }
8
8
 
9
9
  attr_reader :enabled_web_actions
10
10
  attr_accessor :public_dashboard
11
+ attr_accessor :missing_fk_constraints_ignore_list
11
12
 
12
13
  def initialize(attrs)
13
14
  self.enabled_web_actions = attrs[:enabled_web_actions]
14
15
  self.public_dashboard = attrs[:public_dashboard]
16
+ self.missing_fk_constraints_ignore_list = attrs[:missing_fk_constraints_ignore_list]
15
17
  end
16
18
 
17
19
  def enabled_web_actions=(*actions)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsPgExtras
4
- VERSION = "5.6.13"
4
+ VERSION = "5.6.15"
5
5
  end
data/spec/smoke_spec.rb CHANGED
@@ -66,4 +66,15 @@ describe RailsPgExtras do
66
66
  }.not_to raise_error
67
67
  end
68
68
  end
69
+
70
+ it "database_url does not affect global connection" do
71
+ original_connection = ActiveRecord::Base.connection
72
+
73
+ RailsPgExtras.database_url = ENV["DATABASE_URL"]
74
+ RailsPgExtras.calls
75
+ RailsPgExtras.database_url = nil
76
+
77
+ # Verify global connection unchanged
78
+ expect(ActiveRecord::Base.connection).to eq(original_connection)
79
+ end
69
80
  end
data/spec/spec_helper.rb CHANGED
@@ -8,15 +8,15 @@ require_relative "../lib/rails-pg-extras"
8
8
  pg_version = ENV["PG_VERSION"]
9
9
 
10
10
  PG_PORTS = {
11
- "12" => "5432",
12
11
  "13" => "5433",
13
12
  "14" => "5434",
14
13
  "15" => "5435",
15
14
  "16" => "5436",
16
15
  "17" => "5437",
16
+ "18" => "5438",
17
17
  }
18
18
 
19
- port = PG_PORTS.fetch(pg_version, "5432")
19
+ port = PG_PORTS.fetch(pg_version, "5438")
20
20
 
21
21
  ENV["DATABASE_URL"] ||= "postgresql://postgres:secret@localhost:#{port}/rails-pg-extras-test"
22
22
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-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: ruby-pg-extras
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 5.6.13
19
+ version: 5.6.15
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 5.6.13
26
+ version: 5.6.15
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: railties
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -133,6 +133,7 @@ files:
133
133
  - app/views/rails_pg_extras/web/queries/_unavailable_extensions_warning.html.erb
134
134
  - app/views/rails_pg_extras/web/queries/index.html.erb
135
135
  - app/views/rails_pg_extras/web/queries/show.html.erb
136
+ - app/views/rails_pg_extras/web/shared/_database_selector.html.erb
136
137
  - app/views/rails_pg_extras/web/shared/_queries_selector.html.erb
137
138
  - assets/pg-extras-rails-ujs.js
138
139
  - assets/pg-extras-tailwind.min.css