hoardable 0.15.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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;$$;