historiographer 4.1.16 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de76c8aaf1054da7a45d6ee7b8d8dd8a7671558ff4b0789726c97295e4f24c53
4
- data.tar.gz: dc5af5d7b25378009cec12c092f19618685e692604bd7f9fc47210c967d5880e
3
+ metadata.gz: e095861ef6cd8df461a227897f73a7f0055c1ed1cc337ec6025be60fa3755a91
4
+ data.tar.gz: dc5272631bc5cf63cc55495842cc3a4407a59729b011499ca34e4248fc75d10d
5
5
  SHA512:
6
- metadata.gz: 698b99835d996003cf895707bc543d6149b39c057db6e4f86535495b8ea6a581a75b500c84487ea5acc3df0493a905e13e2ceb35e94784a6732c7be730df18db
7
- data.tar.gz: d4c85721df30cdd16992ba8043afe2a6e9eca34fe8cad77d9360bc0f03a122b526d1aec5d7c727275d788afbf59098255564ae041fa65f8eb08d27237ca031f0
6
+ metadata.gz: d165aa2cc4f216c3abb3c30cf0b456fc517d3ea4ce9aed12afb109f0ecc01e95a8ab2affb374602b2d6fec9ed9983d1d28831365f768c64c3e785903c5a455a5
7
+ data.tar.gz: 8a6a3a0ecae45fe220d99f8644a925103aaf686c2048ca8c75cb26e3d785f939affb72fb732c7997346bd1d4ed7cf1029f293178b61c5ef1c7f677850859a140
data/README.md CHANGED
@@ -130,174 +130,6 @@ This can be useful when:
130
130
  - You're versioning training data for machine learning models
131
131
  - You need to maintain immutable audit trails at specific checkpoints
132
132
 
133
- ## Single Table Inheritance (STI)
134
-
135
- Historiographer fully supports Single Table Inheritance, both with the default `type` column and with custom inheritance columns.
136
-
137
- ### Default STI with `type` column
138
-
139
- ```ruby
140
- class Post < ActiveRecord::Base
141
- include Historiographer
142
- end
143
-
144
- class PrivatePost < Post
145
- end
146
-
147
- # The history classes follow the same inheritance pattern:
148
- class PostHistory < ActiveRecord::Base
149
- include Historiographer::History
150
- end
151
-
152
- class PrivatePostHistory < PostHistory
153
- end
154
- ```
155
-
156
- History records automatically maintain the correct STI type:
157
-
158
- ```ruby
159
- private_post = PrivatePost.create(title: "Secret", history_user_id: current_user.id)
160
- private_post.snapshot
161
-
162
- # History records are the correct subclass
163
- history = PostHistory.last
164
- history.is_a?(PrivatePostHistory) #=> true
165
- history.type #=> "PrivatePostHistory"
166
- ```
167
-
168
- ### Custom Inheritance Columns
169
-
170
- You can also use a custom column for STI instead of the default `type`:
171
-
172
- ```ruby
173
- class MLModel < ActiveRecord::Base
174
- self.inheritance_column = :model_type
175
- include Historiographer
176
- end
177
-
178
- class XGBoost < MLModel
179
- self.table_name = "ml_models"
180
- end
181
-
182
- # History classes use the same custom column
183
- class MLModelHistory < MLModel
184
- self.inheritance_column = :model_type
185
- self.table_name = "ml_model_histories"
186
- end
187
-
188
- class XGBoostHistory < MLModelHistory
189
- end
190
- ```
191
-
192
- Migration for custom inheritance column:
193
-
194
- ```ruby
195
- create_table :ml_models do |t|
196
- t.string :name
197
- t.string :model_type # Custom inheritance column
198
- t.jsonb :parameters
199
- t.timestamps
200
-
201
- t.index :model_type
202
- end
203
-
204
- create_table :ml_model_histories do |t|
205
- t.histories # Includes all columns from parent table
206
- end
207
- ```
208
-
209
- The custom inheritance column works just like the default `type`:
210
-
211
- ```ruby
212
- model = XGBoost.create(name: "My Model", history_user_id: current_user.id)
213
- model.snapshot
214
-
215
- # History records maintain the correct subclass
216
- history = MLModelHistory.last
217
- history.is_a?(XGBoostHistory) #=> true
218
- history.model_type #=> "XGBoostHistory"
219
- ```
220
-
221
- ### STI and Snapshots: Perfect for Model Versioning
222
-
223
- Single Table Inheritance combined with Historiographer's snapshot feature is particularly powerful for versioning machine learning models and other complex systems that need immutable historical records. Here's why:
224
-
225
- 1. **Type-Safe History**: When you snapshot an ML model, both the model and its parameters are preserved with their exact implementation type. This ensures that when you retrieve historical versions, you get back exactly the right subclass with its specific behavior:
226
-
227
- ```ruby
228
- # Create and configure an XGBoost model
229
- model = XGBoost.create(
230
- name: "Customer Churn Predictor v1",
231
- parameters: { max_depth: 3, eta: 0.1 },
232
- history_user_id: current_user.id
233
- )
234
-
235
- # Take a snapshot before training
236
- model.snapshot
237
-
238
- # Update the model after training
239
- model.update(
240
- name: "Customer Churn Predictor v2",
241
- parameters: { max_depth: 5, eta: 0.2 },
242
- history_user_id: current_user.id
243
- )
244
-
245
- # Later, retrieve the exact pre-training version
246
- historical_model = MLModel.latest_snapshot
247
- historical_model.is_a?(XGBoostHistory) #=> true
248
- historical_model.parameters #=> { max_depth: 3, eta: 0.1 }
249
- ```
250
-
251
- 2. **Implementation Versioning**: Different model types often have different parameters, preprocessing steps, or scoring methods. STI ensures these differences are preserved in history:
252
-
253
- ```ruby
254
- class XGBoost < MLModel
255
- def predict(data)
256
- # XGBoost-specific prediction logic
257
- end
258
- end
259
-
260
- class RandomForest < MLModel
261
- def predict(data)
262
- # RandomForest-specific prediction logic
263
- end
264
- end
265
-
266
- # Your historical records maintain these implementation differences
267
- old_model = MLModel.latest_snapshot
268
- old_model.predict(data) # Uses the exact prediction logic from that point in time
269
- ```
270
-
271
- 3. **Reproducibility**: Essential for ML workflows where you need to reproduce results or audit model behavior:
272
-
273
- ```ruby
274
- # Create model and snapshot at each significant stage
275
- model = XGBoost.create(name: "Risk Scorer v1", history_user_id: current_user.id)
276
-
277
- # Snapshot after initial configuration
278
- model.snapshot(metadata: { stage: "configuration" })
279
-
280
- # Snapshot after training
281
- model.update(parameters: trained_parameters)
282
- model.snapshot(metadata: { stage: "post_training" })
283
-
284
- # Snapshot after validation
285
- model.update(parameters: validated_parameters)
286
- model.snapshot(metadata: { stage: "validated" })
287
-
288
- # Later, you can retrieve any version to reproduce results
289
- initial_version = model.histories.find_by(metadata: { stage: "configuration" })
290
- trained_version = model.histories.find_by(metadata: { stage: "post_training" })
291
- ```
292
-
293
- This combination of STI and snapshots is particularly valuable for:
294
-
295
- - Model governance and compliance
296
- - A/B testing different model types
297
- - Debugging model behavior
298
- - Reproducing historical predictions
299
- - Maintaining audit trails for regulatory requirements
300
-
301
133
  ## Namespaced Models
