hoardable 0.12.6 → 0.13.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: 6f7aee7a5af21b6cb079385ea2e49eb243b420c797eb8044f14af18f14abdd23
4
- data.tar.gz: c92fc3a0adc3c253856f393c6478514e6f1fe83237b75a37b89dd01325e94773
3
+ metadata.gz: 28edc85fff69a3851a5d2c653368d732bd9e6fe8ad5544bfa7f81147c6007aab
4
+ data.tar.gz: 2d7c3abc9eedaddf3180b9f368c90fa25addaffa6c54bc76bf82880b54e7fc72
5
5
  SHA512:
6
- metadata.gz: c24c3d1f3a985c0169a9c5d89335ef322fb86d45b8492e81197c089bf30f7431d94340d2b9d1805f42fd381bf52fcc4991ceb7fb66429ee87c5cf7025fc29c99
7
- data.tar.gz: 171dc6db5742a1bde4f78aa9c9990325cf9658884e147888d938e02f79e0d739dd145d266c8ede202d0ee859e8a4daa323b9c8c9d12af173d002f5535577dadb
6
+ metadata.gz: 76dfd695ad62332f876aad4d25e8d1b220970bcd8f2d2eeb69a91e4a441300fc356984dc5ef25be9dd01e745a305448a7f55b439199ef475932910bde02e8f9d
7
+ data.tar.gz: '081e30627e731ed8d9b4f42536d9e67926bb37348075efa10d2ee3f5ee3eb37476ba3b0ecf1c6ac6fb4dfa7fa700e4d02fd10c3c3f537ef7ba433e605c9d1039'
data/README.md CHANGED
@@ -1,23 +1,21 @@
1
1
  # Hoardable ![gem version](https://img.shields.io/gem/v/hoardable?style=flat-square)
2
2
 
3
- Hoardable is an ActiveRecord extension for Ruby 2.6+, Rails 6.1+, and PostgreSQL that allows for
4
- versioning and soft-deletion of records through the use of _uni-temporal inherited tables_.
3
+ Hoardable is an ActiveRecord extension for Ruby 2.6+, Rails 6.1+, and PostgreSQL that allows for versioning
4
+ and soft-deletion of records through the use of _uni-temporal inherited tables_.
5
5
 
