rails-pg-extras 5.6.15 → 5.6.16

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: e1b176294ac8dfee0602dac9a7719b685cbe2b4303bf96421853a87a1f7fdd1b
4
- data.tar.gz: 2a9208ba3675bda0ff32fa6da55cf68f8bcfa70b1ecdf51d3456d505acaf56a9
3
+ metadata.gz: 714a0f5659226e30d168cac200be9fa570ff608977b2ac8400a13e18a1c1e1f5
4
+ data.tar.gz: 803ec924433eeda21828d30ff23524007a108db4dfc02d83002fb4b0b2527fc6
5
5
  SHA512:
6
- metadata.gz: 6fde9b532cc4afeb2581405cb11f51d965de7d5a68f7dd89feddf2646a9d1c2281b354a8f7a67d74378758f8f1cacf6658dabac8dc8d9b30f2b387231fb23dc9
7
- data.tar.gz: 341f88edb6018e20d42994f9520961a0f5fc0c0a82118d4ceff76b626ef86188d7f9253b92b964048036555ab819b56eead63943cfa3f1e364871c051f4c9d5b
6
+ metadata.gz: d0da5717b4d0483ff82707da9ba19715e2d6d8a26feb46d77e0fae3a41a75d9c7dbc03f1881998752f6ece95984c4db408050218fc87bea43731ef3ebe1f0dc5
7
+ data.tar.gz: 1d72247f4fed436a0220051b98ba8e0ea619b2f12737c2958c895c73d8e19050a707d3baa96479de968248f5aa431a532f9aee358e533322727d12f2c9fa1ec7
data/README.md CHANGED
@@ -12,7 +12,7 @@ Optionally you can enable a visual interface:
12
12
 
