hoardable 0.14.2 → 0.16.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.
data/README.md CHANGED
@@ -1,21 +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.7+, Rails 6.1+, and PostgreSQL that allows for versioning
4
- and soft-deletion of records through the use of _uni-temporal inherited tables_.
3
+ Hoardable is an ActiveRecord extension for Ruby 3+, Rails 7+, and PostgreSQL 9+ that allows for
4
+ versioning 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 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".
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".
9
10
 
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.
11
+ [Table inheritance](https://www.postgresql.org/docs/current/ddl-inherit.html) is a feature of
12
+ PostgreSQL that allows one table to inherit all columns from a parent. The descendant table’s schema
13
+ will stay in sync with its parent; if a new column is added to or removed from the parent, the
14
+ schema change is reflected on its descendants.
14
15
 
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.
16
+ With these concepts combined, `hoardable` offers a model versioning and soft deletion system for
17
+ Rails. Versions of records are stored in separate, inherited tables along with their valid time
18
+ ranges and contextual data.
19
19
 
20
20
  [👉 Documentation](https://www.rubydoc.info/gems/hoardable)
21
21
 
@@ -24,7 +24,7 @@ and obvious on the lower database level, while still familiar and convenient to
24
24
  Add this line to your application's Gemfile:
25
25
 
26
26
  ```ruby
27
- gem 'hoardable'
27
+ gem "hoardable"
28
28
  ```
29
29
 
30
30
  Run `bundle install`, and then run:
@@ -34,12 +34,9 @@ bin/rails g hoardable:install
34
34
  bin/rails db:migrate
35
35
  ```
36
36
 
37
- This will generate PostgreSQL functions, an enum and an initiailzer. It will also set
38
- `config.active_record.schema_format = :sql` in `application.rb` if you are using Rails < 7.
37
+ ### Model installation
39
38
 
40
- ### Model Installation
41
-
42
- You must include `Hoardable::Model` into an ActiveRecord model that you would like to hoard versions of:
39
+ Include `Hoardable::Model` into an ActiveRecord model you would like to hoard versions of:
43
40
 
44
41
  ```ruby
45
42
  class Post < ActiveRecord::Base
@@ -48,38 +45,31 @@ class Post < ActiveRecord::Base
48
45
  end
49
46
  ```
50
47
 
51
- Then, run the generator command to create a database migration and migrate it:
48
+ Run the generator command to create a database migration and migrate it:
52
49
 
53
50
  ```
54
51
  bin/rails g hoardable:migration Post
55
52
  bin/rails db:migrate
56
53
  ```
57
54
 
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:
60
-
61
- ```
62
- bin/rails g hoardable:migration Post --foreign-key-type uuid
63
- ```
64
-
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.
55
+ _*Note*:_ Creating an inherited table does not inherit the indexes from the parent table. If you
56
+ need to query versions often, you should add appropriate indexes to the `_versions` tables.
67
57
 
68
58
  ## Usage
69
59
 
70
60
  ### Overview
71
61
 
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:
62
+ Once you include `Hoardable::Model` into a model, it will dynamically generate a "Version" subclass
63
+ of that model. As we continue our example from above:
74
64
 
75
65
  ```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)
66
+ Post #=> Post(id: integer, ..., hoardable_id: integer)
67
+ PostVersion #=> PostVersion(id: integer, ..., hoardable_id: integer, _data: jsonb, _during: tsrange, _event_uuid: uuid, _operation: enum)
78
68
  Post.version_class #=> same as `PostVersion`
79
69
  ```
80
70
 
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:
71
+ A `Post` now `has_many :versions`. With the default configuration, whenever an update or deletion of
72
+ a `post` occurs, a version is created:
83
73
 
84
74
  ```ruby
85
75
  post = Post.create!(title: "Title")
@@ -96,7 +86,7 @@ Post.find(post.id) # raises ActiveRecord::RecordNotFound
96
86
  Each `PostVersion` has access to the same attributes, relationships, and other model behavior that
97
87
  `Post` has, but as a read-only record:
98
88
 
99
- ``` ruby
89
+ ```ruby
100
90
  post.versions.last.update!(title: "Rewrite history") #=> raises ActiveRecord::ReadOnlyRecord
101
91
  ```
102
92
 
@@ -105,31 +95,60 @@ If you ever need to revert to a specific version, you can call `version.revert!`
105
95
  ```ruby
106
96
  post = Post.create!(title: "Title")
107
97
  post.update!(title: "Whoops")
108
- post.reload.versions.last.revert!
98
+ version = post.reload.versions.last
99
+ version.title # -> "Title"
100
+ version.revert!
109
101
  post.title # => "Title"
110
102
  ```
111
103
 
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.
104
+ If you would like to untrash a specific version of a record you deleted, you can call
105
+ `version.untrash!` on it. This will re-insert the model in the parent class’s table with the
106
+ original primary key.
114
107
 
115
108
  ```ruby
116
109
  post = Post.create!(title: "Title")
117
- post.id # => 1
118
110
  post.destroy!
119
111
  post.versions.size # => 1
120
112
  Post.find(post.id) # raises ActiveRecord::RecordNotFound
121
113
  trashed_post = post.versions.trashed.last
122
- trashed_post.id # => 2
123
114
  trashed_post.untrash!
124
115
  Post.find(post.id) # #<Post>
125
116
  ```
126
117
 
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.
118
+ Source and version records pull from the same ID sequence. This allows for uniquely identifying
119
+ records from each other. Both source record and version have an automatically managed `hoardable_id`
120
+ attribute that always represents the primary key value of the original source record:
121
+
122
+ ```ruby
123
+ post = Post.create!(title: "Title")
124
+ post.id # => 1
125
+ post.hoardable_id # => 1
126
+ post.version? # => false
127
+ post.update!(title: "New Title")
128
+ version = post.reload.versions.last
129
+ version.id # => 2
130
+ version.hoardable_id # => 1
131
+ version.version? # => true
132
+ ```
133
+
134
+ ### Querying and temporal lookup
135
+
136
+ Including `Hoardable::Model` into your source model modifies `default_scope` to make sure you only
137
+ ever query the parent table and not the inherited ones:
138
+
139
+ ```ruby
140
+ Post.where(state: :draft).to_sql # => SELECT posts.* FROM ONLY posts WHERE posts.status = 'draft'
141
+ ```
142
+
143
+ Note the `FROM ONLY` above. If you are executing raw SQL, you will need to include the `ONLY`
144
+ keyword if you do not wish to return versions in your results. This includes `JOIN`-ing on this
145
+ table as well.
146
+
147
+ ```ruby
148
+ User.joins(:posts).to_sql # => SELECT users.* FROM users INNER JOIN ONLY posts ON posts.user_id = users.id
149
+ ```
131
150
 
132
- ### Querying and Temporal Lookup
151
+ Learn more about table inheritance in [the PostgreSQL documentation](https://www.postgresql.org/docs/current/ddl-inherit.html).
133
152
 
134
153
  Since a `PostVersion` is an `ActiveRecord` class, you can query them like another model resource:
135
154
 
@@ -137,6 +156,14 @@ Since a `PostVersion` is an `ActiveRecord` class, you can query them like anothe
137
156
  post.versions.where(state: :draft)
138
157
  ```
139
158
 
159
+ By default, `hoardable` will keep copies of records you have destroyed. You can query them
160
+ specifically with:
161
+
162
+ ```ruby
163
+ PostVersion.trashed.where(user_id: user.id)
164
+ Post.version_class.trashed.where(user_id: user.id) # <- same as above
165
+ ```
166
+
140
167
  If you want to look-up the version of a record at a specific time, you can use the `.at` method:
141
168
 
142
169
  ```ruby
@@ -152,32 +179,22 @@ The source model class also has an `.at` method:
152
179
  Post.at(1.day.ago) # => [#<Post>, #<Post>]
153
180
  ```
154
181
 
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`.
157
-
158
- There is also an `at` method on `Hoardable` itself for more complex and experimental temporal resource
159
- querying. See [Relationships](#relationships) for more.
160
-
161
- By default, `hoardable` will keep copies of records you have destroyed. You can query them specifically with:
162
-
163
- ```ruby
164
- PostVersion.trashed
165
- Post.version_class.trashed # <- same as above
166
- ```
182
+ This will return an ActiveRecord scoped query of all `Post` and `PostVersion` records that were
183
+ valid at that time, all cast as instances of `Post`. Updates to the versions table are forbidden in
184
+ this case by a database trigger.
167
185
 
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.
186
+ There is also `Hoardable.at` for more complex and experimental temporal resource querying. See
187
+ [Relationships](#relationships) for more.
171
188
 
172
- ### Tracking Contextual Data
189
+ ### Tracking contextual data
173
190
 
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:
191
+ You’ll often want to track contextual data about the creation of a version. There are 2 options that
192
+ can be provided for tracking this:
176
193
 
177
- - `:whodunit` - an identifier for who is responsible for creating the version
194
+ - `:whodunit` - an identifier for who/what is responsible for creating the version
178
195
  - `:meta` - any other contextual information you’d like to store along with the version
179
196
 
180
- This information is stored in a `jsonb` column. Each key’s value can be in the format of your choosing.
197
+ This information is stored in a `jsonb` column. Each value can be the data type of your choosing.
181
198
 
182
199
  One convenient way to assign contextual data to these is by defining a proc in an initializer, i.e.:
183
200
 
@@ -186,22 +203,14 @@ One convenient way to assign contextual data to these is by defining a proc in a
186
203
  Hoardable.whodunit = -> { Current.user&.id }
187
204
 
188
205
  # somewhere in your app code
189
- Current.user = User.find(123)
190
- post.update!(status: 'live')
191
- post.reload.versions.last.hoardable_whodunit # => 123
192
- ```
193
-
194
- You can also set this context manually as well:
195
-
196
- ```ruby
197
- Hoardable.meta = { note: "reverting due to accidental deletion" }
198
- post.update!(title: "We’re back!")
199
- Hoardable.meta = nil
200
- post.reload.versions.last.hoardable_meta['note'] # => "reverting due to accidental deletion"
206
+ Current.set(user: User.find(123)) do
207
+ post.update!(status: :live)
208
+ post.reload.versions.last.hoardable_whodunit # => 123
209
+ end
201
210
  ```
202
211
 
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`:
212
+ Another useful pattern would be to use `Hoardable.with` to set the context around a block. For
213
+ example, you could have the following in your `ApplicationController`:
205
214
 
206
215
  ```ruby
207
216
  class ApplicationController < ActionController::Base
@@ -213,30 +222,27 @@ class ApplicationController < ActionController::Base
213
222
  Hoardable.with(whodunit: current_user.id, meta: { request_uuid: request.uuid }) do
214
223
  yield
215
224
  end
216
- # `Hoardable.whodunit` and `Hoardable.meta` are back to nil or their previously set values
217
225
  end
218
226
  end
219
227
  ```
220
228
 
221
- `hoardable` will also automatically capture the ActiveRecord
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
+ [ActiveRecord changes](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes)
230
+ are also automatically captured along with the `operation` that caused the version (`update` or
231
+ `delete`). These values are available as:
225
232
 
226
233
  ```ruby
227
- version.changes
228
- version.hoardable_operation
229
- version.hoardable_event_uuid
234
+ version.changes # => { "title"=> ["Title", "New Title"] }
235
+ version.hoardable_operation # => "update"
230
236
  ```
231
237
 
232
238
  ### Model Callbacks
233
239
 
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.
240
+ Sometimes you might want to do something with a version after it gets inserted to the database. You
241
+ can access it in `after_versioned` callbacks on the source record as `hoardable_version`. These
242
+ happen within `ActiveRecord#save`'s transaction.
237
243
 
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
+ There are also `after_reverted` and `after_untrashed` callbacks available as well, which are called
245
+ on the source record after a version is reverted or untrashed.
240
246
 
241
247
  ```ruby
242
248
  class User
@@ -266,23 +272,24 @@ end
266
272
  The configurable options are:
267
273
 
268
274
  ```ruby
269
- Hoardable.enabled # => default true
270
- Hoardable.version_updates # => default true
271
- Hoardable.save_trash # => default true
275
+ Hoardable.enabled # => true
276
+ Hoardable.version_updates # => true
277
+ Hoardable.save_trash # => true
272
278
  ```
273
279
 
274
280
  `Hoardable.enabled` globally controls whether versions will be ever be created.
275
281
 
276
282
  `Hoardable.version_updates` globally controls whether versions get created on record updates.
277
283
 
278
- `Hoardable.save_trash` globally controls whether to create versions upon record deletion. When this is set to
279
- `false`, all versions of a record will be deleted when the record is destroyed.
284
+ `Hoardable.save_trash` globally controls whether to create versions upon source record deletion.
285
+ When this is set to `false`, all versions of a source record will be deleted when the record is
286
+ destroyed.
280
287
 
281
- If you would like to temporarily set a config setting, you can use `Hoardable.with`:
288
+ If you would like to temporarily set a config value, you can use `Hoardable.with`:
282
289
 
283
290
  ```ruby
284
291
  Hoardable.with(enabled: false) do
285
- post.update!(title: 'unimportant change to create version for')
292
+ post.update!(title: "replace title without creating a version")
286
293
  end
287
294
  ```
288
295
 
@@ -304,30 +311,40 @@ Comment.with_hoardable_config(version_updates: true) do
304
311
  end
305
312
  ```
306
313
 
307
- If a model-level option exists, it will use that. Otherwise, it will fall back to the global `Hoardable`
308
- config.
314
+ Model-level configuration overrides global configuration.
309
315
 
310
316
  ## Relationships
311
317
 
312
- ### Belongs To Trashable
318
+ ### `belongs_to`
313
319
 
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`:
320
+ Sometimes you’ll have a record that belongs to a parent record that you’ll trash. Now the child
321
+ record’s foreign key will point to the non-existent trashed version of the parent. If you would like
322
+ to have `belongs_to` resolve to the trashed parent model in this case, you can give it the option of
323
+ `trashable: true`:
318
324
 
319
325
  ```ruby
326
+ class Post
327
+ include Hoardable::Model
328
+ has_many :comments, dependent: nil
329
+ end
330
+
320
331
  class Comment
321
332
  include Hoardable::Associations # <- This includes is not required if this model already includes `Hoardable::Model`
322
333
  belongs_to :post, trashable: true
323
334
  end
335
+
336
+ post = Post.create!(title: "Title")
337
+ comment = post.comments.create!(body: "Comment")
338
+ post.destroy!
339
+ comment.post # => #<PostVersion>
324
340
  ```
325
341
 
326
- ### Hoardable Has Many & Has One
342
+ ### `has_many` & `has_one`
327
343
 
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 certain point in time. You accomplish
330
- this by adding `hoardable: true` to the `has_many` relationship and using the `Hoardable.at` method:
344
+ Sometimes you'll have a Hoardable record that `has_one` or `has_many` other Hoardable records and
345
+ you’ll want to know the state of both the parent record and the children at a certain point in time.
346
+ You can accomplish this by adding `hoardable: true` to the `has_many` relationship and using the
347
+ `Hoardable.at` method:
331
348
 
332
349
  ```ruby
333
350
  class Post
@@ -335,53 +352,48 @@ class Post
335
352
  has_many :comments, hoardable: true
336
353
  end
337
354
 
338
- def Comment
355
+ class Comment
339
356
  include Hoardable::Model
340
357
  end
341
358
 
342
- post = Post.create!(title: 'Title')
343
- comment1 = post.comments.create!(body: 'Comment')
344
- comment2 = post.comments.create!(body: 'Comment')
359
+ post = Post.create!(title: "Title")
360
+ comment1 = post.comments.create!(body: "Comment")
361
+ comment2 = post.comments.create!(body: "Comment")
345
362
  datetime = DateTime.current
363
+
346
364
  comment2.destroy!
347
- post.update!(title: 'New Title')
365
+ post.update!(title: "New Title")
348
366
  post_id = post.id # 1
349
367
 
350
368
  Hoardable.at(datetime) do
351
369
  post = Post.find(post_id)
352
- post.title # => 'Title'
370
+ post.title # => "Title"
353
371
  post.comments.size # => 2
354
- post.id # => 2
355
372
  post.version? # => true
373
+ post.id # => 2
356
374
  post.hoardable_id # => 1
357
375
  end
358
376
  ```
359
377
 
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`.
369
-
370
- _Note:_ `Hoardable.at` is still very experimental and is potentially not very performant for querying large
371
- data sets.
378
+ _*Note*:_ `Hoardable.at` is experimental and potentially not performant for querying very large data
379
+ sets.
372
380
 
373
381
  ### Cascading Untrashing
374
382
 
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:
383
+ Sometimes you’ll trash something that `has_many :children, dependent: :destroy` and if you untrash
384
+ the parent record, you’ll want to also untrash the children. Whenever a hoardable versions are
385
+ created, it will share a unique event UUID for all other versions created in the same database
386
+ transaction. That way, when you `untrash!` a record, you could find and `untrash!` records that were
387
+ trashed with it:
380
388
 
381
389
  ```ruby
390
+ class Comment < ActiveRecord::Base
391
+ include Hoardable::Model
392
+ end
393
+
382
394
  class Post < ActiveRecord::Base
383
395
  include Hoardable::Model
384
- has_many :comments, hoardable: true, dependent: :destroy # `Comment` also includes `Hoardable::Model`
396
+ has_many :comments, hoardable: true, dependent: :destroy
385
397
 
386
398
  after_untrashed do
387
399
  Comment
@@ -395,20 +407,21 @@ end
395
407
 
396
408
  ### Action Text
397
409
 
398
- Hoardable provides support for ActiveRecord models with `has_rich_text`. First, you must create a temporal
399
- table for `ActionText::RichText`:
410
+ Hoardable provides support for ActiveRecord models with `has_rich_text`. First, you must create a
411
+ temporal table for `ActionText::RichText`:
400
412
 
401
413
  ```
402
414
  bin/rails g hoardable:migration ActionText::RichText
403
415
  bin/rails db:migrate
404
416
  ```
405
417
 
406
- Then in your model include `Hoardable::Model` and provide the `hoardable: true` keyword to `has_rich_text`:
418
+ Then in your model include `Hoardable::Model` and provide the `hoardable: true` keyword to
419
+ `has_rich_text`:
407
420
 
408
421
  ```ruby
409
422
  class Post < ActiveRecord::Base
410
423
  include Hoardable::Model # or `Hoardable::Associations` if you don't need `PostVersion`
411
- has_rich_text :content, hoardable: true
424
+ has_rich_text :content, hoardable: true # or `has_hoardable_rich_text :content`
412
425
  end
413
426
  ```
414
427
 
@@ -425,65 +438,73 @@ Hoardable.at(datetime) do
425
438
  end
426
439
  ```
427
440
 
428
- ## Known Gotchas
441
+ ## Known gotchas
429
442
 
430
- ### Rails Fixtures
443
+ ### Rails fixtures
431
444
 
432
445
  Rails uses a method called
433
446
  [`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.
447
+ when inserting fixtures into the database. This disables PostgreSQL triggers, which Hoardable relies
448
+ on for assigning `hoardable_id` from the primary key’s value. If you would still like to use
449
+ fixtures, you must specify the primary key’s value and `hoardable_id` to the same identifier value
450
+ in the fixture.
439
451
 
440
- ## Gem Comparison
452
+ ## Gem comparison
441
453
 
442
454
  #### [`paper_trail`](https://github.com/paper-trail-gem/paper_trail)
443
455
 
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.
456
+ `paper_trail` is maybe the most popular and fully featured gem in this space. It works for other
457
+ database types than PostgeSQL. Bby default it stores all versions of all versioned models in a
458
+ single `versions` table. It stores changes in a `text`, `json`, or `jsonb` column. In order to
459
+ efficiently query the `versions` table, a `jsonb` column should be used, which can take up a lot of
460
+ space to index. Unless you customize your configuration, all `versions` for all models types are in
461
+ the same table which is inefficient if you are only interested in querying versions of a single
462
+ model. By contrast, `hoardable` stores versions in smaller, isolated, inherited tables with the same
463
+ database columns as their parents, which are more efficient for querying as well as auditing for
464
+ truncating and dropping. The concept of a temporal timeframe does not exist for a single version
465
+ since there is only a `created_at` timestamp.
453
466
 
454
467
  #### [`audited`](https://github.com/collectiveidea/audited)
455
468
 
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.
469
+ `audited` works in a similar manner as `paper_trail`. It stores all versions for all model types in
470
+ a single table, you must opt into using `jsonb` as the column type to store "changes", in case you
471
+ want to query them, and there is no concept of a temporal timeframe for a single version. It makes
472
+ opinionated decisions about contextual data requirements and stores them as top level data types on
473
+ the `audited` table.
460
474
 
461
475
  #### [`discard`](https://github.com/jhawthorn/discard)
462
476
 
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.
477
+ `discard` only covers soft-deletion. The act of "soft deleting" a record is only captured through
478
+ the time-stamping of a `discarded_at` column on the records table. There is no other capturing of
479
+ the event that caused the soft deletion unless you implement it yourself. Once the "discarded"
480
+ record is restored, the previous "discarded" awareness is lost. Since "discarded" records exist in
481
+ the same table as "undiscarded" records, you must explicitly omit the discarded records from queries
482
+ across your app to keep them from leaking in.
469
483
 
470
484
  #### [`paranoia`](https://github.com/rubysherpas/paranoia)
471
485
 
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.
486
+ `paranoia` also only covers soft-deletion. In their README, they recommend using `discard` instead
487
+ of `paranoia` because of the fact they override ActiveRecord’s `delete` and `destroy` methods.
488
+ `hoardable` employs callbacks to create trashed versions instead of overriding methods. Otherwise,
489
+ `paranoia` works similarly to `discard` in that it keeps deleted records in the same table and tags
490
+ them with a `deleted_at` timestamp. No other information about the soft-deletion event is stored.
477
491
 
478
492
  #### [`logidze`](https://github.com/palkan/logidze)
479
493
 
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.
494
+ `logidze` is an interesting versioning alternative that leverages the power of PostgreSQL triggers.
495
+ Instead of storing the previous versions or changes in a separate table, it stores them in a
496
+ proprietary JSON format directly on the database row of the record itself. If does not support soft
497
+ deletion.
483
498
 
484
- ## Contributing
499
+ ## Testing
485
500
 
486
- This gem still quite new and very open to feedback.
501
+ Hoardable is tested against a matrix of Ruby 3 versions and Rails 7 & 8. To run tests locally, run:
502
+
503
+ ```
504
+ rake
505
+ ```
506
+
507
+ ## Contributing
487
508
 
488
509
  Bug reports and pull requests are welcome on GitHub at https://github.com/waymondo/hoardable.
489
510
 
data/Rakefile CHANGED
@@ -1,16 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/gem_tasks'
4
- require 'rake/testtask'
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+ require "syntax_tree/rake_tasks"
5
6
 
6
7
  Rake::TestTask.new(:test) do |t|
7
- t.libs << 'test'
8
- t.libs << 'lib'
9
- t.test_files = FileList['test/**/test_*.rb']
8
+ t.libs << "test"
9
+ t.libs << "lib"
10
+ t.test_files = FileList["test/**/test_*.rb"]
10
11
  end
11
12
 
12
- require 'rubocop/rake_task'
13
+ SOURCE_FILES = %w[test/**/*.rb lib/**/*.rb Rakefile Gemfile bin/console hoardable.gemspec]
13
14
 
14
- RuboCop::RakeTask.new
15
+ SyntaxTree::Rake::CheckTask.new(:check) do |t|
16
+ t.source_files = SOURCE_FILES
17
+ t.print_width = 100
18
+ end
19
+
20
+ SyntaxTree::Rake::WriteTask.new(:write) do |t|
21
+ t.source_files = SOURCE_FILES
22
+ t.print_width = 100
23
+ end
24
+
25
+ task :typeprof do
26
+ `typeprof lib/hoardable.rb`
27
+ end
15
28
 
16
- task default: %i[test rubocop]
29
+ task default: %i[check test]
30
+ task pre_commit: %i[write typeprof]