historiographer 4.1.14 → 4.3.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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.document +5 -0
  3. data/.rspec +1 -0
  4. data/.ruby-version +1 -0
  5. data/.standalone_migrations +6 -0
  6. data/Gemfile +33 -0
  7. data/Gemfile.lock +341 -0
  8. data/Guardfile +4 -0
  9. data/README.md +0 -168
  10. data/Rakefile +54 -0
  11. data/VERSION +1 -0
  12. data/historiographer-4.1.12.gem +0 -0
  13. data/historiographer-4.1.13.gem +0 -0
  14. data/historiographer-4.1.14.gem +0 -0
  15. data/historiographer.gemspec +136 -0
  16. data/init.rb +18 -0
  17. data/instructions/implementation.md +282 -0
  18. data/instructions/todo.md +96 -0
  19. data/lib/historiographer/history.rb +1 -20
  20. data/lib/historiographer/version.rb +1 -1
  21. data/lib/historiographer.rb +27 -14
  22. data/spec/db/database.yml +27 -0
  23. data/spec/db/migrate/20161121212228_create_posts.rb +19 -0
  24. data/spec/db/migrate/20161121212229_create_post_histories.rb +10 -0
  25. data/spec/db/migrate/20161121212230_create_authors.rb +13 -0
  26. data/spec/db/migrate/20161121212231_create_author_histories.rb +10 -0
  27. data/spec/db/migrate/20161121212232_create_users.rb +9 -0
  28. data/spec/db/migrate/20171011194624_create_safe_posts.rb +19 -0
  29. data/spec/db/migrate/20171011194715_create_safe_post_histories.rb +9 -0
  30. data/spec/db/migrate/20191024142304_create_thing_with_compound_index.rb +10 -0
  31. data/spec/db/migrate/20191024142352_create_thing_with_compound_index_history.rb +11 -0
  32. data/spec/db/migrate/20191024203106_create_thing_without_history.rb +7 -0
  33. data/spec/db/migrate/20221018204220_create_silent_posts.rb +21 -0
  34. data/spec/db/migrate/20221018204255_create_silent_post_histories.rb +9 -0
  35. data/spec/db/migrate/20241109182017_create_comments.rb +13 -0
  36. data/spec/db/migrate/20241109182020_create_comment_histories.rb +9 -0
  37. data/spec/db/migrate/20241119000000_create_datasets.rb +17 -0
  38. data/spec/db/migrate/2025082100000_create_projects.rb +14 -0
  39. data/spec/db/migrate/2025082100001_create_project_files.rb +18 -0
  40. data/spec/db/schema.rb +352 -0
  41. data/spec/factories/post.rb +7 -0
  42. data/spec/historiographer_spec.rb +920 -0
  43. data/spec/models/application_record.rb +3 -0
  44. data/spec/models/author.rb +5 -0
  45. data/spec/models/author_history.rb +4 -0
  46. data/spec/models/comment.rb +5 -0
  47. data/spec/models/comment_history.rb +5 -0
  48. data/spec/models/easy_ml/column.rb +6 -0
  49. data/spec/models/easy_ml/column_history.rb +6 -0
  50. data/spec/models/post.rb +45 -0
  51. data/spec/models/post_history.rb +8 -0
  52. data/spec/models/project.rb +4 -0
  53. data/spec/models/project_file.rb +5 -0
  54. data/spec/models/project_file_history.rb +4 -0
  55. data/spec/models/project_history.rb +4 -0
  56. data/spec/models/safe_post.rb +5 -0
  57. data/spec/models/safe_post_history.rb +5 -0
  58. data/spec/models/silent_post.rb +3 -0
  59. data/spec/models/silent_post_history.rb +4 -0
  60. data/spec/models/thing_with_compound_index.rb +3 -0
  61. data/spec/models/thing_with_compound_index_history.rb +4 -0
  62. data/spec/models/thing_without_history.rb +2 -0
  63. data/spec/models/user.rb +2 -0
  64. data/spec/spec_helper.rb +105 -0
  65. metadata +62 -31