6
- [Temporal tables](https://en.wikipedia.org/wiki/Temporal_database) are a database design pattern
7
- where each row of a table contains data along with one or more time ranges. In the case of this gem,
8
- each database row has a time range that represents the row’s valid time range - hence
9
- "uni-temporal".
6
+ [Temporal tables](https://en.wikipedia.org/wiki/Temporal_database) are a database design pattern where each
7
+ row of a table contains data along with one or more time ranges. In the case of this gem, each database row
8
+ has a time range that represents the row’s valid time range - hence "uni-temporal".
10
9
 
11
- [Table inheritance](https://www.postgresql.org/docs/14/ddl-inherit.html) is a feature of PostgreSQL
12
- that allows a table to inherit all columns of a parent table. The descendant table’s schema will
13
- stay in sync with its parent. If a new column is added to or removed from the parent, the schema
14
- change is reflected on its descendants.
10
+ [Table inheritance](https://www.postgresql.org/docs/14/ddl-inherit.html) is a feature of PostgreSQL that
11
+ allows a table to inherit all columns of a parent table. The descendant table’s schema will stay in sync with
12
+ its parent. If a new column is added to or removed from the parent, the schema change is reflected on its
13
+ descendants.
15
14
 
16
- With these concepts combined, `hoardable` offers a simple and effective model versioning system for
17
- Rails. Versions of records are stored in separate, inherited tables along with their valid time
18
- ranges and contextual data. Compared to other Rails-oriented versioning systems, this gem strives to
19
- be more explicit and obvious on the lower RDBS level while still familiar and convenient to use
20
- within Ruby on Rails.
15
+ With these concepts combined, `hoardable` offers a simple and effective model versioning system for Rails.
16
+ Versions of records are stored in separate, inherited tables along with their valid time ranges and
17
+ contextual data. Compared to other Rails-oriented versioning systems, this gem strives to be more explicit
18
+ and obvious on the lower database level, while still familiar and convenient to use within Ruby on Rails.
21
19
 
22
20
  [👉 Documentation](https://www.rubydoc.info/gems/hoardable)
23
21
 
@@ -36,22 +34,16 @@ bin/rails g hoardable:install
36
34
  bin/rails db:migrate
37
35
  ```
38
36
 
39
- This will generate a PostgreSQL function and an initiailzer.
40
-
41
- _Note:_ It is recommended to set `config.active_record.schema_format = :sql` in `application.rb`, so
42
- that the function and triggers in the migrations that prevent updates to the versions table get
43
- captured in your schema.
37
+ This will generate PostgreSQL functions, an initiailzer, and set `config.active_record.schema_format = :sql`
38
+ in `application.rb`.
44
39
 
45
40
  ### Model Installation
46
41
 
47
- You must include `Hoardable::Model` into an ActiveRecord model that you would like to hoard versions
48
- of:
42
+ You must include `Hoardable::Model` into an ActiveRecord model that you would like to hoard versions of:
49
43
 
50
44
  ```ruby
51
45
  class Post < ActiveRecord::Base
52
46
  include Hoardable::Model
53
- belongs_to :user
54
- has_many :comments, dependent: :destroy
55
47
  ...
56
48
  end
57
49
  ```
@@ -63,34 +55,31 @@ bin/rails g hoardable:migration Post
63
55
  bin/rails db:migrate
64
56
  ```
65
57
 
66
- By default, it will try to guess the foreign key type for the `_versions` table based on the primary
67
- key of the model specified in the migration generator above. If you want/need to specify this
68
- explicitly, you can do so:
58
+ By default, it will guess the foreign key type for the `_versions` table based on the primary key of the
59
+ model specified in the migration generator above. If you want/need to specify this explicitly, you can do so:
69
60
 
70
61
  ```
71
62
  bin/rails g hoardable:migration Post --foreign-key-type uuid
72
63
  ```
73
64
 
74
- _Note:_ Creating an inherited table does not copy over the indexes from the parent table. If you
75
- need to query versions often, you should add appropriate indexes to the `_versions` tables.
65
+ _Note:_ Creating an inherited table does not inherit the indexes from the parent table. If you need to query
66
+ versions often, you should add appropriate indexes to the `_versions` tables.
76
67
 
77
68
  ## Usage
78
69
 
79
70
  ### Overview
80
71
 
81
- Once you include `Hoardable::Model` into a model, it will dynamically generate a "Version" subclass
82
- of that model. As we continue our example from above:
72
+ Once you include `Hoardable::Model` into a model, it will dynamically generate a "Version" subclass of that
73
+ model. As we continue our example from above:
83
74
 
84
- ```
85
- $ irb
86
- >> Post
87
- => Post(id: integer, body: text, user_id: integer, created_at: datetime, hoardable_id: integer)
88
- >> PostVersion
89
- => PostVersion(id: integer, body: text, user_id: integer, created_at: datetime, hoardable_id: integer, _data: jsonb, _during: tsrange)
75
+ ```ruby
76
+ Post #=> Post(id: integer, created_at: datetime, updated_at: datetime, hoardable_id: integer)
77
+ PostVersion #=> PostVersion(id: integer, created_at: datetime, updated_at: datetime, hoardable_id: integer, _data: jsonb, _during: tsrange, _event_uuid: uuid, _operation: enum)
78
+ Post.version_class #=> same as `PostVersion`
90
79
  ```
91
80
 
92
- A `Post` now `has_many :versions`. With the default configuration, whenever an update and deletion
93
- of a `Post` occurs, a version is created:
81
+ A `Post` now `has_many :versions`. With the default configuration, whenever an update and deletion of a
82
+ `Post` occurs, a version is created:
94
83
 
95
84
  ```ruby
96
85
  post = Post.create!(title: "Title")
@@ -105,7 +94,11 @@ Post.find(post.id) # raises ActiveRecord::RecordNotFound
105
94
  ```
106
95
 
107
96
  Each `PostVersion` has access to the same attributes, relationships, and other model behavior that
108
- `Post` has, but as a read-only record.
97
+ `Post` has, but as a read-only record:
98
+
99
+ ``` ruby
100
+ post.versions.last.update!(title: "Rewrite history") #=> raises ActiveRecord::ReadOnlyRecord
101
+ ```
109
102
 
110
103
  If you ever need to revert to a specific version, you can call `version.revert!` on it.
111
104
 
@@ -116,8 +109,8 @@ post.reload.versions.last.revert!
116
109
  post.title # => "Title"
117
110
  ```
118
111
 
119
- If you would like to untrash a specific version, you can call `version.untrash!` on it. This will
120
- re-insert the model in the parent class’s table with the original primary key.
112
+ If you would like to untrash a specific version of a record you deleted, you can call `version.untrash!` on
113
+ it. This will re-insert the model in the parent class’s table with the original primary key.
121
114
 
122
115
  ```ruby
123
116
  post = Post.create!(title: "Title")
@@ -131,12 +124,17 @@ trashed_post.untrash!
131
124
  Post.find(post.id) # #<Post>
132
125
  ```
133
126
 
127
+ _Note:_ You will notice above that both `posts` and `post_versions` pull from the same ID sequence. This
128
+ allows for uniquely identifying source records and versions when results are mixed together. Both a source
129
+ record and versions have an automatically managed `hoardable_id` that always represents the primary key value
130
+ of the original source record.
131
+
134
132
  ### Querying and Temporal Lookup
135
133
 
136
134
  Since a `PostVersion` is an `ActiveRecord` class, you can query them like another model resource:
137
135
 
138
136
  ```ruby
139
- post.versions.where(user_id: Current.user.id, body: "Cool!")
137
+ post.versions.where(state: :draft)
140
138
  ```
141
139
 
142
140
  If you want to look-up the version of a record at a specific time, you can use the `.at` method:
@@ -144,7 +142,8 @@ If you want to look-up the version of a record at a specific time, you can use t
144
142
  ```ruby
145
143
  post.at(1.day.ago) # => #<PostVersion>
146
144
  # or you can use the scope on the version model class
147
- PostVersion.at(1.day.ago).find_by(hoardable_id: post.id) # => #<PostVersion>
145
+ post.versions.at(1.day.ago) # => #<PostVersion>
146
+ PostVersion.at(1.day.ago).find_by(hoardable_id: post.id) # => same as above
148
147
  ```
149
148
 
150
149
  The source model class also has an `.at` method:
@@ -153,35 +152,32 @@ The source model class also has an `.at` method:
153
152
  Post.at(1.day.ago) # => [#<Post>, #<Post>]
154
153
  ```
155
154
 
156
- This will return an ActiveRecord scoped query of all `Post` and `PostVersion` records that were
157
- valid at that time, all cast as instances of `Post`.
155
+ This will return an ActiveRecord scoped query of all `Post` and `PostVersion` records that were valid at that
156
+ time, all cast as instances of `Post`.
158
157
 
159
- There is also an `at` method on `Hoardable` itself for more complex temporal resource querying. See
160
- [Relationships](#relationships) for more.
158
+ There is also an `at` method on `Hoardable` itself for more complex and experimental temporal resource
159
+ querying. See [Relationships](#relationships) for more.
161
160
 
162
- By default, `hoardable` will keep copies of records you have destroyed. You can query them
163
- specifically with:
161
+ By default, `hoardable` will keep copies of records you have destroyed. You can query them specifically with:
164
162
 
165
163
  ```ruby
166
164
  PostVersion.trashed
167
- Post.version_class.trashed # <- same thing as above
165
+ Post.version_class.trashed # <- same as above
168
166
  ```
169
167
 
170
- _Note:_ A `Version` is not created upon initial parent model creation. To accurately track the
171
- beginning of the first temporal period, you will need to ensure the source model table has a
172
- `created_at` timestamp column. If this is missing, an error will be raised.
168
+ _Note:_ A `Version` is not created upon initial parent model creation. To accurately track the beginning of
169
+ the first temporal period, you will need to ensure the source model table has a `created_at` timestamp
170
+ column. If this is missing, an error will be raised.
173
171
 
174
172
  ### Tracking Contextual Data
175
173
 
176
- You’ll often want to track contextual data about the creation of a version. There are 3 optional
177
- symbol keys that are provided for tracking contextual information:
174
+ You’ll often want to track contextual data about the creation of a version. There are 2 options that can be
175
+ provided for tracking contextual information:
178
176
 
179
177
  - `:whodunit` - an identifier for who is responsible for creating the version
180
- - `:note` - a description regarding the versioning
181
178
  - `:meta` - any other contextual information you’d like to store along with the version
182
179
 
183
- This information is stored in a `jsonb` column. Each key’s value can be in the format of your
184
- choosing.
180
+ This information is stored in a `jsonb` column. Each key’s value can be in the format of your choosing.
185
181
 
186
182
  One convenient way to assign contextual data to these is by defining a proc in an initializer, i.e.:
187
183
 
@@ -195,17 +191,17 @@ post.update!(status: 'live')
195
191
  post.reload.versions.last.hoardable_whodunit # => 123
196
192
  ```
197
193
 
198
- You can also set this context manually as well, just remember to clear them afterwards.
194
+ You can also set this context manually as well:
199
195
 
200
196
  ```ruby
201
- Hoardable.note = "reverting due to accidental deletion"
197
+ Hoardable.meta = { note: "reverting due to accidental deletion" }
202
198
  post.update!(title: "We’re back!")
203
- Hoardable.note = nil
204
- post.reload.versions.last.hoardable_note # => "reverting due to accidental deletion"
199
+ Hoardable.meta = nil
200
+ post.reload.versions.last.hoardable_meta['note'] # => "reverting due to accidental deletion"
205
201
  ```
206
202
 
207
- A more useful pattern is to use `Hoardable.with` to set the context around a block. A good example
208
- of this would be in `ApplicationController`:
203
+ A more useful pattern however is to use `Hoardable.with` to set the context around a block. For example, you
204
+ could have the following in your `ApplicationController`:
209
205
 
210
206
  ```ruby
211
207
  class ApplicationController < ActionController::Base
@@ -223,9 +219,9 @@ end
223
219
  ```
224
220
 
225
221
  `hoardable` will also automatically capture the ActiveRecord
226
- [changes](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes) hash, the
227
- `operation` that cause the version (`update` or `delete`), and it will also tag all versions created
228
- in the same database transaction with a shared and unique `event_uuid`. These are available as:
222
+ [changes](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes) hash, the `operation`
223
+ that cause the version (`update` or `delete`), and it will also tag all versions created in the same database
224
+ transaction with a shared and unique `event_uuid` for that transaction. These are available as:
229
225
 
230
226
  ```ruby
231
227
  version.changes
@@ -235,12 +231,12 @@ version.hoardable_event_uuid
235
231
 
236
232
  ### Model Callbacks
237
233
 
238
- Sometimes you might want to do something with a version after it gets inserted to the database. You
239
- can access it in `after_versioned` callbacks on the source record as `hoardable_version`. These
240
- happen within `ActiveRecord`’s `.save`, which is enclosed in an ActiveRecord transaction.
234
+ Sometimes you might want to do something with a version after it gets inserted to the database. You can
235
+ access it in `after_versioned` callbacks on the source record as `hoardable_version`. These happen within
236
+ `ActiveRecord`’s `.save`, which is enclosed in an ActiveRecord transaction.
241
237
 
242
- There are also `after_reverted` and `after_untrashed` callbacks available as well, which are called
243
- on the source record after a version is reverted or untrashed.
238
+ There are also `after_reverted` and `after_untrashed` callbacks available as well, which are called on the
239
+ source record after a version is reverted or untrashed.
244
240
 
245
241
  ```ruby
246
242
  class User
@@ -275,11 +271,11 @@ Hoardable.version_updates # => default true
275
271
  Hoardable.save_trash # => default true
276
272
  ```
277
273
 
278
- `Hoardable.enabled` controls whether versions will be ever be created.
274
+ `Hoardable.enabled` globally controls whether versions will be ever be created.
279
275
 
280
- `Hoardable.version_updates` controls whether versions get created on record updates.
276
+ `Hoardable.version_updates` globally controls whether versions get created on record updates.
281
277
 
282
- `Hoardable.save_trash` controls whether to create versions upon record deletion. When this is set to
278
+ `Hoardable.save_trash` globally controls whether to create versions upon record deletion. When this is set to
283
279
  `false`, all versions of a record will be deleted when the record is destroyed.
284
280
 
285
281
  If you would like to temporarily set a config setting, you can use `Hoardable.with`:
@@ -290,7 +286,7 @@ Hoardable.with(enabled: false) do
290
286
  end
291
287
  ```
292
288
 
293
- You can also configure these variables per `ActiveRecord` class as well using `hoardable_config`:
289
+ You can also configure these settings per `ActiveRecord` class using `hoardable_config`:
294
290
 
295
291
  ```ruby
296
292
  class Comment < ActiveRecord::Base
@@ -308,18 +304,17 @@ Comment.with_hoardable_config(version_updates: true) do
308
304
  end
309
305
  ```
310
306
 
311
- If a model-level option exists, it will use that. Otherwise, it will fall back to the global
312
- `Hoardable` config.
307
+ If a model-level option exists, it will use that. Otherwise, it will fall back to the global `Hoardable`
308
+ config.
313
309
 
314
- ### Relationships
310
+ ## Relationships
315
311
 
316
- As in life, sometimes relationships can be hard, but here are some pointers on handling associations
317
- with `Hoardable` considerations.
312
+ ### Belongs To Trashable
318
313
 
319
- Sometimes you’ll have a record that belongs to a parent record that you’ll trash. Now the child
320
- record’s foreign key will point to the non-existent trashed version of the parent. If you would like
321
- to have `belongs_to` resolve to the trashed parent model in this case, you can give it the option of
322
- `trashable: true`:
314
+ Sometimes you’ll have a record that belongs to a parent record that you’ll trash. Now the child record’s
315
+ foreign key will point to the non-existent trashed version of the parent. If you would like to have
316
+ `belongs_to` resolve to the trashed parent model in this case, you can give it the option of `trashable:
317
+ true`:
323
318
 
324
319
  ```ruby
325
320
  class Comment
@@ -328,10 +323,11 @@ class Comment
328
323
  end
329
324
  ```
330
325
 
331
- Sometimes you'll have a Hoardable record that `has_many` other Hoardable records and you will want
332
- to know the state of both the parent record and the children at a cetain point in time. You
333
- accomplish this by adding `hoardable: true` to the `has_many` relationship and using the
334
- `Hoardable.at` method:
326
+ ### Hoardable Has Many & Has One
327
+
328
+ Sometimes you'll have a Hoardable record that `has_one` or `has_many` other Hoardable records and you will
329
+ want to know the state of both the parent record and the children at a cetain point in time. You accomplish
330
+ this by adding `hoardable: true` to the `has_many` relationship and using the `Hoardable.at` method:
335
331
 
336
332
  ```ruby
337
333
  class Post
@@ -361,22 +357,26 @@ Hoardable.at(datetime) do
361
357
  end
362
358
  ```
363
359
 
364
- There are some additional details to point out above. Firstly, it is important to note that the
365
- final `post.id` yields a different value than the originally created `Post`. This is because the
366
- `post` within the `#at` block is actually a temporal version, since it has been subsequently
367
- updated, but it has been reified as a `Post` for the purposes of your business logic (serialization,
368
- rendering views, exporting, etc). Don’t fret - you will not be able to commit any updates to the
369
- version, even though it is masquerading as a `Post`.
360
+ There are some additional details to point out above. Firstly, it is important to note that the final
361
+ `post.id` yields a different value than the originally created `Post`. This is because the `post` within the
362
+ `#at` block is actually a temporal version, since it has been subsequently updated, but is reified as a
363
+ `Post` for the purposes of your business logic (serialization, rendering views, exporting, etc). Don’t fret -
364
+ you will not be able to commit any updates to the version, even though it is masquerading as a `Post` because
365
+ a database trigger won’t allow you to.
366
+
367
+ If you are ever unsure if a Hoardable record is a source record or a version, you can be sure by calling
368
+ `version?` on it. If you want to get the true original source record ID, you can call `hoardable_id`.
370
369
 
371
- If you are ever unsure if a Hoardable record is a "source" or a "version", you can be sure by
372
- calling `version?` on it. If you want to get the true original source record ID, you can call
373
- `hoardable_id`.
370
+ _Note:_ `Hoardable.at` is still very experimental and is potentially not very performant for querying large
371
+ data sets.
374
372
 
375
- Sometimes you’ll trash something that `has_many :children, dependent: :destroy` and want
376
- to untrash everything in a similar dependent manner. Whenever a hoardable version is created in a
377
- database transaction, it will create or re-use a unique event UUID for that transaction and tag all
378
- versions created with it. That way, when you `untrash!` a record, you can find and `untrash!`
379
- records that were trashed with it:
373
+ ### Cascading Untrashing
374
+
375
+ Sometimes you’ll trash something that `has_many :children, dependent: :destroy` and if you untrash the parent
376
+ record, you’ll want to also untrash the children. Whenever a hoardable version is created in a database
377
+ transaction, it will create or re-use a unique event UUID for the current database transaction and tag all
378
+ versions created with it. That way, when you `untrash!` a record, you could find and `untrash!` records that
379
+ were trashed with it:
380
380
 
381
381
  ```ruby
382
382
  class Post < ActiveRecord::Base
@@ -393,20 +393,19 @@ class Post < ActiveRecord::Base
393
393
  end
394
394
  ```
395
395
 
396
- ## Action Text
396
+ ### Action Text
397
397
 
398
- Hoardable provides support for ActiveRecord models with `has_rich_text`. First, you must create a
399
- temporal table for `ActionText::RichText`:
398
+ Hoardable provides support for ActiveRecord models with `has_rich_text`. First, you must create a temporal
399
+ table for `ActionText::RichText`:
400
400
 
401
401
  ```
402
402
  bin/rails g hoardable:migration ActionText::RichText
403
403
  bin/rails db:migrate
404
404
  ```
405
405
 
406
- Then in your model, include `Hoardable::Model` and provide the `hoardable: true` keyword to
407
- `has_rich_text`:
406
+ Then in your model include `Hoardable::Model` and provide the `hoardable: true` keyword to `has_rich_text`:
408
407
 
409
- ``` ruby
408
+ ```ruby
410
409
  class Post < ActiveRecord::Base
411
410
  include Hoardable::Model # or `Hoardable::Associations` if you don't need `PostVersion`
412
411
  has_rich_text :content, hoardable: true
@@ -415,7 +414,7 @@ end
415
414
 
416
415
  Now the `rich_text_content` relationship will be managed as a Hoardable `has_one` relationship:
417
416
 
418
- ``` ruby
417
+ ```ruby
419
418
  post = Post.create!(content: '<div>Hello World</div>')
420
419
  datetime = DateTime.current
421
420
  post.update!(content: '<div>Goodbye Cruel World</div>')
@@ -426,52 +425,61 @@ Hoardable.at(datetime) do
426
425
  end
427
426
  ```
428
427
 
428
+ ## Known Gotchas
429
+
430
+ ### Rails Fixtures
431
+
432
+ Rails uses a method called
433
+ [`disable_referential_integrity`](https://github.com/rails/rails/blob/06e9fbd954ab113108a7982357553fdef285bff1/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb#L7)
434
+ when inserting fixtures into the database. This disables PostgreSQL triggers, which Hoardable relies on for
435
+ assigning `hoardable_id` from the primary key’s value. If you would still like to use fixtures, you must
436
+ specify the primary key’s value and `hoardable_id` to the same identifier value in the fixture. This is not
437
+ an issue with fixture replacement libraries like `factory_bot` or
438
+ [`world_factory`](https://github.com/FutureProofRetail/world_factory) however.
439
+
429
440
  ## Gem Comparison
430
441
 
431
442
  #### [`paper_trail`](https://github.com/paper-trail-gem/paper_trail)
432
443
 
433
- `paper_trail` is maybe the most popular and fully featured gem in this space. It works for other
434
- database types than PostgeSQL and (by default) stores all versions of all versioned models in a
435
- single `versions` table. It stores changes in a `text`, `json`, or `jsonb` column. In order to
436
- efficiently query the `versions` table, a `jsonb` column should be used, which takes up a lot of
437
- space to index. Unless you customize your configuration, all `versions` for all models types are
438
- in the same table which is inefficient if you are only interested in querying versions of a single
439
- model. By contrast, `hoardable` stores versions in smaller, isolated and inherited tables with the
440
- same database columns as their parents, which are more efficient for querying as well as auditing
441
- for truncating and dropping. The concept of a `temporal` time-frame does not exist for a single
442
- version since there is only a `created_at` timestamp.
444
+ `paper_trail` is maybe the most popular and fully featured gem in this space. It works for other database
445
+ types than PostgeSQL and (by default) stores all versions of all versioned models in a single `versions`
446
+ table. It stores changes in a `text`, `json`, or `jsonb` column. In order to efficiently query the `versions`
447
+ table, a `jsonb` column should be used, which takes up a lot of space to index. Unless you customize your
448
+ configuration, all `versions` for all models types are in the same table which is inefficient if you are only
449
+ interested in querying versions of a single model. By contrast, `hoardable` stores versions in smaller,
450
+ isolated and inherited tables with the same database columns as their parents, which are more efficient for
451
+ querying as well as auditing for truncating and dropping. The concept of a `temporal` time-frame does not
452
+ exist for a single version since there is only a `created_at` timestamp.
443
453
 
444
454
  #### [`audited`](https://github.com/collectiveidea/audited)
445
455
 
446
- `audited` works in a similar manner as `paper_trail`. It stores all versions for all model types in
447
- a single table, you must opt into using `jsonb` as the column type to store "changes", in case you
448
- want to query them, and there is no concept of a `temporal` time-frame for a single version. It
449
- makes opinionated decisions about contextual data requirements and stores them as top level data
450
- types on the `audited` table.
456
+ `audited` works in a similar manner as `paper_trail`. It stores all versions for all model types in a single
457
+ table, you must opt into using `jsonb` as the column type to store "changes", in case you want to query them,
458
+ and there is no concept of a `temporal` time-frame for a single version. It makes opinionated decisions about
459
+ contextual data requirements and stores them as top level data types on the `audited` table.
451
460
 
452
461
  #### [`discard`](https://github.com/jhawthorn/discard)
453
462
 
454
- `discard` only covers soft-deletion. The act of "soft deleting" a record is only captured through
455
- the time-stamping of a `discarded_at` column on the records table; there is no other capturing of
456
- the event that caused the soft deletion unless you implement it yourself. Once the "discarded"
457
- record is restored, the previous "discarded" awareness is lost. Since "discarded" records exist in
458
- the same table as "undiscarded" records, you must explicitly omit the discarded records from queries
459
- across your app to keep them from leaking in.
463
+ `discard` only covers soft-deletion. The act of "soft deleting" a record is only captured through the
464
+ time-stamping of a `discarded_at` column on the records table; there is no other capturing of the event that
465
+ caused the soft deletion unless you implement it yourself. Once the "discarded" record is restored, the
466
+ previous "discarded" awareness is lost. Since "discarded" records exist in the same table as "undiscarded"
467
+ records, you must explicitly omit the discarded records from queries across your app to keep them from
468
+ leaking in.
460
469
 
461
470
  #### [`paranoia`](https://github.com/rubysherpas/paranoia)
462
471
 
463
- `paranoia` also only covers soft-deletion. In their README, they recommend using `discard` instead
464
- of `paranoia` because of the fact they override ActiveRecord’s `delete` and `destroy` methods.
465
- `hoardable` employs callbacks to create trashed versions instead of overriding methods. Otherwise,
466
- `paranoia` works similarly to `discard` in that it keeps deleted records in the same table and tags
467
- them with a `deleted_at` timestamp. No other information about the soft-deletion event is stored.
472
+ `paranoia` also only covers soft-deletion. In their README, they recommend using `discard` instead of
473
+ `paranoia` because of the fact they override ActiveRecord’s `delete` and `destroy` methods. `hoardable`
474
+ employs callbacks to create trashed versions instead of overriding methods. Otherwise, `paranoia` works
475
+ similarly to `discard` in that it keeps deleted records in the same table and tags them with a `deleted_at`
476
+ timestamp. No other information about the soft-deletion event is stored.
468
477
 
469
478
  #### [`logidze`](https://github.com/palkan/logidze)
470
479
 
471
- `logidze` is an interesting versioning alternative that leverages the power of PostgreSQL triggers.
472
- Instead of storing the previous versions or changes in a separate table, it stores them in a
473
- proprietary JSON format directly on the database row of the record itself. If does not support soft
474
- deletion.
480
+ `logidze` is an interesting versioning alternative that leverages the power of PostgreSQL triggers. Instead
481
+ of storing the previous versions or changes in a separate table, it stores them in a proprietary JSON format
482
+ directly on the database row of the record itself. If does not support soft deletion.
475
483
 
476
484
  ## Contributing
477
485
 
@@ -26,6 +26,10 @@ module Hoardable
26
26
  migration_template 'install.rb.erb', 'db/migrate/install_hoardable.rb'
27
27
  end
28
28
 
29
+ def change_schema_format_to_sql
30
+ application 'config.active_record.schema_format = :sql'
31
+ end
32
+
29
33
  def self.next_migration_number(dir)
30
34
  ::ActiveRecord::Generators::Base.next_migration_number(dir)
31
35
  end
@@ -3,7 +3,7 @@
3
3
  class InstallHoardable < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
4
  def up
5
5
  execute(
6
- <<~SQL
6
+ <<~SQL.squish
7
7
  DO $$
8
8
  BEGIN
9
9
  IF NOT EXISTS (
@@ -51,9 +51,10 @@ class InstallHoardable < ActiveRecord::Migration[<%= ActiveRecord::Migration.cur
51
51
  SQL
52
52
  )
53
53
  end
54
+
54
55
  def down
55
56
  execute(
56
- <<~SQL
57
+ <<~SQL.squish
57
58
  DROP TYPE IF EXISTS hoardable_operation;
58
59
  DROP FUNCTION IF EXISTS hoardable_version_prevent_update();
59
60
  DROP FUNCTION IF EXISTS hoardable_source_set_id();
@@ -55,7 +55,21 @@ module Hoardable
55
55
  end
56
56
 
57
57
  def source_attributes_without_primary_key
58
- source_record.attributes_before_type_cast.without(source_primary_key)
58
+ source_record.attributes.without(source_primary_key, *generated_column_names).merge(
59
+ source_record.class.select(refreshable_column_names).find(source_record.id).slice(refreshable_column_names)
60
+ )
61
+ end
62
+
63
+ def generated_column_names
64
+ @generated_column_names ||= source_record.class.columns.select(&:virtual?).map(&:name)
65
+ rescue NoMethodError
66
+ []
67
+ end
68
+
69
+ def refreshable_column_names
70
+ @refreshable_column_names ||= source_record.class.columns.select(&:default_function).reject do |column|
71
+ column.name == source_primary_key || column.name.in?(generated_column_names)
72
+ end.map(&:name)
59
73
  end
60
74
 
61
75
  def initialize_temporal_range
@@ -4,7 +4,7 @@
4
4
  module Hoardable
5
5
  # Symbols for use with setting contextual data, when creating versions. See
6
6
  # {file:README.md#tracking-contextual-data README} for more.
7
- DATA_KEYS = %i[meta whodunit note event_uuid].freeze
7
+ DATA_KEYS = %i[meta whodunit event_uuid].freeze
8
8
 
9
9
  # Symbols for use with setting {Hoardable} configuration. See {file:README.md#configuration
10
10
  # README} for more.
@@ -34,9 +34,12 @@ module Hoardable
34
34
  end.freeze
35
35
  private_constant :HOARDABLE_VERSION_UPDATES
36
36
 
37
- SUPPORTS_ENCRYPTED_ACTION_TEXT = ActiveRecord.version >= ::Gem::Version.new('7.0')
37
+ SUPPORTS_ENCRYPTED_ACTION_TEXT = ActiveRecord.version >= ::Gem::Version.new('7.0.4')
38
38
  private_constant :SUPPORTS_ENCRYPTED_ACTION_TEXT
39
39
 
40
+ SUPPORTS_VIRTUAL_COLUMNS = ActiveRecord.version >= ::Gem::Version.new('7.0.0')
41
+ private_constant :SUPPORTS_VIRTUAL_COLUMNS
42
+
40
43
  @context = {}
41
44
  @config = CONFIG_KEYS.to_h do |key|
42
45
  [key, true]
@@ -17,11 +17,10 @@ module Hoardable
17
17
  def #{name}
18
18
  reflection = _reflections['#{name}']
19
19
  return super if reflection.klass.name.match?(/^ActionText/)
20
+ return super unless (timestamp = hoardable_client.has_one_at_timestamp)
20
21
 
21
- super&.at(hoardable_client.has_one_at_timestamp) ||
22
- reflection.klass.at(hoardable_client.has_one_at_timestamp).find_by(
23
- hoardable_client.has_one_find_conditions(reflection)
24
- )
22
+ super&.at(timestamp) ||
23
+ reflection.klass.at(timestamp).find_by(hoardable_client.has_one_find_conditions(reflection))
25
24
  end
26
25
  RUBY
27
26
  end
@@ -73,7 +73,7 @@ module Hoardable
73
73
  private
74
74
 
75
75
  def tableoid
76
- connection.execute("SELECT oid FROM pg_class WHERE relname = '#{table_name}'")[0]['oid']
76
+ @tableoid ||= connection.execute("SELECT oid FROM pg_class WHERE relname = '#{table_name}'")[0]['oid']
77
77
  end
78
78
  end
79
79
  end
@@ -75,6 +75,8 @@ module Hoardable
75
75
  #
76
76
  # @param datetime [DateTime, Time]
77
77
  def at(datetime)
78
+ return self if datetime.nil? || !created_at
79
+
78
80
  version_at(datetime) || (self if created_at < datetime)
79
81
  end
80
82
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hoardable
4
- VERSION = '0.12.6'
4
+ VERSION = '0.13.0'
5
5
  end
@@ -86,7 +86,7 @@ module Hoardable
86
86
 
87
87
  transaction do
88
88
  hoardable_source.tap do |reverted|
89
- reverted.update!(hoardable_source_attributes.without(self.class.superclass.primary_key))
89
+ reverted.reload.update!(hoardable_source_attributes.without(self.class.superclass.primary_key))
90
90
  reverted.instance_variable_set(:@hoardable_version, self)
91
91
  reverted.run_callbacks(:reverted)
92
92
  end
@@ -130,7 +130,10 @@ module Hoardable
130
130
  end
131
131
 
132
132
  def hoardable_source_attributes
133
- attributes_before_type_cast.without(self.class.column_names - self.class.superclass.column_names)
133
+ attributes.without(
134
+ (self.class.column_names - self.class.superclass.column_names) +
135
+ (SUPPORTS_VIRTUAL_COLUMNS ? self.class.columns.select(&:virtual?).map(&:name) : [])
136
+ )
134
137
  end
135
138
  end
136
139
  end
data/sig/hoardable.rbs CHANGED
@@ -1,6 +1,6 @@
1
1
  module Hoardable
2
2
  VERSION: String
3
- DATA_KEYS: [:meta, :whodunit, :note, :event_uuid]
3
+ DATA_KEYS: [:meta, :whodunit, :event_uuid]
4
4
  CONFIG_KEYS: [:enabled, :version_updates, :save_trash]
5
5
  VERSION_CLASS_SUFFIX: String
6
6
  VERSION_TABLE_SUFFIX: String
@@ -57,7 +57,7 @@ module Hoardable
57
57
  def source_attributes_without_primary_key: -> untyped
58
58
  def initialize_temporal_range: -> Range
59
59
  def initialize_hoardable_data: -> untyped
60
- def assign_hoardable_context: (:event_uuid | :meta | :note | :whodunit key) -> nil
60
+ def assign_hoardable_context: (:event_uuid | :meta | :whodunit key) -> nil
61
61
  def unset_hoardable_version_and_event_uuid: -> nil
62
62
  def previous_temporal_tsrange_end: -> untyped
63
63
  def hoardable_source_epoch: -> untyped
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hoardable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.6
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - justin talbott
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-10-14 00:00:00.000000000 Z
11
+ date: 2022-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord