historiographer 4.4.1 → 4.4.2

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: a9299cb62bdfd77ef8cec2c614a592166c67d489a1c36a77d83d78eb3dca48d1
4
- data.tar.gz: 9e03988f85e756bbbc2789c86dc4df255df837aef07bdc718ee2774f5a5956cb
3
+ metadata.gz: 66fb9c7d713df84dd4ad5470c6ace5730e5a762d378d0d1fc523a00767b94a2b
4
+ data.tar.gz: e9b1cfaa4911d4e596aaa8808d1e4cf69b7e57a40101bae4cf826ec48832d3c9
5
5
  SHA512:
6
- metadata.gz: 70e7054198d9010c9d7cc01986e9d8d92b64f4f6a65d4ba2c96d66b7d903d07dd21ac2690407ff40d8bfd685c174e1f9c65b0c659e7d915d1398a637808435bb
7
- data.tar.gz: b2ccf22179ec57e5cf045628eadd76b7e755ac3d9951b838030d72f132461f524752774e5d5baeefcb01bfdb6a47a430c8eaa08ed6382fa54dddb84ec5b1ec6d
6
+ metadata.gz: a6291260e457cc89940cbdd622e1e2075e05ad7682005f4bd4dab9e63d302e2cd1dd3d55d494f336b442fe36827b5934251f3fb155480ab46e1bf7e84cb77414
7
+ data.tar.gz: e1a2afe3202d4c3949424afda77a5c9b307baa50c1e43bde0c488ecff3bd85aff08b5ce57c6664dba058b188603e35c845e508487769848fa811003174b751e0
data/VERSION CHANGED
@@ -1 +1 @@
1
- 4.4.1
1
+ 4.4.2
@@ -2,16 +2,16 @@
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.4.1 ruby lib
5
+ # stub: historiographer 4.4.2 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "historiographer".freeze
9
- s.version = "4.4.1"
9
+ s.version = "4.4.2"
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]
13
13
  s.authors = ["brettshollenberger".freeze]
14
- s.date = "2025-08-21"
14
+ s.date = "2025-08-22"
15
15
  s.description = "Creates separate tables for each history table".freeze
16
16
  s.email = "brett.shollenberger@gmail.com".freeze
