historiographer 3.1.2 → 4.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +115 -39
- data/{Gemfile → Users/brettshollenberger/programming/historiographer/Gemfile} +1 -2
- data/Users/brettshollenberger/programming/historiographer/Gemfile.lock +341 -0
- data/Users/brettshollenberger/programming/historiographer/Guardfile +4 -0
- data/Users/brettshollenberger/programming/historiographer/LICENSE.txt +20 -0
- data/Users/brettshollenberger/programming/historiographer/README.md +298 -0
- data/Users/brettshollenberger/programming/historiographer/historiographer-4.1.0.gem +0 -0
- data/{historiographer.gemspec → Users/brettshollenberger/programming/historiographer/historiographer.gemspec} +5 -46
- data/Users/brettshollenberger/programming/historiographer/lib/historiographer/configuration.rb +36 -0
- data/{lib → Users/brettshollenberger/programming/historiographer/lib}/historiographer/history.rb +9 -2
- data/{lib → Users/brettshollenberger/programming/historiographer/lib}/historiographer/history_migration.rb +13 -5
- data/{lib → Users/brettshollenberger/programming/historiographer/lib}/historiographer/relation.rb +1 -1
- data/Users/brettshollenberger/programming/historiographer/lib/historiographer/version.rb +3 -0
- data/{lib → Users/brettshollenberger/programming/historiographer/lib}/historiographer.rb +183 -13
- data/{spec → Users/brettshollenberger/programming/historiographer/spec}/db/database.yml +5 -3
- data/Users/brettshollenberger/programming/historiographer/spec/db/migrate/20241109182017_create_comments.rb +13 -0
- data/Users/brettshollenberger/programming/historiographer/spec/db/migrate/20241109182020_create_comment_histories.rb +9 -0
- data/{spec → Users/brettshollenberger/programming/historiographer/spec}/db/schema.rb +80 -41
- data/Users/brettshollenberger/programming/historiographer/spec/examples.txt +40 -0
- data/{spec → Users/brettshollenberger/programming/historiographer/spec}/historiographer_spec.rb +265 -40
- data/{spec → Users/brettshollenberger/programming/historiographer/spec}/spec_helper.rb +8 -4
- metadata +43 -41
- data/.document +0 -5
- data/.rspec +0 -1
- data/.ruby-version +0 -1
- data/.standalone_migrations +0 -6
- data/Gemfile.lock +0 -289
- data/Guardfile +0 -70
- data/VERSION +0 -1
- data/spec/examples.txt +0 -29
- /data/{Rakefile → Users/brettshollenberger/programming/historiographer/Rakefile} +0 -0
- /data/{init.rb → Users/brettshollenberger/programming/historiographer/init.rb} +0 -0
- /data/{lib → Users/brettshollenberger/programming/historiographer/lib}/historiographer/history_migration_mysql.rb +0 -0
- /data/{lib → Users/brettshollenberger/programming/historiographer/lib}/historiographer/mysql_migration.rb +0 -0
- /data/{lib → Users/brettshollenberger/programming/historiographer/lib}/historiographer/postgres_migration.rb +0 -0
- /data/{lib → Users/brettshollenberger/programming/historiographer/lib}/historiographer/safe.rb +0 -0
- /data/{lib → Users/brettshollenberger/programming/historiographer/lib}/historiographer/silent.rb +0 -0
- /data/{spec → Users/brettshollenberger/programming/historiographer/spec}/db/migrate/20161121212228_create_posts.rb +0 -0
- /data/{spec → Users/brettshollenberger/programming/historiographer/spec}/db/migrate/20161121212229_create_post_histories.rb +0 -0
- /data/{spec → Users/brettshollenberger/programming/historiographer/spec}/db/migrate/20161121212230_create_authors.rb +0 -0
- /data/{spec → Users/brettshollenberger/programming/historiographer/spec}/db/migrate/20161121212231_create_author_histories.rb +0 -0
- /data/{spec → Users/brettshollenberger/programming/historiographer/spec}/db/migrate/20161121212232_create_users.rb +0 -0
- /data/{spec → Users/brettshollenberger/programming/historiographer/spec}/db/migrate/20171011194624_create_safe_posts.rb +0 -0
- /data/{spec → Users/brettshollenberger/programming/historiographer/spec}/db/migrate/20171011194715_create_safe_post_histories.rb +0 -0
- /data/{spec → Users/brettshollenberger/programming/historiographer/spec}/db/migrate/20191024142304_create_thing_with_compound_index.rb +0 -0
- /data/{spec → Users/brettshollenberger/programming/historiographer/spec}/db/migrate/20191024142352_create_thing_with_compound_index_history.rb +0 -0
- /data/{spec → Users/brettshollenberger/programming/historiographer/spec}/db/migrate/20191024203106_create_thing_without_history.rb +0 -0
- /data/{spec → Users/brettshollenberger/programming/historiographer/spec}/db/migrate/20221018204220_create_silent_posts.rb +0 -0
- /data/{spec → Users/brettshollenberger/programming/historiographer/spec}/db/migrate/20221018204255_create_silent_post_histories.rb +0 -0
- /data/{spec → Users/brettshollenberger/programming/historiographer/spec}/factories/post.rb +0 -0
@@ -0,0 +1,298 @@
|
|
1
|
+
# Historiographer
|
2
|
+
|
3
|
+
Losing data sucks. Every time you update or destroy a record in Rails, you lose the old data.
|
4
|
+
|
5
|
+
Historiographer fixes this problem in a better way than existing auditing gems.
|
6
|
+
|
7
|
+
## Existing auditing gems for Rails suck
|
8
|
+
|
9
|
+
The Audited gem has some serious flaws.
|
10
|
+
|
11
|
+
1. The `versions` table quickly grows too large to query
|
12
|
+
|
13
|
+
2. It doesn't provide the indexes you need from your primary tables
|
14
|
+
|
15
|
+
3. It doesn't provdie out-of-the-box snapshots
|
16
|
+
|
17
|
+
## How does Historiographer solve these problems?
|
18
|
+
|
19
|
+
Historiographer introduces the concept of _history tables:_ append-only tables that have the same structure and indexes as your primary table.
|
20
|
+
|
21
|
+
If you have a `posts` table:
|
22
|
+
|
23
|
+
| id | title |
|
24
|
+
| :-- | :------------- |
|
25
|
+
| 1 | My Great Post |
|
26
|
+
| 2 | My Second Post |
|
27
|
+
|
28
|
+
You'll also have a `post_histories_table`:
|
29
|
+
|
30
|
+
| id | post_id | title | history_started_at | history_ended_at | history_user_id |
|
31
|
+
| :-- | :------ | :------------- | :----------------- | :--------------- | :-------------- |
|
32
|
+
| 1 | 1 | My Great Post | '2019-11-08' | NULL | 1 |
|
33
|
+
| 2 | 2 | My Second Post | '2019-11-08' | NULL | 1 |
|
34
|
+
|
35
|
+
If you change the title of the 1st post:
|
36
|
+
|
37
|
+
`Post.find(1).update(title: "Title With Better SEO", history_user_id: current_user.id)`
|
38
|
+
|
39
|
+
You'll expect your `posts` table to be updated directly:
|
40
|
+
|
41
|
+
| id | title |
|
42
|
+
| :-- | :-------------------- |
|
43
|
+
| 1 | Title With Better SEO |
|
44
|
+
| 2 | My Second Post |
|
45
|
+
|
46
|
+
But also, your `histories` table will be updated:
|
47
|
+
|
48
|
+
| id | post_id | title | history_started_at | history_ended_at | history_user_id |
|
49
|
+
| :-- | :------ | :-------------------- | :----------------- | :--------------- | :-------------- |
|
50
|
+
| 1 | 1 | My Great Post | '2019-11-08' | '2019-11-09' | 1 |
|
51
|
+
| 2 | 2 | My Second Post | '2019-11-08' | NULL | 1 |
|
52
|
+
| 1 | 1 | Title With Better SEO | '2019-11-09' | NULL | 1 |
|
53
|
+
|
54
|
+
A few things have happened here:
|
55
|
+
|
56
|
+
1. The primary table (`posts`) is updated directly
|
57
|
+
2. The existing history for `post_id=1` is timestamped when its `history_ended_at`, so that we can see when the post had the title "My Great Post"
|
58
|
+
3. A new history record is appended to the table containing a complete snapshot of the record, and a `NULL` `history_ended_at`. That's because this is the current history.
|
59
|
+
4. A record of _who_ made the change is saved (`history_user_id`). You can join to your users table to see more data.
|
60
|
+
|
61
|
+
## Snapshots
|
62
|
+
|
63
|
+
Snapshots are particularly useful for two key use cases:
|
64
|
+
|
65
|
+
### 1. Time Travel & Auditing
|
66
|
+
|
67
|
+
When you need to see exactly what your data looked like at a specific point in time - not just individual records, but entire object graphs with all their associations. This is invaluable for:
|
68
|
+
|
69
|
+
- Debugging production issues ("What did the entire order look like when this happened?")
|
70
|
+
- Compliance requirements ("Show me the exact state of this patient's record on January 1st")
|
71
|
+
- Auditing complex workflows ("What was the state of this loan application when it was approved?")
|
72
|
+
|
73
|
+
### 2. Machine Learning & Analytics
|
74
|
+
|
75
|
+
When you need immutable snapshots of data for:
|
76
|
+
|
77
|
+
- Training data versioning
|
78
|
+
- Feature engineering
|
79
|
+
- Model validation
|
80
|
+
- A/B test analysis
|
81
|
+
- Ensuring reproducibility of results
|
82
|
+
|
83
|
+
### Taking Snapshots
|
84
|
+
|
85
|
+
You can take a snapshot of a record and all its associated records:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
post = Post.find(1)
|
89
|
+
post.snapshot(history_user_id: current_user.id)
|
90
|
+
```
|
91
|
+
|
92
|
+
This will:
|
93
|
+
|
94
|
+
1. Create a history record for the post
|
95
|
+
2. Create history records for all associated records (comments, author, etc.)
|
96
|
+
3. Link these history records together with a shared `snapshot_id`
|
97
|
+
|
98
|
+
You can retrieve the latest snapshot using:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
post = Post.find(1)
|
102
|
+
snapshot = post.latest_snapshot
|
103
|
+
|
104
|
+
# Access associated records from the snapshot
|
105
|
+
snapshot.comments # Returns CommentHistory records
|
106
|
+
snapshot.author # Returns AuthorHistory record
|
107
|
+
```
|
108
|
+
|
109
|
+
Snapshots are immutable - you cannot modify history records that are part of a snapshot. This guarantees that your historical data remains unchanged, which is crucial for both auditing and machine learning applications.
|
110
|
+
|
111
|
+
### Snapshot-Only Mode
|
112
|
+
|
113
|
+
If you want to only track snapshots and not record every individual change, you can configure Historiographer to operate in snapshot-only mode:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
Historiographer::Configuration.mode = :snapshot_only
|
117
|
+
```
|
118
|
+
|
119
|
+
In this mode:
|
120
|
+
|
121
|
+
- Regular updates/changes will not create history records
|
122
|
+
- Only explicit calls to `snapshot` will create history records
|
123
|
+
- Each snapshot still captures the complete state of the record and its associations
|
124
|
+
|
125
|
+
This can be useful when:
|
126
|
+
|
127
|
+
- You only care about specific points in time rather than every change
|
128
|
+
- You want to reduce the number of history records created
|
129
|
+
- You need to capture the state of complex object graphs at specific moments
|
130
|
+
- You're versioning training data for machine learning models
|
131
|
+
- You need to maintain immutable audit trails at specific checkpoints
|
132
|
+
|
133
|
+
# Getting Started
|
134
|
+
|
135
|
+
Whenever you include the `Historiographer` gem in your ActiveRecord model, it allows you to insert, update, or delete data as you normally would.
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
class Post < ActiveRecord::Base
|
139
|
+
include Historiographer
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
### History Modes
|
144
|
+
|
145
|
+
Historiographer supports two modes of operation:
|
146
|
+
|
147
|
+
1. **:histories mode** (default) - Records history for every change to a record
|
148
|
+
2. **:snapshot_only mode** - Only records history when explicitly taking snapshots
|
149
|
+
|
150
|
+
You can configure the mode globally:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
# In an initializer
|
154
|
+
Historiographer::Configuration.mode = :histories # Default mode
|
155
|
+
# or
|
156
|
+
Historiographer::Configuration.mode = :snapshot_only
|
157
|
+
```
|
158
|
+
|
159
|
+
Or per model using `historiographer_mode`:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
class Post < ActiveRecord::Base
|
163
|
+
include Historiographer
|
164
|
+
historiographer_mode :snapshot_only # Only record history when .snapshot is called
|
165
|
+
end
|
166
|
+
|
167
|
+
class Comment < ActiveRecord::Base
|
168
|
+
include Historiographer
|
169
|
+
historiographer_mode :histories # Record history for every change (default)
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
## Create A Migration
|
174
|
+
|
175
|
+
You need a separate table to store histories for each model.
|
176
|
+
|
177
|
+
So if you have a Posts model:
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
class CreatePosts < ActiveRecord::Migration
|
181
|
+
def change
|
182
|
+
create_table :posts do |t|
|
183
|
+
t.string :title, null: false
|
184
|
+
t.boolean :enabled
|
185
|
+
end
|
186
|
+
add_index :posts, :enabled
|
187
|
+
end
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
You should create a model named _posts_histories_:
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
require "historiographer/postgres_migration"
|
195
|
+
class CreatePostHistories < ActiveRecord::Migration
|
196
|
+
def change
|
197
|
+
create_table :post_histories do |t|
|
198
|
+
t.histories
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
```
|
203
|
+
|
204
|
+
The `t.histories` method will automatically create a table with the following columns:
|
205
|
+
|
206
|
+
- `id` (because every model has a primary key)
|
207
|
+
- `post_id` (because this is the foreign key)
|
208
|
+
- `title` (because it was on the original model)
|
209
|
+
- `enabled` (because it was on the original model)
|
210
|
+
- `history_started_at` (to denote when this history became the canonical version)
|
211
|
+
- `history_ended_at` (to denote when this history was no longer the canonical version, if it has stopped being the canonical version)
|
212
|
+
- `history_user_id` (to denote the user that made this change, if one is known)
|
213
|
+
|
214
|
+
Additionally it will add indices on:
|
215
|
+
|
216
|
+
- The same columns that had indices on the original model (e.g. `enabled`)
|
217
|
+
- `history_started_at`, `history_ended_at`, and `history_user_id`
|
218
|
+
|
219
|
+
## Models
|
220
|
+
|
221
|
+
The primary model should include `Historiographer`:
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
class Post < ActiveRecord::Base
|
225
|
+
include Historiographer
|
226
|
+
end
|
227
|
+
```
|
228
|
+
|
229
|
+
You should also make a `PostHistory` class if you're going to query `PostHistory` from Rails:
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
class PostHistory < ActiveRecord::Base
|
233
|
+
end
|
234
|
+
```
|
235
|
+
|
236
|
+
The `Posts` class will acquire a `histories` method, and the `PostHistory` model will gain a `post` method:
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
p = Post.first
|
240
|
+
p.histories.first.class
|
241
|
+
|
242
|
+
# => "PostHistory"
|
243
|
+
|
244
|
+
p.histories.first.post == p
|
245
|
+
# => true
|
246
|
+
```
|
247
|
+
|
248
|
+
## Creating, Updating, and Destroying Data:
|
249
|
+
|
250
|
+
You can just use normal ActiveRecord methods, and all will record histories:
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
Post.create(title: "My Great Title", history_user_id: current_user.id)
|
254
|
+
Post.find_by(title: "My Great Title").update(title: "A New Title", history_user_id: current_user.id)
|
255
|
+
Post.update_all(title: "They're all the same!", history_user_id: current_user.id)
|
256
|
+
Post.last.destroy!(history_user_id: current_user.id)
|
257
|
+
Post.destroy_all(history_user_id: current_user.id)
|
258
|
+
```
|
259
|
+
|
260
|
+
The `histories` classes have a `current` method, which only finds current history records. These records will also be the same as the data in the primary table.
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
p = Post.first
|
264
|
+
p.current_history
|
265
|
+
|
266
|
+
PostHistory.current
|
267
|
+
```
|
268
|
+
|
269
|
+
### What to do when generated index names are too long
|
270
|
+
|
271
|
+
Sometimes the generated index names are too long. Just like with standard Rails migrations, you can override the name of the index to fix this problem. To do so, use the `index_names` argument to override individual index names:
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
require "historiographer/postgres_migration"
|
275
|
+
class CreatePostHistories < ActiveRecord::Migration
|
276
|
+
def change
|
277
|
+
create_table :post_histories do |t|
|
278
|
+
t.histories index_names: {
|
279
|
+
title: "my_index_name",
|
280
|
+
[:compound, :index] => "my_compound_index_name"
|
281
|
+
}
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
```
|
286
|
+
|
287
|
+
== Mysql Install
|
288
|
+
|
289
|
+
For contributors on OSX, you may have difficulty installing mysql:
|
290
|
+
|
291
|
+
```
|
292
|
+
gem install mysql2 -v '0.4.10' --source 'https://rubygems.org/' -- --with-ldflags=-L/usr/local/opt/openssl/lib --with-cppflags=-I/usr/local/opt/openssl/include
|
293
|
+
```
|
294
|
+
|
295
|
+
== Copyright
|
296
|
+
|
297
|
+
Copyright (c) 2016-2020 brettshollenberger. See LICENSE.txt for
|
298
|
+
further details.
|
Binary file
|
@@ -2,67 +2,26 @@
|
|
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
|
5
|
+
# stub: historiographer 4.0.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "historiographer".freeze
|
9
|
-
s.version = "
|
9
|
+
s.version = "4.1.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]
|
13
13
|
s.authors = ["brettshollenberger".freeze]
|
14
|
-
s.date = "
|
14
|
+
s.date = "2023-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 = [
|
18
18
|
"LICENSE.txt",
|
19
19
|
"README.md"
|
20
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.gemspec",
|
34
|
-
"init.rb",
|
35
|
-
"lib/historiographer.rb",
|
36
|
-
"lib/historiographer/history.rb",
|
37
|
-
"lib/historiographer/history_migration.rb",
|
38
|
-
"lib/historiographer/history_migration_mysql.rb",
|
39
|
-
"lib/historiographer/mysql_migration.rb",
|
40
|
-
"lib/historiographer/postgres_migration.rb",
|
41
|
-
"lib/historiographer/relation.rb",
|
42
|
-
"lib/historiographer/safe.rb",
|
43
|
-
"lib/historiographer/silent.rb",
|
44
|
-
"spec/db/database.yml",
|
45
|
-
"spec/db/migrate/20161121212228_create_posts.rb",
|
46
|
-
"spec/db/migrate/20161121212229_create_post_histories.rb",
|
47
|
-
"spec/db/migrate/20161121212230_create_authors.rb",
|
48
|
-
"spec/db/migrate/20161121212231_create_author_histories.rb",
|
49
|
-
"spec/db/migrate/20161121212232_create_users.rb",
|
50
|
-
"spec/db/migrate/20171011194624_create_safe_posts.rb",
|
51
|
-
"spec/db/migrate/20171011194715_create_safe_post_histories.rb",
|
52
|
-
"spec/db/migrate/20191024142304_create_thing_with_compound_index.rb",
|
53
|
-
"spec/db/migrate/20191024142352_create_thing_with_compound_index_history.rb",
|
54
|
-
"spec/db/migrate/20191024203106_create_thing_without_history.rb",
|
55
|
-
"spec/db/migrate/20221018204220_create_silent_posts.rb",
|
56
|
-
"spec/db/migrate/20221018204255_create_silent_post_histories.rb",
|
57
|
-
"spec/db/schema.rb",
|
58
|
-
"spec/examples.txt",
|
59
|
-
"spec/factories/post.rb",
|
60
|
-
"spec/historiographer_spec.rb",
|
61
|
-
"spec/spec_helper.rb"
|
62
|
-
]
|
21
|
+
s.files = Dir[File.expand_path("**/*")]
|
63
22
|
s.homepage = "http://github.com/brettshollenberger/historiographer".freeze
|
64
23
|
s.licenses = ["MIT".freeze]
|
65
|
-
s.rubygems_version = "3.2.
|
24
|
+
s.rubygems_version = "3.2.22".freeze
|
66
25
|
s.summary = "Create histories of your ActiveRecord tables".freeze
|
67
26
|
|
68
27
|
if s.respond_to? :specification_version then
|
data/Users/brettshollenberger/programming/historiographer/lib/historiographer/configuration.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require "singleton"
|
2
|
+
|
3
|
+
module Historiographer
|
4
|
+
class Configuration
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
OPTS = {
|
8
|
+
mode: {
|
9
|
+
default: :histories
|
10
|
+
}
|
11
|
+
}
|
12
|
+
OPTS.each do |key, options|
|
13
|
+
attr_accessor key
|
14
|
+
end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def configure
|
18
|
+
yield instance
|
19
|
+
end
|
20
|
+
|
21
|
+
OPTS.each do |key, options|
|
22
|
+
define_method "#{key}=" do |value|
|
23
|
+
instance.send("#{key}=", value)
|
24
|
+
end
|
25
|
+
|
26
|
+
define_method key do
|
27
|
+
instance.send(key) || options.dig(:default)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
@mode = :histories
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/{lib → Users/brettshollenberger/programming/historiographer/lib}/historiographer/history.rb
RENAMED
@@ -139,7 +139,7 @@ module Historiographer
|
|
139
139
|
# If the record was not already persisted, proceed as normal.
|
140
140
|
#
|
141
141
|
def save(*args)
|
142
|
-
if persisted? && (changes.keys - %w(history_ended_at)).any?
|
142
|
+
if persisted? && (changes.keys - %w(history_ended_at snapshot_id)).any?
|
143
143
|
false
|
144
144
|
else
|
145
145
|
super
|
@@ -147,12 +147,19 @@ module Historiographer
|
|
147
147
|
end
|
148
148
|
|
149
149
|
def save!(*args)
|
150
|
-
if persisted? && (changes.keys - %w(history_ended_at)).any?
|
150
|
+
if persisted? && (changes.keys - %w(history_ended_at snapshot_id)).any?
|
151
151
|
false
|
152
152
|
else
|
153
153
|
super
|
154
154
|
end
|
155
155
|
end
|
156
|
+
|
157
|
+
# Returns the most recent snapshot for each snapshot_id
|
158
|
+
# Orders by history_started_at and id to handle cases where multiple records
|
159
|
+
# have the same history_started_at timestamp
|
160
|
+
scope :latest_snapshot, -> {
|
161
|
+
where.not(snapshot_id: nil).order('id DESC').limit(1)&.first
|
162
|
+
}
|
156
163
|
end
|
157
164
|
|
158
165
|
class_methods do
|
@@ -29,17 +29,24 @@ module Historiographer
|
|
29
29
|
original_columns = klass.columns.reject { |c| c.name == "id" || except.include?(c.name) || (only.any? && only.exclude?(c.name)) || no_business_columns }
|
30
30
|
|
31
31
|
integer foreign_key.to_sym, null: false
|
32
|
+
valid_keys = [:limit, :precision, :scale, :default, :null, :collation, :comment,
|
33
|
+
:primary_key, :if_exists, :if_not_exists, :array, :using,
|
34
|
+
:cast_as, :as, :type, :enum_type, :stored]
|
32
35
|
|
33
36
|
original_columns.each do |column|
|
34
|
-
opts =
|
35
|
-
opts.merge!(column.as_json.clone)
|
37
|
+
opts = column.as_json.symbolize_keys.slice(*valid_keys) # Only keep valid keys
|
36
38
|
|
37
|
-
|
39
|
+
if RUBY_VERSION.to_i >= 3
|
40
|
+
send(column.type, column.name, **opts)
|
41
|
+
else
|
42
|
+
send(column.type, column.name, opts)
|
43
|
+
end
|
38
44
|
end
|
39
45
|
|
40
46
|
datetime :history_started_at, null: false
|
41
47
|
datetime :history_ended_at
|
42
48
|
integer :history_user_id
|
49
|
+
string :snapshot_id
|
43
50
|
|
44
51
|
indices_sql = %q(
|
45
52
|
SELECT
|
@@ -70,7 +77,8 @@ module Historiographer
|
|
70
77
|
foreign_key,
|
71
78
|
:history_started_at,
|
72
79
|
:history_ended_at,
|
73
|
-
:history_user_id
|
80
|
+
:history_user_id,
|
81
|
+
:snapshot_id
|
74
82
|
])
|
75
83
|
|
76
84
|
indexes.each do |index_definition|
|
@@ -86,4 +94,4 @@ module Historiographer
|
|
86
94
|
|
87
95
|
end
|
88
96
|
end
|
89
|
-
end
|
97
|
+
end
|