historiographer 4.4.1 → 4.4.3

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/DEVELOPMENT.md +124 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +14 -0
  5. data/README.md +16 -1
  6. data/Rakefile +54 -0
  7. data/VERSION +1 -1
  8. data/bin/console +10 -0
  9. data/bin/setup +15 -0
  10. data/bin/test +5 -0
  11. data/bin/test-all +10 -0
  12. data/bin/test-rails +5 -0
  13. data/historiographer.gemspec +38 -4
  14. data/lib/historiographer/history.rb +193 -60
  15. data/spec/combustion_helper.rb +34 -0
  16. data/spec/db/migrate/20250823000000_create_easy_ml_columns.rb +26 -0
  17. data/spec/db/migrate/20250824000000_create_test_articles.rb +26 -0
  18. data/spec/db/migrate/20250824000001_create_test_categories.rb +26 -0
  19. data/spec/db/migrate/20250825000000_create_bylines.rb +11 -0
  20. data/spec/db/migrate/20250826000000_create_test_users.rb +8 -0
  21. data/spec/db/migrate/20250826000001_create_test_user_histories.rb +18 -0
  22. data/spec/db/migrate/20250826000002_create_test_websites.rb +9 -0
  23. data/spec/db/migrate/20250826000003_create_test_website_histories.rb +19 -0
  24. data/spec/db/schema.rb +110 -40
  25. data/spec/historiographer_spec.rb +319 -1
  26. data/spec/integration/historiographer_safe_integration_spec.rb +154 -0
  27. data/spec/internal/app/models/application_record.rb +5 -0
  28. data/spec/internal/app/models/deploy.rb +5 -0
  29. data/spec/internal/app/models/user.rb +4 -0
  30. data/spec/internal/app/models/website.rb +5 -0
  31. data/spec/internal/app/models/website_history.rb +7 -0
  32. data/spec/internal/config/database.yml +9 -0
  33. data/spec/internal/config/routes.rb +2 -0
  34. data/spec/internal/db/schema.rb +48 -0
  35. data/spec/models/author.rb +1 -0
  36. data/spec/models/byline.rb +4 -0
  37. data/spec/models/post.rb +2 -0
  38. data/spec/models/test_article.rb +4 -0
  39. data/spec/models/test_article_history.rb +3 -0
  40. data/spec/models/test_category.rb +4 -0
  41. data/spec/models/test_category_history.rb +3 -0
  42. data/spec/models/test_user.rb +4 -0
  43. data/spec/models/test_user_history.rb +3 -0
  44. data/spec/models/test_website.rb +4 -0
  45. data/spec/models/test_website_history.rb +3 -0
  46. data/spec/rails_integration/historiographer_rails_integration_spec.rb +106 -0
  47. data/spec/spec_helper.rb +2 -3
  48. metadata +42 -4
  49. data/spec/foreign_key_spec.rb +0 -189
data/spec/db/schema.rb CHANGED
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema[7.1].define(version: 2024_11_19_000000) do
13
+ ActiveRecord::Schema[7.1].define(version: 2025_08_26_000003) do
14
14
  # These are extensions that must be enabled in order to support this database
15
15
  enable_extension "plpgsql"
16
16
 
@@ -42,6 +42,14 @@ ActiveRecord::Schema[7.1].define(version: 2024_11_19_000000) do
42
42
  t.index ["deleted_at"], name: "index_authors_on_deleted_at"
43
43
  end
44
44
 
45
+ create_table "bylines", force: :cascade do |t|
46
+ t.string "name", null: false
47
+ t.integer "author_id"
48
+ t.datetime "created_at", null: false
49
+ t.datetime "updated_at", null: false
50
+ t.index ["author_id"], name: "index_bylines_on_author_id"
51
+ end
52
+
45
53
  create_table "comment_histories", force: :cascade do |t|
46
54
  t.integer "comment_id", null: false
47
55
  t.integer "post_id"
@@ -99,57 +107,26 @@ ActiveRecord::Schema[7.1].define(version: 2024_11_19_000000) do
99
107
  end
100
108
 
101
109
  create_table "easy_ml_column_histories", force: :cascade do |t|
