Skip to content

Commit f51d798

Browse files
authored
Merge pull request #243 from kufu/localize-primary-key-patch
Localize patch for AR::Relation#primary_key in Rails 8.0+
2 parents c5df5f8 + d1c4328 commit f51d798

4 files changed

Lines changed: 197 additions & 11 deletions

File tree

lib/activerecord-bitemporal/bitemporal.rb

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,36 @@ def bitemporal_option_storage=(value)
112112
end
113113

114114
module Relation
115-
module Finder
116-
def find(*ids)
117-
return super if block_given?
118-
all.spawn.yield_self { |obj|
119-
def obj.primary_key
120-
"bitemporal_id"
115+
module BitemporalIdAsPrimaryKey # :nodoc:
116+
private
117+
118+
# Generate a method that temporarily changes the primary key to
119+
# bitemporal_id for localizing the effect of the change to only the
120+
# method specified by `name`.
121+
#
122+
# DO NOT use this method outside of this module.
123+
def use_bitemporal_id_as_primary_key(name) # :nodoc:
124+
module_eval <<~RUBY, __FILE__, __LINE__ + 1
125+
def #{name}(...)
126+
all.spawn.yield_self { |relation|
127+
def relation.primary_key
128+
bitemporal_id_key
129+
end
130+
relation.method(:#{name}).super_method.call(...)
131+
}
121132
end
122-
obj.method(:find).super_method.call(*ids)
123-
}
133+
RUBY
134+
end
135+
end
136+
extend BitemporalIdAsPrimaryKey
137+
138+
module Finder
139+
extend BitemporalIdAsPrimaryKey
140+
141+
use_bitemporal_id_as_primary_key :find
142+
143+
if ActiveRecord.version >= Gem::Version.new("8.0.0")
144+
use_bitemporal_id_as_primary_key :exists?
124145
end
125146

126147
def find_at_time!(datetime, *ids)
@@ -136,6 +157,10 @@ def find_at_time(datetime, *ids)
136157
end
137158
include Finder
138159

160+
if ActiveRecord.version >= Gem::Version.new("8.0.0")
161+
use_bitemporal_id_as_primary_key :ids
162+
end
163+
139164
def build_arel(*)
140165
ActiveRecord::Bitemporal.with_bitemporal_option(**bitemporal_option) {
141166
super
@@ -168,8 +193,12 @@ def load
168193
end
169194
end
170195

171-
def primary_key
172-
bitemporal_id_key
196+
# Use original primary_key for Active Record 8.0+ as much as possible
197+
# to avoid issues with patching primary_key of AR::Relation globally.
198+
if ActiveRecord.version < Gem::Version.new("8.0.0")
199+
def primary_key
200+
bitemporal_id_key
201+
end
173202
end
174203
end
175204

lib/activerecord-bitemporal/scope.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,13 @@ module CollectionProxy
160160
# @see https://github.com/rails/rails/blob/v7.1.3.4/activerecord/lib/active_record/relation/delegation.rb#L117
161161
delegate :bitemporal_value, :bitemporal_value=, :valid_datetime, :valid_date,
162162
:transaction_datetime, :bitemporal_option, :bitemporal_option_merge!,
163-
:build_arel, :primary_key, to: :scope
163+
:build_arel, to: :scope
164+
165+
if ActiveRecord.version < Gem::Version.new("8.0.0")
166+
delegate :primary_key, to: :scope
167+
else
168+
delegate :ids, :exists?, to: :scope
169+
end
164170
end
165171

166172
module Scope
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
RSpec.describe ActiveRecord::Bitemporal::Relation do
6+
describe "#exists?" do
7+
def create_employees_with_history
8+
Employee
9+
.create!([{ name: "Jane" }, { name: "Tom" } ])
10+
.each { |e| e.update!(updated_at: Time.current) }
11+
end
12+
13+
# Assuming that the number of records will not reach the maximum value in tests.
14+
# employees.bitemporal_id is a 4-byte signed integer.
15+
MAX_BITEMPORAL_ID = 1 << 31 - 1
16+
17+
context "on BTDM" do
18+
let(:employees) { create_employees_with_history }
19+
20+
it "finds existing bitemporal IDs" do
21+
first_employee, second_employee = *employees
22+
23+
expect(Employee.exists?(first_employee.bitemporal_id)).to eq true
24+
expect(Employee.exists?(second_employee.bitemporal_id)).to eq true
25+
expect(Employee.exists?(MAX_BITEMPORAL_ID)).to eq false
26+
end
27+
end
28+
29+
context "on relation" do
30+
let(:employees) { create_employees_with_history }
31+
32+
it "finds existing bitemporal IDs" do
33+
first_employee, second_employee = *employees
34+
35+
expect(Employee.all.exists?(first_employee.bitemporal_id)).to eq true
36+
expect(Employee.all.exists?(second_employee.bitemporal_id)).to eq true
37+
expect(Employee.all.exists?(MAX_BITEMPORAL_ID)).to eq false
38+
end
39+
end
40+
41+
context "on loaded relation" do
42+
let(:employees) { create_employees_with_history }
43+
44+
it "finds existing bitemporal IDs" do
45+
first_employee, second_employee = *employees
46+
47+
expect(Employee.all.load.exists?(first_employee.bitemporal_id)).to eq true
48+
expect(Employee.all.load.exists?(second_employee.bitemporal_id)).to eq true
49+
expect(Employee.all.load.exists?(MAX_BITEMPORAL_ID)).to eq false
50+
end
51+
end
52+
53+
context "with eager loading by includes" do
54+
let(:company) {
55+
Company
56+
.create!(name: "Company")
57+
.tap { |c| c.employees << create_employees_with_history }
58+
}
59+
60+
it "finds existing bitemporal IDs" do
61+
first_employee, second_employee = *company.employees
62+
63+
expect(Employee.includes(:company).exists?(first_employee.bitemporal_id)).to eq true
64+
expect(Employee.includes(:company).exists?(second_employee.bitemporal_id)).to eq true
65+
expect(Employee.includes(:company).exists?(MAX_BITEMPORAL_ID)).to eq false
66+
end
67+
end
68+
69+
context "on association" do
70+
let(:company) {
71+
Company
72+
.create!(name: "Company")
73+
.tap { |c| c.employees << create_employees_with_history }
74+
}
75+
76+
it "finds existing bitemporal IDs" do
77+
first_employee, second_employee = *company.employees
78+
79+
expect(company.employees.reset.exists?(first_employee.bitemporal_id)).to eq true
80+
expect(company.employees.reset.exists?(second_employee.bitemporal_id)).to eq true
81+
expect(company.employees.reset.exists?(MAX_BITEMPORAL_ID)).to eq false
82+
end
83+
end
84+
end
85+
end
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
RSpec.describe ActiveRecord::Bitemporal::Relation do
6+
describe "#ids" do
7+
def create_employees_with_history
8+
Employee
9+
.create!([{ name: "Jane" }, { name: "Tom" } ])
10+
.each { |e| e.update!(updated_at: Time.current) }
11+
end
12+
13+
context "on BTDM" do
14+
let(:employees) { create_employees_with_history }
15+
16+
it "returns all bitemporal IDs" do
17+
expected = [employees[0].bitemporal_id, employees[1].bitemporal_id]
18+
expect(Employee.ids).to match_array expected
19+
end
20+
end
21+
22+
context "on relation" do
23+
let(:employees) { create_employees_with_history }
24+
25+
it "returns all bitemporal IDs" do
26+
expected = [employees[0].bitemporal_id, employees[1].bitemporal_id]
27+
expect(Employee.all.ids).to match_array expected
28+
end
29+
end
30+
31+
context "on loaded relation" do
32+
let(:employees) { create_employees_with_history }
33+
34+
it "returns all bitemporal IDs" do
35+
expected = [employees[0].bitemporal_id, employees[1].bitemporal_id]
36+
expect(Employee.all.load.ids).to match_array expected
37+
end
38+
end
39+
40+
context "with eager loading by includes" do
41+
let(:company) {
42+
Company
43+
.create!(name: "Company")
44+
.tap { |c| c.employees << create_employees_with_history }
45+
}
46+
47+
it "returns bitemporal IDs" do
48+
expected = [company.employees[0].bitemporal_id, company.employees[1].bitemporal_id]
49+
expect(Employee.includes(:company).ids).to match_array expected
50+
end
51+
end
52+
53+
context "on association" do
54+
let(:company) {
55+
Company
56+
.create!(name: "Company")
57+
.tap { |c| c.employees << create_employees_with_history }
58+
}
59+
60+
it "returns bitemporal IDs" do
61+
expected = [company.employees[0].bitemporal_id, company.employees[1].bitemporal_id]
62+
expect(company.employees.reset.ids).to match_array expected
63+
end
64+
end
65+
end
66+
end

0 commit comments

Comments
 (0)