13
13
  ![Web interface](https://github.com/pawurb/rails-pg-extras/raw/main/pg-extras-ui-3.png)
14
14
 
15
- [rails-pg-extras-mcp gem](https://github.com/pawurb/rails-pg-extras-mcp) provides an MCP (Model Context Protocol) interface enabling PostgreSQL metadata and performance analysis with an LLM support.
15
+ [rails-pg-extras-mcp gem](https://github.com/pawurb/rails-pg-extras-mcp) provides an MCP (Model Context Protocol) interface enabling PostgreSQL metadata and performance analysis with an LLM support.
16
16
 
17
17
  ![LLM interface](https://github.com/pawurb/rails-pg-extras/raw/main/pg-extras-mcp.png)
18
18
 
@@ -57,16 +57,27 @@ You should see the similar line in the output:
57
57
  RailsPgExtras.add_extensions
58
58
  ```
59
59
 
60
- By default a primary ActiveRecord database connection is used for running metadata queries, rake tasks and web UI. To connect to a different database you can specify an `ENV['RAILS_PG_EXTRAS_DATABASE_URL']` value in the following format:
60
+ By default rails-pg-extras uses your app’s default `ActiveRecord::Base.connection` (typically `primary`) for running metadata queries, rake tasks and the web UI.
61
+
62
+ If your app uses Rails multiple databases, the web UI can switch connections dynamically. It reads the available database names from `ActiveRecord::Base.configurations` for the current environment and selects one via the `db_key` query param (thread-local per request, so it’s safe under concurrency):
63
+
64
+ ```ruby
65
+ # examples
66
+ /pg_extras?db_key=primary
67
+ /pg_extras?db_key=animals
68
+ ```
69
+
70
+ To connect to a database that isn’t defined in `database.yml` (or when using rake tasks / Ruby API outside the web UI), you can also provide an explicit URL via `ENV['RAILS_PG_EXTRAS_DATABASE_CONFIG']`:
61
71
 
62
72
  ```ruby
63
- ENV["RAILS_PG_EXTRAS_DATABASE_URL"] = "postgresql://postgres:secret@localhost:5432/database_name"
73
+ ENV["RAILS_PG_EXTRAS_DATABASE_CONFIG"] = "postgresql://postgres:secret@localhost:5432/database_name"
64
74
  ```
65
75
 
66
- Alternatively, you can specify database URL with a method call:
76
+ Alternatively, you can specify database configuration with a method call:
67
77
 
68
78
  ```ruby
69
- RailsPgExtras.database_url = "postgresql://postgres:secret@localhost:5432/database_name"
79
+ RailsPgExtras.database_config = "postgresql://postgres:secret@localhost:5432/database_name"
80
+ RailsPgExtras.database_config = :rails_pg_extras
70
81
  ```
71
82
 
72
83
  ## Usage
@@ -158,7 +169,7 @@ RailsPgExtras.configure do |config|
158
169
  end
159
170
  ```
160
171
 
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.
172
+ You can also configure default ignore lists for the missing foreign key checkers. This helps skip columns that you know should not be considered foreign keys (constraints) or you intentionally do not want to index (indexes).
162
173
 
163
174
  ```ruby
164
175
  RailsPgExtras.configure do |config|
@@ -168,9 +179,11 @@ RailsPgExtras.configure do |config|
168
179
  # - "posts.*" (ignore all columns on a table)
169
180
  # - "*" (ignore everything)
170
181
  config.missing_fk_constraints_ignore_list = ["posts.category_id", "category_id"]
182
+ config.missing_fk_indexes_ignore_list = ["feedbacks.team_id", "legacy_id"]
171
183
 
172
184
  # Or as a comma-separated string:
173
185
  # config.missing_fk_constraints_ignore_list = "posts.category_id, category_id"
186
+ # config.missing_fk_indexes_ignore_list = "feedbacks.team_id, legacy_id"
174
187
  end
175
188
  ```
176
189
 
@@ -238,7 +251,7 @@ you can add this info to the output:
238
251
 
239
252
  ### `missing_fk_indexes`
240
253
 
241
- This method lists columns likely to be foreign keys (i.e. column name ending in `_id` and related table exists) which don't have an index. It's recommended to always index foreign key columns because they are used for searching relation objects.
254
+ This method lists **actual foreign key columns** (based on existing foreign key constraints) which don't have a supporting index. It's recommended to always index foreign key columns because they are commonly used for lookups and join conditions.
242
255
 
243
256
  You can add indexes on the columns returned by this query and later check if they are receiving scans using the [unused_indexes method](#unused_indexes). Please remember that each index decreases write performance and autovacuuming overhead, so be careful when adding multiple indexes to often updated tables.
244
257
 
@@ -258,12 +271,32 @@ RailsPgExtras.missing_fk_indexes(args: { table_name: "users" })
258
271
 
259
272
  `table_name` argument is optional, if omitted, the method will display missing fk indexes for all the tables.
260
273
 
274
+ You can also exclude known/intentional cases using `ignore_list` (array or comma-separated string), with entries like:
275
+ - "posts.category_id" (ignore a specific table+column)
276
+ - "category_id" (ignore this column name for all tables)
277
+ - "posts.*" (ignore all columns on a table)
278
+ - "*" (ignore everything)
279
+
280
+ ```ruby
281
+ RailsPgExtras.missing_fk_indexes(args: { table_name: "users", ignore_list: ["feedbacks.team_id", "posts.*"] })
282
+ ```
283
+
261
284
  ## `missing_fk_constraints`
262
285
 
263
- Similarly to the previous method, this one shows columns likely to be foreign keys that don't have a corresponding foreign key constraint. Foreign key constraints improve data integrity in the database by preventing relations with nonexisting objects. You can read more about the benefits of using foreign keys [in this blog post](https://pawelurbanek.com/rails-postgresql-data-integrity).
286
+ This method shows **columns that look like foreign keys** but don't have a corresponding foreign key constraint yet. Foreign key constraints improve data integrity in the database by preventing relations with nonexisting objects. You can read more about the benefits of using foreign keys [in this blog post](https://pawelurbanek.com/rails-postgresql-data-integrity).
287
+
288
+ Heuristic notes:
289
+ - A column is considered a candidate if it matches `<table_singular>_id` and the related table exists (underscored prefixes like `account_user_id` are supported).
290
+ - Rails polymorphic associations (`<name>_id` + `<name>_type`) are ignored since they cannot be expressed as real FK constraints.
291
+
292
+ You can also exclude known/intentional cases using `ignore_list` (array or comma-separated string), with entries like:
293
+ - "posts.category_id" (ignore a specific table+column)
294
+ - "category_id" (ignore this column name for all tables)
295
+ - "posts.*" (ignore all columns on a table)
296
+ - "*" (ignore everything)
264
297
 
265
298
  ```ruby
266
- RailsPgExtras.missing_fk_constraints(args: { table_name: "users" })
299
+ RailsPgExtras.missing_fk_constraints(args: { table_name: "users", ignore_list: ["users.customer_id", "posts.*"] })
267
300
 
268
301
  +---------------------------------+
269
302
  | Missing foreign key constraints |
@@ -278,12 +311,6 @@ RailsPgExtras.missing_fk_constraints(args: { table_name: "users" })
278
311
 
279
312
  `table_name` argument is optional, if omitted, method will display missing fk constraints for all the tables.
280
313
 
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
314
  Examples:
288
315
 
289
316
  ```ruby
@@ -533,7 +560,7 @@ $ rake pg_extras:calls
533
560
  (truncated results for brevity)
534
561
  ```
535
562
 
536
- This command is much like `pg:outliers`, but ordered by the number of times a statement has been called.
563
+ This command is much like `pg:outliers`, but ordered by the number of times a statement has been called.
537
564
 
538
565
  [More info](https://pawelurbanek.com/postgresql-fix-performance#missing-indexes)
539
566
 
@@ -3,7 +3,8 @@
3
3
 
4
4
  <br>
5
5
 
6
- <%= link_to "← Back to Diagnose", queries_path,
6
+ <%= link_to "← Back to Diagnose",
7
+ queries_path(params[:db_key].present? ? { db_key: params[:db_key] } : {}),
7
8
  class: "inline-block bg-blue-500 text-white font-medium px-4 py-2 rounded-lg shadow-md hover:bg-blue-600 transition" %>
8
9
 
9
10
  <% if @error %>
@@ -12,7 +12,10 @@ require "rails_pg_extras/table_info"
12
12
  require "rails_pg_extras/table_info_print"
13
13
 
14
14
  module RailsPgExtras
15
- @@database_url = nil
15
+ extend self
16
+
17
+ @@database_config = nil
18
+
16
19
  QUERIES = RubyPgExtras::QUERIES
17
20
  DEFAULT_ARGS = RubyPgExtras::DEFAULT_ARGS
18
21
  NEW_PG_STAT_STATEMENTS = RubyPgExtras::NEW_PG_STAT_STATEMENTS
@@ -28,7 +31,7 @@ module RailsPgExtras
28
31
  end
29
32
  end
30
33
 
31
- def self.run_query(query_name:, in_format:, args: {})
34
+ def run_query(query_name:, in_format:, args: {})
32
35
  RubyPgExtras.run_query_base(
33
36
  query_name: query_name,
34
37
  conn: connection,
@@ -38,7 +41,7 @@ module RailsPgExtras
38
41
  )
39
42
  end
40
43
 
41
- def self.diagnose(in_format: :display_table)
44
+ def diagnose(in_format: :display_table)
42
45
  data = RailsPgExtras::DiagnoseData.call
43
46
 
44
47
  if in_format == :display_table
@@ -50,14 +53,14 @@ module RailsPgExtras
50
53
  end
51
54
  end
52
55
 
53
- def self.measure_duration(&block)
56
+ def measure_duration(&block)
54
57
  starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
55
58
  block.call
56
59
  ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
57
60
  (ending - starting) * 1000
58
61
  end
59
62
 
60
- def self.measure_queries(&block)
63
+ def measure_queries(&block)
61
64
  queries = {}
62
65
  sql_duration = 0
63
66
 
@@ -105,7 +108,7 @@ module RailsPgExtras
105
108
  }
106
109
  end
107
110
 
108
- def self.index_info(args: {}, in_format: :display_table)
111
+ def index_info(args: {}, in_format: :display_table)
109
112
  data = RailsPgExtras::IndexInfo.call(args[:table_name])
110
113
 
111
114
  if in_format == :display_table
@@ -119,7 +122,7 @@ module RailsPgExtras
119
122
  end
120
123
  end
121
124
 
122
- def self.table_info(args: {}, in_format: :display_table)
125
+ def table_info(args: {}, in_format: :display_table)
123
126
  data = RailsPgExtras::TableInfo.call(args[:table_name])
124
127
 
125
128
  if in_format == :display_table
@@ -133,77 +136,82 @@ module RailsPgExtras
133
136
  end
134
137
  end
135
138
 
136
- def self.missing_fk_indexes(args: {}, in_format: :display_table)
137
- result = RailsPgExtras::MissingFkIndexes.call(args[:table_name])
139
+ def missing_fk_indexes(args: {}, in_format: :display_table)
140
+ ignore_list = args[:ignore_list]
141
+ ignore_list ||= RailsPgExtras.configuration.missing_fk_indexes_ignore_list
142
+ result = RailsPgExtras::MissingFkIndexes.call(args[:table_name], ignore_list: ignore_list)
138
143
  RubyPgExtras.display_result(result, title: "Missing foreign key indexes", in_format: in_format)
139
144
  end
140
145
 
141
- def self.missing_fk_constraints(args: {}, in_format: :display_table)
146
+ def missing_fk_constraints(args: {}, in_format: :display_table)
142
147
  ignore_list = args[:ignore_list]
143
148
  ignore_list ||= RailsPgExtras.configuration.missing_fk_constraints_ignore_list
144
149
  result = RailsPgExtras::MissingFkConstraints.call(args[:table_name], ignore_list: ignore_list)
145
150
  RubyPgExtras.display_result(result, title: "Missing foreign key constraints", in_format: in_format)
146
151
  end
147
152
 
148
- def self.database_url=(value)
149
- @@database_url = value
153
+ def database_config
154
+ @@database_config
155
+ end
156
+
157
+ # Deprecated
158
+ alias_method :database_url, :database_config
159
+
160
+ def database_config=(value)
161
+ @@database_config = value
150
162
  end
151
163
 
152
- def self.connection
164
+ # Deprecated
165
+ alias_method :database_url=, :database_config=
166
+
167
+ def connection
153
168
  # Priority:
154
169
  # 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
170
+ # 2) Explicit config via setter or ENV override
156
171
  # 3) Default ActiveRecord::Base connection
157
172
  selected_db_key = Thread.current[:rails_pg_extras_db_key]
158
- db_url = @@database_url || ENV["RAILS_PG_EXTRAS_DATABASE_URL"]
173
+ config = @@database_config || ENV["RAILS_PG_EXTRAS_DATABASE_CONFIG"] || ENV["RAILS_PG_EXTRAS_DATABASE_URL"] # Latter is deprecated
159
174
 
160
175
  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)
176
+ # Prefix thread class key to avoid collisions
177
+ ar_class = fetch_or_define_ar_class(thread_class_key: "database_#{selected_db_key}", const_name: selected_db_key.classify)
173
178
 
174
- connector = ar_class.establish_connection(selected_db_key.to_sym)
179
+ establish_connection(ar_class: ar_class, config: selected_db_key.to_sym)
180
+ elsif config.present?
181
+ ar_class = fetch_or_define_ar_class(thread_class_key: :database_config, const_name: :PgExtrasDatabaseConfig)
175
182
 
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)
183
+ establish_connection(ar_class: ar_class, config: config)
184
+ else
185
+ ActiveRecord::Base.connection
186
+ end
187
+ end
188
+
189
+ private
195
190
 
196
- connector = ar_class.establish_connection(db_url)
191
+ # Use an isolated abstract class to avoid changing the global connection
192
+ def fetch_or_define_ar_class(thread_class_key:, const_name:)
193
+ thread_classes = Thread.current[:rails_pg_extras_ar_classes] ||= {}
197
194
 
198
- if connector.respond_to?(:connection)
199
- connector.connection
200
- elsif connector.respond_to?(:lease_connection)
201
- connector.lease_connection
195
+ thread_classes[thread_class_key] ||=
196
+ if const_defined?(const_name, false)
197
+ const_get(const_name, false)
202
198
  else
203
- raise "Unsupported connector: #{connector.class}"
199
+ klass = Class.new(ActiveRecord::Base)
200
+ klass.abstract_class = true
201
+
202
+ const_set(const_name, klass)
204
203
  end
204
+ end
205
+
206
+ def establish_connection(ar_class:, config:)
207
+ connector = ar_class.establish_connection(config)
208
+
209
+ if connector.respond_to?(:connection)
210
+ connector.connection
211
+ elsif connector.respond_to?(:lease_connection)
212
+ connector.lease_connection
205
213
  else
206
- ActiveRecord::Base.connection
214
+ raise "Unsupported connector: #{connector.class}"
207
215
  end
208
216
  end
209
217
  end
@@ -4,16 +4,18 @@ 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", missing_fk_constraints_ignore_list: [] }
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: [], missing_fk_indexes_ignore_list: [] }
8
8
 
9
9
  attr_reader :enabled_web_actions
10
10
  attr_accessor :public_dashboard
11
11
  attr_accessor :missing_fk_constraints_ignore_list
12
+ attr_accessor :missing_fk_indexes_ignore_list
12
13
 
13
14
  def initialize(attrs)
14
15
  self.enabled_web_actions = attrs[:enabled_web_actions]
15
16
  self.public_dashboard = attrs[:public_dashboard]
16
17
  self.missing_fk_constraints_ignore_list = attrs[:missing_fk_constraints_ignore_list]
18
+ self.missing_fk_indexes_ignore_list = attrs[:missing_fk_indexes_ignore_list]
17
19
  end
18
20
 
19
21
  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.15"
4
+ VERSION = "5.6.16"
5
5
  end
data/spec/smoke_spec.rb CHANGED
@@ -40,15 +40,29 @@ describe RailsPgExtras do
40
40
  expect(output.fetch(:count) > 0).to eq(true)
41
41
  end
42
42
 
43
+ it "supports custom RAILS_PG_EXTRAS_DATABASE_CONFIG that specifies an URL" do
44
+ old_value = ENV["RAILS_PG_EXTRAS_DATABASE_CONFIG"]
45
+ ENV["RAILS_PG_EXTRAS_DATABASE_CONFIG"] = ENV["DATABASE_URL"]
46
+ puts ENV["RAILS_PG_EXTRAS_DATABASE_CONFIG"]
47
+
48
+ expect do
49
+ RailsPgExtras.calls
50
+ end.not_to raise_error
51
+ ensure
52
+ ENV["RAILS_PG_EXTRAS_DATABASE_CONFIG"] = old_value
53
+ end
54
+
55
+ # Deprecated
43
56
  it "supports custom RAILS_PG_EXTRAS_DATABASE_URL" do
57
+ old_value = ENV["RAILS_PG_EXTRAS_DATABASE_URL"]
44
58
  ENV["RAILS_PG_EXTRAS_DATABASE_URL"] = ENV["DATABASE_URL"]
45
59
  puts ENV["RAILS_PG_EXTRAS_DATABASE_URL"]
46
60
 
47
61
  expect do
48
62
  RailsPgExtras.calls
49
63
  end.not_to raise_error
50
-
51
- ENV["RAILS_PG_EXTRAS_DATABASE_URL"] = nil
64
+ ensure
65
+ ENV["RAILS_PG_EXTRAS_DATABASE_URL"] = old_value
52
66
  end
53
67
 
54
68
  describe "missing_fk_indexes" do
@@ -67,14 +81,30 @@ describe RailsPgExtras do
67
81
  end
68
82
  end
69
83
 
84
+ it "database_config does not affect global connection" do
85
+ original_connection = ActiveRecord::Base.connection
86
+
87
+ old_value = RailsPgExtras.database_config
88
+ RailsPgExtras.database_config = ENV["DATABASE_URL"]
89
+ RailsPgExtras.calls
90
+
91
+ # Verify global connection unchanged
92
+ expect(ActiveRecord::Base.connection).to eq(original_connection)
93
+ ensure
94
+ RailsPgExtras.database_config = old_value
95
+ end
96
+
97
+ # Deprecated
70
98
  it "database_url does not affect global connection" do
71
99
  original_connection = ActiveRecord::Base.connection
72
100
 
101
+ old_value = RailsPgExtras.database_url
73
102
  RailsPgExtras.database_url = ENV["DATABASE_URL"]
74
103
  RailsPgExtras.calls
75
- RailsPgExtras.database_url = nil
76
104
 
77
105
  # Verify global connection unchanged
78
106
  expect(ActiveRecord::Base.connection).to eq(original_connection)
107
+ ensure
108
+ RailsPgExtras.database_url = old_value
79
109
  end
80
110
  end
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.15
4
+ version: 5.6.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - pawurb
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-12-16 00:00:00.000000000 Z
11
+ date: 2026-01-10 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.15
19
+ version: 5.6.16
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.15
26
+ version: 5.6.16
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: railties
29
29
  requirement: !ruby/object:Gem::Requirement