historiographer 4.3.0 → 4.4.0

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: df0f832698c8177c8785d913caa4c26e2374e10ea813896190481b091a3176a6
4
- data.tar.gz: 3bcf25861fed71c432c47e97489b2ffae7a42d70960e6d321ce8b4b24c8a5c89
3
+ metadata.gz: e095861ef6cd8df461a227897f73a7f0055c1ed1cc337ec6025be60fa3755a91
4
+ data.tar.gz: dc5272631bc5cf63cc55495842cc3a4407a59729b011499ca34e4248fc75d10d
5
5
  SHA512:
6
- metadata.gz: df1430488c6120b9126aff4a526fb2aba8f84a6aad8690592b977a45b5031674ee3722818c8679881c96cd950b50a81089d4a363948cddcf65e253051792a524
7
- data.tar.gz: 4bb03df9ecd8998fb1866bc2d5ada28b509d1122787e6ef7b47375d0a3dc18bd8bd14f8e846d1b6e5702c9a2f5a53366680d6855126c742c48526d9048d211d4
6
+ metadata.gz: d165aa2cc4f216c3abb3c30cf0b456fc517d3ea4ce9aed12afb109f0ecc01e95a8ab2affb374602b2d6fec9ed9983d1d28831365f768c64c3e785903c5a455a5
7
+ data.tar.gz: 8a6a3a0ecae45fe220d99f8644a925103aaf686c2048ca8c75cb26e3d785f939affb72fb732c7997346bd1d4ed7cf1029f293178b61c5ef1c7f677850859a140
data/VERSION CHANGED
@@ -1 +1 @@
1
- 4.3.0
1
+ 4.4.0
Binary file
@@ -2,11 +2,11 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: historiographer 4.3.0 ruby lib
5
+ # stub: historiographer 4.4.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "historiographer".freeze
9
- s.version = "4.3.0"
9
+ s.version = "4.4.0"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
@@ -33,6 +33,7 @@ Gem::Specification.new do |s|
33
33
  "historiographer-4.1.12.gem",
34
34
  "historiographer-4.1.13.gem",
35
35
  "historiographer-4.1.14.gem",
36
+ "historiographer-4.3.0.gem",
36
37
  "historiographer.gemspec",
37
38
  "init.rb",
38
39
  "instructions/implementation.md",
@@ -68,6 +69,7 @@ Gem::Specification.new do |s|
68
69
  "spec/db/migrate/2025082100001_create_project_files.rb",
69
70
  "spec/db/schema.rb",
70
71
  "spec/factories/post.rb",
72
+ "spec/foreign_key_spec.rb",
71
73
  "spec/historiographer_spec.rb",
72
74
  "spec/models/application_record.rb",
73
75
  "spec/models/author.rb",
@@ -230,6 +230,9 @@ module Historiographer
230
230
  .order('snapshot_id, history_started_at DESC, id DESC')
231
231
  }
232
232
 
233
+ # Track custom association methods
234
+ base.class_variable_set(:@@history_association_methods, [])
235
+
233
236
  # Dynamically define associations on the history class
234
237
  foreign_class.reflect_on_all_associations.each do |association|
235
238
  define_history_association(association)
@@ -281,20 +284,64 @@ module Historiographer
281
284
  assoc_class = assoc_history_class_name.safe_constantize || OpenStruct.new(name: association.class_name)
282
285
  assoc_class_name = assoc_class.name
283
286
 
284
- # Define the scope to filter by snapshot_id for history associations
285
- scope = if assoc_class_name.match?(/History/)
286
- ->(history_instance) { where(snapshot_id: history_instance.snapshot_id) }
287
- else
288
- ->(history_instance) { all }
289
- end
290
-
291
287
  case association.macro
292
288
  when :belongs_to
