hoardable 0.10.1 → 0.12.2

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: dad9a103d70ce10905528d1d7e56e67a4a8fb89e675b3097165ccede6262a015
4
- data.tar.gz: a2dc95873a08175254cca315caa3de9b087674df1e4dadd17f42fb26a9080a9d
3
+ metadata.gz: eec6cda69bb2bbb1aeec6a0c460881c33abbe1de123ef9178d2c16378374b2bd
4
+ data.tar.gz: f3f57007e08f958017483dfffd0275b5b70c0ef1522984ea78da4c595a9e5506
5
5
  SHA512:
6
- metadata.gz: 02b542ba1fe562750eb16bf6f310b08e2340610e01e7db68eaedb9185e4da4a1d7e7a5a42885fb09376759bf936b8b527de4bedec8399b71119541162e57604f
7
- data.tar.gz: 77edd48d420fea18333996280d6e31e0daba198b96e67fdc49b4fc7fe1884cb86cc88416e1f486691e2292465818fb50e3c4fc4367d83cc6d751d0adcb39acf6
6
+ metadata.gz: 04a13fec8198d4c4fda05b5364a7925755e7f2708004e15a564f243b64836577f4393cb502e7dd7d88ea123df29909ccb36a1d603cae8b2e711958250dc45763
7
+ data.tar.gz: 34a59cb91ed643784e4aa6ab88abac2d3e7a59a59945bcdce6e849aa441bc1490a6870c2661f65c31d246e4cb87bb39c4502e6cb8ce05cb3c42a6ad0e56f1140
data/Gemfile CHANGED
@@ -5,6 +5,7 @@ source 'https://rubygems.org'
5
5
  gem 'benchmark-ips', '~> 2.10'
6
6
  gem 'debug', '~> 1.6'
7
7
  gem 'minitest', '~> 5.0'
8
+ gem 'rails', '>= 6.1'
8
9
  gem 'rake', '~> 13.0'
9
10
  gem 'rubocop', '~> 1.21'
10
11
  gem 'rubocop-minitest', '~> 0.20'
data/README.md CHANGED
@@ -29,14 +29,19 @@ Add this line to your application's Gemfile:
29
29
  gem 'hoardable'
30
30
  ```
31
31
 
32
- And then execute `bundle install`.
33
-
34
- If you would like to generate an initializer with the global [configuration](#configuration) options:
32
+ Run `bundle install`, and then run:
35
33
 
36
34
  ```
37
- rails g hoardable:initializer
35
+ bin/rails g hoardable:install
36
+ bin/rails db:migrate
38
37
  ```
39
38
 
39
+ This will generate a PostgreSQL function and an initiailzer.
40
+
41
+ _Note:_ It is recommended to set `config.active_record.schema_format = :sql` in `application.rb`, so
42
+ that the function and triggers in the migrations that prevent updates to the versions table get
43
+ captured in your schema.
44
+
40
45
  ### Model Installation
41
46
 
42
47
  You must include `Hoardable::Model` into an ActiveRecord model that you would like to hoard versions
@@ -66,10 +71,6 @@ explicitly, you can do so:
66
71
  bin/rails g hoardable:migration Post --foreign-key-type uuid
67
72
  ```
68
73
 
69
- _Note:_ If you are on Rails 6.1, you might want to set `config.active_record.schema_format = :sql`
70
- in `application.rb`, so that the enum type is captured in your schema dump. This is not required in
71
- Rails 7.
72
-
73
74
  _Note:_ Creating an inherited table does not copy over the indexes from the parent table. If you
74
75
  need to query versions often, you should add appropriate indexes to the `_versions` tables.
75
76
 
@@ -83,9 +84,9 @@ of that model. As we continue our example from above:
83
84
  ```
84
85
  $ irb
85
86
  >> Post
86
- => Post(id: integer, body: text, user_id: integer, created_at: datetime)
87
+ => Post(id: integer, body: text, user_id: integer, created_at: datetime, hoardable_id: integer)
87
88
  >> PostVersion
88
- => PostVersion(id: integer, body: text, user_id: integer, created_at: datetime, _data: jsonb, _during: tsrange, hoardable_source_id: integer)
89
+ => PostVersion(id: integer, body: text, user_id: integer, created_at: datetime, hoardable_id: integer, _data: jsonb, _during: tsrange)
89
90
  ```
90
91
 
91
92
  A `Post` now `has_many :versions`. With the default configuration, whenever an update and deletion
@@ -143,7 +144,7 @@ If you want to look-up the version of a record at a specific time, you can use t
143
144
  ```ruby
144
145
  post.at(1.day.ago) # => #<PostVersion>
145
146
  # or you can use the scope on the version model class
146
- PostVersion.at(1.day.ago).find_by(hoardable_source_id: post.id) # => #<PostVersion>
147
+ PostVersion.at(1.day.ago).find_by(hoardable_id: post.id) # => #<PostVersion>
147
148
  ```
148
149
 
149
150
  The source model class also has an `.at` method:
@@ -356,7 +357,7 @@ Hoardable.at(datetime) do
356
357
  post.comments.size # => 2
357
358
  post.id # => 2
358
359
  post.version? # => true
359
- post.hoardable_source_id # => 1
360
+ post.hoardable_id # => 1
360
361
  end
361
362
  ```
362
363
 
@@ -369,9 +370,9 @@ version, even though it is masquerading as a `Post`.
369
370
 
370
371
  If you are ever unsure if a Hoardable record is a "source" or a "version", you can be sure by
371
372
  calling `version?` on it. If you want to get the true original source record ID, you can call
372
- `hoardable_source_id`.
373
+ `hoardable_id`.
373
374
 
374
- Sometimes you’ll trash something that `has_many_hoardable :children, dependent: :destroy` and want
375
+ Sometimes you’ll trash something that `has_many :children, dependent: :destroy` and want
375
376
  to untrash everything in a similar dependent manner. Whenever a hoardable version is created in a
376
377
  database transaction, it will create or re-use a unique event UUID for that transaction and tag all
377
378
  versions created with it. That way, when you `untrash!` a record, you can find and `untrash!`
@@ -380,7 +381,7 @@ records that were trashed with it:
380
381
  ```ruby
