hoardable 0.14.2 → 0.16.0

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