@@ -0,0 +1,136 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+ # stub: historiographer 4.3.0 ruby lib
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "historiographer".freeze
9
+ s.version = "4.3.0"
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib".freeze]
13
+ s.authors = ["brettshollenberger".freeze]
14
+ s.date = "2025-08-21"
15
+ s.description = "Creates separate tables for each history table".freeze
16
+ s.email = "brett.shollenberger@gmail.com".freeze
17
+ s.extra_rdoc_files = [
18
+ "LICENSE.txt",
19
+ "README.md"
20
+ ]
21
+ s.files = [
22
+ ".document",
23
+ ".rspec",
24
+ ".ruby-version",
25
+ ".standalone_migrations",
26
+ "Gemfile",
27
+ "Gemfile.lock",
28
+ "Guardfile",
29
+ "LICENSE.txt",
30
+ "README.md",
31
+ "Rakefile",
32
+ "VERSION",
33
+ "historiographer-4.1.12.gem",
34
+ "historiographer-4.1.13.gem",
35
+ "historiographer-4.1.14.gem",
36
+ "historiographer.gemspec",
37
+ "init.rb",
38
+ "instructions/implementation.md",
39
+ "instructions/todo.md",
40
+ "lib/historiographer.rb",
41
+ "lib/historiographer/configuration.rb",
42
+ "lib/historiographer/history.rb",
43
+ "lib/historiographer/history_migration.rb",
44
+ "lib/historiographer/history_migration_mysql.rb",
45
+ "lib/historiographer/mysql_migration.rb",
46
+ "lib/historiographer/postgres_migration.rb",
47
+ "lib/historiographer/relation.rb",
48
+ "lib/historiographer/safe.rb",
49
+ "lib/historiographer/silent.rb",
50
+ "lib/historiographer/version.rb",
51
+ "spec/db/database.yml",
52
+ "spec/db/migrate/20161121212228_create_posts.rb",
53
+ "spec/db/migrate/20161121212229_create_post_histories.rb",
54
+ "spec/db/migrate/20161121212230_create_authors.rb",
55
+ "spec/db/migrate/20161121212231_create_author_histories.rb",
56
+ "spec/db/migrate/20161121212232_create_users.rb",
57
+ "spec/db/migrate/20171011194624_create_safe_posts.rb",
58
+ "spec/db/migrate/20171011194715_create_safe_post_histories.rb",
59
+ "spec/db/migrate/20191024142304_create_thing_with_compound_index.rb",
60
+ "spec/db/migrate/20191024142352_create_thing_with_compound_index_history.rb",
61
+ "spec/db/migrate/20191024203106_create_thing_without_history.rb",
62
+ "spec/db/migrate/20221018204220_create_silent_posts.rb",
63
+ "spec/db/migrate/20221018204255_create_silent_post_histories.rb",
64
+ "spec/db/migrate/20241109182017_create_comments.rb",
65
+ "spec/db/migrate/20241109182020_create_comment_histories.rb",
66
+ "spec/db/migrate/20241119000000_create_datasets.rb",
67
+ "spec/db/migrate/2025082100000_create_projects.rb",
68
+ "spec/db/migrate/2025082100001_create_project_files.rb",
69
+ "spec/db/schema.rb",
70
+ "spec/factories/post.rb",
71
+ "spec/historiographer_spec.rb",
72
+ "spec/models/application_record.rb",
73
+ "spec/models/author.rb",
74
+ "spec/models/author_history.rb",
75
+ "spec/models/comment.rb",
76
+ "spec/models/comment_history.rb",
77
+ "spec/models/easy_ml/column.rb",
78
+ "spec/models/easy_ml/column_history.rb",
79
+ "spec/models/post.rb",
80
+ "spec/models/post_history.rb",
81
+ "spec/models/project.rb",
82
+ "spec/models/project_file.rb",
83
+ "spec/models/project_file_history.rb",
84
+ "spec/models/project_history.rb",
85
+ "spec/models/safe_post.rb",
86
+ "spec/models/safe_post_history.rb",
87
+ "spec/models/silent_post.rb",
88
+ "spec/models/silent_post_history.rb",
89
+ "spec/models/thing_with_compound_index.rb",
90
+ "spec/models/thing_with_compound_index_history.rb",
91
+ "spec/models/thing_without_history.rb",
92
+ "spec/models/user.rb",
93
+ "spec/spec_helper.rb"
94
+ ]
95
+ s.homepage = "http://github.com/brettshollenberger/historiographer".freeze
96
+ s.licenses = ["MIT".freeze]
97
+ s.rubygems_version = "3.2.22".freeze
98
+ s.summary = "Create histories of your ActiveRecord tables".freeze
99
+
100
+ if s.respond_to? :specification_version then
101
+ s.specification_version = 4
102
+ end
103
+
104
+ if s.respond_to? :add_runtime_dependency then
105
+ s.add_runtime_dependency(%q<activerecord>.freeze, [">= 6"])
106
+ s.add_runtime_dependency(%q<activerecord-import>.freeze, [">= 0"])
107
+ s.add_runtime_dependency(%q<activesupport>.freeze, [">= 0"])
108
+ s.add_runtime_dependency(%q<rails>.freeze, [">= 6"])
109
+ s.add_runtime_dependency(%q<rollbar>.freeze, [">= 0"])
110
+ s.add_development_dependency(%q<mysql2>.freeze, ["= 0.5"])
111
+ s.add_development_dependency(%q<paranoia>.freeze, [">= 0"])
112
+ s.add_development_dependency(%q<pg>.freeze, [">= 0"])
113
+ s.add_development_dependency(%q<pry>.freeze, [">= 0"])
114
+ s.add_development_dependency(%q<standalone_migrations>.freeze, [">= 0"])
115
+ s.add_development_dependency(%q<timecop>.freeze, [">= 0"])
116
+ s.add_development_dependency(%q<jeweler>.freeze, [">= 0"])
117
+ s.add_development_dependency(%q<rdoc>.freeze, ["~> 3.12"])
118
+ s.add_development_dependency(%q<simplecov>.freeze, [">= 0"])
119
+ else
120
+ s.add_dependency(%q<activerecord>.freeze, [">= 6"])
121
+ s.add_dependency(%q<activerecord-import>.freeze, [">= 0"])
122
+ s.add_dependency(%q<activesupport>.freeze, [">= 0"])
123
+ s.add_dependency(%q<rails>.freeze, [">= 6"])
124
+ s.add_dependency(%q<rollbar>.freeze, [">= 0"])
125
+ s.add_dependency(%q<mysql2>.freeze, ["= 0.5"])
126
+ s.add_dependency(%q<paranoia>.freeze, [">= 0"])
127
+ s.add_dependency(%q<pg>.freeze, [">= 0"])
128
+ s.add_dependency(%q<pry>.freeze, [">= 0"])
129
+ s.add_dependency(%q<standalone_migrations>.freeze, [">= 0"])
130
+ s.add_dependency(%q<timecop>.freeze, [">= 0"])
131
+ s.add_dependency(%q<jeweler>.freeze, [">= 0"])
132
+ s.add_dependency(%q<rdoc>.freeze, ["~> 3.12"])
133
+ s.add_dependency(%q<simplecov>.freeze, [">= 0"])
134
+ end
135
+ end
136
+
data/init.rb ADDED
@@ -0,0 +1,18 @@
1
+ require "yaml"
2
+
3
+ Bundler.require(:default, :development, :test)
4
+
5
+ Dir.glob(File.expand_path("lib/**/*.rb")).each do |file|
6
+ require file
7
+ end
8
+
9
+ database_config = YAML.load(File.open(File.expand_path("spec/db/database.yml")).read)
10
+
11
+ env = ENV["HISTORIOGRAPHER_ENV"] || "development"
12
+
13
+ db_env_config = database_config[env]
14
+
15
+ if defined?(ActiveRecord::Base)
16
+ # new settings as specified here: https://devcenter.heroku.com/articles/concurrency-and-database-connections
17
+ ActiveRecord::Base.establish_connection(db_env_config)
18
+ end
@@ -0,0 +1,282 @@
1
+ # Single Table Inheritance (STI) Implementation for Historiographer
2
+
3
+ You are assisting with a Rails gem called Historiographer. Your role is to make precise, surgical edits to the codebase based on specific tasks. The project has a complex architecture with interdependent components, so caution is required.
4
+
5
+ ## Overview
6
+
7
+ This document outlines the steps needed to properly implement Single Table Inheritance (STI) for the Historiographer gem, following Rails best practices. The current implementation has a complex method forwarding mechanism that can lead to subtle bugs. We need to refactor this to use a clean, standard Rails STI approach within separate inheritance hierarchies.
8
+
9
+ ## Current State Analysis
10
+
11
+ The current implementation has the following issues:
12
+
13
+ 1. History classes implement a complex method delegation approach using `method_missing` and dynamic method definition
14
+ 2. STI is partially implemented but doesn't correctly handle the inheritance hierarchy within history classes
15
+ 3. The `type` column isn't properly set or managed according to Rails STI conventions
16
+ 4. Method delegation between original and history models is overly complex and error-prone
17
+
18
+ ## Implementation Requirements
19
+
20
+ 1. User can define a `type` for their class, and that will be used to identify the STI class
21
+ 2. The `type` should be a string, and the default value should be the class name
22
+ 3. When a user creates a new instance, the `type` should be set to the class name automatically
23
+ 4. STI subclasses should automatically inherit from their parent class
24
+ 5. History classes should maintain a parallel inheritance hierarchy to original classes
25
+ 6. When finding a history instance, it should automatically instantiate the correct subclass based on `type`
26
+ 7. Implement clean method delegation from history classes to original models
27
+
28
+ ## Implementation Approach: STI Within History Models
29
+
30
+ We will implement STI separately within original models and history models, maintaining two parallel inheritance hierarchies:
31
+
32
+ 1. **Original Models Hierarchy**: `PrivatePost < Post < ActiveRecord::Base`
33
+ 2. **History Models Hierarchy**: `PrivatePostHistory < PostHistory < ActiveRecord::Base`
34
+
35
+ This approach follows Rails conventions within each table while providing a clean way to implement method delegation between related models.
36
+
37
+ ## Implementation Steps
38
+
39
+ ### 1. Update `Historiographer::History` Module
40
+
41
+ #### Changes Required:
42
+
43
+ - Implement a clean method delegation system using `method_missing`
44
+ - Support proper STI inheritance within history models
45
+ - Ensure history classes correctly set and manage their `type` column
46
+ - Maintain table separation between original and history models
47
+
48
+ ```ruby
49
+ module Historiographer
50
+ module History
51
+ extend ActiveSupport::Concern
52
+
53
+ included do |base|
54
+ clear_validators! if respond_to?(:clear_validators!)
55
+
56
+ # Current scope for finding the most recent history
57
+ scope :current, -> { where(history_ended_at: nil).order(id: :desc) }
58
+
59
+ # Determine original class name
60
+ cattr_accessor :original_class_name
61
+ self.original_class_name = base.name.gsub(/History$/, '')
62
+
63
+ # Setup inheritance column for history classes
64
+ if self.original_class_name.constantize.respond_to?(:inheritance_column)
65
+ original_inheritance_column = self.original_class_name.constantize.inheritance_column
66
+ self.inheritance_column = original_inheritance_column
67
+ end
68
+
69
+ # Set up user association
70
+ unless self.original_class_name.constantize.ancestors.include?(Historiographer::Silent)
71
+ belongs_to :user, foreign_key: :history_user_id
72
+ end
73
+
74
+ # Ensure we can't destroy history records
75
+ before_destroy { |record| raise "Cannot destroy history records" }
76
+
77
+ # Handle type column for history classes
78
+ after_initialize do
79
+ # Set type to the history class if not already set
80
+ if self.type.nil? || !self.type.ends_with?('History')
81
+ self.type = self.class.name
82
+ end
83
+ end
84
+
85
+ # Setup class accessors for delegation
86
+ cattr_accessor :delegated_methods
87
+ self.delegated_methods = []
88
+ end
89
+
90
+ # Method delegation system
91
+ def method_missing(method, *args, &block)
92
+ # Try to find the original record to delegate to
93
+ foreign_key = self.class.determine_foreign_key
94
+ original_record = self.class.original_class_name.constantize.find_by(id: send(foreign_key))
95
+
96
+ if original_record && original_record.respond_to?(method)
97
+ # Cache the method for future calls
98
+ self.class.delegate_method(method)
99
+ # Call the method on the original record
100
+ original_record.send(method, *args, &block)
101
+ else
102
+ super
103
+ end
104
+ end
105
+
106
+ def respond_to_missing?(method, include_private = false)
107
+ # Check if the original class responds to this method
108
+ foreign_key = self.class.determine_foreign_key
109
+ original_record = self.class.original_class_name.constantize.find_by(id: send(foreign_key))
110
+ original_record&.respond_to?(method, include_private) || super
111
+ end
112
+
113
+ class_methods do
114
+ # Method to delegate methods from original class
115
+ def delegate_method(method_name)
116
+ return if method_defined?(method_name) || delegated_methods.include?(method_name.to_sym)
117
+
118
+ delegated_methods << method_name.to_sym
119
+
120
+ define_method(method_name) do |*args, &block|
121
+ foreign_key = self.class.determine_foreign_key
122
+ original_record = self.class.original_class_name.constantize.find_by(id: send(foreign_key))
123
+
124
+ if original_record
125
+ original_record.send(method_name, *args, &block)
126
+ else
127
+ raise NoMethodError, "undefined method `#{method_name}' for #{self}"
128
+ end
129
+ end
130
+ end
131
+
132
+ # Determine the foreign key based on the original class
133
+ def determine_foreign_key
134
+ association_name = self.original_class_name.split("::").last.underscore
135
+ "#{association_name}_id"
136
+ end
137
+ end
138
+
139
+ # Prevent destroying history records
140
+ def destroy
141
+ false
142
+ end
143
+
144
+ def destroy!
145
+ false
146
+ end
147
+
148
+ # Other existing scopes and methods...
149
+ end
150
+ end
151
+ ```
152
+
153
+ ### 2. Update Original Model STI Support in `Historiographer` Module
154
+
155
+ #### Changes Required:
156
+
157
+ - Ensure proper type column handling in original models
158
+ - Support proper mapping between original and history class hierarchy
159
+ - Handle custom inheritance columns
160
+
161
+ ```ruby
162
+ module Historiographer
163
+ extend ActiveSupport::Concern
164
+
165
+ included do |base|
166
+ # Existing code...
167
+
168
+ # Set default type for original models
169
+ if base.respond_to?(:inheritance_column) && base.column_names.include?(base.inheritance_column)
170
+ before_validation do
171
+ self[self.class.inheritance_column] ||= self.class.name
172
+ end
173
+ end
174
+
175
+ # Ensure history class creation supports STI
176
+ class_name = "#{base.name}History"
177
+
178
+ begin
179
+ history_class = class_name.constantize
180
+ rescue NameError
181
+ # Get the base table name without _histories suffix
182
+ base_table = base.table_name.singularize.sub(/_histories$/, '')
183
+
184
+ # Find the correct parent history class for STI
185
+ if base.superclass != ActiveRecord::Base && base.superclass.include?(Historiographer)
186
+ parent_history_class_name = "#{base.superclass.name}History"
187
+ begin
188
+ parent_history_class = parent_history_class_name.constantize
189
+ rescue NameError
190
+ parent_history_class = ActiveRecord::Base
191
+ end
192
+ else
193
+ parent_history_class = ActiveRecord::Base
194
+ end
195
+
196
+ # Create history class with proper inheritance
197
+ history_class_initializer = Class.new(parent_history_class) do
198
+ self.table_name = "#{base_table}_histories"
199
+ include Historiographer::History
200
+
201
+ # Set original class name for delegation
202
+ self.original_class_name = base.name
203
+
204
+ # Handle inheritance column
205
+ if base.respond_to?(:inheritance_column)
206
+ self.inheritance_column = base.inheritance_column
207
+ end
208
+ end
209
+
210
+ # Register the new class in the proper namespace
211
+ module_parts = class_name.split('::')
212
+ final_class_name = module_parts.pop
213
+
214
+ # Find or create module nesting
215
+ parent_module = Object
216
+ module_parts.each do |part|
217
+ parent_module = if parent_module.const_defined?(part)
218
+ parent_module.const_get(part)
219
+ else
220
+ parent_module.const_set(part, Module.new)
221
+ end
222
+ end
223
+
224
+ # Define the history class
225
+ history_class = parent_module.const_set(final_class_name, history_class_initializer)
226
+ end
227
+
228
+ # Existing code...
229
+ end
230
+
231
+ # Add helper methods for STI
232
+ module ClassMethods
233
+ def history_class_for_type(type_value)
234
+ if type_value.present?
235
+ "#{type_value}History".constantize
236
+ else
237
+ "#{self.name}History".constantize
238
+ end
239
+ rescue NameError
240
+ history_class
241
+ end
242
+ end
243
+
244
+ # Instance methods related to history and STI
245
+ def create_history(snapshot_id: nil)
246
+ # Use the correct history class based on the current type
247
+ type_column = self.class.inheritance_column
248
+ current_type = self[type_column] || self.class.name
249
+
250
+ begin
251
+ specific_history_class = self.class.history_class_for_type(current_type)
252
+ rescue NameError
253
+ specific_history_class = self.class.history_class
254
+ end
255
+
256
+ # Create history record with the proper type
257
+ history_record = record_history(specific_history_class, snapshot_id: snapshot_id)
258
+ history_record
259
+ end
260
+ end
261
+ ```
262
+
263
+ ## Migration Testing Requirements
264
+
265
+ 1. Create comprehensive test cases for STI behavior in original and history models
266
+ 2. Test inheritance between original models (e.g., `PrivatePost < Post`)
267
+ 3. Test inheritance between history models (e.g., `PrivatePostHistory < PostHistory`)
268
+ 4. Test method delegation from history models to original models
269
+ 5. Test custom inheritance columns
270
+ 6. Test namespaced models
271
+ 7. Verify proper handling of the `type` column in both original and history tables
272
+
273
+ ## Backward Compatibility
274
+
275
+ To ensure backward compatibility:
276
+
277
+ 1. Maintain existing history table structures
278
+ 2. Ensure old history records continue to work with the new implementation
279
+ 3. Support the same public API for accessing history records
280
+ 4. Handle existing applications that may have customized history class behavior
281
+
282
+ By implementing STI separately within original and history models, we maintain Rails conventions while providing clean method delegation between related models.
@@ -0,0 +1,96 @@
1
+ ## Context
2
+
3
+ You are assisting with a Rails gem called Historiographer. Your role is to make precise, surgical edits to the codebase based on specific tasks. The project has a complex architecture with interdependent components, so caution is required.
4
+
5
+ ## Task:
6
+
7
+ We are going to PROPERLY implement Single Table Inheritance (STI) for Historiographer, in line with Rails best practices.
8
+
9
+ There is some existing code in here that implements STI, but it is not done correctly. We need to fix that.
10
+
11
+ ## Requirements:
12
+
13
+ 1. User can define a `type` for their class, and that will be used to identify the STI class
14
+ 2. In keeping with STI conventions, the `type` should be a string, and the default value should be the class name
15
+ 3. When a user creates a new instance of a class, the `type` should be set to the class name, and the class should automatically inherit the STI class
16
+
17
+ In historiographer, we use a `histories` table for each model, for example:
18
+
19
+ datasources => datasource_histories
20
+
21
+ With STI, it is okay for us to have:
22
+
23
+ class Datasource < ActiveRecord::Base
24
+ def refresh # implemented in sub-classes
25
+ end
26
+ end
27
+
28
+ class S3Datasource < Datasource
29
+ def refresh
30
+ s3.refresh
31
+ end
32
+ end
33
+
34
+ class DatasourceHistory < Datasource
35
+ end
36
+
37
+ class S3DatasourceHistory < DatasourceHistory
38
+ end
39
+
40
+ 4. But the STI class should automatically inherit the STI class, allowing it to use methods defined on the STI class, meaning that S3DatasourceHistory should have access to the `refresh` method that uses s3
41
+
42
+ 5. When we find an instance of DatasourceHistory, it should automatically give us the S3DatasourceHistory class if the `type` is "S3Datasource"
43
+
44
+ 6. The History classes currently have a VERY tricky and complicated way of doing STI that allows them to both act as proper history classes, and "proxy all requests" to the original class. This causes complicated and subtle bugs, and we need to fix that. I would assume that we should do this by inheriting from the original class, which will handle regular inheritance... but then we maybe need to include Historiographer::History to gain access to the history functionality.
45
+
46
+ 7. Implement tests which verify that the STI is working correctly, and that History objects can properly call all methods on the original class.
47
+
48
+ ## Rules
49
+
50
+ ### Do No Harm
51
+
52
+ - Do not remove any code that seems to be irrelevant to your task. You do not have full context of the application, so you should err on the side of NOT removing code, unless the code is clearly duplication.
53
+ - Preserve existing formatting, naming conventions, and code style whenever possible.
54
+ - Keep changes minimal and focused on the specific task at hand.
55
+
56
+ ### Before Starting
57
+
58
+ - Look for any files you might need to understand the context better
59
+ - If you have any questions, DO NOT WRITE CODE. Ask the question. I will be happy to answer all your questions to your satisfaction before you start.
60
+ - Measure twice, cut once!
61
+ - Understand the full impact of your changes before implementing them.
62
+
63
+ ### Working Process
64
+
65
+ 1. **Analyze First**: Carefully review the code before suggesting any changes.
66
+ 2. **Ask Questions**: If anything is unclear, ask before proceeding.
67
+ 3. **Plan Your Approach**: Outline your intended changes before executing them.
68
+ 4. **Make Minimal Changes**: Focus only on what's needed for the task.
69
+ 5. **Explain Your Changes**: Document what you've done and why.
70
+
71
+ ## Technology-Specific Guidelines
72
+
73
+ ### Rails
74
+
75
+ - Be aware of model associations and their dependencies.
76
+ - Don't alter database migrations unless specifically asked.
77
+ - Pay attention to Rails conventions and patterns in the existing code.
78
+ - Be cautious when modifying controllers that might affect multiple views.
79
+
80
+ ## Common Pitfalls to Avoid
81
+
82
+ - Adding unnecessary abstractions or "improvements" beyond the scope of the task
83
+ - Rewriting functional code in your preferred style when it's not needed
84
+ - Making major architectural changes when only small fixes are required
85
+ - Assuming you understand the full context of the application
86
+ - Using libraries or approaches not already in use in the project
87
+
88
+ ## Communication Guidelines
89
+
90
+ - Be specific about what you're changing and why
91
+ - If you're uncertain about something, ask first
92
+ - Identify potential risks or side effects of your changes
93
+ - If you spot issues unrelated to your task, note them separately without fixing them
94
+ - Provide clear explanations of your thought process
95
+
96
+ Remember: Your primary goal is to complete the specific task assigned with minimal disruption to the existing codebase. Quality and precision are more important than clever or extensive changes.
@@ -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
 
@@ -312,19 +307,9 @@ module Historiographer
312
307
  return @history_foreign_key if @history_foreign_key
313
308
 
314
309
  # CAN THIS BE TABLE OR MODEL?
315
- @history_foreign_key = sti_base_class.name.singularize.foreign_key
310
+ @history_foreign_key = original_class.base_class.name.singularize.foreign_key
316
311
  end
317
312
 
318
- def sti_base_class
319
- return @sti_base_class if @sti_base_class
320
-
321
- base_name = name.gsub(/History$/, '')
322
- base_class = base_name.constantize
323
- while base_class.superclass != ActiveRecord::Base
324
- base_class = base_class.superclass
325
- end
326
- @sti_base_class = base_class
327
- end
328
313
  end
329
314
 
330
315
  def original_class
@@ -343,10 +328,6 @@ module Historiographer
343
328
  attrs = attributes.clone
344
329
  # attrs[original_class.primary_key] = attrs[self.class.history_foreign_key]
345
330
 
346
- if original_class.sti_enabled?
347
- # Remove History suffix from type if present
348
- attrs[original_class.inheritance_column] = attrs[original_class.inheritance_column]&.gsub(/History$/, '')
349
- end
350
331
 
351
332
  # Manually handle creating instance WITHOUT running find or initialize callbacks
352
333
  # We will manually run callbacks below
@@ -1,3 +1,3 @@
1
1
  module Historiographer
2
- VERSION = "4.1.14"
2
+ VERSION = "4.1.16"
3
3
  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?