381
382
  class Post < ActiveRecord::Base
382
383
  include Hoardable::Model
383
- has_many_hoardable :comments, dependent: :destroy # `Comment` also includes `Hoardable::Model`
384
+ has_many :comments, hoardable: true, dependent: :destroy # `Comment` also includes `Hoardable::Model`
384
385
 
385
386
  after_untrashed do
386
387
  Comment
@@ -392,9 +393,42 @@ class Post < ActiveRecord::Base
392
393
  end
393
394
  ```
394
395
 
396
+ ## Action Text
397
+
398
+ Hoardable provides support for ActiveRecord models with `has_rich_text`. First, you must create a
399
+ temporal table for `ActionText::RichText`:
400
+
401
+ ```
402
+ bin/rails g hoardable:migration ActionText::RichText
403
+ bin/rails db:migrate
404
+ ```
405
+
406
+ Then in your model, include `Hoardable::Model` and provide the `hoardable: true` keyword to
407
+ `has_rich_text`:
408
+
409
+ ``` ruby
410
+ class Post < ActiveRecord::Base
411
+ include Hoardable::Model # or `Hoardable::Associations` if you don't need `PostVersion`
412
+ has_rich_text :content, hoardable: true
413
+ end
414
+ ```
415
+
416
+ Now the `rich_text_content` relationship will be managed as a Hoardable `has_one` relationship:
417
+
418
+ ``` ruby
419
+ post = Post.create!(content: '<div>Hello World</div>')
420
+ datetime = DateTime.current
421
+ post.update!(content: '<div>Goodbye Cruel World</div>')
422
+ post.content.versions.size # => 1
423
+ post.content.to_plain_text # => 'Goodbye Cruel World'
424
+ Hoardable.at(datetime) do
425
+ post.content.to_plain_text # => 'Hello World'
426
+ end
427
+ ```
428
+
395
429
  ## Gem Comparison
396
430
 
397
- ### [`paper_trail`](https://github.com/paper-trail-gem/paper_trail)
431
+ #### [`paper_trail`](https://github.com/paper-trail-gem/paper_trail)
398
432
 
399
433
  `paper_trail` is maybe the most popular and fully featured gem in this space. It works for other
400
434
  database types than PostgeSQL and (by default) stores all versions of all versioned models in a
@@ -407,7 +441,7 @@ same database columns as their parents, which are more efficient for querying as
407
441
  for truncating and dropping. The concept of a `temporal` time-frame does not exist for a single
408
442
  version since there is only a `created_at` timestamp.
409
443
 
410
- ### [`audited`](https://github.com/collectiveidea/audited)
444
+ #### [`audited`](https://github.com/collectiveidea/audited)
411
445
 
412
446
  `audited` works in a similar manner as `paper_trail`. It stores all versions for all model types in
413
447
  a single table, you must opt into using `jsonb` as the column type to store "changes", in case you
@@ -415,7 +449,7 @@ want to query them, and there is no concept of a `temporal` time-frame for a sin
415
449
  makes opinionated decisions about contextual data requirements and stores them as top level data
416
450
  types on the `audited` table.
417
451
 
418
- ### [`discard`](https://github.com/jhawthorn/discard)
452
+ #### [`discard`](https://github.com/jhawthorn/discard)
419
453
 
420
454
  `discard` only covers soft-deletion. The act of "soft deleting" a record is only captured through
421
455
  the time-stamping of a `discarded_at` column on the records table; there is no other capturing of
@@ -424,7 +458,7 @@ record is restored, the previous "discarded" awareness is lost. Since "discarded
424
458
  the same table as "undiscarded" records, you must explicitly omit the discarded records from queries
425
459
  across your app to keep them from leaking in.
426
460
 
427
- ### [`paranoia`](https://github.com/rubysherpas/paranoia)
461
+ #### [`paranoia`](https://github.com/rubysherpas/paranoia)
428
462
 
429
463
  `paranoia` also only covers soft-deletion. In their README, they recommend using `discard` instead
430
464
  of `paranoia` because of the fact they override ActiveRecord’s `delete` and `destroy` methods.
@@ -432,6 +466,13 @@ of `paranoia` because of the fact they override ActiveRecord’s `delete` and `d
432
466
  `paranoia` works similarly to `discard` in that it keeps deleted records in the same table and tags
433
467
  them with a `deleted_at` timestamp. No other information about the soft-deletion event is stored.
434
468
 
469
+ #### [`logidze`](https://github.com/palkan/logidze)
470
+
471
+ `logidze` is an interesting versioning alternative that leverages the power of PostgreSQL triggers.
472
+ Instead of storing the previous versions or changes in a separate table, it stores them in a
473
+ proprietary JSON format directly on the database row of the record itself. If does not support soft
474
+ deletion.
475
+
435
476
  ## Contributing
436
477
 
437
478
  This gem still quite new and very open to feedback.
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module Hoardable
6
+ # Generates an initializer file for {Hoardable} configuration and a migration with a PostgreSQL
7
+ # function.
8
+ class InstallGenerator < Rails::Generators::Base
9
+ source_root File.expand_path('templates', __dir__)
10
+ include Rails::Generators::Migration
11
+
12
+ def create_initializer_file
13
+ create_file(
14
+ 'config/initializers/hoardable.rb',
15
+ <<~TEXT
16
+ # Hoardable configuration defaults are below. Learn more at https://github.com/waymondo/hoardable#configuration
17
+ #
18
+ # Hoardable.enabled = true
19
+ # Hoardable.version_updates = true
20
+ # Hoardable.save_trash = true
21
+ TEXT
22
+ )
23
+ end
24
+
25
+ def create_migration_file
26
+ migration_template 'install.rb.erb', 'db/migrate/install_hoardable.rb'
27
+ end
28
+
29
+ def self.next_migration_number(dir)
30
+ ::ActiveRecord::Generators::Base.next_migration_number(dir)
31
+ end
32
+ end
33
+ end
@@ -12,7 +12,7 @@ module Hoardable
12
12
  class_option :foreign_key_type, type: :string