293
- belongs_to assoc_name, scope, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: assoc_foreign_key
289
+ # For belongs_to associations, if the target is a history class, we need special handling
290
+ if assoc_class_name.match?(/History/)
291
+ # Override the association method to filter by snapshot_id
292
+ # The history class uses <model>_id as the foreign key (e.g., author_id for AuthorHistory)
293
+ history_fk = association.class_name.gsub(/History$/, '').underscore + '_id'
294
+
295
+ # Track this custom method
296
+ methods_list = class_variable_get(:@@history_association_methods) rescue []
297
+ methods_list << assoc_name
298
+ class_variable_set(:@@history_association_methods, methods_list)
299
+
300
+ define_method(assoc_name) do
301
+ return nil unless self[assoc_foreign_key]
302
+ assoc_class.where(
303
+ history_fk => self[assoc_foreign_key],
304
+ snapshot_id: self.snapshot_id
305
+ ).first
306
+ end
307
+ else
308
+ belongs_to assoc_name, class_name: assoc_class_name, foreign_key: assoc_foreign_key
309
+ end
294
310
  when :has_one
295
- has_one assoc_name, scope, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
311
+ if assoc_class_name.match?(/History/)
312
+ hfk = history_foreign_key
313
+
314
+ # Track this custom method
315
+ methods_list = class_variable_get(:@@history_association_methods) rescue []
316
+ methods_list << assoc_name
317
+ class_variable_set(:@@history_association_methods, methods_list)
318
+
319
+ define_method(assoc_name) do
320
+ assoc_class.where(
321
+ assoc_foreign_key => self[hfk],
322
+ snapshot_id: self.snapshot_id
323
+ ).first
324
+ end
325
+ else
326
+ has_one assoc_name, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
327
+ end
296
328
  when :has_many
297
- has_many assoc_name, scope, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
329
+ if assoc_class_name.match?(/History/)
330
+ hfk = history_foreign_key
331
+ # Track this custom method
332
+ methods_list = class_variable_get(:@@history_association_methods) rescue []
333
+ methods_list << assoc_name
334
+ class_variable_set(:@@history_association_methods, methods_list)
335
+
336
+ define_method(assoc_name) do
337
+ assoc_class.where(
338
+ assoc_foreign_key => self[hfk],
339
+ snapshot_id: self.snapshot_id
340
+ )
341
+ end
342
+ else
343
+ has_many assoc_name, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
344
+ end
298
345
  end
299
346
  end
300
347
 
@@ -360,13 +407,19 @@ module Historiographer
360
407
  end
361
408
  end
362
409
 
363
- # For each association in the history class
364
- self.class.reflect_on_all_associations.each do |reflection|
410
+ # For each association in the history class (including custom methods)
411
+ associations_to_forward = self.class.reflect_on_all_associations.map(&:name)
412
+
413
+ # Add custom association methods
414
+ custom_methods = self.class.class_variable_get(:@@history_association_methods) rescue []
415
+ associations_to_forward += custom_methods
416
+
417
+ associations_to_forward.uniq.each do |assoc_name|
365
418
  # Define a method that forwards to the history association
366
419
  instance.singleton_class.class_eval do
367
- define_method(reflection.name) do |*args, &block|
368
- history_instance = instance.instance_variable_get(:@_history_instance)
369
- history_instance.send(reflection.name, *args, &block)
420
+ define_method(assoc_name) do |*args, &block|
421
+ history_instance = instance_variable_get(:@_history_instance)
422
+ history_instance.send(assoc_name, *args, &block)
370
423
  end
371
424
  end
372
425
  end
