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.
- checksums.yaml +4 -4
- data/DEVELOPMENT.md +124 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +14 -0
- data/README.md +16 -1
- data/Rakefile +54 -0
- data/VERSION +1 -1
- data/bin/console +10 -0
- data/bin/setup +15 -0
- data/bin/test +5 -0
- data/bin/test-all +10 -0
- data/bin/test-rails +5 -0
- data/historiographer.gemspec +38 -4
- data/lib/historiographer/history.rb +193 -60
- data/spec/combustion_helper.rb +34 -0
- data/spec/db/migrate/20250823000000_create_easy_ml_columns.rb +26 -0
- data/spec/db/migrate/20250824000000_create_test_articles.rb +26 -0
- data/spec/db/migrate/20250824000001_create_test_categories.rb +26 -0
- data/spec/db/migrate/20250825000000_create_bylines.rb +11 -0
- data/spec/db/migrate/20250826000000_create_test_users.rb +8 -0
- data/spec/db/migrate/20250826000001_create_test_user_histories.rb +18 -0
- data/spec/db/migrate/20250826000002_create_test_websites.rb +9 -0
- data/spec/db/migrate/20250826000003_create_test_website_histories.rb +19 -0
- data/spec/db/schema.rb +110 -40
- data/spec/historiographer_spec.rb +319 -1
- data/spec/integration/historiographer_safe_integration_spec.rb +154 -0
- data/spec/internal/app/models/application_record.rb +5 -0
- data/spec/internal/app/models/deploy.rb +5 -0
- data/spec/internal/app/models/user.rb +4 -0
- data/spec/internal/app/models/website.rb +5 -0
- data/spec/internal/app/models/website_history.rb +7 -0
- data/spec/internal/config/database.yml +9 -0
- data/spec/internal/config/routes.rb +2 -0
- data/spec/internal/db/schema.rb +48 -0
- data/spec/models/author.rb +1 -0
- data/spec/models/byline.rb +4 -0
- data/spec/models/post.rb +2 -0
- data/spec/models/test_article.rb +4 -0
- data/spec/models/test_article_history.rb +3 -0
- data/spec/models/test_category.rb +4 -0
- data/spec/models/test_category_history.rb +3 -0
- data/spec/models/test_user.rb +4 -0
- data/spec/models/test_user_history.rb +3 -0
- data/spec/models/test_website.rb +4 -0
- data/spec/models/test_website_history.rb +3 -0
- data/spec/rails_integration/historiographer_rails_integration_spec.rb +106 -0
- data/spec/spec_helper.rb +2 -3
- metadata +42 -4
- 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:
|
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 "
|
103
|
-
t.string "name"
|
104
|
-
t.string "data_type"
|
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 ["
|
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 "
|
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('
|
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
|