302
134
 
303
135
  When using namespaced models, Rails handles foreign key naming differently than with non-namespaced models. For example, if you have a model namespaced like this:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 4.1.16
1
+ 4.4.0
Binary file
@@ -2,11 +2,11 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: historiographer 4.1.16 ruby lib
5
+ # stub: historiographer 4.4.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "historiographer".freeze
9
- s.version = "4.1.16"
9
+ s.version = "4.4.0"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
@@ -33,6 +33,7 @@ Gem::Specification.new do |s|
33
33
  "historiographer-4.1.12.gem",
34
34
  "historiographer-4.1.13.gem",
35
35
  "historiographer-4.1.14.gem",
36
+ "historiographer-4.3.0.gem",
36
37
  "historiographer.gemspec",
37
38
  "init.rb",
38
39
  "instructions/implementation.md",
@@ -63,31 +64,26 @@ Gem::Specification.new do |s|
63
64
  "spec/db/migrate/20221018204255_create_silent_post_histories.rb",
64
65
  "spec/db/migrate/20241109182017_create_comments.rb",
65
66
  "spec/db/migrate/20241109182020_create_comment_histories.rb",
66
- "spec/db/migrate/20241118000000_add_type_to_posts.rb",
67
- "spec/db/migrate/20241118000001_add_type_to_post_histories.rb",
68
- "spec/db/migrate/20241118000002_create_ml_models.rb",
69
- "spec/db/migrate/20241118000003_create_easy_ml_columns.rb",
70
67
  "spec/db/migrate/20241119000000_create_datasets.rb",
68
+ "spec/db/migrate/2025082100000_create_projects.rb",
69
+ "spec/db/migrate/2025082100001_create_project_files.rb",
71
70
  "spec/db/schema.rb",
72
71
  "spec/factories/post.rb",
72
+ "spec/foreign_key_spec.rb",
73
73
  "spec/historiographer_spec.rb",
74
74
  "spec/models/application_record.rb",
75
75
  "spec/models/author.rb",
76
76
  "spec/models/author_history.rb",
77
77
  "spec/models/comment.rb",
78
78
  "spec/models/comment_history.rb",
79
- "spec/models/dataset.rb",
80
- "spec/models/dataset_history.rb",
81
79
  "spec/models/easy_ml/column.rb",
82
80
  "spec/models/easy_ml/column_history.rb",
83
- "spec/models/easy_ml/encrypted_column.rb",
84
- "spec/models/easy_ml/encrypted_column_history.rb",
85
- "spec/models/ml_model.rb",
86
- "spec/models/ml_model_history.rb",
87
81
  "spec/models/post.rb",
88
82
  "spec/models/post_history.rb",
89
- "spec/models/private_post.rb",
90
- "spec/models/private_post_history.rb",
83
+ "spec/models/project.rb",
84
+ "spec/models/project_file.rb",
85
+ "spec/models/project_file_history.rb",
86
+ "spec/models/project_history.rb",
91
87
  "spec/models/safe_post.rb",
92
88
  "spec/models/safe_post_history.rb",
93
89
  "spec/models/silent_post.rb",
@@ -96,8 +92,6 @@ Gem::Specification.new do |s|
96
92
  "spec/models/thing_with_compound_index_history.rb",
97
93
  "spec/models/thing_without_history.rb",
98
94
  "spec/models/user.rb",
99
- "spec/models/xgboost.rb",
100
- "spec/models/xgboost_history.rb",
101
95
  "spec/spec_helper.rb"
102
96
  ]
103
97
  s.homepage = "http://github.com/brettshollenberger/historiographer".freeze
@@ -179,11 +179,6 @@ module Historiographer
179
179
  belongs_to association_name, class_name: foreign_class_name
180
180
  end
181
181
 
182
- # Enable STI for history classes
183
- if foreign_class.sti_enabled?
184
- self.inheritance_column = 'type'
185
- end
186
-
187
182
  # Ensure we can't destroy history records
188
183
  before_destroy { |record| raise "Cannot destroy history records" }
189
184
 
@@ -235,6 +230,9 @@ module Historiographer
235
230
  .order('snapshot_id, history_started_at DESC, id DESC')
236
231
  }
237
232
 
233
+ # Track custom association methods
234
+ base.class_variable_set(:@@history_association_methods, [])
235
+
238
236
  # Dynamically define associations on the history class
239
237
  foreign_class.reflect_on_all_associations.each do |association|
240
238
  define_history_association(association)
@@ -286,20 +284,64 @@ module Historiographer
286
284
  assoc_class = assoc_history_class_name.safe_constantize || OpenStruct.new(name: association.class_name)
287
285
  assoc_class_name = assoc_class.name
288
286
 
289
- # Define the scope to filter by snapshot_id for history associations
290
- scope = if assoc_class_name.match?(/History/)
291
- ->(history_instance) { where(snapshot_id: history_instance.snapshot_id) }
292
- else
293
- ->(history_instance) { all }
294
- end
295
-
296
287
  case association.macro
297
288
  when :belongs_to