13
13
 
14
14
  def create_versions_table
15
- migration_template migration_template_name, "db/migrate/create_#{singularized_table_name}_versions.rb"
15
+ migration_template 'migration.rb.erb', "db/migrate/create_#{singularized_table_name}_versions.rb"
16
16
  end
17
17
 
18
18
  no_tasks do
@@ -23,12 +23,10 @@ module Hoardable
23
23
  'bigint'
24
24
  end
25
25
 
26
- def migration_template_name
27
- if Gem::Version.new(ActiveRecord::Migration.current_version.to_s) < Gem::Version.new('7')
28
- 'migration_6.rb.erb'
29
- else
30
- 'migration.rb.erb'
31
- end
26
+ def primary_key
27
+ options[:primary_key] || class_name.singularize.constantize.primary_key
28
+ rescue StandardError
29
+ 'id'
32
30
  end
33
31
 
34
32
  def singularized_table_name
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ class InstallHoardable < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
+ def up
5
+ execute(
6
+ <<~SQL
7
+ DO $$
8
+ BEGIN
9
+ IF NOT EXISTS (
10
+ SELECT 1 FROM pg_type t WHERE t.typname = 'hoardable_operation'
11
+ ) THEN
12
+ CREATE TYPE hoardable_operation AS ENUM ('update', 'delete', 'insert');
13
+ END IF;
14
+ END
15
+ $$;
16
+ CREATE OR REPLACE FUNCTION hoardable_source_set_id() RETURNS trigger
17
+ LANGUAGE plpgsql AS
18
+ $$
19
+ DECLARE
20
+ _pk information_schema.constraint_column_usage.column_name%TYPE;
21
+ _id _pk%TYPE;
22
+ BEGIN
23
+ SELECT c.column_name
24
+ FROM information_schema.table_constraints t
25
+ JOIN information_schema.constraint_column_usage c
26
+ ON c.constraint_name = t.constraint_name
27
+ WHERE c.table_name = TG_TABLE_NAME AND t.constraint_type = 'PRIMARY KEY'
28
+ LIMIT 1
29
+ INTO _pk;
30
+ EXECUTE format('SELECT $1.%I', _pk) INTO _id USING NEW;
31
+ NEW.hoardable_id = _id;
32
+ RETURN NEW;
33
+ END;$$;
34
+ CREATE OR REPLACE FUNCTION hoardable_version_prevent_update() RETURNS trigger
35
+ LANGUAGE plpgsql AS
36
+ $$BEGIN
37
+ RAISE EXCEPTION 'updating a version is not allowed';
38
+ RETURN NEW;
39
+ END;$$;
40
+ SQL
41
+ )
42
+ end
43
+ def down
44
+ execute(
45
+ <<~SQL
46
+ DROP TYPE IF EXISTS hoardable_operation;
47
+ DROP FUNCTION IF EXISTS hoardable_version_prevent_update();
48
+ DROP FUNCTION IF EXISTS hoardable_source_set_id();
49
+ SQL
50
+ )
51
+ end
52
+ end
@@ -1,33 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Create<%= class_name.singularize %>Versions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
3
+ class Create<%= class_name.singularize.delete(':') %>Versions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
4
  def change
5
- create_enum :hoardable_operation, %w[update delete insert]
5
+ add_column :<%= table_name %>, :hoardable_id, :<%= foreign_key_type %>
6
+ add_index :<%= table_name %>, :hoardable_id
6
7
  create_table :<%= singularized_table_name %>_versions, id: false, options: 'INHERITS (<%= table_name %>)' do |t|
7
8
  t.jsonb :_data
8
9
  t.tsrange :_during, null: false
9
10
  t.uuid :_event_uuid, null: false, index: true
10
- t.enum :_operation, enum_type: 'hoardable_operation', null: false, index: true
11
- t.<%= foreign_key_type %> :hoardable_source_id, null: false, index: true
11
+ t.column :_operation, :hoardable_operation, null: false, index: true
12
12
  end
13
- execute(
14
- <<~SQL
15
- CREATE OR REPLACE FUNCTION hoardable_version_prevent_update() RETURNS trigger
16
- LANGUAGE plpgsql AS
17
- $$BEGIN
18
- RAISE EXCEPTION 'updating a version is not allowed';
19
- RETURN NEW;
20
- END;$$;
21
-
22
- CREATE TRIGGER <%= singularized_table_name %>_versions_prevent_update
23
- BEFORE UPDATE ON <%= singularized_table_name %>_versions FOR EACH ROW
24
- EXECUTE PROCEDURE hoardable_version_prevent_update();
25
- SQL
26
- )
27
- add_index(:<%= singularized_table_name %>_versions, :id, unique: true)
13
+ reversible do |dir|
14
+ dir.up do
15
+ execute(
16
+ <<~SQL
17
+ UPDATE <%= table_name %> SET hoardable_id = <%= primary_key %>;
18
+ CREATE TRIGGER <%= singularized_table_name %>_versions_prevent_update
19
+ BEFORE UPDATE ON <%= singularized_table_name %>_versions FOR EACH ROW
20
+ EXECUTE PROCEDURE hoardable_version_prevent_update();
21
+ CREATE TRIGGER <%= table_name %>_set_hoardable_id
22
+ BEFORE INSERT ON <%= table_name %> FOR EACH ROW
23
+ EXECUTE PROCEDURE hoardable_source_set_id();
24
+ SQL
25
+ )
26
+ end
27
+ dir.down do
28
+ execute(
29
+ <<~SQL
30
+ DROP TRIGGER <%= singularized_table_name %>_versions_prevent_update
31
+ ON <%= singularized_table_name %>_versions;
32
+ DROP TRIGGER <%= table_name %>_set_hoardable_id
33
+ ON <%= table_name %>;
34
+ SQL
35
+ )
36
+ end
37
+ end
38
+ change_column_null :<%= table_name %>, :hoardable_id, false
39
+ add_index(:<%= singularized_table_name %>_versions, :<%= primary_key %>, unique: true)
28
40
  add_index(
29
41
  :<%= singularized_table_name %>_versions,
30
- %i[_during hoardable_source_id],
42
+ %i[_during hoardable_id],
31
43
  name: 'idx_<%= singularized_table_name %>_versions_temporally'
32
44
  )
