Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## Unreleased - xxxx-xx-xx
- `db_belongs_to` handles a violated composite foreign key on PostgreSQL. Its error names every column of the key, `Key (tenant_id, company_id)`; each is now a candidate, so the association's `must exist` error is added instead of the exception being raised
- Fix `validates_db_uniqueness_of` raising `NoMethodError` on PostgreSQL when the table has row-level security. PostgreSQL omits the `Key (...)=(...)` detail from the unique violation there, so the error is now matched by its index name only

## [2.1.1] - 2026-03-31
Expand Down
6 changes: 4 additions & 2 deletions lib/database_validations/lib/adapters/postgresql_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ def unique_error_columns(error)
error.message[/Key \((.+)\)=/, 1]&.split(', ')
end

# A composite key reports every column, `Key (tenant_id, company_id)=(1, 2)`; each is a candidate, and the
# rescuer picks the one with a matching `db_belongs_to`.
def foreign_key_error_column(error)
column = error.message[/Key \(([^)]+)\)/, 1]
column ? [column] : []
columns = error.message[/Key \(([^)]+)\)/, 1]
columns ? columns.split(', ') : []
end
end
end
Expand Down
9 changes: 9 additions & 0 deletions spec/adapters/foreign_key_error_column_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ def build_error(message:, sql: nil)
)
expect(described_class.foreign_key_error_column(error)).to eq(['company_id'])
end

it 'extracts every column of a composite key' do
error = build_error(
message: 'PG::ForeignKeyViolation: ERROR: insert or update on table "db_belongs_users" violates ' \
'foreign key constraint "fk_tenant_company" DETAIL: Key (tenant_id, company_id)=(2, 1) is not ' \
'present in table "companies".'
)
expect(described_class.foreign_key_error_column(error)).to eq(%w[tenant_id company_id])
end
end

describe DatabaseValidations::Adapters::MysqlAdapter do
Expand Down
29 changes: 29 additions & 0 deletions spec/validations/db_belongs_to_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class BelongsUser < ActiveRecord::Base; end
class DbBelongsUser < ActiveRecord::Base; end
class Department < ActiveRecord::Base; end
class MultiFkUser < ActiveRecord::Base; end
class TenantUser < ActiveRecord::Base; end
# rubocop:enable RSpec/LeakyConstantDeclaration
# rubocop:enable Lint/ConstantDefinitionInBlock

Expand Down Expand Up @@ -196,6 +197,34 @@ def define_tables
end

include_examples 'works as belongs_to'

describe 'a composite foreign key that leads with a tenant column' do
let(:tenant_user_klass) do
define_class(TenantUser, :tenant_users) do
db_belongs_to :company
end
end

before do
ActiveRecord::Schema.define(version: 3) do
add_column :companies, :tenant_id, :integer, null: false, default: 1
add_index :companies, %i[id tenant_id], unique: true
create_table :tenant_users do |t|
t.integer :tenant_id, null: false, default: 1
t.belongs_to :company, foreign_key: true
end
add_foreign_key :tenant_users, :companies, column: %i[tenant_id company_id], primary_key: %i[tenant_id id]
end
end

it "adds the association's error for a parent of another tenant" do
company = company_klass.create!(tenant_id: 1)
record = tenant_user_klass.new(company_id: company.id, tenant_id: 2)

expect(record.save).to be false
expect(record.errors[:company]).to be_present
end
end
end

describe 'sqlite3' do
Expand Down
Loading