298
- belongs_to assoc_name, scope, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: assoc_foreign_key
289
+ # For belongs_to associations, if the target is a history class, we need special handling
290
+ if assoc_class_name.match?(/History/)
291
+ # Override the association method to filter by snapshot_id
292
+ # The history class uses <model>_id as the foreign key (e.g., author_id for AuthorHistory)
293
+ history_fk = association.class_name.gsub(/History$/, '').underscore + '_id'
294
+
295
+ # Track this custom method
296
+ methods_list = class_variable_get(:@@history_association_methods) rescue []
297
+ methods_list << assoc_name
298
+ class_variable_set(:@@history_association_methods, methods_list)
299
+
300
+ define_method(assoc_name) do
301
+ return nil unless self[assoc_foreign_key]
302
+ assoc_class.where(
303
+ history_fk => self[assoc_foreign_key],
304
+ snapshot_id: self.snapshot_id
305
+ ).first
306
+ end
307
+ else
308
+ belongs_to assoc_name, class_name: assoc_class_name, foreign_key: assoc_foreign_key
309
+ end
299
310
  when :has_one
300
- has_one assoc_name, scope, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
311
+ if assoc_class_name.match?(/History/)
312
+ hfk = history_foreign_key
313
+
314
+ # Track this custom method
315
+ methods_list = class_variable_get(:@@history_association_methods) rescue []
316
+ methods_list << assoc_name
317
+ class_variable_set(:@@history_association_methods, methods_list)
318
+
319
+ define_method(assoc_name) do
320
+ assoc_class.where(
321
+ assoc_foreign_key => self[hfk],
322
+ snapshot_id: self.snapshot_id
323
+ ).first
324
+ end
325
+ else
326
+ has_one assoc_name, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
327
+ end
301
328
  when :has_many
302
- has_many assoc_name, scope, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
329
+ if assoc_class_name.match?(/History/)
330
+ hfk = history_foreign_key
331
+ # Track this custom method
332
+ methods_list = class_variable_get(:@@history_association_methods) rescue []
333
+ methods_list << assoc_name
334
+ class_variable_set(:@@history_association_methods, methods_list)
335
+
336
+ define_method(assoc_name) do
337
+ assoc_class.where(
338
+ assoc_foreign_key => self[hfk],
339
+ snapshot_id: self.snapshot_id
340
+ )
341
+ end
342
+ else
343
+ has_many assoc_name, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
344
+ end
303
345
  end
304
346
  end
305
347
 
@@ -312,13 +354,9 @@ module Historiographer
312
354
  return @history_foreign_key if @history_foreign_key
313
355
 
314
356
  # CAN THIS BE TABLE OR MODEL?
315
- @history_foreign_key = sti_base_class.name.singularize.foreign_key
357
+ @history_foreign_key = original_class.base_class.name.singularize.foreign_key
316
358
  end
317
359
 
318
- def sti_base_class
319
- return @sti_base_class if @sti_base_class
320
- @sti_base_class = original_class.base_class
321
- end
322
360
  end
323
361
 
324
362
  def original_class
@@ -337,10 +375,6 @@ module Historiographer
337
375
  attrs = attributes.clone
338
376
  # attrs[original_class.primary_key] = attrs[self.class.history_foreign_key]
339
377
 
340
- if original_class.sti_enabled?
341
- # Remove History suffix from type if present
342
- attrs[original_class.inheritance_column] = attrs[original_class.inheritance_column]&.gsub(/History$/, '')
343
- end
344
378
 
345
379
  # Manually handle creating instance WITHOUT running find or initialize callbacks
346
380
  # We will manually run callbacks below
@@ -373,13 +407,19 @@ module Historiographer
373
407
  end
374
408
  end
375
409
 
376
- # For each association in the history class
377
- self.class.reflect_on_all_associations.each do |reflection|
410
+ # For each association in the history class (including custom methods)
411
+ associations_to_forward = self.class.reflect_on_all_associations.map(&:name)
412
+
413
+ # Add custom association methods
414
+ custom_methods = self.class.class_variable_get(:@@history_association_methods) rescue []
415
+ associations_to_forward += custom_methods
416
+
417
+ associations_to_forward.uniq.each do |assoc_name|
378
418
  # Define a method that forwards to the history association
379
419
  instance.singleton_class.class_eval do
380
- define_method(reflection.name) do |*args, &block|
381
- history_instance = instance.instance_variable_get(:@_history_instance)
382
- history_instance.send(reflection.name, *args, &block)
420
+ define_method(assoc_name) do |*args, &block|
421
+ history_instance = instance_variable_get(:@_history_instance)
422
+ history_instance.send(assoc_name, *args, &block)
383
423
  end
384
424
  end
385
425
  end
@@ -78,6 +78,7 @@ module Historiographer
78
78
  extend ActiveSupport::Concern
79
79
 
80
80
  class HistoryUserIdMissingError < StandardError; end
81
+ class HistoryInsertionError < StandardError; end
81
82
 
82
83
  UTC = Time.now.in_time_zone('UTC').time_zone
83
84
 
@@ -190,9 +191,6 @@ module Historiographer
190
191
 
191
192
  history_class_initializer = Class.new(ActiveRecord::Base) do
192
193
  self.table_name = "#{base_table}_histories"
193
-
194
- # Handle STI properly
195
- self.inheritance_column = base.inheritance_column if base.sti_enabled?
196
194
  end
197
195
 
198
196
  # Split the class name into module parts and the actual class name
@@ -295,10 +293,11 @@ module Historiographer
295
293
  existing_snapshot = history_class.where(foreign_key => attrs[primary_key], snapshot_id: snapshot_id)
296
294
  return if existing_snapshot.present?
297
295
 
298
- null_snapshot = history_class.where(foreign_key => attrs[primary_key], snapshot_id: nil)
296
+ null_snapshot = history_class.where(foreign_key => attrs[primary_key], snapshot_id: nil).first
299
297
  snapshot = nil
300
298
  if null_snapshot.present?
301
- snapshot = null_snapshot.update(snapshot_id: snapshot_id)
299
+ null_snapshot.update(snapshot_id: snapshot_id)
300
+ snapshot = null_snapshot
302
301
  else
303
302
  snapshot = record_history(snapshot_id: snapshot_id)
304
303
  end
@@ -344,12 +343,6 @@ module Historiographer
344
343
  attrs.merge!(foreign_key => attrs['id'], history_started_at: now, history_user_id: history_user_id)
345
344
  attrs.merge!(snapshot_id: snapshot_id) if snapshot_id.present?
346
345
 
347
- # For STI, ensure we use the correct history class type
348
- if self.class.sti_enabled?
349
- type_column = self.class.inheritance_column
350
- attrs[type_column] = "#{self.class.name}History"
351
- end
352
-
353
346
  attrs = attrs.except('id')