33
45
  end
@@ -7,104 +7,11 @@ module Hoardable
7
7
  module Associations
8
8
  extend ActiveSupport::Concern
9
9
 
10
- # An +ActiveRecord+ extension that allows looking up {VersionModel}s by +hoardable_source_id+ as
11
- # if they were {SourceModel}s when using {Hoardable#at}.
12
- module HasManyExtension
13
- def scope
14
- @scope ||= hoardable_scope
15
- end
16
-
17
- private
18
-
19
- def hoardable_scope
20
- if Hoardable.instance_variable_get('@at') &&
21
- (hoardable_source_id = @association.owner.hoardable_source_id)
22
- @association.scope.rewhere(@association.reflection.foreign_key => hoardable_source_id)
23
- else
24
- @association.scope
25
- end
26
- end
27
- end
28
- private_constant :HasManyExtension
29
-
30
- # A private service class for installing +ActiveRecord+ association overrides.
31
- class Overrider
32
- attr_reader :klass
33
-
34
- def initialize(klass)
35
- @klass = klass
36
- end
37
-
38
- def override_belongs_to(name)
39
- klass.define_method("trashable_#{name}") do
40
- source_reflection = klass.reflections[name.to_s]
41
- source_reflection.version_class.trashed.only_most_recent.find_by(
42
- hoardable_source_id: source_reflection.foreign_key
43
- )
44
- end
45
-
46
- klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
47
- def #{name}
48
- super || trashable_#{name}
49
- end
50
- RUBY
51
- end
52
-
53
- def override_has_one(name)
54
- klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
55
- def #{name}
56
- return super unless (at = Hoardable.instance_variable_get('@at'))
57
-
58
- super&.version_at(at) ||
59
- _reflections["profile"].klass.where(_reflections["profile"].foreign_key => id).first
60
- end
61
- RUBY
62
- end
63
-
64
- def override_has_many(name)
65
- # This hack is needed to force Rails to not use any existing method cache so that the
66
- # {HasManyExtension} scope is always used.
67
- klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
68
- def #{name}
69
- super.extending
70
- end
71
- RUBY
72
- end
73
- end
74
- private_constant :Overrider
75
-
76
- class_methods do
77
- def belongs_to(*args)
78
- options = args.extract_options!
79
- trashable = options.delete(:trashable)
80
- super(*args, **options)
81
- return unless trashable
82
-
83
- hoardable_association_overrider.override_belongs_to(args.first)
84
- end
85
-
86
- def has_one(*args)
87
- options = args.extract_options!
88
- hoardable = options.delete(:hoardable)
89
- super(*args, **options)
90
- return unless hoardable
91
-
92
- hoardable_association_overrider.override_has_one(args.first)
93
- end
94
-
95
- def has_many(*args, &block)
96
- options = args.extract_options!
97
- options[:extend] = Array(options[:extend]).push(HasManyExtension) if options.delete(:hoardable)
98
- super(*args, **options, &block)
99
-
100
- hoardable_association_overrider.override_has_many(args.first)
101
- end
102
-
103
- private
104
-
105
- def hoardable_association_overrider
106
- @hoardable_association_overrider ||= Overrider.new(self)
107
- end
10
+ included do
11
+ include HasMany
12
+ include HasOne
13
+ include BelongsTo
14
+ include HasRichText
108
15
  end
109
16
  end
110
17
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ # Provides awareness of trashed source records to +belongs_to+ relationships.
5
+ module BelongsTo
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def belongs_to(*args)
10
+ options = args.extract_options!
11
+ trashable = options.delete(:trashable)
12
+ super(*args, **options)
13
+ return unless trashable
14
+
15
+ hoardable_override_belongs_to(args.first)
16
+ end
17
+
18
+ private
19
+
20
+ def hoardable_override_belongs_to(name)
21
+ define_method("trashed_#{name}") do
22
+ source_reflection = reflections[name.to_s]
23
+ source_reflection.version_class.trashed.only_most_recent.find_by(
24
+ hoardable_id: source_reflection.foreign_key
25
+ )
26
+ end
27
+
28
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
29
+ def #{name}
30
+ super || trashed_#{name}
31
+ end
32
+ RUBY
33
+ end
34
+ end
35
+ end
36
+ private_constant :BelongsTo
37
+ end
@@ -13,30 +13,25 @@ module Hoardable
13
13
  delegate :version_class, to: :source_record
14
14
 
15
15
  def insert_hoardable_version(operation, &block)
16
- version = version_class.insert(initialize_version_attributes(operation), returning: :id)
17
- version_id = version[0]['id']
16
+ version = version_class.insert(initialize_version_attributes(operation), returning: source_primary_key.to_sym)
17
+ version_id = version[0][source_primary_key]
18
18
  source_record.instance_variable_set('@hoardable_version', version_class.find(version_id))
19
19
  source_record.run_callbacks(:versioned, &block)
20
20
  end
21
21
 
22
- def find_or_initialize_hoardable_event_uuid
23
- Thread.current[:hoardable_event_uuid] ||= ActiveRecord::Base.connection.query('SELECT gen_random_uuid();')[0][0]
24
- end
25
-
26
- def hoardable_version_source_id
27
- @hoardable_version_source_id ||= query_hoardable_version_source_id
22
+ def source_primary_key
23
+ source_record.class.primary_key
28
24
  end
29
25
 
30
- def query_hoardable_version_source_id
31
- primary_key = source_record.class.primary_key
32
- version_class.where(primary_key => source_record.read_attribute(primary_key)).pluck('hoardable_source_id')[0]
26
+ def find_or_initialize_hoardable_event_uuid
27
+ Thread.current[:hoardable_event_uuid] ||= ActiveRecord::Base.connection.query('SELECT gen_random_uuid();')[0][0]
33
28
  end
