hoardable 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2b34416224e686978b85cd78c0a80eae6e09f727a87c58060355671c6af8334
4
- data.tar.gz: 401049a8d781e695fd691f1a9cf0c0ea67560c535efd773b738c40041ca80869
3
+ metadata.gz: 888659d9c6655e69c72fd8cf5b10b3697d5a81f432a44c8fa39984a05f4b23d9
4
+ data.tar.gz: ba850c1bf2363dd965a6e7780435d4feaf2ad417f2712f5f4635398ad5e9913d
5
5
  SHA512:
6
- metadata.gz: 3b39f34db9a87e2403b6a7529a035e0ccea464fbc6cda100b7069a2cc271e63112f0299dcae283e8b7c1d8f17e14892d58b9089e9e4b7fcc178cd94e808014a1
7
- data.tar.gz: e2a8b1e9c9e5362711b2a5170a74d6316a115c290bf285e0ff60f3eed6937a4e4838000b2042a19bc79a97f3d7970882894bb519f244f6bac1eee1d1e529acac
6
+ metadata.gz: 851d039e113df11834b18bb69edf786ba1d91598e408a45fab6ae7bb65efbc37264e4b803b8c8335f6839784f964f20d29c4c94b02aa6b994a21f3fd346ec162
7
+ data.tar.gz: 022a7ada38d996f8116a7ab8ed3d671e29055a8c75cc4155789ea3be135d87f24625ab67311c22f3b89b172320a973cbf203586407c6788750443bec90b461be
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 0.17.0
2
+
3
+ - Much improved performance of setting `hoardable_id` for versions.
4
+
5
+ ## 0.16.0
6
+
7
+ - Rails 8 support introduced
8
+
1
9
  ## 0.15.0
2
10
 
3
11
  - *Breaking Change* - Support for Ruby 2.7 and Rails 6.1 is dropped
data/Gemfile CHANGED
@@ -4,7 +4,7 @@ source "https://rubygems.org"
4
4
 
5
5
  gem "debug"
6
6
  if (rails_version = ENV["RAILS_VERSION"])
7
- gem "rails", "~> #{rails_version}.0"
7
+ gem "rails", "~> #{rails_version}"
8
8
  else
9
9
  gem "rails"
10
10
  end
data/README.md CHANGED
@@ -1,6 +1,6 @@
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 3+, Rails 7+, and PostgreSQL that allows for
3
+ Hoardable is an ActiveRecord extension for Ruby 3+, Rails 7+, and PostgreSQL 9+ that allows for
4
4
  versioning and soft-deletion of records through the use of _uni-temporal inherited tables_.
5
5
 