354
347
  attrs.stringify_keys!
355
348
 
@@ -385,6 +378,29 @@ module Historiographer
385
378
 
386
379
  if history_class.history_foreign_key.present? && history_class.present?
387
380
  result = history_class.insert_all([attrs])
381
+
382
+ # Check if the insertion was successful
383
+ if result.rows.empty?
384
+ # insert_all returned empty rows, likely due to a duplicate/conflict
385
+ # Try to find the existing record that prevented insertion
386
+ foreign_key = history_class.history_foreign_key
387
+ existing_history = history_class.where(
388
+ foreign_key => attrs[foreign_key],
389
+ history_started_at: attrs['history_started_at']
390
+ ).first
391
+
392
+ if existing_history
393
+ # A duplicate history already exists (race condition or retry)
394
+ # This is acceptable - return the existing history
395
+ Rails.logger.warn("Duplicate history detected for #{self.class.name} ##{id} at #{attrs['history_started_at']}. Using existing history record ##{existing_history.id}.") if Rails.logger
396
+ current_history.update_columns(history_ended_at: now) if current_history.present?
397
+ return existing_history
398
+ else
399
+ # No rows inserted and can't find an existing record - this is unexpected
400
+ raise HistoryInsertionError, "Failed to insert history record for #{self.class.name} ##{id}, and no existing history was found. This may indicate a database constraint preventing insertion."
401
+ end
402
+ end
403
+
388
404
  inserted_id = result.rows.first.first if history_class.primary_key == 'id'
389
405
  instance = history_class.find(inserted_id)
390
406
  current_history.update_columns(history_ended_at: now) if current_history.present?
@@ -434,9 +450,6 @@ module Historiographer
434
450
  @historiographer_mode || Historiographer::Configuration.mode
435
451
  end
436
452
 
437
- def sti_enabled?
438
- columns.map(&:name).include?(inheritance_column)
439
- end
440
453
  end
441
454
 
442
455
  def is_history_class?
@@ -0,0 +1,14 @@
1
+ require 'historiographer/postgres_migration'
2
+
3
+ class CreateProjects < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :projects do |t|
6
+ t.string :name, null: false
7
+ t.timestamps
8
+ end
9
+
10
+ create_table :project_histories do |t|
11
+ t.histories
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+
2
+ require 'historiographer/postgres_migration'
3
+
4
+ class CreateProjectFiles < ActiveRecord::Migration[7.1]
5
+ def change
6
+ create_table :project_files do |t|
7
+ t.bigint :project_id
8
+ t.string :name, null: false
9
+ t.string :content
10
+ t.timestamps
11
+ t.index :project_id
12
+ end
13
+
14
+ create_table :project_file_histories do |t|
15
+ t.histories
16
+ end
17
+ end
18
+ end
data/spec/db/schema.rb CHANGED
@@ -195,6 +195,50 @@ ActiveRecord::Schema[7.1].define(version: 2024_11_19_000000) do
195
195
  t.index ["type"], name: "index_posts_on_type"
196
196
  end
197
197
 
198
+ create_table "project_file_histories", force: :cascade do |t|
199
+ t.integer "project_file_id", null: false
200
+ t.string "name", null: false
201
+ t.datetime "created_at", null: false
202
+ t.datetime "updated_at", null: false
203
+ t.datetime "history_started_at", null: false
204
+ t.datetime "history_ended_at"
205
+ t.integer "history_user_id"
206
+ t.string "snapshot_id"
207
+ t.index ["history_ended_at"], name: "index_project_file_histories_on_history_ended_at"
208
+ t.index ["history_started_at"], name: "index_project_file_histories_on_history_started_at"
209
+ t.index ["history_user_id"], name: "index_project_file_histories_on_history_user_id"
210
+ t.index ["project_file_id"], name: "index_project_file_histories_on_project_file_id"
211
+ t.index ["snapshot_id"], name: "index_project_file_histories_on_snapshot_id"
212
+ end
213
+
214
+ create_table "project_files", force: :cascade do |t|
215
+ t.string "name", null: false
216
+ t.datetime "created_at", null: false
217
+ t.datetime "updated_at", null: false
218
+ end
219
+
220
+ create_table "project_histories", force: :cascade do |t|
221
+ t.integer "project_id", null: false
222
+ t.string "name", null: false
223
+ t.datetime "created_at", null: false
224
+ t.datetime "updated_at", null: false
225
+ t.datetime "history_started_at", null: false
226
+ t.datetime "history_ended_at"
227
+ t.integer "history_user_id"
228
+ t.string "snapshot_id"
229
+ t.index ["history_ended_at"], name: "index_project_histories_on_history_ended_at"
230
+ t.index ["history_started_at"], name: "index_project_histories_on_history_started_at"
231
+ t.index ["history_user_id"], name: "index_project_histories_on_history_user_id"
232
+ t.index ["project_id"], name: "index_project_histories_on_project_id"
233
+ t.index ["snapshot_id"], name: "index_project_histories_on_snapshot_id"
234
+ end
235
+
236
+ create_table "projects", force: :cascade do |t|
237
+ t.string "name", null: false
238
+ t.datetime "created_at", null: false
239
+ t.datetime "updated_at", null: false
240
+ end
241
+
198
242
  create_table "safe_post_histories", force: :cascade do |t|
199
243
  t.integer "safe_post_id", null: false
200
244
  t.string "title", null: false