34
29
 
35
30
  def initialize_version_attributes(operation)
36
- source_record.attributes_before_type_cast.without('id').merge(
31
+ source_attributes_without_primary_key.merge(
37
32
  source_record.changes.transform_values { |h| h[0] },
38
33
  {
39
- 'hoardable_source_id' => source_record.id,
34
+ 'hoardable_id' => source_record.id,
40
35
  '_event_uuid' => find_or_initialize_hoardable_event_uuid,
41
36
  '_operation' => operation,
42
37
  '_data' => initialize_hoardable_data.merge(changes: source_record.changes),
@@ -45,6 +40,10 @@ module Hoardable
45
40
  )
46
41
  end
47
42
 
43
+ def source_attributes_without_primary_key
44
+ source_record.attributes_before_type_cast.without(source_primary_key)
45
+ end
46
+
48
47
  def initialize_temporal_range
49
48
  ((previous_temporal_tsrange_end || hoardable_source_epoch)..Time.now.utc)
50
49
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ # A {Hoardable} subclass of {ActionText::EncryptedRichText}.
5
+ class EncryptedRichText < ActionText::EncryptedRichText
6
+ include Model
7
+ end
8
+ end
@@ -34,6 +34,9 @@ module Hoardable
34
34
  end.freeze
35
35
  private_constant :HOARDABLE_VERSION_UPDATES
36
36
 
37
+ SUPPORTS_ENCRYPTED_ACTION_TEXT = ActiveRecord.version >= ::Gem::Version.new('7.0')
38
+ private_constant :SUPPORTS_ENCRYPTED_ACTION_TEXT
39
+
37
40
  @context = {}
38
41
  @config = CONFIG_KEYS.to_h do |key|
39
42
  [key, true]
@@ -91,4 +94,16 @@ module Hoardable
91
94
  @logger ||= ActiveSupport::TaggedLogging.new(Logger.new($stdout))
92
95
  end
93
96
  end
97
+
98
+ # A +Rails+ engine for providing support for +ActionText+
99
+ class Engine < ::Rails::Engine
100
+ isolate_namespace Hoardable
101
+
102
+ initializer 'hoardable.action_text' do
103
+ ActiveSupport.on_load(:action_text_rich_text) do
104
+ require_relative 'rich_text'
105
+ require_relative 'encrypted_rich_text' if SUPPORTS_ENCRYPTED_ACTION_TEXT
106
+ end
107
+ end
108
+ end
94
109
  end
@@ -7,18 +7,18 @@ module Hoardable
7
7
  # with the class method +hoardable+.
8
8
  module FinderMethods
9
9
  def find_one(id)
10
- super(hoardable_source_ids([id])[0])
10
+ super(hoardable_ids([id])[0])
11
11
  end
12
12
 
13
13
  def find_some(ids)
14
- super(hoardable_source_ids(ids))
14
+ super(hoardable_ids(ids))
15
15
  end
16
16
 
17
17
  private
18
18
 
19
- def hoardable_source_ids(ids)
19
+ def hoardable_ids(ids)
20
20
  ids.map do |id|
21
- version_class.where(hoardable_source_id: id).select(primary_key).ids[0] || id
21
+ version_class.where(hoardable_id: id).select(primary_key).ids[0] || id
22
22
  end
23
23
  end
24
24
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ # Provides temporal version awareness to +has_many+ relationships.
5
+ module HasMany
6
+ extend ActiveSupport::Concern
7
+
8
+ # An +ActiveRecord+ extension that allows looking up {VersionModel}s by +hoardable_id+ as
9
+ # if they were {SourceModel}s when using {Hoardable#at}.
10
+ module HasManyExtension
11
+ def scope
12
+ @scope ||= hoardable_scope
13
+ end
14
+
15
+ private
16
+
17
+ def hoardable_scope
18
+ if Hoardable.instance_variable_get('@at') && (hoardable_id = @association.owner.hoardable_id)
19
+ if @association.reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
20
+ @association.reflection.source_reflection.instance_variable_set(
21
+ '@active_record_primary_key', 'hoardable_id'
22
+ )
23
+ end
24
+ @association.scope.rewhere(@association.reflection.foreign_key => hoardable_id)
25
+ else
26
+ @association.scope
27
+ end
28
+ end
29
+ end
30
+ private_constant :HasManyExtension
31
+
32
+ class_methods do
33
+ def has_many(*args, &block)
34
+ options = args.extract_options!
35
+ options[:extend] = Array(options[:extend]).push(HasManyExtension) if options.delete(:hoardable)
36
+ super(*args, **options, &block)
37
+
38
+ # This hack is needed to force Rails to not use any existing method cache so that the
39
+ # {HasManyExtension} scope is always used.
40
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
41
+ def #{args.first}
42
+ super.extending
43
+ end
44
+ RUBY
45
+ end
46
+ end
47
+ end
48
+ private_constant :HasMany
49
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ # Provides temporal version awareness to +has_one+ relationships.
5
+ module HasOne
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def has_one(*args)
10
+ options = args.extract_options!
11
+ hoardable = options.delete(:hoardable)
12
+ association = super(*args, **options)
13
+ name = args.first
14
+ return unless hoardable || association[name.to_s].options[:class_name].match?(/RichText$/)
15
+
16
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
17
+ def #{name}
18
+ return super unless (at = Hoardable.instance_variable_get('@at'))
19
+
20
+ super&.version_at(at) ||
21
+ _reflections['profile'].klass.where(_reflections['profile'].foreign_key => id).first
22
+ end
23
+ RUBY
24
+ end
25
+ end
26
+ end
27
+ private_constant :HasOne
28
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ # Provides temporal version awareness for +ActionText+.
5
+ module HasRichText
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def has_rich_text(name, encrypted: false, hoardable: false)
10
+ if SUPPORTS_ENCRYPTED_ACTION_TEXT
11
+ super(name, encrypted: encrypted)
12
+ else
13
+ super(name)
14
+ end
15
+ return unless hoardable
16
+
17
+ reflection_options = reflections["rich_text_#{name}"].options
18
+ reflection_options[:class_name] = reflection_options[:class_name].sub(/ActionText/, 'Hoardable')
19
+ end
20
+ end
21
+ end
22
+ private_constant :HasRichText
23
+ end
@@ -53,11 +53,17 @@ module Hoardable
53
53
  TracePoint.new(:end) do |trace|
54
54
  next unless self == trace.self
55
55
 
56
- version_class_name = "#{name}#{VERSION_CLASS_SUFFIX}"
57
- unless Object.const_defined?(version_class_name)
58
- Object.const_set(version_class_name, Class.new(self) { include VersionModel })
56
+ full_version_class_name = "#{name}#{VERSION_CLASS_SUFFIX}"
57
+ if (namespace_match = full_version_class_name.match(/(.*)::(.*)/))
58
+ object_namespace = namespace_match[1].constantize
59
+ version_class_name = namespace_match[2]
60
+ else
61
+ object_namespace = Object
62
+ version_class_name = full_version_class_name
63
+ end
64
+ unless Object.const_defined?(full_version_class_name)
65
+ object_namespace.const_set(version_class_name, Class.new(self) { include VersionModel })
59
66
  end
60
-
61
67
  include SourceModel
62
68
 
63
69
  trace.disable
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ # A {Hoardable} subclass of {ActionText::RichText}
5
+ class RichText < ActionText::RichText
6
+ include Model
7
+ end
8
+ end
@@ -62,10 +62,10 @@ module Hoardable
62
62
  scope :at, lambda { |datetime|
63
63
  raise(CreatedAtColumnMissingError, @klass.table_name) unless @klass.column_names.include?('created_at')
64
64
 
65
- include_versions.where(id: version_class.at(datetime).select('id')).or(
65
+ include_versions.where(id: version_class.at(datetime).select(@klass.primary_key)).or(
66
66
  exclude_versions
67
67
  .where("#{table_name}.created_at < ?", datetime)
68
- .where.not(id: version_class.select(:hoardable_source_id).where(DURING_QUERY, datetime))
68
+ .where.not(id: version_class.select(:hoardable_id).where(DURING_QUERY, datetime))
69
69
  ).hoardable
70
70
  }
71
71
  end
@@ -51,7 +51,7 @@ module Hoardable
51
51
  dependent: nil,
52
52
  class_name: version_class.to_s,
53
53
  inverse_of: :hoardable_source,
54
- foreign_key: :hoardable_source_id
54
+ foreign_key: :hoardable_id
55
55
  )
56
56
  end
57
57
 
@@ -60,7 +60,7 @@ module Hoardable
60
60
  #
61
61
  # @return [Boolean]
62
62
  def trashed?
63
- versions.trashed.only_most_recent.first&.hoardable_source_id == id
63
+ !self.class.exists?(self.class.primary_key => id)
64
64
  end
65
65
 
66
66
  # Returns a boolean of whether the record is actually a +version+ cast as an instance of the
@@ -68,7 +68,7 @@ module Hoardable
68
68
  #
69
69
  # @return [Boolean]
70
70
  def version?
71
- !!hoardable_client.hoardable_version_source_id
71
+ hoardable_id != id
72
72
  end
73
73
 
74
74
  # Returns the +version+ at the supplied +datetime+ or +time+, or +self+ if there is none.
@@ -98,13 +98,8 @@ module Hoardable
98
98
  version.is_a?(version_class) ? version.revert! : self
99
99
  end
100
100
 
101
- # Returns the +hoardable_source_id+ that represents the original {SourceModel} record’s ID. Will
102
- # return nil if the current {SourceModel} record is not an instance of a {VersionModel} cast as
103
- # {SourceModel}.
104
- #
105
- # @return [Integer, nil]
106
- def hoardable_source_id
107
- hoardable_client.hoardable_version_source_id || id
101
+ def hoardable_id
102
+ read_attribute('hoardable_id')
108
103
  end
109
104
 
110
105
  delegate :version_class, to: :class
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hoardable
4
- VERSION = '0.10.1'
4
+ VERSION = '0.12.2'
5
5
  end
@@ -7,6 +7,13 @@ module Hoardable
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  class_methods do
10
+ # This is needed to allow {FinderMethods} to work with the version class.
11
+ #
12
+ # @!visibility private
13
+ def version_class
14
+ self
15
+ end
16
+
10
17
  # This is needed to omit the pseudo row of 'tableoid' when using +ActiveRecord+’s +insert+.
11
18
  #
12
19
  # @!visibility private
@@ -20,6 +27,7 @@ module Hoardable
20
27
  belongs_to(
21
28
  :hoardable_source,
22
29
  inverse_of: :versions,
30
+ foreign_key: :hoardable_id,
23
31
  class_name: superclass.model_name
24
32
  )
25
33
 
@@ -37,7 +45,7 @@ module Hoardable
37
45
  # Returns only trashed +versions+ that are currently orphans.
38
46
  scope :trashed, lambda {
39
47
  left_outer_joins(:hoardable_source)
40
- .where(superclass.table_name => { id: nil })
48
+ .where(superclass.table_name => { superclass.primary_key => nil })
41
49
  .where(_operation: 'delete')
42
50
  }
43
51
 
@@ -78,7 +86,7 @@ module Hoardable
78
86
 
79
87
  transaction do
80
88
  hoardable_source.tap do |reverted|
81
- reverted.update!(hoardable_source_attributes.without('id'))
89
+ reverted.update!(hoardable_source_attributes.without(self.class.superclass.primary_key))
82
90
  reverted.instance_variable_set(:@hoardable_version, self)
83
91
  reverted.run_callbacks(:reverted)
84
92
  end
@@ -113,24 +121,16 @@ module Hoardable
113
121
  _data&.dig('changes')
114
122
  end
115
123
 
116
- # Returns the ID of the {SourceModel} that created this {VersionModel}
117
- def hoardable_source_id
118
- read_attribute('hoardable_source_id')
119
- end
120
-
121
124
  private
122
125
 
123
126
  def insert_untrashed_source
124
127
  superscope = self.class.superclass.unscoped
125
- superscope.insert(hoardable_source_attributes.merge('id' => hoardable_source_id))
126
- superscope.find(hoardable_source_id)
128
+ superscope.insert(hoardable_source_attributes.merge(superscope.primary_key => hoardable_id))
129
+ superscope.find(hoardable_id)
127
130
  end
128
131
 
129
132
  def hoardable_source_attributes
130
- @hoardable_source_attributes ||=
131
- attributes_before_type_cast
132
- .without('hoardable_source_id')
133
- .reject { |k, _v| k.start_with?('_') }
133
+ attributes_before_type_cast.without(self.class.column_names - self.class.superclass.column_names)
134
134
  end
135
135
  end
136
136
  end
data/lib/hoardable.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_record'
3
4
  require_relative 'hoardable/version'
4
- require_relative 'hoardable/hoardable'
5
+ require_relative 'hoardable/engine'
5
6
  require_relative 'hoardable/finder_methods'
6
7
  require_relative 'hoardable/scopes'
7
8
  require_relative 'hoardable/error'
@@ -10,5 +11,9 @@ require_relative 'hoardable/source_model'
10
11
  require_relative 'hoardable/version_model'
11
12
  require_relative 'hoardable/model'
12
13
  require_relative 'hoardable/associations'
14
+ require_relative 'hoardable/has_many'
15
+ require_relative 'hoardable/belongs_to'
16
+ require_relative 'hoardable/has_one'
17
+ require_relative 'hoardable/has_rich_text'
13
18
  require_relative 'generators/hoardable/migration_generator'
14
- require_relative 'generators/hoardable/initializer_generator'
19
+ require_relative 'generators/hoardable/install_generator'
data/sig/hoardable.rbs CHANGED
@@ -8,6 +8,7 @@ module Hoardable
8
8
  HOARDABLE_CALLBACKS_ENABLED: ^(untyped) -> untyped
9
9
  HOARDABLE_SAVE_TRASH: ^(untyped) -> untyped
10
10
  HOARDABLE_VERSION_UPDATES: ^(untyped) -> untyped
11
+ SUPPORTS_ENCRYPTED_ACTION_TEXT: untyped
11
12
  self.@context: Hash[untyped, untyped]
12
13
  self.@config: untyped
13
14
  self.@at: nil
@@ -17,8 +18,20 @@ module Hoardable
17
18
  def self.at: (untyped datetime) -> untyped
18
19
  def self.logger: -> untyped
19
20
 
21
+ class Engine
22
+ end
23
+
24
+ module FinderMethods
25
+ def find_one: (untyped id) -> untyped
26
+ def find_some: (untyped ids) -> untyped
27
+
28
+ private
29
+ def hoardable_ids: ([untyped] ids) -> Array[untyped]
30
+ end
31
+
20
32
  module Scopes
21
33
  TABLEOID_AREL_CONDITIONS: Proc
34
+ self.@klass: bot
22
35
 
23
36
  private
24
37
  def tableoid: -> untyped
@@ -30,22 +43,24 @@ module Hoardable
30
43
  class Error < StandardError
31
44
  end
32
45
 
33
- class DatabaseClient
34
- @hoardable_version_source_id: untyped
46
+ class CreatedAtColumnMissingError < Error
47
+ def initialize: (untyped source_table_name) -> void
48
+ end
35
49
 
50
+ class DatabaseClient
36
51
  attr_reader source_record: SourceModel
37
52
  def initialize: (SourceModel source_record) -> void
38
53
  def insert_hoardable_version: (untyped operation) -> untyped
54
+ def source_primary_key: -> untyped
39
55
  def find_or_initialize_hoardable_event_uuid: -> untyped
40
- def hoardable_version_source_id: -> untyped
41
- def query_hoardable_version_source_id: -> untyped
42
56
  def initialize_version_attributes: (untyped operation) -> untyped
57
+ def source_attributes_without_primary_key: -> untyped
43
58
  def initialize_temporal_range: -> Range
44
59
  def initialize_hoardable_data: -> untyped
45
60
  def assign_hoardable_context: (:event_uuid | :meta | :note | :whodunit key) -> nil
46
61
  def unset_hoardable_version_and_event_uuid: -> nil
47
62
  def previous_temporal_tsrange_end: -> untyped
48
- def hoardable_source_epoch: -> Time
63
+ def hoardable_source_epoch: -> untyped
49
64
  end
50
65
 
51
66
  module SourceModel
@@ -55,9 +70,10 @@ module Hoardable
55
70
  attr_reader hoardable_version: untyped
56
71
  def trashed?: -> untyped
57
72
  def version?: -> untyped
58
- def at: (untyped datetime) -> SourceModel
73
+ def at: (untyped datetime) -> SourceModel?
74
+ def version_at: (untyped datetime) -> untyped
59
75
  def revert_to!: (untyped datetime) -> SourceModel?
60
- def hoardable_source_id: -> untyped
76
+ def hoardable_id: -> untyped
61
77
 
62
78
  private
63
79
  def hoardable_client: -> DatabaseClient
@@ -65,25 +81,19 @@ module Hoardable
65
81
  public
66
82
  def version_class: -> untyped
67
83
  def hoardable: -> untyped
68
-
69
- module FinderMethods
70
- def find_one: (untyped id) -> untyped
71
- end
72
84
  end
73
85
 
74
86
  module VersionModel
75
- @hoardable_source_attributes: untyped
76
-
77
87
  def revert!: -> untyped
78
88
  def untrash!: -> untyped
79
89
  def changes: -> untyped
80
- def hoardable_source_id: -> untyped
81
90
 
82
91
  private
83
92
  def insert_untrashed_source: -> untyped
84
93
  def hoardable_source_attributes: -> untyped
85
94
 
86
95
  public
96
+ def version_class: -> VersionModel
87
97
  def scope_attributes: -> untyped
88
98
  end
89
99
 
@@ -98,10 +108,16 @@ module Hoardable
98
108
  end
99
109
 
100
110
  module Associations
101
- def belongs_to_trashable: (untyped name, ?nil scope, **untyped) -> untyped
102
- def has_many_hoardable: (untyped name, ?nil scope, **untyped) -> untyped
111
+ include HasRichText
112
+ include BelongsTo
113
+ include HasOne
114
+ include HasMany
115
+ end
103
116
 
104
- module HasManyScope
117
+ module HasMany
118
+ def has_many: (*untyped args) -> untyped
119
+
120
+ module HasManyExtension
105
121
  @scope: untyped
106
122
  @association: bot
107
123
 
@@ -112,16 +128,41 @@ module Hoardable
112
128
  end
113
129
  end
114
130
 
131
+ module BelongsTo
132
+ def belongs_to: (*untyped args) -> nil
133
+
134
+ private
135
+ def hoardable_override_belongs_to: (untyped name) -> untyped
136
+ end
137
+
138
+ module HasOne
139
+ def has_one: (*untyped args) -> nil
140
+ end
141
+
142
+ module HasRichText
143
+ def has_rich_text: (untyped name, ?encrypted: false, ?hoardable: false) -> nil
144
+ end
145
+
115
146
  class MigrationGenerator
116
147
  @singularized_table_name: untyped
117
148
 
118
149
  def create_versions_table: -> untyped
119
150
  def foreign_key_type: -> String
120
- def migration_template_name: -> String
151
+ def primary_key: -> String
121
152
  def singularized_table_name: -> untyped
122
153
  end
123
154
 
124
- class InitializerGenerator
155
+ class InstallGenerator
125
156
  def create_initializer_file: -> untyped
157
+ def create_migration_file: -> untyped
158
+ def self.next_migration_number: (untyped dir) -> untyped
159
+ end
160
+
161
+ class RichText
162
+ include Model
163
+ end
164
+
165
+ class EncryptedRichText
166
+ include Model
126
167
  end
127
168
  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.10.1
4
+ version: 0.12.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - justin talbott
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-10-07 00:00:00.000000000 Z
11
+ date: 2022-10-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -104,17 +104,23 @@ files:
104
104
  - LICENSE.txt
105
105
  - README.md
106
106
  - Rakefile
107
- - lib/generators/hoardable/initializer_generator.rb
107
+ - lib/generators/hoardable/install_generator.rb
108
108
  - lib/generators/hoardable/migration_generator.rb
109
+ - lib/generators/hoardable/templates/install.rb.erb
109
110
  - lib/generators/hoardable/templates/migration.rb.erb
110
- - lib/generators/hoardable/templates/migration_6.rb.erb
111
111
  - lib/hoardable.rb
112
112
  - lib/hoardable/associations.rb
113
+ - lib/hoardable/belongs_to.rb
113
114
  - lib/hoardable/database_client.rb
115
+ - lib/hoardable/encrypted_rich_text.rb
116
+ - lib/hoardable/engine.rb
114
117
  - lib/hoardable/error.rb
115
118
  - lib/hoardable/finder_methods.rb
116
- - lib/hoardable/hoardable.rb
119
+ - lib/hoardable/has_many.rb
120
+ - lib/hoardable/has_one.rb
121
+ - lib/hoardable/has_rich_text.rb
117
122
  - lib/hoardable/model.rb
123
+ - lib/hoardable/rich_text.rb
118
124
  - lib/hoardable/scopes.rb
119
125
  - lib/hoardable/source_model.rb
120
126
  - lib/hoardable/version.rb
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rails/generators'
4
-
5
- module Hoardable
6
- # Generates an initializer file for {Hoardable} configuration.
7
- class InitializerGenerator < Rails::Generators::Base
8
- def create_initializer_file
9
- create_file(
10
- 'config/initializers/hoardable.rb',
11
- <<~TEXT
12
- # Hoardable configuration defaults are below. Learn more at https://github.com/waymondo/hoardable#configuration
13
- #
14
- # Hoardable.enabled = true
15
- # Hoardable.version_updates = true
16
- # Hoardable.save_trash = true
17
- TEXT
18
- )
19
- end
20
- end
21
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Create<%= class_name.singularize %>Versions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
- def change
5
- reversible do |dir|
6
- dir.up do
7
- execute <<~SQL
8
- DO $$
9
- BEGIN
10
- IF NOT EXISTS (
11
- SELECT 1 FROM pg_type t
12
- WHERE t.typname = 'hoardable_operation'
13
- ) THEN
14
- CREATE TYPE hoardable_operation AS ENUM ('update', 'delete', 'insert');
15
- END IF;
16
- END
17
- $$;
18
- SQL
19
- end
20
- end
21
- create_table :<%= singularized_table_name %>_versions, id: false, options: 'INHERITS (<%= table_name %>)' do |t|
22
- t.jsonb :_data
23
- t.tsrange :_during, null: false
24
- t.uuid :_event_uuid, null: false, index: true
25
- t.column :_operation, :hoardable_operation, null: false, index: true
26
- t.<%= foreign_key_type %> :hoardable_source_id, null: false, index: true
27
- end
28
- execute(
29
- <<~SQL
30
- CREATE OR REPLACE FUNCTION hoardable_version_prevent_update() RETURNS trigger
31
- LANGUAGE plpgsql AS
32
- $$BEGIN
33
- RAISE EXCEPTION 'updating a version is not allowed';
34
- RETURN NEW;
35
- END;$$;
36
-
37
- CREATE TRIGGER <%= singularized_table_name %>_versions_prevent_update
38
- BEFORE UPDATE ON <%= singularized_table_name %>_versions FOR EACH ROW
39
- EXECUTE PROCEDURE hoardable_version_prevent_update();
40
- SQL
41
- )
42
- add_index(:<%= singularized_table_name %>_versions, :id, unique: true)
43
- add_index(
44
- :<%= singularized_table_name %>_versions,
45
- %i[_during hoardable_source_id],
46
- name: 'idx_<%= singularized_table_name %>_versions_temporally'
47
- )
48
- end
49
- end