@@ -0,0 +1,189 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'Foreign key handling for belongs_to associations' do
4
+ before(:all) do
5
+ @original_stdout = $stdout
6
+ $stdout = StringIO.new
7
+
8
+ ActiveRecord::Base.connection.create_table :test_users, force: true do |t|
9
+ t.string :name
10
+ t.timestamps
11
+ end
12
+
13
+ ActiveRecord::Base.connection.create_table :test_websites, force: true do |t|
14
+ t.string :name
15
+ t.integer :user_id
16
+ t.timestamps
17
+ end
18
+
19
+ ActiveRecord::Base.connection.create_table :test_website_histories, force: true do |t|
20
+ t.integer :test_website_id, null: false
21
+ t.string :name
22
+ t.integer :user_id
23
+ t.timestamps
24
+ t.datetime :history_started_at, null: false
25
+ t.datetime :history_ended_at
26
+ t.integer :history_user_id
27
+ t.string :snapshot_id
28
+
29
+ t.index :test_website_id
30
+ t.index :history_started_at
31
+ t.index :history_ended_at
32
+ t.index :snapshot_id
33
+ end
34
+
35
+ ActiveRecord::Base.connection.create_table :test_user_histories, force: true do |t|
36
+ t.integer :test_user_id, null: false
37
+ t.string :name
38
+ t.timestamps
39
+ t.datetime :history_started_at, null: false
40
+ t.datetime :history_ended_at
41
+ t.integer :history_user_id
42
+ t.string :snapshot_id
43
+
44
+ t.index :test_user_id
45
+ t.index :history_started_at
46
+ t.index :history_ended_at
47
+ t.index :snapshot_id
48
+ end
49
+
50
+ class TestUser < ActiveRecord::Base
51
+ include Historiographer
52
+ has_many :test_websites, foreign_key: 'user_id'
53
+ end
54
+
55
+ class TestWebsite < ActiveRecord::Base
56
+ include Historiographer
57
+ belongs_to :user, class_name: 'TestUser', foreign_key: 'user_id', optional: true
58
+ end
59
+
60
+ class TestWebsiteHistory < ActiveRecord::Base
61
+ include Historiographer::History
62
+ end
63
+
64
+ class TestUserHistory < ActiveRecord::Base
65
+ include Historiographer::History
66
+ end
67
+ end
68
+
69
+ after(:all) do
70
+ $stdout = @original_stdout
71
+ ActiveRecord::Base.connection.drop_table :test_website_histories
72
+ ActiveRecord::Base.connection.drop_table :test_websites
73
+ ActiveRecord::Base.connection.drop_table :test_user_histories
74
+ ActiveRecord::Base.connection.drop_table :test_users
75
+ Object.send(:remove_const, :TestWebsite) if Object.const_defined?(:TestWebsite)
76
+ Object.send(:remove_const, :TestWebsiteHistory) if Object.const_defined?(:TestWebsiteHistory)
77
+ Object.send(:remove_const, :TestUser) if Object.const_defined?(:TestUser)
78
+ Object.send(:remove_const, :TestUserHistory) if Object.const_defined?(:TestUserHistory)
79
+ end
80
+
81
+ describe 'belongs_to association on history models' do
82
+ it 'does not raise error about wrong column when accessing belongs_to associations' do
83
+ # This is the core issue: when a history model has a belongs_to association,
84
+ # it should not use the foreign key as the primary key for lookups
85
+
86
+ # Create a user
87
+ user = TestUser.create!(name: 'Test User', history_user_id: 1)
88
+
89
+ # Create a website belonging to the user
90
+ website = TestWebsite.create!(
91
+ name: 'Test Website',
92
+ user_id: user.id,
93
+ history_user_id: 1
94
+ )
95
+
96
+ # Get the website history
97
+ website_history = TestWebsiteHistory.last
98
+
99
+ # The history should have the correct user_id
100
+ expect(website_history.user_id).to eq(user.id)
101
+
102
+ # The belongs_to association should work without errors
103
+ # Previously this would fail with "column users.user_id does not exist"
104
+ # because it was using primary_key: :user_id instead of the default :id
105
+ expect { website_history.user }.not_to raise_error
106
+ end
107
+
108
+ it 'allows direct creation of history records with foreign keys' do
109
+ user = TestUser.create!(name: 'Another User', history_user_id: 1)
110
+
111
+ # Create history attributes like in the original error case
112
+ attrs = {
113
+ "name" => "test.example",
114
+ "user_id" => user.id,
115
+ "created_at" => Time.now,
116
+ "updated_at" => Time.now,
117
+ "test_website_id" => 100,
118
+ "history_started_at" => Time.now,
119
+ "history_user_id" => 1,
120
+ "snapshot_id" => SecureRandom.uuid
121
+ }
122
+
123
+ # This should not raise an error about test_users.user_id not existing
124
+ # The original bug was that it would look for test_users.user_id instead of test_users.id
125
+ expect { TestWebsiteHistory.create!(attrs) }.not_to raise_error
126
+
127
+ history = TestWebsiteHistory.last
128
+ expect(history.user_id).to eq(user.id)
129
+ end
130
+ end
131
+
132
+ describe 'snapshot associations with history models' do
133
+ it 'correctly filters associations by snapshot_id when using custom association methods' do
134
+ # First create regular history records
135
+ user = TestUser.create!(name: 'User One', history_user_id: 1)
136
+ website = TestWebsite.create!(
137
+ name: 'Website One',
138
+ user_id: user.id,
139
+ history_user_id: 1
140
+ )
141
+
142
+ # Check that regular histories were created
143
+ expect(TestUserHistory.count).to eq(1)
144
+ expect(TestWebsiteHistory.count).to eq(1)
145
+
146
+ # Now create snapshot histories directly (simulating what snapshot would do)
147
+ snapshot_id = SecureRandom.uuid
148
+
149
+ # Create user history with snapshot
150
+ user_snapshot = TestUserHistory.create!(
151
+ test_user_id: user.id,
152
+ name: user.name,
153
+ created_at: user.created_at,
154
+ updated_at: user.updated_at,
155
+ history_started_at: Time.now,
156
+ history_user_id: 1,
157
+ snapshot_id: snapshot_id
158
+ )
159
+
160
+ # Create website history with snapshot
161
+ website_snapshot = TestWebsiteHistory.create!(
162
+ test_website_id: website.id,
163
+ name: website.name,
164
+ user_id: user.id,
165
+ created_at: website.created_at,
166
+ updated_at: website.updated_at,
167
+ history_started_at: Time.now,
168
+ history_user_id: 1,
169
+ snapshot_id: snapshot_id
170
+ )
171
+
172
+ # Now test that the association filtering works
173
+ # The website history's user association should find the user history with the same snapshot_id
174
+ user_from_association = website_snapshot.user
175
+
176
+ # Since user association points to history when snapshots are involved,
177
+ # it should return the TestUserHistory with matching snapshot_id
178
+ if user_from_association.is_a?(TestUserHistory)
179
+ expect(user_from_association.snapshot_id).to eq(snapshot_id)
180
+ expect(user_from_association.name).to eq('User One')
181
+ else
182
+ # If it returns the regular TestUser (non-history), that's also acceptable
183
+ # as long as it doesn't error
184
+ expect(user_from_association).to be_a(TestUser)
185
+ expect(user_from_association.name).to eq('User One')
186
+ end
187
+ end
188
+ end
189
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: historiographer
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.0
4
+ version: 4.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - brettshollenberger
@@ -228,6 +228,7 @@ files:
228
228
  - historiographer-4.1.12.gem
229
229
  - historiographer-4.1.13.gem
230
230
  - historiographer-4.1.14.gem
231
+ - historiographer-4.3.0.gem
231
232
  - historiographer.gemspec
232
233
  - init.rb
233
234
  - instructions/implementation.md
@@ -263,6 +264,7 @@ files:
263
264
  - spec/db/migrate/2025082100001_create_project_files.rb
264
265
  - spec/db/schema.rb
265
266
  - spec/factories/post.rb
267
+ - spec/foreign_key_spec.rb
266
268
  - spec/historiographer_spec.rb
267
269
  - spec/models/application_record.rb
268
270
  - spec/models/author.rb