@@ -0,0 +1,189 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'Foreign key handling for belongs_to associations' do
4
+ before(:all) do
5
+ @original_stdout = $stdout
6
+ $stdout = StringIO.new
7
+
8
+ ActiveRecord::Base.connection.create_table :test_users, force: true do |t|
9
+ t.string :name
10
+ t.timestamps
11
+ end
12
+
13
+ ActiveRecord::Base.connection.create_table :test_websites, force: true do |t|
14
+ t.string :name
15
+ t.integer :user_id
16
+ t.timestamps
17
+ end
18
+
19
+ ActiveRecord::Base.connection.create_table :test_website_histories, force: true do |t|
20
+ t.integer :test_website_id, null: false
21
+ t.string :name
22
+ t.integer :user_id
23
+ t.timestamps
24
+ t.datetime :history_started_at, null: false
25
+ t.datetime :history_ended_at
26
+ t.integer :history_user_id
27
+ t.string :snapshot_id
28
+
29
+ t.index :test_website_id
30
+ t.index :history_started_at
31
+ t.index :history_ended_at
32
+ t.index :snapshot_id
33
+ end
34
+
35
+ ActiveRecord::Base.connection.create_table :test_user_histories, force: true do |t|
36
+ t.integer :test_user_id, null: false
37
+ t.string :name
38
+ t.timestamps
39
+ t.datetime :history_started_at, null: false
40
+ t.datetime :history_ended_at
41
+ t.integer :history_user_id
42
+ t.string :snapshot_id
43
+
44
+ t.index :test_user_id
45
+ t.index :history_started_at
46
+ t.index :history_ended_at
47
+ t.index :snapshot_id
48
+ end
49
+
50
+ class TestUser < ActiveRecord::Base
51
+ include Historiographer
52
+ has_many :test_websites, foreign_key: 'user_id'
53
+ end
54
+
55
+ class TestWebsite < ActiveRecord::Base
56
+ include Historiographer
57
+ belongs_to :user, class_name: 'TestUser', foreign_key: 'user_id', optional: true
58
+ end
59
+
60
+ class TestWebsiteHistory < ActiveRecord::Base
61
+ include Historiographer::History
62
+ end
63
+
64
+ class TestUserHistory < ActiveRecord::Base
65
+ include Historiographer::History
66
+ end
67
+ end
68
+
69
+ after(:all) do
70
+ $stdout = @original_stdout
71
+ ActiveRecord::Base.connection.drop_table :test_website_histories
72
+ ActiveRecord::Base.connection.drop_table :test_websites
73
+ ActiveRecord::Base.connection.drop_table :test_user_histories
74
+ ActiveRecord::Base.connection.drop_table :test_users
75
+ Object.send(:remove_const, :TestWebsite) if Object.const_defined?(:TestWebsite)
76
+ Object.send(:remove_const, :TestWebsiteHistory) if Object.const_defined?(:TestWebsiteHistory)
77
+ Object.send(:remove_const, :TestUser) if Object.const_defined?(:TestUser)
78
+ Object.send(:remove_const, :TestUserHistory) if Object.const_defined?(:TestUserHistory)
79
+ end
80
+
81
+ describe 'belongs_to association on history models' do
82
+ it 'does not raise error about wrong column when accessing belongs_to associations' do
83
+ # This is the core issue: when a history model has a belongs_to association,
84
+ # it should not use the foreign key as the primary key for lookups
85
+
86
+ # Create a user
87
+ user = TestUser.create!(name: 'Test User', history_user_id: 1)
88
+
89
+ # Create a website belonging to the user
90
+ website = TestWebsite.create!(
91
+ name: 'Test Website',
92
+ user_id: user.id,
93
+ history_user_id: 1
94
+ )
95
+
96
+ # Get the website history
97
+ website_history = TestWebsiteHistory.last
98
+
99
+ # The history should have the correct user_id
100
+ expect(website_history.user_id).to eq(user.id)
101
+
102
+ # The belongs_to association should work without errors
103
+ # Previously this would fail with "column users.user_id does not exist"
104
+ # because it was using primary_key: :user_id instead of the default :id
105
+ expect { website_history.user }.not_to raise_error
106
+ end
107
+
108
+ it 'allows direct creation of history records with foreign keys' do
109
+ user = TestUser.create!(name: 'Another User', history_user_id: 1)
110
+
111
+ # Create history attributes like in the original error case
112
+ attrs = {
113
+ "name" => "test.example",
114
+ "user_id" => user.id,
115
+ "created_at" => Time.now,
116
+ "updated_at" => Time.now,
117
+ "test_website_id" => 100,
118
+ "history_started_at" => Time.now,
119
+ "history_user_id" => 1,
120
+ "snapshot_id" => SecureRandom.uuid
121
+ }
122
+
123
+ # This should not raise an error about test_users.user_id not existing
124
+ # The original bug was that it would look for test_users.user_id instead of test_users.id
125
+ expect { TestWebsiteHistory.create!(attrs) }.not_to raise_error
126
+
127
+ history = TestWebsiteHistory.last
128
+ expect(history.user_id).to eq(user.id)
129
+ end
130
+ end
131
+
132
+ describe 'snapshot associations with history models' do
133
+ it 'correctly filters associations by snapshot_id when using custom association methods' do
134
+ # First create regular history records
135
+ user = TestUser.create!(name: 'User One', history_user_id: 1)
136
+ website = TestWebsite.create!(
137
+ name: 'Website One',
138
+ user_id: user.id,
139
+ history_user_id: 1
140
+ )
141
+
142
+ # Check that regular histories were created
143
+ expect(TestUserHistory.count).to eq(1)
144
+ expect(TestWebsiteHistory.count).to eq(1)
145
+
146
+ # Now create snapshot histories directly (simulating what snapshot would do)
147
+ snapshot_id = SecureRandom.uuid
148
+
149
+ # Create user history with snapshot
150
+ user_snapshot = TestUserHistory.create!(
151
+ test_user_id: user.id,
152
+ name: user.name,
153
+ created_at: user.created_at,
154
+ updated_at: user.updated_at,
155
+ history_started_at: Time.now,
156
+ history_user_id: 1,
157
+ snapshot_id: snapshot_id
158
+ )
159
+
160
+ # Create website history with snapshot
161
+ website_snapshot = TestWebsiteHistory.create!(
162
+ test_website_id: website.id,
163
+ name: website.name,
164
+ user_id: user.id,
165
+ created_at: website.created_at,
166
+ updated_at: website.updated_at,
167
+ history_started_at: Time.now,
168
+ history_user_id: 1,
169
+ snapshot_id: snapshot_id
170
+ )
171
+
172
+ # Now test that the association filtering works
173
+ # The website history's user association should find the user history with the same snapshot_id
174
+ user_from_association = website_snapshot.user
175
+
176
+ # Since user association points to history when snapshots are involved,
177
+ # it should return the TestUserHistory with matching snapshot_id
178
+ if user_from_association.is_a?(TestUserHistory)
179
+ expect(user_from_association.snapshot_id).to eq(snapshot_id)
180
+ expect(user_from_association.name).to eq('User One')
181
+ else
182
+ # If it returns the regular TestUser (non-history), that's also acceptable
183
+ # as long as it doesn't error
184
+ expect(user_from_association).to be_a(TestUser)
185
+ expect(user_from_association.name).to eq('User One')
186
+ end
187
+ end
188
+ end
189
+ end
@@ -397,20 +397,6 @@ describe Historiographer do
397
397
  end
