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 +4 -4
- data/VERSION +1 -1
- data/historiographer.gemspec +12 -3
- data/lib/historiographer/history.rb +141 -43
- 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/schema.rb +66 -40
- data/spec/historiographer_spec.rb +155 -1
- 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/spec_helper.rb +2 -3
- metadata +11 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 66fb9c7d713df84dd4ad5470c6ace5730e5a762d378d0d1fc523a00767b94a2b
|
4
|
+
data.tar.gz: e9b1cfaa4911d4e596aaa8808d1e4cf69b7e57a40101bae4cf826ec48832d3c9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a6291260e457cc89940cbdd622e1e2075e05ad7682005f4bd4dab9e63d302e2cd1dd3d55d494f336b442fe36827b5934251f3fb155480ab46e1bf7e84cb77414
|
7
|
+
data.tar.gz: e1a2afe3202d4c3949424afda77a5c9b307baa50c1e43bde0c488ecff3bd85aff08b5ce57c6664dba058b188603e35c845e508487769848fa811003174b751e0
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
4.4.
|
1
|
+
4.4.2
|
data/historiographer.gemspec
CHANGED
@@ -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.
|
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.
|
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-
|
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
|
-
|
137
|
-
|
138
|
-
|
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
|
-
#
|
237
|
-
|
238
|
-
|
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
|
-
#
|
284
|
-
|
285
|
-
|
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
|
-
#
|
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
|
-
#
|
292
|
-
|
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
|
-
#
|
296
|
-
|
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,
|
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
|
-
|
394
|
+
# Create the Rails association first
|
395
|
+
has_one assoc_name, **options
|
313
396
|
|
314
|
-
#
|
315
|
-
|
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,
|
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,
|
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
|
-
#
|
357
|
-
|
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
|
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_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 "
|
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,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('
|
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
|
data/spec/models/author.rb
CHANGED
data/spec/models/post.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
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.
|
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-
|
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
|