102
- t.integer "column_id", null: false
103
- t.string "name", null: false
104
- t.string "data_type", null: false
105
- t.string "column_type"
110
+ t.integer "easy_ml_column_id", null: false
111
+ t.string "name"
112
+ t.string "data_type"
106
113
  t.datetime "created_at", null: false
107
114
  t.datetime "updated_at", null: false
108
115
  t.datetime "history_started_at", null: false
109
116
  t.datetime "history_ended_at"
110
117
  t.integer "history_user_id"
111
118
  t.string "snapshot_id"
112
- t.index ["column_id"], name: "index_easy_ml_column_histories_on_column_id"
119
+ t.index ["easy_ml_column_id"], name: "index_easy_ml_column_histories_on_easy_ml_column_id"
113
120
  t.index ["history_ended_at"], name: "index_easy_ml_column_histories_on_history_ended_at"
114
121
  t.index ["history_started_at"], name: "index_easy_ml_column_histories_on_history_started_at"
115
- t.index ["history_user_id"], name: "index_easy_ml_column_histories_on_history_user_id"
116
122
  t.index ["snapshot_id"], name: "index_easy_ml_column_histories_on_snapshot_id"
117
123
  end
118
124
 
119
125
  create_table "easy_ml_columns", force: :cascade do |t|
120
- t.string "name", null: false
121
- t.string "data_type", null: false
122
- t.string "column_type"
123
- t.datetime "created_at", null: false
124
- t.datetime "updated_at", null: false
125
- end
126
-
127
- create_table "ml_model_histories", force: :cascade do |t|
128
- t.integer "ml_model_id", null: false
129
- t.string "name"
130
- t.string "model_type"
131
- t.jsonb "parameters"
132
- t.datetime "created_at", null: false
133
- t.datetime "updated_at", null: false
134
- t.datetime "history_started_at", null: false
135
- t.datetime "history_ended_at"
136
- t.integer "history_user_id"
137
- t.string "snapshot_id"
138
- t.index ["history_ended_at"], name: "index_ml_model_histories_on_history_ended_at"
139
- t.index ["history_started_at"], name: "index_ml_model_histories_on_history_started_at"
140
- t.index ["history_user_id"], name: "index_ml_model_histories_on_history_user_id"
141
- t.index ["ml_model_id"], name: "index_ml_model_histories_on_ml_model_id"
142
- t.index ["model_type"], name: "index_ml_model_histories_on_model_type"
143
- t.index ["snapshot_id"], name: "index_ml_model_histories_on_snapshot_id"
144
- end
145
-
146
- create_table "ml_models", force: :cascade do |t|
147
126
  t.string "name"
148
- t.string "model_type"
149
- t.jsonb "parameters"
127
+ t.string "data_type"
150
128
  t.datetime "created_at", null: false
151
129
  t.datetime "updated_at", null: false
152
- t.index ["model_type"], name: "index_ml_models_on_model_type"
153
130
  end
154
131
 
155
132
  create_table "post_histories", force: :cascade do |t|
@@ -166,7 +143,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_11_19_000000) do
166
143
  t.datetime "history_ended_at", precision: nil
167
144
  t.integer "history_user_id"
168
145
  t.string "snapshot_id"
169
- t.string "type"
170
146
  t.index ["author_id"], name: "index_post_histories_on_author_id"
171
147
  t.index ["deleted_at"], name: "index_post_histories_on_deleted_at"
172
148
  t.index ["enabled"], name: "index_post_histories_on_enabled"
@@ -187,17 +163,17 @@ ActiveRecord::Schema[7.1].define(version: 2024_11_19_000000) do
187
163
  t.datetime "deleted_at", precision: nil
188
164
  t.datetime "created_at", precision: nil, null: false
189
165
  t.datetime "updated_at", precision: nil, null: false
190
- t.string "type"
191
166
  t.index ["author_id"], name: "index_posts_on_author_id"
192
167
  t.index ["deleted_at"], name: "index_posts_on_deleted_at"
193
168
  t.index ["enabled"], name: "index_posts_on_enabled"
194
169
  t.index ["live_at"], name: "index_posts_on_live_at"
195
- t.index ["type"], name: "index_posts_on_type"
196
170
  end
197
171
 
198
172
  create_table "project_file_histories", force: :cascade do |t|
199
173
  t.integer "project_file_id", null: false