398
398
  end
399
399
 
400
- describe 'Method stubbing' do
401
- it 'handles adding method appropriately' do
402
- post = PrivatePost.create(title: 'Post 1', body: "Hello", author_id: 1, history_user_id: 1)
403
- expect(post.formatted_title).to eq("Private — You cannot see!")
404
-
405
- allow_any_instance_of(PrivatePost).to receive(:formatted_title).and_return("New Title")
406
- expect(post.formatted_title).to eq("New Title")
407
-
408
- # Ensure history still works
409
- post.update(title: 'Updated Title', history_user_id: user.id)
410
- expect(post.histories.count).to eq(2)
411
- expect(post.histories.first.class).to eq(PrivatePostHistory) # Verify correct history class
412
- end
413
- end
414
400
 
415
401
  describe 'Scopes' do
416
402
  it 'finds current histories' do
@@ -517,6 +503,118 @@ describe Historiographer do
517
503
  end
518
504
  end
519
505
 
506
+ describe 'Empty insertion handling' do
507
+ it 'handles duplicate history gracefully by returning existing record' do
508
+ # Create post without history tracking to avoid initial history
509
+ post = Post.new(
510
+ title: 'Post 1',
511
+ body: 'Great post',
512
+ author_id: 1,
513
+ history_user_id: user.id
514
+ )
515
+ post.save_without_history
516
+
517
+ # Freeze time to ensure same timestamp
518
+ Timecop.freeze do
519
+ # Create a history record with current timestamp
520
+ now = Historiographer::UTC.now
521
+ attrs = post.send(:history_attrs, now: now)
522
+ existing_history = PostHistory.create!(attrs)
523
+
524
+ # Mock insert_all to return empty result (simulating duplicate constraint)
525
+ empty_result = double('result')
526
+ allow(empty_result).to receive(:rows).and_return([])
527
+
528
+ allow(PostHistory).to receive(:insert_all).and_return(empty_result)
529
+
530
+ # The method should find and return the existing history
531
+ allow(Rails.logger).to receive(:warn).with(/Duplicate history detected/) if Rails.logger
532
+ result = post.send(:record_history)
533
+ expect(result.id).to eq(existing_history.id)
534
+ expect(result.post_id).to eq(post.id)
535
+ end
536
+ end
537
+
538
+ it 'raises error when insert fails and no existing record found' do
539
+ post = create_post
540
+
541
+ # Mock insert_all to return an empty result
542
+ empty_result = double('result')
543
+ allow(empty_result).to receive(:rows).and_return([])
544
+
545
+ allow(PostHistory).to receive(:insert_all).and_return(empty_result)
546
+
547
+ # Mock the where clause for finding existing history to return nothing
548
+ # We need to be specific about the where clause we're mocking
549
+ original_where = PostHistory.method(:where)
550
+ allow(PostHistory).to receive(:where) do |*args|
551
+ # Check if this is the specific query for finding duplicates
552
+ # The foreign key is "post_id" (string) and we're checking for history_started_at
553
+ if args.first.is_a?(Hash) && args.first.keys.include?("post_id") && args.first.keys.include?(:history_started_at)
554
+ # Return a double that returns nil when .first is called
555
+ double('where').tap { |d| allow(d).to receive(:first).and_return(nil) }
556
+ else
557
+ # For all other queries, use the original behavior
558
+ original_where.call(*args)
559
+ end
560
+ end
561
+
562
+ # This should raise a meaningful error
563
+ expect {
564
+ post.send(:record_history)
565
+ }.to raise_error(Historiographer::HistoryInsertionError, /Failed to insert history record.*no existing history was found/)
566
+ end
567
+
568
+ it 'provides meaningful error when insertion fails' do
569
+ post = create_post
570
+
571
+ # Mock insert_all to simulate a database-level failure
572
+ # This could happen due to various reasons:
573
+ # - Database is read-only
574
+ # - Connection issues
575
+ # - Constraint violations that prevent insertion
576
+ allow(PostHistory).to receive(:insert_all).and_raise(ActiveRecord::StatementInvalid, "PG::ReadOnlySqlTransaction: ERROR: cannot execute INSERT in a read-only transaction")
577
+
578
+ expect {
579
+ post.send(:record_history)
580
+ }.to raise_error(ActiveRecord::StatementInvalid)
581
+ end
582
+
583
+ it 'successfully inserts history when everything is valid' do
584
+ post = create_post
585
+
586
+ # Clear existing histories
587
+ PostHistory.where(post_id: post.id).destroy_all
588
+
589
+ # Record a new history
590
+ history = post.send(:record_history)
591
+
592
+ expect(history).to be_a(PostHistory)
593
+ expect(history).to be_persisted
594
+ expect(history.post_id).to eq(post.id)
595
+ expect(history.title).to eq(post.title)
596
+ expect(history.body).to eq(post.body)
597
+ end
598
+
599
+ it 'handles race conditions by returning existing history' do
600
+ post = create_post
601
+
602
+ # Simulate a race condition where the same history_started_at timestamp is used
603
+ now = Time.now
604
+ allow(Historiographer::UTC).to receive(:now).and_return(now)
605
+
606
+ # First process creates history
607
+ history1 = post.histories.last
608
+
609
+ # Second process tries to create history with same timestamp
610
+ # This would normally cause insert_all to return empty rows
611
+ history2 = post.send(:record_history)
612
+
613
+ # Should handle gracefully
614
+ expect(history2).to be_a(PostHistory)
615
+ end
616
+ end
617
+
520
618
  describe 'Scopes' do
521
619
  it 'finds current' do
522
620
  post = create_post
@@ -724,102 +822,24 @@ describe Historiographer do
724
822
  expect(post.comment_count).to eq 2
725
823
  expect(post.latest_snapshot.comment_count).to eq 1
726
824
  end
727
- end
728
825
 
729
- describe 'Single Table Inheritance' do
730
- let(:user) { User.create(name: 'Test User') }
731
- let(:private_post) do
732
- PrivatePost.create(
733
- title: 'Private Post',
734
- body: 'Test',
735
- history_user_id: user.id,
736
- author_id: 1
737
- )
738
- end
826
+ it "doesn't explode" do
827
+ project = Project.create(name: "test_project")
828
+ project_file = ProjectFile.create(project: project, name: "test_file", content: "Hello world")
739
829
 
740
- it 'maintains original class type on create' do
741
- post_history = private_post.histories.first
742
- expect(post_history.original_class).to eq(PrivatePost)
743
- end
830
+ original_snapshot = project.snapshot
744
831
 
745
- it 'maintains original class in history records' do
746
- post_history = private_post.histories.first
747
- expect(post_history.original_class).to eq(PrivatePost)
748
- expect(post_history.title).to eq('Private — You cannot see!')
749
- end
832
+ project_file.update(content: "Goodnight moon")
833
+ new_snapshot = project.snapshot
750
834
 
751
- it 'maintains original class behavior when updating' do
752
- private_post.update(title: 'Updated Private Post', history_user_id: user.id)
753
- new_history = private_post.histories.current&.first
754
- expect(new_history.original_class).to eq(PrivatePost)
755
- expect(new_history.title).to eq('Private — You cannot see!')
756
- end
835
+ expect(original_snapshot.files.map(&:class)).to eq [ProjectFileHistory]
836
+ expect(new_snapshot.files.map(&:class)).to eq [ProjectFileHistory]
757
837
 
758
- it 'maintains original class behavior when reifying' do
759
- private_post.update(title: 'Updated Private Post', history_user_id: user.id)
760
- old_history = private_post.histories.first
761
- reified = old_history
762
- expect(reified.title).to eq('Private — You cannot see!')
763
- expect(reified.original_class).to eq(PrivatePost)
838
+ expect(new_snapshot.files.first.content).to eq "Goodnight moon"
839
+ expect(original_snapshot.files.first.content).to eq "Hello world"
764
840
  end
765
841
  end
766
842
 
767
- describe 'Single Table Inheritance with Associations' do
768
- let(:user) { User.create(name: 'Test User') }
769
-
770
- it 'inherits associations in history classes' do
771
- dataset = Dataset.create(name: "test_dataset", history_user_id: user.id)
772
- model = XGBoost.create(name: "test_model", dataset: dataset, history_user_id: user.id)
773
- model.snapshot
774
-
775
- dataset.update(name: "new_dataset", history_user_id: user.id)
776
-
777
- expect(dataset.ml_model).to eq model # This is still a live model
778
- expect(model.dataset).to eq(dataset)
779
- expect(model.histories.first).to respond_to(:dataset)
780
- expect(model.histories.first.dataset).to be_a(DatasetHistory)
781
-
782
- model_history = model.latest_snapshot
783
- expect(model_history.dataset.name).to eq "test_dataset"
784
- end
785
- end
786
-
787
- describe 'Single Table Inheritance with custom inheritance column' do
788
- let(:user) { User.create(name: 'Test User') }
789
- let(:xgboost) do
790
- XGBoost.create(
791
- name: 'My XGBoost Model',
792
- parameters: { max_depth: 3, eta: 0.1 },
793
- history_user_id: user.id
794
- )
795
- end
796
-
797
- it 'creates history records with correct inheritance' do
798
- model = xgboost
799
- expect(model.model_name).to eq('XGBoost')
800
- expect(model.current_history).to be_a(XGBoostHistory)
801
- expect(model.current_history.model_name).to eq('XGBoostHistory')
802
- end
803
-
804
- it 'maintains inheritance through updates' do
805
- model = xgboost
806
- model.update(name: 'Updated XGBoost Model', history_user_id: user.id)
807
-
808
- expect(model.histories.count).to eq(2)
809
- expect(model.histories.all? { |h| h.is_a?(XGBoostHistory) }).to be true
810
- end
811
-
812
- it 'reifies with correct class' do
813
- model = xgboost
814
- original_name = model.name
815
- model.update(name: 'Updated XGBoost Model', history_user_id: user.id)
816
- model.snapshot
817
-
818
- reified = model.latest_snapshot
819
- expect(reified).to be_a(XGBoostHistory)
820
- expect(reified.name).to eq("Updated XGBoost Model")
821
- end
822
- end
823
843
 
824
844
  describe 'Class-level mode setting' do
825
845
  before(:each) do
@@ -882,24 +902,6 @@ describe Historiographer do
882
902
  expect(col_history).to be_a(EasyML::ColumnHistory)
883
903
  end
884
904
 
885
- it 'establishes correct associations for child classes' do
886
- encrypted_col = EasyML::Column.create(
887
- name: 'secret_feature',
888
- data_type: 'numeric',
889
- history_user_id: user.id,
890
- column_type: "EasyML::EncryptedColumn"
891
- )
892
-
893
- # Verify the base record
894
- expect(encrypted_col).to be_a(EasyML::EncryptedColumn)
895
- expect(encrypted_col.encrypted?).to be true
896
-
897
- # Verify history record
898
- col_history = encrypted_col.histories.last
899
- expect(col_history).to be_a(EasyML::EncryptedColumnHistory)
900
- expect(col_history.class.history_foreign_key).to eq('column_id')
901
- expect(col_history.encrypted?).to be true
902
- end
903
905
 
904
906
  it 'uses correct table names' do
905
907
  expect(EasyML::Column.table_name).to eq('easy_ml_columns')
@@ -1,7 +1,6 @@
1
1
  module EasyML
2
2
  class Column < ActiveRecord::Base
3
3
  self.table_name = "easy_ml_columns"
4
- self.inheritance_column = "column_type"
5
4
  include Historiographer
6
5
  end
7
6
  end
@@ -0,0 +1,4 @@
1
+ class Project < ApplicationRecord
2
+ include Historiographer::Safe
3
+ has_many :files, class_name: "ProjectFile"
4
+ end
@@ -0,0 +1,5 @@
1
+ class ProjectFile < ApplicationRecord
2
+ include Historiographer::Safe
3
+
4
+ belongs_to :project
5
+ end
@@ -0,0 +1,4 @@
1
+ class ProjectFileHistory < ActiveRecord::Base
2
+ self.table_name = "project_file_histories"
3
+ include Historiographer::History
4
+ end
@@ -0,0 +1,4 @@
1
+ class ProjectHistory < ActiveRecord::Base
2
+ self.table_name = "project_histories"
3
+ include Historiographer::History
4
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: historiographer
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.16
4
+ version: 4.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - brettshollenberger
@@ -228,6 +228,7 @@ files:
228
228
  - historiographer-4.1.12.gem
229
229
  - historiographer-4.1.13.gem
230
230
  - historiographer-4.1.14.gem
231
+ - historiographer-4.3.0.gem
231
232
  - historiographer.gemspec
232
233
  - init.rb
233
234
  - instructions/implementation.md
