hoardable 0.12.6 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
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