174
+ t.integer "project_id"
200
175
  t.string "name", null: false
176
+ t.string "content"
201
177
  t.datetime "created_at", null: false
202
178
  t.datetime "updated_at", null: false
203
179
  t.datetime "history_started_at", null: false
@@ -208,13 +184,17 @@ ActiveRecord::Schema[7.1].define(version: 2024_11_19_000000) do
208
184
  t.index ["history_started_at"], name: "index_project_file_histories_on_history_started_at"
209
185
  t.index ["history_user_id"], name: "index_project_file_histories_on_history_user_id"
210
186
  t.index ["project_file_id"], name: "index_project_file_histories_on_project_file_id"
187
+ t.index ["project_id"], name: "index_project_file_histories_on_project_id"
211
188
  t.index ["snapshot_id"], name: "index_project_file_histories_on_snapshot_id"
212
189
  end
213
190
 
214
191
  create_table "project_files", force: :cascade do |t|
192
+ t.bigint "project_id"
215
193
  t.string "name", null: false
194
+ t.string "content"
216
195
  t.datetime "created_at", null: false
217
196
  t.datetime "updated_at", null: false
197
+ t.index ["project_id"], name: "index_project_files_on_project_id"
218
198
  end
219
199
 
220
200
  create_table "project_histories", force: :cascade do |t|
@@ -319,6 +299,96 @@ ActiveRecord::Schema[7.1].define(version: 2024_11_19_000000) do
319
299
  t.index ["live_at"], name: "index_silent_posts_on_live_at"
320
300
  end
321
301
 
302
+ create_table "test_article_histories", force: :cascade do |t|
303
+ t.integer "test_article_id", null: false
304
+ t.string "title"
305
+ t.integer "test_category_id"
306
+ t.datetime "created_at", null: false
307
+ t.datetime "updated_at", null: false
308
+ t.datetime "history_started_at", null: false
309
+ t.datetime "history_ended_at"
310
+ t.integer "history_user_id"
311
+ t.string "snapshot_id"
312
+ t.index ["history_ended_at"], name: "index_test_article_histories_on_history_ended_at"
313
+ t.index ["history_started_at"], name: "index_test_article_histories_on_history_started_at"
314
+ t.index ["snapshot_id"], name: "index_test_article_histories_on_snapshot_id"
315
+ t.index ["test_article_id"], name: "index_test_article_histories_on_test_article_id"
316
+ end
317
+
318
+ create_table "test_articles", force: :cascade do |t|
319
+ t.string "title"
320
+ t.integer "test_category_id"
321
+ t.datetime "created_at", null: false
322
+ t.datetime "updated_at", null: false
323
+ end
324
+
325
+ create_table "test_categories", force: :cascade do |t|
326
+ t.string "name"
327
+ t.integer "test_articles_count", default: 0
328
+ t.datetime "created_at", null: false
329
+ t.datetime "updated_at", null: false
330
+ end
331
+
332
+ create_table "test_category_histories", force: :cascade do |t|
333
+ t.integer "test_category_id", null: false
334
+ t.string "name"
335
+ t.integer "test_articles_count", default: 0
336
+ t.datetime "created_at", null: false
337
+ t.datetime "updated_at", null: false
338
+ t.datetime "history_started_at", null: false
339
+ t.datetime "history_ended_at"
340
+ t.integer "history_user_id"
341
+ t.string "snapshot_id"
342
+ t.index ["history_ended_at"], name: "index_test_category_histories_on_history_ended_at"
343
+ t.index ["history_started_at"], name: "index_test_category_histories_on_history_started_at"
344
+ t.index ["snapshot_id"], name: "index_test_category_histories_on_snapshot_id"
345
+ t.index ["test_category_id"], name: "index_test_category_histories_on_test_category_id"
346
+ end
347
+
348
+ create_table "test_user_histories", force: :cascade do |t|
349
+ t.integer "test_user_id", null: false
350
+ t.string "name"
351
+ t.datetime "created_at", null: false
352
+ t.datetime "updated_at", null: false
353
+ t.datetime "history_started_at", null: false
354
+ t.datetime "history_ended_at"
355
+ t.integer "history_user_id"
356
+ t.string "snapshot_id"
357
+ t.index ["history_ended_at"], name: "index_test_user_histories_on_history_ended_at"
358
+ t.index ["history_started_at"], name: "index_test_user_histories_on_history_started_at"
359
+ t.index ["snapshot_id"], name: "index_test_user_histories_on_snapshot_id"
360
+ t.index ["test_user_id"], name: "index_test_user_histories_on_test_user_id"
361
+ end
362
+
363
+ create_table "test_users", force: :cascade do |t|
364
+ t.string "name"
365
+ t.datetime "created_at", null: false
366
+ t.datetime "updated_at", null: false
367
+ end
368
+
369
+ create_table "test_website_histories", force: :cascade do |t|
370
+ t.integer "test_website_id", null: false
371
+ t.string "name"
372
+ t.integer "user_id"
373
+ t.datetime "created_at", null: false
374
+ t.datetime "updated_at", null: false
375
+ t.datetime "history_started_at", null: false
376
+ t.datetime "history_ended_at"
377
+ t.integer "history_user_id"
378
+ t.string "snapshot_id"
379
+ t.index ["history_ended_at"], name: "index_test_website_histories_on_history_ended_at"
380
+ t.index ["history_started_at"], name: "index_test_website_histories_on_history_started_at"
381
+ t.index ["snapshot_id"], name: "index_test_website_histories_on_snapshot_id"
382
+ t.index ["test_website_id"], name: "index_test_website_histories_on_test_website_id"
383
+ end
384
+
385
+ create_table "test_websites", force: :cascade do |t|
386
+ t.string "name"
387
+ t.integer "user_id"
388
+ t.datetime "created_at", null: false
389
+ t.datetime "updated_at", null: false
390
+ end
391
+
322
392
  create_table "thing_with_compound_index_histories", force: :cascade do |t|