17
17
  s.extra_rdoc_files = [
@@ -67,6 +67,10 @@ Gem::Specification.new do |s|
67
67
  "spec/db/migrate/20241119000000_create_datasets.rb",
68
68
  "spec/db/migrate/2025082100000_create_projects.rb",
69
69
  "spec/db/migrate/2025082100001_create_project_files.rb",
70
+ "spec/db/migrate/20250823000000_create_easy_ml_columns.rb",
71
+ "spec/db/migrate/20250824000000_create_test_articles.rb",
72
+ "spec/db/migrate/20250824000001_create_test_categories.rb",
73
+ "spec/db/migrate/20250825000000_create_bylines.rb",
70
74
  "spec/db/schema.rb",
71
75
  "spec/factories/post.rb",
72
76
  "spec/foreign_key_spec.rb",
@@ -74,6 +78,7 @@ Gem::Specification.new do |s|
74
78
  "spec/models/application_record.rb",
75
79
  "spec/models/author.rb",
76
80
  "spec/models/author_history.rb",
81
+ "spec/models/byline.rb",
77
82
  "spec/models/comment.rb",
78
83
  "spec/models/comment_history.rb",
79
84
  "spec/models/easy_ml/column.rb",
@@ -88,6 +93,10 @@ Gem::Specification.new do |s|
88
93
  "spec/models/safe_post_history.rb",
89
94
  "spec/models/silent_post.rb",
90
95
  "spec/models/silent_post_history.rb",
96
+ "spec/models/test_article.rb",
97
+ "spec/models/test_article_history.rb",
98
+ "spec/models/test_category.rb",
99
+ "spec/models/test_category_history.rb",
91
100
  "spec/models/thing_with_compound_index.rb",
92
101
  "spec/models/thing_with_compound_index_history.rb",
93
102
  "spec/models/thing_without_history.rb",
@@ -133,10 +133,14 @@ module Historiographer
133
133
  end
134
134
  end
135
135
 
136
- (foreign_class.columns.map(&:name) - ["id"]).each do |method_name|
137
- define_method(method_name) do |*args, **kwargs, &block|
138
- forward_method(method_name, *args, **kwargs, &block)
136
+ begin
137
+ (foreign_class.columns.map(&:name) - ["id"]).each do |method_name|
138
+ define_method(method_name) do |*args, **kwargs, &block|
139
+ forward_method(method_name, *args, **kwargs, &block)
140
+ end
139
141
  end
142
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished
143
+ # Table might not exist yet during setup
140
144
  end
141
145
 
142
146
  # Add method_missing for any methods we might have missed
@@ -233,9 +237,52 @@ module Historiographer
233
237
  # Track custom association methods
234
238
  base.class_variable_set(:@@history_association_methods, [])
235
239
 
236
- # Dynamically define associations on the history class
237
- foreign_class.reflect_on_all_associations.each do |association|
238
- define_history_association(association)
240
+ # Register this history class to have its associations set up after initialization
241
+ history_classes = Thread.current[:historiographer_history_classes] ||= []
242
+ history_classes << base
243
+
244
+ # Set up the after_initialize hook if we're in a Rails app
245
+ if defined?(Rails) && Rails.respond_to?(:application) && Rails.application && Rails.application.config.respond_to?(:after_initialize)
246
+ Rails.application.config.after_initialize do
247
+ history_classes.each do |history_class|
248
+ history_class.setup_history_associations
249
+ end
250
+ end
251
+ else
252
+ # For non-Rails environments (like our test suite), set up associations immediately
253
+ # but defer if models aren't loaded yet
254
+ base.define_singleton_method :setup_history_associations do |force = false|
255
+ return if !force && class_variable_defined?(:@@associations_set_up) && class_variable_get(:@@associations_set_up)
256
+ class_variable_set(:@@associations_set_up, true)
257
+
258
+ foreign_class.reflect_on_all_associations.each do |association|
259
+ begin
260
+ define_history_association(association)
261
+ rescue => e
262
+ # Log but don't fail
263
+ puts "Warning: Could not define history association #{association.name}: #{e.message}" if ENV['DEBUG']
264
+ end
265
+ end
266
+ end
267
+
268
+ # Try to set up now if possible
269
+ begin
270
+ base.setup_history_associations
271
+ rescue => e
272
+ # Will retry later
273
+ end
274
+
275
+ # Override reflect_on_association to ensure associations are defined
276
+ base.define_singleton_method :reflect_on_association do |name|
277
+ setup_history_associations rescue nil
278
+ super(name)
279
+ end
280
+
281
+ # Override reflect_on_all_associations to ensure associations are defined
282
+ base.define_singleton_method :reflect_on_all_associations do |*args|
283
+ setup_history_associations rescue nil
284
+ super(*args)
285
+ end
239
286
  end
240
287
 
241
288
  def snapshot
@@ -264,83 +311,133 @@ module Historiographer
264
311
  def define_history_association(association)
265
312
  if association.is_a?(Symbol) || association.is_a?(String)
266
313
  association = original_class.reflect_on_association(association)
314
+ # If the association doesn't exist on the original class, skip it
315
+ return unless association
267
316
  end
317
+
268
318
  assoc_name = association.name
269
- assoc_module = association.active_record.module_parent
270
- assoc_history_class_name = "#{association.class_name}History"
271
-
272
- begin
273
- assoc_module.const_get(assoc_history_class_name)
274
- assoc_history_class_name = "#{assoc_module}::#{assoc_history_class_name}" unless assoc_history_class_name.match?(Regexp.new("#{assoc_module}::"))
275
- rescue
276
- end
277
-
278
319
  assoc_foreign_key = association.foreign_key
279
320
 
280
321
  # Skip through associations to history classes to avoid infinite loops
281
322
  return if association.class_name.end_with?('History')
282
323
 
283
- # Always use the history class if it exists
284
- assoc_class = assoc_history_class_name.safe_constantize || OpenStruct.new(name: association.class_name)
285
- assoc_class_name = assoc_class.name
324
+ # Get the associated model's table name
325
+ original_assoc_class = association.class_name.safe_constantize
326
+ return unless original_assoc_class # Can't proceed without the class
327
+
328
+ assoc_table_name = original_assoc_class.table_name
329
+ history_table_name = "#{assoc_table_name.singularize}_histories"
330
+
331
+ # Check if a history table exists for this association
332
+ has_history_table = ActiveRecord::Base.connection.tables.include?(history_table_name)
333
+
334
+ if has_history_table
335
+ # This model has history tracking, use the history class
336
+ assoc_history_class_name = "#{association.class_name}History"
337
+ assoc_module = association.active_record.module_parent
338
+
339
+ begin
340
+ assoc_module.const_get(assoc_history_class_name)
341
+ assoc_history_class_name = "#{assoc_module}::#{assoc_history_class_name}" unless assoc_history_class_name.match?(Regexp.new("#{assoc_module}::"))
342
+ rescue
343
+ end
344
+
345
+ assoc_class = assoc_history_class_name.safe_constantize || OpenStruct.new(name: assoc_history_class_name)
346
+ assoc_class_name = assoc_class.name
347
+ else
348
+ # No history table, use the original model
349
+ assoc_class_name = association.class_name
350
+ end
286
351
 
287
352
  case association.macro
288
353
  when :belongs_to
289
- # For belongs_to associations, if the target is a history class, we need special handling
354
+ # Start with all original association options
355
+ options = association.options.dup
356
+
357
+ # Override the class name and foreign key
358
+ options[:class_name] = assoc_class_name
359
+ options[:foreign_key] = assoc_foreign_key
360
+
361
+ # For history associations, we need to handle snapshot filtering differently
362
+ # We'll create the association but override the accessor method
290
363
  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'
364
+ # Create the Rails association first
365
+ belongs_to assoc_name, **options
294
366
 
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)
367
+ # Then override the accessor to filter by snapshot_id
368
+ history_fk = association.class_name.gsub(/History$/, '').underscore + '_id'
299
369
 
300
- define_method(assoc_name) do
370
+ define_method("#{assoc_name}_with_snapshot") do
301
371
  return nil unless self[assoc_foreign_key]
302
372
  assoc_class.where(
303
373
  history_fk => self[assoc_foreign_key],
304
374
  snapshot_id: self.snapshot_id
305
375
  ).first
306
376
  end
377
+
378
+ # Alias the original method and replace it
379
+ alias_method "#{assoc_name}_without_snapshot", assoc_name
380
+ alias_method assoc_name, "#{assoc_name}_with_snapshot"
307
381
  else
308
- belongs_to assoc_name, class_name: assoc_class_name, foreign_key: assoc_foreign_key
382
+ belongs_to assoc_name, **options
309
383
  end
310
384
  when :has_one
385
+ # Start with all original association options
386
+ options = association.options.dup
387
+
388
+ # Override the class name and keys
389
+ options[:class_name] = assoc_class_name
390
+ options[:foreign_key] = assoc_foreign_key
391
+ options[:primary_key] = history_foreign_key
392
+
311
393
  if assoc_class_name.match?(/History/)
312
- hfk = history_foreign_key
394
+ # Create the Rails association first
395
+ has_one assoc_name, **options
313
396
 
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)
397
+ # Then override the accessor to filter by snapshot_id
398
+ hfk = history_foreign_key
318
399
 
319
- define_method(assoc_name) do
400
+ define_method("#{assoc_name}_with_snapshot") do
320
401
  assoc_class.where(
321
402
  assoc_foreign_key => self[hfk],
322
403
  snapshot_id: self.snapshot_id
323
404
  ).first
324
405
  end
406
+
407
+ # Alias the original method and replace it
408
+ alias_method "#{assoc_name}_without_snapshot", assoc_name
409
+ alias_method assoc_name, "#{assoc_name}_with_snapshot"
325
410
  else
326
- has_one assoc_name, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
411
+ has_one assoc_name, **options
327
412
  end
328
413
  when :has_many