6
6
  [Temporal tables](https://en.wikipedia.org/wiki/Temporal_database) are a database design pattern
@@ -9,15 +9,13 @@ each database row has a time range that represents the row’s valid time range
9
9
  "uni-temporal".
10
10
 
11
11
  [Table inheritance](https://www.postgresql.org/docs/current/ddl-inherit.html) is a feature of
12
- PostgreSQL that allows a table to inherit all columns of a parent table. The descendant table’s
13
- schema will stay in sync with its parent. If a new column is added to or removed from the parent,
14
- the schema change is reflected on its descendants.
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.
15
15
 
16
16
  With these concepts combined, `hoardable` offers a model versioning and soft deletion system for
17
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 database level, while still familiar and convenient to use
20
- within Ruby on Rails.
18
+ ranges and contextual data.
21
19
 
22
20
  [👉 Documentation](https://www.rubydoc.info/gems/hoardable)
23
21
 
@@ -26,7 +24,7 @@ within Ruby on Rails.
26
24
  Add this line to your application's Gemfile:
27
25
 
28
26
  ```ruby
29
- gem 'hoardable'
27
+ gem "hoardable"
30
28
  ```
31
29
 
32
30
  Run `bundle install`, and then run:
@@ -36,10 +34,9 @@ bin/rails g hoardable:install
36
34
  bin/rails db:migrate
37
35
  ```
38
36
 
39
- ### Model Installation
37
+ ### Model installation
40
38
 
41
- You must include `Hoardable::Model` into an ActiveRecord model that you would like to hoard versions
42
- 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,24 +45,15 @@ 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
59
- the model specified in the migration generator above. If you want/need to specify this explicitly,
60
- you can do so:
61
-
62
- ```
63
- bin/rails g hoardable:migration Post --foreign-key-type uuid
64
- ```
65
-
66
55
  _*Note*:_ Creating an inherited table does not inherit the indexes from the parent table. If you
67
- need to query versions often, you should add appropriate indexes to the `_versions` tables. See
68
- [here](https://github.com/waymondo/hoardable/issues/30) for more info.
56
+ need to query versions often, you should add appropriate indexes to the `_versions` tables.
69
57
 
70
58
  ## Usage
71
59
 
@@ -119,33 +107,48 @@ original primary key.
119
107
 
120
108
  ```ruby
121
109
  post = Post.create!(title: "Title")
122
- post.id # => 1
123
110
  post.destroy!
124
111
  post.versions.size # => 1
125
112
  Post.find(post.id) # raises ActiveRecord::RecordNotFound
126
113
  trashed_post = post.versions.trashed.last
127
- trashed_post.id # => 2
128
114
  trashed_post.untrash!
129
115
  Post.find(post.id) # #<Post>
130
116
  ```
131
117
 
132
- _*Note*:_ You will notice above that both `posts` and `post_versions` pull from the same ID
133
- sequence. This allows for uniquely identifying source records and versions when results are mixed
134
- together. Both a source record and versions have an automatically managed `hoardable_id` that always
135
- represents the primary key value 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:
136
121
 
137
- ### Querying and Temporal Lookup
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
+ ```
138
133
 
139
- Including `Hoardable::Model` into your source model modifies its default scope to make sure you only
140
- query the parent table:
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:
141
138
 
142
139
  ```ruby
143
140
  Post.where(state: :draft).to_sql # => SELECT posts.* FROM ONLY posts WHERE posts.status = 'draft'
144
141
  ```
145
142
 
146
- _*Note*:_ If you are executing raw SQL, you will need to include the `ONLY` keyword you see above to
147
- the select statement if you do not wish to return versions in the results. Learn more about table
148
- inheritance in [the PostgreSQL documentation](https://www.postgresql.org/docs/current/ddl-inherit.html).
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
+ ```
150
+
151
+ Learn more about table inheritance in [the PostgreSQL documentation](https://www.postgresql.org/docs/current/ddl-inherit.html).
149
152
 
150
153
  Since a `PostVersion` is an `ActiveRecord` class, you can query them like another model resource:
151
154
 
@@ -153,6 +156,14 @@ Since a `PostVersion` is an `ActiveRecord` class, you can query them like anothe
153
156
  post.versions.where(state: :draft)
154
157
  ```
155
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
+
156
167
  If you want to look-up the version of a record at a specific time, you can use the `.at` method:
157
168
 
158
169
  ```ruby
@@ -169,24 +180,13 @@ Post.at(1.day.ago) # => [#<Post>, #<Post>]
169
180
  ```
170
181
 
171
182
  This will return an ActiveRecord scoped query of all `Post` and `PostVersion` records that were
172
- valid at that time, all cast as instances of `Post`.
173
-
174
- There is also an `at` method on `Hoardable` itself for more complex and experimental temporal
175
- resource querying. See [Relationships](#relationships) for more.
176
-
177
- By default, `hoardable` will keep copies of records you have destroyed. You can query them
178
- specifically with:
179
-
180
- ```ruby
181
- PostVersion.trashed.where(user_id: user.id)
182
- Post.version_class.trashed.where(user_id: user.id) # <- same as above
183
- ```
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.
184
185
 
185
- _*Note*:_ A `Version` is not created upon initial source model creation. To accurately track the
186
- beginning of the first temporal period, you will need to ensure the source model table has a
187
- `created_at` timestamp 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.
188
188
 
189
- ### Tracking Contextual Data
189
+ ### Tracking contextual data
190
190
 
191
191
  You’ll often want to track contextual data about the creation of a version. There are 2 options that
192
192
  can be provided for tracking this:
@@ -194,8 +194,7 @@ can be provided for tracking this:
194
194
  - `:whodunit` - an identifier for who/what is responsible for creating the version
195
195
  - `:meta` - any other contextual information you’d like to store along with the version
196
196
 
197
- This information is stored in a `jsonb` column. Each key’s value can be in the format of your
198
- choosing.
197
+ This information is stored in a `jsonb` column. Each value can be the data type of your choosing.
199
198
 
200
199
  One convenient way to assign contextual data to these is by defining a proc in an initializer, i.e.:
201
200
 
@@ -210,16 +209,7 @@ Current.set(user: User.find(123)) do
210
209
  end
211
210
  ```
212
211
 
213
- You can also set these context values manually as well:
214
-
215
- ```ruby
216
- Hoardable.meta = {note: "reverting due to accidental deletion"}
217
- post.update!(title: "We’re back!")
218
- Hoardable.meta = nil
219
- post.reload.versions.last.hoardable_meta['note'] # => "reverting due to accidental deletion"
220
- ```
221
-
222
- A more useful pattern would be to use `Hoardable.with` to set the context around a block. For
212
+ Another useful pattern would be to use `Hoardable.with` to set the context around a block. For
223
213
  example, you could have the following in your `ApplicationController`:
224
214
 
225
215
  ```ruby
@@ -229,31 +219,42 @@ class ApplicationController < ActionController::Base
229
219
  private
230
220
 
231
221
  def use_hoardable_context
232
- Hoardable.with(whodunit: current_user.id, meta: {request_uuid: request.uuid}) do
222
+ Hoardable.with(whodunit: current_user.id, meta: { request_uuid: request.uuid }) do
233
223
  yield
234
224
  end
235
- # `Hoardable.whodunit` and `Hoardable.meta` are back to nil or their previously set values
236
225
  end
237
226
  end
238
227
  ```
239
228
 
240
- `hoardable` will also automatically capture the ActiveRecord
241
- [changes](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes) hash, the
242
- `operation` that cause the version (`update` or `delete`), and it will also tag all versions created
243
- in the same database transaction with a shared and unique `event_uuid` for that transaction. These
244
- values 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:
245
232
 
246
233
  ```ruby
247
- version.changes
248
- version.hoardable_operation
249
- version.hoardable_event_uuid
234
+ version.changes # => { "title"=> ["Title", "New Title"] }
235
+ version.hoardable_operation # => "update"
250
236
  ```
251
237
 
238
+ ### Overriding the temporal range
239
+
240
+ When calculating the temporal range for a given version, the default upper bound is `Time.now.utc`.
241
+
242
+ You can, however, use the `Hoardable.travel_to` class method to specify a custom upper bound for the time range. This allows
243
+ you to specify the datetime that a particular change should be recorded at by passing a block:
244
+
245
+ ```ruby
246
+ Hoardable.travel_to(2.weeks.ago) do
247
+ post.destroy!
248
+ end
249
+ ```
250
+
251
+ Note: If the provided datetime pre-dates the calculated lower bound then an `InvalidTemporalUpperBoundError` will be raised.
252
+
252
253
  ### Model Callbacks
253
254
 
254
255
  Sometimes you might want to do something with a version after it gets inserted to the database. You
255
256
  can access it in `after_versioned` callbacks on the source record as `hoardable_version`. These
256
- happen within `ActiveRecord`’s `.save`, which is enclosed in an ActiveRecord transaction.
257
+ happen within `ActiveRecord#save`'s transaction.
257
258
 
258
259
  There are also `after_reverted` and `after_untrashed` callbacks available as well, which are called
259
260
  on the source record after a version is reverted or untrashed.
@@ -286,9 +287,9 @@ end
286
287
  The configurable options are:
287
288
 
288
289
  ```ruby
289
- Hoardable.enabled # => default true
290
- Hoardable.version_updates # => default true
291
- Hoardable.save_trash # => default true
290
+ Hoardable.enabled # => true
291
+ Hoardable.version_updates # => true
292
+ Hoardable.save_trash # => true
292
293
  ```
293
294
 
294
295
  `Hoardable.enabled` globally controls whether versions will be ever be created.
@@ -299,7 +300,7 @@ Hoardable.save_trash # => default true
299
300
  When this is set to `false`, all versions of a source record will be deleted when the record is
300
301
  destroyed.
301
302
 
302
- If you would like to temporarily set a config setting, you can use `Hoardable.with`:
303
+ If you would like to temporarily set a config value, you can use `Hoardable.with`:
303
304
 
304
305
  ```ruby
305
306
  Hoardable.with(enabled: false) do
@@ -325,12 +326,11 @@ Comment.with_hoardable_config(version_updates: true) do
325
326
  end
326
327
  ```
327
328
 
328
- If a model-level option exists, it will use that. Otherwise, it will fall back to the global
329
- `Hoardable` config.
329
+ Model-level configuration overrides global configuration.
330
330
 
331
331
  ## Relationships
332
332
 
333
- ### Belongs To Trashable
333
+ ### `belongs_to`
334
334
 
335
335
  Sometimes you’ll have a record that belongs to a parent record that you’ll trash. Now the child
336
336
  record’s foreign key will point to the non-existent trashed version of the parent. If you would like
@@ -338,17 +338,28 @@ to have `belongs_to` resolve to the trashed parent model in this case, you can g
338
338
  `trashable: true`:
339
339
 
340
340
  ```ruby
341
+ class Post
342
+ include Hoardable::Model
343
+ has_many :comments, dependent: nil
344
+ end
345
+
341
346
  class Comment
342
347
  include Hoardable::Associations # <- This includes is not required if this model already includes `Hoardable::Model`
343
348
  belongs_to :post, trashable: true
344
349
  end
350
+
351
+ post = Post.create!(title: "Title")
352
+ comment = post.comments.create!(body: "Comment")
353
+ post.destroy!
354
+ comment.post # => #<PostVersion>
345
355
  ```
346
356
 
347
- ### Hoardable Has Many & Has One
357
+ ### `has_many` & `has_one`
348
358
 
349
- Sometimes you'll have a Hoardable record that `has_one` or `has_many` other Hoardable records and you will
350
- want to know the state of both the parent record and the children at a certain point in time. You accomplish
351
- this by adding `hoardable: true` to the `has_many` relationship and using the `Hoardable.at` method:
359
+ Sometimes you'll have a Hoardable record that `has_one` or `has_many` other Hoardable records and
360
+ you’ll want to know the state of both the parent record and the children at a certain point in time.
361
+ You can accomplish this by adding `hoardable: true` to the `has_many` relationship and using the
362
+ `Hoardable.at` method:
352
363
 
353
364
  ```ruby
354
365
  class Post
@@ -364,6 +375,7 @@ post = Post.create!(title: "Title")
364
375
  comment1 = post.comments.create!(body: "Comment")
365
376
  comment2 = post.comments.create!(body: "Comment")
366
377
  datetime = DateTime.current
378
+
367
379
  comment2.destroy!
368
380
  post.update!(title: "New Title")
369
381
  post_id = post.id # 1
@@ -372,37 +384,31 @@ Hoardable.at(datetime) do
372
384
  post = Post.find(post_id)
373
385
  post.title # => "Title"
374
386
  post.comments.size # => 2
375
- post.id # => 2
376
387
  post.version? # => true
388
+ post.id # => 2
377
389
  post.hoardable_id # => 1
378
390
  end
379
391
  ```
380
392
 
381
- You’ll notice above that the `post` within the `#at` block is actually a temporal `post_version`,
382
- since it has been subsequently updated and has a different id - it is reified as a `post` for the
383
- purposes of your business logic (serialization, rendering views, exporting, etc). Don’t fret - you
384
- will not be able to commit any updates to the version, even though it is masquerading as a `Post`
385
- because a database trigger won’t allow it.
386
-
387
- If you are ever unsure if a Hoardable record is a source record or a version, you can be sure by
388
- calling `version?` on it. If you want to get the true original source record ID, you can call
389
- `hoardable_id`.
390
-
391
- _*Note*:_ `Hoardable.at` is still very experimental and is potentially not performant for querying
392
- large data sets.
393
+ _*Note*:_ `Hoardable.at` is experimental and potentially not performant for querying very large data
394
+ sets.
393
395
 
394
396
  ### Cascading Untrashing
395
397
 
396
398
  Sometimes you’ll trash something that `has_many :children, dependent: :destroy` and if you untrash
397
- the parent record, you’ll want to also untrash the children. Whenever a hoardable version is created
398
- in a database transaction, it will create or re-use a unique event UUID for the current database
399
- transaction and tag all versions created with it. That way, when you `untrash!` a record, you could
400
- find and `untrash!` records that were trashed with it:
399
+ the parent record, you’ll want to also untrash the children. Whenever a hoardable versions are
400
+ created, it will share a unique event UUID for all other versions created in the same database
401
+ transaction. That way, when you `untrash!` a record, you could find and `untrash!` records that were
402
+ trashed with it:
401
403
 
402
404
  ```ruby
405
+ class Comment < ActiveRecord::Base
406
+ include Hoardable::Model
407
+ end
408
+
403
409
  class Post < ActiveRecord::Base
404
410
  include Hoardable::Model
405
- has_many :comments, hoardable: true, dependent: :destroy # `Comment` also includes `Hoardable::Model`
411
+ has_many :comments, hoardable: true, dependent: :destroy
406
412
 
407
413
  after_untrashed do
408
414
  Comment
@@ -430,8 +436,7 @@ Then in your model include `Hoardable::Model` and provide the `hoardable: true`
430
436
  ```ruby
431
437
  class Post < ActiveRecord::Base
432
438
  include Hoardable::Model # or `Hoardable::Associations` if you don't need `PostVersion`
433
- has_rich_text :content, hoardable: true
434
- # alternately, this could be `has_hoardable_rich_text :content`
439
+ has_rich_text :content, hoardable: true # or `has_hoardable_rich_text :content`
435
440
  end
436
441
  ```
437
442
 
@@ -448,19 +453,18 @@ Hoardable.at(datetime) do
448
453
  end
449
454
  ```
450
455
 
451
- ## Known Gotchas
456
+ ## Known gotchas
452
457
 
453
- ### Rails Fixtures
458
+ ### Rails fixtures
454
459
 
455
460
  Rails uses a method called
456
461
  [`disable_referential_integrity`](https://github.com/rails/rails/blob/06e9fbd954ab113108a7982357553fdef285bff1/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb#L7)
457
462
  when inserting fixtures into the database. This disables PostgreSQL triggers, which Hoardable relies
458
463
  on for assigning `hoardable_id` from the primary key’s value. If you would still like to use
459
464
  fixtures, you must specify the primary key’s value and `hoardable_id` to the same identifier value
460
- in the fixture. This is not an issue with fixture replacement libraries like `factory_bot` or
461
- [`world_factory`](https://github.com/FutureProofRetail/world_factory) however.
465
+ in the fixture.
462
466
 
463
- ## Gem Comparison
467
+ ## Gem comparison
464
468
 
465
469
  #### [`paper_trail`](https://github.com/paper-trail-gem/paper_trail)
466
470
 
@@ -507,11 +511,18 @@ Instead of storing the previous versions or changes in a separate table, it stor
507
511
  proprietary JSON format directly on the database row of the record itself. If does not support soft
508
512
  deletion.
509
513
 
514
+ ## Testing
515
+
516
+ Hoardable is tested against a matrix of Ruby 3 versions and Rails 7 & 8. To run tests locally, run:
517
+
518
+ ```
519
+ rake
520
+ ```
521
+
510
522
  ## Contributing
511
523
 
512
524
  Bug reports and pull requests are welcome on GitHub at https://github.com/waymondo/hoardable.
513
525
 
514
526
  ## License
515
527
 
516
- The gem is available as open source under the terms of the [MIT
517
- License](https://opensource.org/licenses/MIT).
528
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,7 @@
1
+ CREATE OR REPLACE FUNCTION <%= function_name %>() RETURNS trigger
2
+ LANGUAGE plpgsql AS
3
+ $$
4
+ BEGIN
5
+ NEW.hoardable_id = NEW.<%= primary_key %>;
6
+ RETURN NEW;
7
+ END;$$;
@@ -25,7 +25,7 @@ module Hoardable
25
25
 
26
26
  def create_functions
27
27
  Dir
28
- .glob(File.join(__dir__, "functions", "*.sql"))
28
+ .glob(File.join(__dir__, "install_functions", "*.sql"))
29
29
  .each do |file_path|
30
30
  file_name = file_path.match(%r{([^/]+)\.sql})[1]
31
31
  template file_path, "db/functions/#{file_name}_v01.sql"
@@ -36,7 +36,21 @@ module Hoardable
36
36
  end
37
37
  end
38
38
 
39
+ def create_function
40
+ template("../functions/set_hoardable_id.sql", "db/functions/#{function_name}_v01.sql")
41
+ end
42
+
39
43
  no_tasks do
44
+ def function_name
45
+ "hoardable_set_hoardable_id_from_#{primary_key}"
46
+ end
47
+
48
+ def table_name
49
+ class_name.singularize.constantize.table_name
50
+ rescue StandardError
51
+ super
52
+ end
53
+
40
54
  def foreign_key_type
41
55
  options[:foreign_key_type] ||
42
56
  class_name.singularize.constantize.columns.find { |col| col.name == primary_key }.sql_type
@@ -4,7 +4,6 @@ class InstallHoardable < ActiveRecord::Migration[<%= ActiveRecord::Migration.cur
4
4
  def change
5
5
  <% if postgres_version < 13 %>enable_extension :pgcrypto
6
6
  <% end %>create_function :hoardable_prevent_update_id
7
- create_function :hoardable_source_set_id
8
7
  create_function :hoardable_version_prevent_update
9
8
  create_enum :hoardable_operation, %w[update delete insert]
10
9
  end
@@ -6,7 +6,7 @@ class Create<%= class_name.singularize.delete(':') %>Versions < ActiveRecord::Mi
6
6
  add_index :<%= table_name %>, :hoardable_id
7
7
  create_table(
8
8
  :<%= singularized_table_name %>_versions,
9
- id: false,
9
+ id: false,
10
10
  options: 'INHERITS (<%= table_name %>)',
11
11
  ) do |t|
12
12
  t.jsonb :_data
@@ -25,6 +25,7 @@ class Create<%= class_name.singularize.delete(':') %>Versions < ActiveRecord::Mi
25
25
  :<%= singularized_table_name %>_versions_prevent_update,
26
26
  on: :<%= singularized_table_name %>_versions
27
27
  )
28
+ create_function :<%= function_name %>
28
29
  create_trigger :<%= table_name %>_set_hoardable_id, on: :<%= table_name %>
29
30
  create_trigger :<%= table_name %>_prevent_update_hoardable_id, on: :<%= table_name %>
30
31
  change_column_null :<%= table_name %>, :hoardable_id, false
@@ -1,3 +1,3 @@
1
1
  CREATE TRIGGER <%= table_name %>_set_hoardable_id
2
2
  BEFORE INSERT ON <%= table_name %> FOR EACH ROW
3
- EXECUTE PROCEDURE hoardable_source_set_id();
3
+ EXECUTE PROCEDURE <%= function_name %>();
@@ -40,10 +40,16 @@ module Hoardable
40
40
  end
41
41
 
42
42
  private def hoardable_maybe_add_only(o, collector)
43
- return unless o.left.instance_variable_get("@klass").in?(Hoardable::REGISTRY)
44
- return if Hoardable.instance_variable_get("@at")
43
+ left = o.left
45
44
 
46
- collector << "ONLY "
45
+ if left.is_a?(Arel::Nodes::TableAlias)
46
+ hoardable_maybe_add_only(left, collector)
47
+ else
48
+ return unless left.instance_variable_get("@klass").in?(Hoardable::REGISTRY)
49
+ return if Hoardable.instance_variable_get("@at")
50
+
51
+ collector << "ONLY "
52
+ end
47
53
  end
48
54
  end
49
55
  end
@@ -93,7 +93,14 @@ module Hoardable
93
93
  end
94
94
 
95
95
  def initialize_temporal_range
96
- ((previous_temporal_tsrange_end || hoardable_source_epoch)..Time.now.utc)
96
+ upper_bound = Hoardable.instance_variable_get("@travel_to") || Time.now.utc
97
+ lower_bound = (previous_temporal_tsrange_end || hoardable_source_epoch)
98
+
99
+ if upper_bound < lower_bound
100
+ raise InvalidTemporalUpperBoundError.new(upper_bound, lower_bound)
101
+ end
102
+
103
+ (lower_bound..upper_bound)
97
104
  end
98
105
 
99
106
  def initialize_hoardable_data
@@ -81,6 +81,16 @@ module Hoardable
81
81
  @at = nil
82
82
  end
83
83
 
84
+ # Allows calling code to set the upper bound for the temporal range for recorded audits.
85
+ #
86
+ # @param datetime [DateTime] the datetime to temporally record versions at
87
+ def travel_to(datetime)
88
+ @travel_to = datetime
89
+ yield
90
+ ensure
91
+ @travel_to = nil
92
+ end
93
+
84
94
  # @!visibility private
85
95
  def logger
86
96
  @logger ||= ActiveSupport::TaggedLogging.new(Logger.new($stdout))
@@ -101,7 +111,7 @@ module Hoardable
101
111
  initializer "hoardable.schema_statements" do
102
112
  ActiveSupport.on_load(:active_record_postgresqladapter) do
103
113
  # We need to control the table dumping order of tables, so revert these to just +super+
104
- Fx::SchemaDumper::Trigger.module_eval("def tables(streams); super; end")
114
+ Fx::SchemaDumper.module_eval("def tables(streams); super; end")
105
115
 
106
116
  ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaDumper.prepend(SchemaDumper)
107
117
  ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaStatements.prepend(SchemaStatements)
@@ -24,4 +24,14 @@ module Hoardable
24
24
  LOG
25
25
  end
26
26
  end
27
+
28
+ # An error to be raised when the provided temporal upper bound is before the calcualated lower bound.
29
+ class InvalidTemporalUpperBoundError < Error
30
+ def initialize(upper, lower)
31
+ super(<<~LOG)
32
+ 'The supplied value to `Hoardable.travel_to` (#{upper}) is before the calculated lower bound (#{lower}).
33
+ You must provide a datetime > the lower bound.
34
+ LOG
35
+ end
36
+ end
27
37
  end
@@ -9,13 +9,13 @@ module Hoardable
9
9
  def has_one(*args)
10
10
  options = args.extract_options!
11
11
  hoardable = options.delete(:hoardable)
12
- association = super(*args, **options)
13
12
  name = args.first
14
- return unless hoardable || association[name.to_s].options[:class_name].match?(/RichText$/)
13
+ association = super(*args, **options).symbolize_keys[name]
14
+ return unless hoardable || (association.options[:class_name].match?(/RichText$/))
15
15
 
16
16
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
17
17
  def #{name}
18
- reflection = _reflections['#{name}']
18
+ reflection = _reflections.symbolize_keys[:#{name}]
19
19
  return super if reflection.klass.name.match?(/^ActionText/)
20
20
  return super unless (timestamp = hoardable_client.has_one_at_timestamp)
21
21
 
@@ -4,7 +4,7 @@ module Hoardable
4
4
  module SchemaStatements
5
5
  def table_options(table_name)
6
6
  options = super || {}
7
- if inherited_table_names = parent_table_names(table_name)
7
+ if !options[:options] && (inherited_table_names = parent_table_names(table_name))
8
8
  options[:options] = "INHERITS (#{inherited_table_names.join(", ")})"
9
9
  end
10
10
  options
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hoardable
4
- VERSION = "0.15.0"
4
+ VERSION = "0.17.0"
5
5
  end
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.15.0
4
+ version: 0.17.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: 2024-01-12 00:00:00.000000000 Z
11
+ date: 2024-11-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -17,9 +17,6 @@ dependencies:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '7'
20
- - - "<"
21
- - !ruby/object:Gem::Version
22
- version: '8'
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
@@ -27,9 +24,6 @@ dependencies:
27
24
  - - ">="
28
25
  - !ruby/object:Gem::Version
29
26
  version: '7'
30
- - - "<"
31
- - !ruby/object:Gem::Version
32
- version: '8'
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: activesupport
35
29
  requirement: !ruby/object:Gem::Requirement
@@ -37,9 +31,6 @@ dependencies:
37
31
  - - ">="
38
32
  - !ruby/object:Gem::Version
39
33
  version: '7'
40
- - - "<"
41
- - !ruby/object:Gem::Version
42
- version: '8'
43
34
  type: :runtime
44
35
  prerelease: false
45
36
  version_requirements: !ruby/object:Gem::Requirement
@@ -47,9 +38,6 @@ dependencies:
47
38
  - - ">="
48
39
  - !ruby/object:Gem::Version
49
40
  version: '7'
50
- - - "<"
51
- - !ruby/object:Gem::Version
52
- version: '8'
53
41
  - !ruby/object:Gem::Dependency
54
42
  name: railties
55
43
  requirement: !ruby/object:Gem::Requirement
@@ -57,9 +45,6 @@ dependencies:
57
45
  - - ">="
58
46
  - !ruby/object:Gem::Version
59
47
  version: '7'
60
- - - "<"
61
- - !ruby/object:Gem::Version
62
- version: '8'
63
48
  type: :runtime
64
49
  prerelease: false
65
50
  version_requirements: !ruby/object:Gem::Requirement
@@ -67,16 +52,13 @@ dependencies:
67
52
  - - ">="
68
53
  - !ruby/object:Gem::Version
69
54
  version: '7'
70
- - - "<"
71
- - !ruby/object:Gem::Version
72
- version: '8'
73
55
  - !ruby/object:Gem::Dependency
74
56
  name: fx
75
57
  requirement: !ruby/object:Gem::Requirement
76
58
  requirements:
77
59
  - - ">="
78
60
  - !ruby/object:Gem::Version
79
- version: '0.8'
61
+ version: '0.9'
80
62
  - - "<"
81
63
  - !ruby/object:Gem::Version
82
64
  version: '1'
@@ -86,7 +68,7 @@ dependencies:
86
68
  requirements:
87
69
  - - ">="
88
70
  - !ruby/object:Gem::Version
89
- version: '0.8'
71
+ version: '0.9'
90
72
  - - "<"
91
73
  - !ruby/object:Gem::Version
92
74
  version: '1'
@@ -124,9 +106,9 @@ files:
124
106
  - LICENSE.txt
125
107
  - README.md
126
108
  - Rakefile
127
- - lib/generators/hoardable/functions/hoardable_prevent_update_id.sql
128
- - lib/generators/hoardable/functions/hoardable_source_set_id.sql
129
- - lib/generators/hoardable/functions/hoardable_version_prevent_update.sql
109
+ - lib/generators/hoardable/functions/set_hoardable_id.sql
110
+ - lib/generators/hoardable/install_functions/hoardable_prevent_update_id.sql
111
+ - lib/generators/hoardable/install_functions/hoardable_version_prevent_update.sql
130
112
  - lib/generators/hoardable/install_generator.rb
131
113
  - lib/generators/hoardable/migration_generator.rb
132
114
  - lib/generators/hoardable/templates/install.rb.erb
@@ -177,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
177
159
  - !ruby/object:Gem::Version
178
160
  version: '0'
179
161
  requirements: []
180
- rubygems_version: 3.5.3
162
+ rubygems_version: 3.5.6
181
163
  signing_key:
182
164
  specification_version: 4
183
165
  summary: An ActiveRecord extension for versioning and soft-deletion of records in
@@ -1,18 +0,0 @@
1
- CREATE OR REPLACE FUNCTION hoardable_source_set_id() RETURNS trigger
2
- LANGUAGE plpgsql AS
3
- $$
4
- DECLARE
5
- _pk information_schema.constraint_column_usage.column_name%TYPE;
6
- _id _pk%TYPE;
7
- BEGIN
8
- SELECT c.column_name
9
- FROM information_schema.table_constraints t
10
- JOIN information_schema.constraint_column_usage c
11
- ON c.constraint_name = t.constraint_name
12
- WHERE c.table_name = TG_TABLE_NAME AND t.constraint_type = 'PRIMARY KEY'
13
- LIMIT 1
14
- INTO _pk;
15
- EXECUTE format('SELECT $1.%I', _pk) INTO _id USING NEW;
16
- NEW.hoardable_id = _id;
17
- RETURN NEW;
18
- END;$$;