323
393
  t.integer "thing_with_compound_index_id", null: false
324
394
  t.string "key"
@@ -898,7 +898,7 @@ describe Historiographer do
898
898
 
899
899
  it 'establishes correct foreign key for history association' do
900
900
  col_history = column.histories.first
901
- expect(col_history.class.history_foreign_key).to eq('column_id')
901
+ expect(col_history.class.history_foreign_key).to eq('easy_ml_column_id')
902
902
  expect(col_history).to be_a(EasyML::ColumnHistory)
903
903
  end
904
904
 
@@ -917,4 +917,322 @@ describe Historiographer do
917
917
  expect(column.histories.last.name).to eq('feature_2')
918
918
  end
919
919
  end
920
+
921
+ describe 'Non-historiographer associations' do
922
+ it 'preserves associations to models without history tracking' do
923
+ # Create an author and byline (byline has no history tracking)
924
+ author = Author.create!(full_name: 'Test Author', history_user_id: 1)
925
+ byline = Byline.create!(name: 'Test Byline', author: author)
926
+
927
+ # The author should have the byline association
928
+ expect(author.bylines).to include(byline)
929
+
930
+ # Get the author's history record
931
+ author_history = AuthorHistory.last
932
+ expect(author_history).not_to be_nil
933
+
934
+ # The history model should still be able to access the byline (non-history model)
935
+ # This should work because Byline doesn't have history tracking
936
+ expect(author_history.bylines).to include(byline)
937
+
938
+ # The association should point to the regular Byline model, not a history model
939
+ byline_association = AuthorHistory.reflect_on_association(:bylines)
940
+ expect(byline_association).not_to be_nil
941
+ expect(byline_association.klass).to eq(Byline)
942
+ end
943
+
944
+ it 'handles mixed associations correctly' do
945
+ # Create an author with both history-tracked and non-history-tracked associations
946
+ author = Author.create!(full_name: 'Test Author', history_user_id: 1)
947
+ post = Post.create!(title: 'Test Post', body: 'Test body', author_id: author.id, history_user_id: 1)
948
+ comment = Comment.create!(body: 'Test comment', author_id: author.id, post_id: post.id, history_user_id: 1)
949
+ byline = Byline.create!(name: 'Test Byline', author: author)
950
+
951
+ author_history = AuthorHistory.last
952
+
953
+ # History-tracked associations should work correctly
954
+ # Note: For history associations, we create custom methods rather than Rails associations
955
+ # so they won't show up in reflect_on_all_associations
956
+ expect(author_history).to respond_to(:posts)
957
+ expect(author_history).to respond_to(:comments)
958
+
959
+ # The methods should return history records filtered by snapshot_id
960
+ post_histories = PostHistory.where(author_id: author.id)
961
+ expect(post_histories).not_to be_empty
962
+
963
+ # When accessing through the history model, it should filter by snapshot_id
964
+ author_history_posts = author_history.posts
965
+ expect(author_history_posts).to be_a(ActiveRecord::Relation)
966
+
967
+ # Non-history-tracked associations should show up as regular Rails associations
968
+ bylines_association = AuthorHistory.reflect_on_association(:bylines)
969
+ expect(bylines_association).not_to be_nil
970
+ expect(bylines_association.klass).to eq(Byline)
971
+
972
+ # And they should work correctly
973
+ expect(author_history.bylines).to include(byline)
974
+ end
975
+ end
976
+
977
+ describe 'Association options preservation' do
978
+ # Test with inline class definitions to ensure associations are defined properly
979
+
980
+ before(:all) do
981
+ # Create test classes inline for this test
982
+ class TestAssocArticle < ActiveRecord::Base
983
+ self.table_name = 'test_articles'
984
+ include Historiographer
985
+ belongs_to :test_assoc_category,
986
+ class_name: 'TestAssocCategory',
987
+ foreign_key: 'test_category_id',
988
+ optional: true,
989
+ touch: true,
990
+ counter_cache: 'test_articles_count'
991
+ end
992
+
993
+ class TestAssocCategory < ActiveRecord::Base
994
+ self.table_name = 'test_categories'
995
+ include Historiographer
996
+ has_many :test_assoc_articles,
997
+ class_name: 'TestAssocArticle',
998
+ foreign_key: 'test_category_id',
999
+ dependent: :restrict_with_error,
1000
+ inverse_of: :test_assoc_category
1001
+ end
1002
+
1003
+ class TestAssocArticleHistory < ActiveRecord::Base
1004
+ self.table_name = 'test_article_histories'
1005
+ include Historiographer::History
1006
+ end
1007
+
1008
+ class TestAssocCategoryHistory < ActiveRecord::Base
1009
+ self.table_name = 'test_category_histories'
1010
+ include Historiographer::History
1011
+ end
1012
+
1013
+ # Manually trigger association setup since we're in a test environment
1014
+ # Force = true because associations may have been partially set up before all models were loaded
1015
+ TestAssocArticleHistory.setup_history_associations(true) if TestAssocArticleHistory.respond_to?(:setup_history_associations)
1016
+ TestAssocCategoryHistory.setup_history_associations(true) if TestAssocCategoryHistory.respond_to?(:setup_history_associations)
1017
+ end
1018
+
1019
+ after(:all) do
1020
+ Object.send(:remove_const, :TestAssocArticle) if Object.const_defined?(:TestAssocArticle)
1021
+ Object.send(:remove_const, :TestAssocArticleHistory) if Object.const_defined?(:TestAssocArticleHistory)
1022
+ Object.send(:remove_const, :TestAssocCategory) if Object.const_defined?(:TestAssocCategory)
1023
+ Object.send(:remove_const, :TestAssocCategoryHistory) if Object.const_defined?(:TestAssocCategoryHistory)
1024
+ end
1025
+
1026
+ it 'preserves optional setting for belongs_to associations' do
1027
+ # Check the original TestAssocArticle belongs_to association
1028
+ article_association = TestAssocArticle.reflect_on_association(:test_assoc_category)
1029
+ expect(article_association).not_to be_nil
1030
+ expect(article_association.options[:optional]).to eq(true)
1031
+
1032
+ # The TestAssocArticleHistory should have the same options
1033
+ article_history_association = TestAssocArticleHistory.reflect_on_association(:test_assoc_category)
1034
+ expect(article_history_association).not_to be_nil
1035
+ expect(article_history_association.options[:optional]).to eq(true)
1036
+ end
1037
+
1038
+ it 'preserves touch and counter_cache options for belongs_to associations' do
1039
+ article_association = TestAssocArticle.reflect_on_association(:test_assoc_category)
1040
+ expect(article_association.options[:touch]).to eq(true)
1041
+ expect(article_association.options[:counter_cache]).to eq('test_articles_count')
1042
+
1043
+ article_history_association = TestAssocArticleHistory.reflect_on_association(:test_assoc_category)
1044
+ expect(article_history_association).not_to be_nil
1045
+ expect(article_history_association.options[:touch]).to eq(true)
1046
+ expect(article_history_association.options[:counter_cache]).to eq('test_articles_count')
1047
+ end
1048
+
1049
+ it 'preserves dependent and inverse_of options for has_many associations' do
1050
+ category_articles_association = TestAssocCategory.reflect_on_association(:test_assoc_articles)
1051
+ expect(category_articles_association.options[:dependent]).to eq(:restrict_with_error)
1052
+ expect(category_articles_association.options[:inverse_of]).to eq(:test_assoc_category)
1053
+
1054
+ # Note: has_many associations might not be copied to history models in the same way
1055
+ # This is expected behavior since history models typically don't need the same associations
1056
+ end
1057
+
1058
+ it 'allows creating history records with nil optional associations' do
1059
+ # Create an article without a category (should be valid since category is optional)
1060
+ article = TestAssocArticle.create!(title: 'Test Article without category', history_user_id: 1)
1061
+ expect(article.test_category_id).to be_nil
1062
+
1063
+ # The history record should also be created successfully
1064
+ history = TestAssocArticleHistory.last
1065
+ expect(history).not_to be_nil
1066
+ expect(history.test_category_id).to be_nil
1067
+ expect(history.test_article_id).to eq(article.id)
1068
+
1069
+ # Creating snapshots should work even with nil associations
1070
+ article.snapshot
1071
+ expect { article.snapshot }.to_not raise_error
1072
+ end
1073
+ end
1074
+
1075
+ describe 'Foreign key handling' do
1076
+ before(:all) do
1077
+ # Ensure test tables exist
1078
+ unless ActiveRecord::Base.connection.table_exists?(:test_users)
1079
+ ActiveRecord::Base.connection.create_table :test_users do |t|
1080
+ t.string :name
1081
+ t.timestamps
1082
+ end
1083
+ end
1084
+
1085
+ unless ActiveRecord::Base.connection.table_exists?(:test_user_histories)
1086
+ ActiveRecord::Base.connection.create_table :test_user_histories do |t|
1087
+ t.integer :test_user_id, null: false
1088
+ t.string :name
1089
+ t.timestamps
1090
+ t.datetime :history_started_at, null: false
1091
+ t.datetime :history_ended_at
1092
+ t.integer :history_user_id
1093
+ t.string :snapshot_id
1094
+
1095
+ t.index :test_user_id
1096
+ t.index :history_started_at
1097
+ t.index :history_ended_at
1098
+ t.index :snapshot_id
1099
+ end
1100
+ end
1101
+
1102
+ unless ActiveRecord::Base.connection.table_exists?(:test_websites)
1103
+ ActiveRecord::Base.connection.create_table :test_websites do |t|
1104
+ t.string :name
1105
+ t.integer :user_id
1106
+ t.timestamps
1107
+ end
1108
+ end
1109
+
1110
+ unless ActiveRecord::Base.connection.table_exists?(:test_website_histories)
1111
+ ActiveRecord::Base.connection.create_table :test_website_histories do |t|
1112
+ t.integer :test_website_id, null: false
1113
+ t.string :name
1114
+ t.integer :user_id
1115
+ t.timestamps
1116
+ t.datetime :history_started_at, null: false
1117
+ t.datetime :history_ended_at
1118
+ t.integer :history_user_id
1119
+ t.string :snapshot_id
1120
+
1121
+ t.index :test_website_id
1122
+ t.index :history_started_at
1123
+ t.index :history_ended_at
1124
+ t.index :snapshot_id
1125
+ end
1126
+ end
1127
+ end
1128
+
1129
+ describe 'belongs_to associations on history models' do
1130
+ it 'does not raise error about wrong column when accessing belongs_to associations' do
1131
+ # This is the core issue: when a history model has a belongs_to association,
1132
+ # it should not use the foreign key as the primary key for lookups
1133
+
1134
+ # Create a user
1135
+ user = TestUser.create!(name: 'Test User', history_user_id: 1)
1136
+
1137
+ # Create a website belonging to the user
1138
+ website = TestWebsite.create!(
1139
+ name: 'Test Website',
1140
+ user_id: user.id,
1141
+ history_user_id: 1
1142
+ )
1143
+
1144
+ # Get the website history
1145
+ website_history = TestWebsiteHistory.last
1146
+
1147
+ # The history should have the correct user_id
1148
+ expect(website_history.user_id).to eq(user.id)
1149
+
1150
+ # The belongs_to association should work without errors
1151
+ # Previously this would fail with "column users.user_id does not exist"
1152
+ # because it was using primary_key: :user_id instead of the default :id
1153
+ expect { website_history.user }.not_to raise_error
1154
+ end
1155
+
1156
+ it 'allows direct creation of history records with foreign keys' do
1157
+ user = TestUser.create!(name: 'Another User', history_user_id: 1)
1158
+
1159
+ # Create history attributes like in the original error case
1160
+ attrs = {
1161
+ "name" => "test.example",
1162
+ "user_id" => user.id,
1163
+ "created_at" => Time.now,
1164
+ "updated_at" => Time.now,
1165
+ "test_website_id" => 100,
1166
+ "history_started_at" => Time.now,
1167
+ "history_user_id" => 1,
1168
+ "snapshot_id" => SecureRandom.uuid
1169
+ }
1170
+
1171
+ # This should not raise an error about test_users.user_id not existing
1172
+ # The original bug was that it would look for test_users.user_id instead of test_users.id
1173
+ expect { TestWebsiteHistory.create!(attrs) }.not_to raise_error
1174
+
1175
+ history = TestWebsiteHistory.last
1176
+ expect(history.user_id).to eq(user.id)
1177
+ end
1178
+ end
1179
+
1180
+ describe 'snapshot associations with history models' do
1181
+ it 'correctly filters associations by snapshot_id when using custom association methods' do
1182
+ # First create regular history records
1183
+ user = TestUser.create!(name: 'User One', history_user_id: 1)
1184
+ website = TestWebsite.create!(
1185
+ name: 'Website One',
1186
+ user_id: user.id,
1187
+ history_user_id: 1
1188
+ )
1189
+
1190
+ # Check that regular histories were created
1191
+ expect(TestUserHistory.count).to eq(1)
1192
+ expect(TestWebsiteHistory.count).to eq(1)
1193
+
1194
+ # Now create snapshot histories directly (simulating what snapshot would do)
1195
+ snapshot_id = SecureRandom.uuid
1196
+
1197
+ # Create user history with snapshot
1198
+ user_snapshot = TestUserHistory.create!(
1199
+ test_user_id: user.id,
1200
+ name: user.name,
1201
+ created_at: user.created_at,
1202
+ updated_at: user.updated_at,
1203
+ history_started_at: Time.now,
1204
+ history_user_id: 1,
1205
+ snapshot_id: snapshot_id
1206
+ )
1207
+
1208
+ # Create website history with snapshot
1209
+ website_snapshot = TestWebsiteHistory.create!(
1210
+ test_website_id: website.id,
1211
+ name: website.name,
1212
+ user_id: user.id,
1213
+ created_at: website.created_at,
1214
+ updated_at: website.updated_at,
1215
+ history_started_at: Time.now,
1216
+ history_user_id: 1,
1217
+ snapshot_id: snapshot_id
1218
+ )
1219
+
1220
+ # Now test that the association filtering works
1221
+ # The website history's user association should find the user history with the same snapshot_id
1222
+ user_from_association = website_snapshot.user
1223
+
1224
+ # Since user association points to history when snapshots are involved,
1225
+ # it should return the TestUserHistory with matching snapshot_id
1226
+ if user_from_association.is_a?(TestUserHistory)
1227
+ expect(user_from_association.snapshot_id).to eq(snapshot_id)
1228
+ expect(user_from_association.name).to eq('User One')
1229
+ else
1230
+ # If it returns the regular TestUser (non-history), that's also acceptable
1231
+ # as long as it doesn't error
1232
+ expect(user_from_association).to be_a(TestUser)
1233
+ expect(user_from_association.name).to eq('User One')
1234
+ end
1235
+ end
1236
+ end
1237
+ end
920
1238
  end