414
+ # Start with all original association options
415
+ options = association.options.dup
416
+
417
+ # Override the class name and keys
418
+ options[:class_name] = assoc_class_name
419
+ options[:foreign_key] = assoc_foreign_key
420
+ options[:primary_key] = history_foreign_key
421
+
329
422
  if assoc_class_name.match?(/History/)
423
+ # Create the Rails association first
424
+ has_many assoc_name, **options
425
+
426
+ # Then override the accessor to filter by snapshot_id
330
427
  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
428
 
336
- define_method(assoc_name) do
429
+ define_method("#{assoc_name}_with_snapshot") do
337
430
  assoc_class.where(
338
431
  assoc_foreign_key => self[hfk],
339
432
  snapshot_id: self.snapshot_id
340
433
  )
341
434
  end
435
+
436
+ # Alias the original method and replace it
437
+ alias_method "#{assoc_name}_without_snapshot", assoc_name
438
+ alias_method assoc_name, "#{assoc_name}_with_snapshot"
342
439
  else
343
- has_many assoc_name, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
440
+ has_many assoc_name, **options
344
441
  end
345
442
  end
346
443
  end
@@ -353,8 +450,9 @@ module Historiographer
353
450
  def history_foreign_key
354
451
  return @history_foreign_key if @history_foreign_key
355
452
 
356
- # CAN THIS BE TABLE OR MODEL?
357
- @history_foreign_key = original_class.base_class.name.singularize.foreign_key
453
+ # Use the table name to generate the foreign key to properly handle namespaced models
454
+ # E.g. EasyML::Column -> easy_ml_columns -> easy_ml_column_id
455
+ @history_foreign_key = original_class.base_class.table_name.singularize.foreign_key
358
456
  end
359
457
 
360
458
  end
@@ -0,0 +1,26 @@
1
+ class CreateEasyMlColumns < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :easy_ml_columns do |t|
4
+ t.string :name
5
+ t.string :data_type
6
+ t.timestamps
7
+ end
8
+
9
+ create_table :easy_ml_column_histories do |t|
10
+ t.integer :easy_ml_column_id, null: false
11
+ t.string :name
12
+ t.string :data_type
13
+ t.timestamps
14
+
15
+ t.datetime :history_started_at, null: false
16
+ t.datetime :history_ended_at
17
+ t.integer :history_user_id
18
+ t.string :snapshot_id
19
+
20
+ t.index :easy_ml_column_id
21
+ t.index :history_started_at
22
+ t.index :history_ended_at
23
+ t.index :snapshot_id
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ class CreateTestArticles < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :test_articles do |t|
4
+ t.string :title
5
+ t.integer :test_category_id
6
+ t.timestamps
7
+ end
8
+
9
+ create_table :test_article_histories do |t|
10
+ t.integer :test_article_id, null: false
11
+ t.string :title
12
+ t.integer :test_category_id
13
+ t.timestamps
14
+
15
+ t.datetime :history_started_at, null: false
16
+ t.datetime :history_ended_at
17
+ t.integer :history_user_id
18
+ t.string :snapshot_id
19
+
20
+ t.index :test_article_id
21
+ t.index :history_started_at
22
+ t.index :history_ended_at
23
+ t.index :snapshot_id
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ class CreateTestCategories < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :test_categories do |t|
4
+ t.string :name
5
+ t.integer :test_articles_count, default: 0
6
+ t.timestamps
7
+ end
8
+
9
+ create_table :test_category_histories do |t|
10
+ t.integer :test_category_id, null: false
11
+ t.string :name
12
+ t.integer :test_articles_count, default: 0
13
+ t.timestamps
14
+
15
+ t.datetime :history_started_at, null: false
16
+ t.datetime :history_ended_at
17
+ t.integer :history_user_id
18
+ t.string :snapshot_id
19
+
20
+ t.index :test_category_id
21
+ t.index :history_started_at
22
+ t.index :history_ended_at
23
+ t.index :snapshot_id
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ class CreateBylines < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :bylines do |t|
4
+ t.string :name, null: false
5
+ t.integer :author_id
6
+ t.timestamps
7
+ end
8
+
9
+ add_index :bylines, :author_id
10
+ end
11
+ end
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_25_000000) 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,52 @@ 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
+
322
348
  create_table "thing_with_compound_index_histories", force: :cascade do |t|