@@ -258,31 +259,26 @@ files:
258
259
  - spec/db/migrate/20221018204255_create_silent_post_histories.rb
259
260
  - spec/db/migrate/20241109182017_create_comments.rb
260
261
  - spec/db/migrate/20241109182020_create_comment_histories.rb
261
- - spec/db/migrate/20241118000000_add_type_to_posts.rb
262
- - spec/db/migrate/20241118000001_add_type_to_post_histories.rb
263
- - spec/db/migrate/20241118000002_create_ml_models.rb
264
- - spec/db/migrate/20241118000003_create_easy_ml_columns.rb
265
262
  - spec/db/migrate/20241119000000_create_datasets.rb
263
+ - spec/db/migrate/2025082100000_create_projects.rb
264
+ - spec/db/migrate/2025082100001_create_project_files.rb
266
265
  - spec/db/schema.rb
267
266
  - spec/factories/post.rb
267
+ - spec/foreign_key_spec.rb
268
268
  - spec/historiographer_spec.rb
269
269
  - spec/models/application_record.rb
270
270
  - spec/models/author.rb
271
271
  - spec/models/author_history.rb
272
272
  - spec/models/comment.rb
273
273
  - spec/models/comment_history.rb
274
- - spec/models/dataset.rb
275
- - spec/models/dataset_history.rb
276
274
  - spec/models/easy_ml/column.rb
277
275
  - spec/models/easy_ml/column_history.rb
278
- - spec/models/easy_ml/encrypted_column.rb
279
- - spec/models/easy_ml/encrypted_column_history.rb
280
- - spec/models/ml_model.rb
281
- - spec/models/ml_model_history.rb
282
276
  - spec/models/post.rb
283
277
  - spec/models/post_history.rb
284
- - spec/models/private_post.rb
285
- - spec/models/private_post_history.rb
278
+ - spec/models/project.rb
279
+ - spec/models/project_file.rb
280
+ - spec/models/project_file_history.rb
281
+ - spec/models/project_history.rb
286
282
  - spec/models/safe_post.rb
287
283
  - spec/models/safe_post_history.rb
288
284
  - spec/models/silent_post.rb
@@ -291,8 +287,6 @@ files:
291
287
  - spec/models/thing_with_compound_index_history.rb
292
288
  - spec/models/thing_without_history.rb
293
289
  - spec/models/user.rb
294
- - spec/models/xgboost.rb
295
- - spec/models/xgboost_history.rb
296
290
  - spec/spec_helper.rb
297
291
  homepage: http://github.com/brettshollenberger/historiographer
298
292
  licenses:
@@ -1,6 +0,0 @@
1
- class AddTypeToPosts < ActiveRecord::Migration[7.1]
2
- def change
3
- add_column :posts, :type, :string
4
- add_index :posts, :type
5
- end
6
- end
@@ -1,5 +0,0 @@
1
- class AddTypeToPostHistories < ActiveRecord::Migration[7.0]
2
- def change
3
- add_column :post_histories, :type, :string
4
- end
5
- end
@@ -1,19 +0,0 @@
1
- require "historiographer/postgres_migration"
2
- require "historiographer/mysql_migration"
3
-
4
- class CreateMlModels < ActiveRecord::Migration[7.0]
5
- def change
6
- create_table :ml_models do |t|
7
- t.string :name
8
- t.string :model_type
9
- t.jsonb :parameters
10
- t.timestamps
11
-
12
- t.index :model_type
13
- end
14
-
15
- create_table :ml_model_histories do |t|
16
- t.histories
17
- end
18
- end
19
- end
@@ -1,17 +0,0 @@
1
- require "historiographer/postgres_migration"
2
- require "historiographer/mysql_migration"
3
-
4
- class CreateEasyMlColumns < ActiveRecord::Migration[7.1]
5
- def change
6
- create_table :easy_ml_columns do |t|
7
- t.string :name, null: false
8
- t.string :data_type, null: false
9
- t.string :column_type
10
- t.timestamps
11
- end
12
-
13
- create_table :easy_ml_column_histories do |t|
14
- t.histories(foreign_key: :column_id)
15
- end
16
- end
17
- end
@@ -1,6 +0,0 @@
1
- class Dataset < ActiveRecord::Base
2
- include Historiographer
3
- self.table_name = "datasets"
4
-
5
- belongs_to :ml_model, class_name: "MLModel"
6
- end
@@ -1,4 +0,0 @@
1
- class DatasetHistory < ActiveRecord::Base
2
- include Historiographer::History
3
- self.table_name = "dataset_histories"
4
- end
@@ -1,10 +0,0 @@
1
- module EasyML
2
- class EncryptedColumn < Column
3
- self.inheritance_column = "column_type"
4
- include Historiographer
5
-
6
- def encrypted?
7
- true
8
- end
9
- end
10
- end
@@ -1,6 +0,0 @@
1
- module EasyML
2
- class EncryptedColumnHistory < ActiveRecord::Base
3
- self.table_name = "easy_ml_column_histories"
4
- include Historiographer::History
5
- end
6
- end
@@ -1,6 +0,0 @@
1
- class MLModel < ActiveRecord::Base
2
- self.inheritance_column = "model_type"
3
- include Historiographer
4
-
5
- has_one :dataset
6
- end
@@ -1,4 +0,0 @@
1
- class MLModelHistory < ActiveRecord::Base
2
- include Historiographer::History
3
- self.table_name = "ml_model_histories"
4
- end
@@ -1,12 +0,0 @@
1
- class PrivatePost < Post
2
- self.table_name = "posts"
3
- include Historiographer
4
-
5
- def title
6
- "Private — You cannot see!"
7
- end
8
-
9
- def formatted_title
10
- "Private — You cannot see!"
11
- end
12
- end
@@ -1,4 +0,0 @@
1
- class PrivatePostHistory < ActiveRecord::Base
2
- self.table_name = "post_histories"
3
- include Historiographer::History
4
- end
@@ -1,10 +0,0 @@
1
- class XGBoost < MLModel
2
- self.table_name = "ml_models"
3
- self.inheritance_column = "model_type"
4
- include Historiographer
5
- after_initialize :set_defaults
6
-
7
- def set_defaults
8
- write_attribute(:model_type, "XGBoost")
9
- end
10
- end
@@ -1,4 +0,0 @@
1
- class XGBoostHistory < ActiveRecord::Base
2
- include Historiographer::History
3
- self.table_name = "ml_model_histories"
4
- end