323
349
  t.integer "thing_with_compound_index_id", null: false
324
350
  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,158 @@ 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
920
1074
  end
@@ -2,4 +2,5 @@ class Author < ActiveRecord::Base
2
2
  include Historiographer
3
3
  has_many :comments
4
4
  has_many :posts
5
+ has_many :bylines # This model doesn't have history tracking
5
6
  end
@@ -0,0 +1,4 @@
1
+ class Byline < ActiveRecord::Base
2
+ # Note: This model does NOT include Historiographer
3
+ belongs_to :author
4
+ end
data/spec/models/post.rb CHANGED
@@ -5,6 +5,8 @@ class Post < ApplicationRecord
5
5
  acts_as_paranoid
6
6
  has_many :comments
7
7
 
8
+ attr_accessor :type
9
+
8
10
  validates :type, inclusion: { in: ['Post', 'PrivatePost', nil] }
9
11
  before_validation :set_defaults
10
12
  after_find :set_comment_count
@@ -0,0 +1,4 @@
1
+ class TestArticle < ActiveRecord::Base
2
+ include Historiographer
3
+ # Association will be defined later to avoid circular dependency
4
+ end
@@ -0,0 +1,3 @@
1
+ class TestArticleHistory < ActiveRecord::Base
2
+ include Historiographer::History
3
+ end
@@ -0,0 +1,4 @@
1
+ class TestCategory < ActiveRecord::Base
2
+ include Historiographer
3
+ # Association will be defined later to avoid circular dependency
4
+ end
@@ -0,0 +1,3 @@
1
+ class TestCategoryHistory < ActiveRecord::Base
2
+ include Historiographer::History
3
+ end
data/spec/spec_helper.rb CHANGED
@@ -45,10 +45,9 @@ module Rails
45
45
  end
46
46
 
47
47
  def self.application
48
- OpenStruct.new(
48
+ @application ||= OpenStruct.new(
49
49
  config: OpenStruct.new(
50
- eager_load_namespaces: [],
51
- autoloader: loader
50
+ eager_load_namespaces: []
52
51
  )
53
52
  )
54
53
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: historiographer
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.4.1
4
+ version: 4.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - brettshollenberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-21 00:00:00.000000000 Z
11
+ date: 2025-08-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -262,6 +262,10 @@ files:
262
262
  - spec/db/migrate/20241119000000_create_datasets.rb
263
263
  - spec/db/migrate/2025082100000_create_projects.rb
264
264
  - spec/db/migrate/2025082100001_create_project_files.rb
265
+ - spec/db/migrate/20250823000000_create_easy_ml_columns.rb
266
+ - spec/db/migrate/20250824000000_create_test_articles.rb
267
+ - spec/db/migrate/20250824000001_create_test_categories.rb
268
+ - spec/db/migrate/20250825000000_create_bylines.rb
265
269
  - spec/db/schema.rb
266
270
  - spec/factories/post.rb
267
271
  - spec/foreign_key_spec.rb
@@ -269,6 +273,7 @@ files:
269
273
  - spec/models/application_record.rb
270
274
  - spec/models/author.rb
271
275
  - spec/models/author_history.rb
276
+ - spec/models/byline.rb
272
277
  - spec/models/comment.rb
273
278
  - spec/models/comment_history.rb
274
279
  - spec/models/easy_ml/column.rb
@@ -283,6 +288,10 @@ files:
283
288
  - spec/models/safe_post_history.rb
284
289
  - spec/models/silent_post.rb
285
290
  - spec/models/silent_post_history.rb
291
+ - spec/models/test_article.rb
292
+ - spec/models/test_article_history.rb
293
+ - spec/models/test_category.rb
294
+ - spec/models/test_category_history.rb
286
295
  - spec/models/thing_with_compound_index.rb
287
296
  - spec/models/thing_with_compound_index_history.rb
288
297
  - spec/models/thing_without_history.rb