diff --git a/.rubocop.yml b/.rubocop.yml index 7bb5fa0f9..04aff53f1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -95,6 +95,10 @@ Naming/PredicateName: Naming/UncommunicativeMethodParamName: Enabled: false +# This cop does not seem to work in rubocop-rspec 1.28.0 +RSpec/DescribeClass: + Enabled: false + # Yes, ideally examples would be short. Is it possible to pick a limit and say, # "no example will ever be longer than this"? Hard to say. Sometimes they're # quite long. diff --git a/CHANGELOG.md b/CHANGELOG.md index 43a379958..99380f872 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,14 +20,6 @@ respectively. - `paper_trail-association_tracking` is no longer a runtime dependency. If you use it (`track_associations = true`) you must now add it to your own `Gemfile`. -- [#1108](https://github.com/paper-trail-gem/paper_trail/pull/1108) - - In `versions.item_type`, we now store the subclass name instead of - the base_class. - - You must migrate existing `versions` records if you use - [STI][1]. A migration generator has been provided. Generator `update_sti` - creates a migration that updates existing `version` entries such that - `item_type` then refers to the specific class name instead of base_class. - See [5.c. Generators][2] for instructions. - [#1130](https://github.com/paper-trail-gem/paper_trail/pull/1130) - Removed `save_changes`. For those wanting to save space, it's more effective to drop the `object` column. If you need ultimate control over the @@ -1060,6 +1052,3 @@ in the `PaperTrail::Version` class through a `Rails::Engine` when the gem is use - [#160](https://github.com/paper-trail-gem/paper_trail/pull/160) - Fixed failing tests and resolved out of date dependency issues. - [#157](https://github.com/paper-trail-gem/paper_trail/pull/157) - Refactored `class_attribute` names on the `ClassMethods` module for names that are not obviously pertaining to PaperTrail to prevent method name collision. - -[1]: https://api.rubyonrails.org/v5.2.0/classes/ActiveRecord/Base.html#class-ActiveRecord::Base-label-Single+table+inheritance -[2]: https://github.com/paper-trail-gem/paper_trail/blob/master/README.md#5c-generators diff --git a/README.md b/README.md index 93d3bc3b5..cf39899e5 100644 --- a/README.md +++ b/README.md @@ -934,15 +934,8 @@ class Banana < Fruit end ``` -A change in what `item_type` stores for subclassed models was introduced in -[PR#1108](https://github.com/paper-trail-gem/paper_trail/pull/1108), recording -the subclass name instead of the base class. This simplifies -reifying through associations, and allows for a change in PT-AT that fixes -[issue 594](https://github.com/paper-trail-gem/paper_trail/issues/594). - -For those that have existing version data from STI models, `item_type` can be -updated by using a generator, `rails generate paper_trail:update_sti`. More -information is found in section [5.c. Generators](#5c-generators). +However, there is a known issue when reifying [associations](#associations), +see https://github.com/paper-trail-gem/paper_trail/issues/594 ### 5.b. Configuring the `versions` Association @@ -962,97 +955,27 @@ Overriding associations is not recommended in general. ### 5.c. Generators -#### `paper_trail:install` - -Used to set up PT for the first time. Writes, but does not run, a migration -file. Creates an initializer for configuration. The migration adds the -`versions` table. +PaperTrail has one generator, `paper_trail:install`. It writes, but does not +run, a migration file. It also creates a PaperTrail configuration initializer. +The migration adds (at least) the `versions` table. The +most up-to-date documentation for this generator can be found by running `rails +generate paper_trail:install --help`, but a copy is included here for +convenience. ``` Usage: - rails generate paper_trail:install --help rails generate paper_trail:install [options] Options: - # Includes the `object_changes` column, for storing changeset (diff) with each version - [--with-changes], [--no-with-changes] + [--with-changes], [--no-with-changes] # Store changeset (diff) with each version Runtime options: -f, [--force] # Overwrite files that already exist -p, [--pretend], [--no-pretend] # Run but do not make any changes -q, [--quiet], [--no-quiet] # Suppress status output -s, [--skip], [--no-skip] # Skip files that already exist -``` - -#### `paper_trail:update_sti` - -Updates `versions.item_type` from base class name to subclass name. If you use -STI, and you have records in `versions` created prior to -[#1108](https://github.com/paper-trail-gem/paper_trail/pull/1108), you must run -this migration. - -Writes, but does not run, a migration. The migration scans existing data in the -versions table to identify models which use STI. Upon finding each entry which -refers to an object from a subclassed model, `versions.item_type` is updated to -reflect the subclass name, providing full compatibility with how the `versions` -association works after PR#1108. In order to more quickly update a large volume -of version records, updates are batched to affect 100 records at a time. - -Hints can be used in rare cases when the STI structure is modified over time -such as when establishing additional intermediary inheritance structure, -pointing to a new base class, abandoning STI entirely, or changing the name of -the inheritance_column. The vast majority of users will probably never need to -supply hints when using this generator. - -Let's give an example where the inheritance_column is changed, say originally -there is an Animal class that uses the default `type` column, but after -generating 500 Bird and Cat objects is modified to instead use the `species` -column with a line like this: - -``` -Animal.inheritance_column = "species" -``` - -With this change a hint can be used to build the migration in a way that -indicates how to treat the older records. For those first 500 Animal objects, -the `item_type` should be derived from `type`, and for the rest, from `species`. -We would need to research the ID numbers for versions that utilise `type` in the -`object` and `object_changes` data. In this example let's say that those first -500 Animals had version IDs between 249 and 1124. These objects would have been -created having an `item_type` of Animal prior to PR#1108. Of course versions -representing all manner of other changes would also be intermingled along with -creates and updates for Animal objects. Perhaps another STI model exists for Car -that inherits from Vehicle, and 50 Car objects were also built around the same -timeframe as Bird and Cat objects, with various creates and updates for those -being referened in versions with IDs from 382 to 516. For Vehicle let's say that -initially the inheritance_column was set to `kind`, but then it changed to -something else after ID 516 in the versions table. In this situation, to -properly use the generator then these hints can be supplied: - -`rails generate update_sti Animal(type):249..1124 Vehicle(kind):382..516` - -The resulting migration will include these lines near the top: - -``` -# Versions of item_type "Animal" with IDs between 249 and 1124 will be updated based on `type` -# Versions of item_type "Vehicle" with IDs between 382 and 516 will be updated based on `kind` -hints = {"Animal"=>{249..1124=>"type"}, "Vehicle"=>{382..516=>"kind"}} -``` - -It is important to note that the IDs are not those of the Bird, Cat, or Car -objects, but rather the IDs from the create, update, and destroy entries in the -versions table. As well, these hints are only needed in situations where the -inheritance_column has changed at some point in time, or in cases where the -entire STI structure is modified or is abandoned. It ultimately facilitates -better reporting for historic subclassed items, and allows PT-AT to properly -reify these historic objects through associations. -Once you have run the migration, please inform PaperTrail, so that it won't warn -you about this. - -``` -# config/initializers/paper_trail.rb -::PaperTrail.config.i_have_updated_my_existing_item_types = true +Generates (but does not run) a migration to add a versions table. Also generates an initializer file for configuring PaperTrail ``` ### 5.d. Protected Attributes diff --git a/lib/generators/paper_trail/install/USAGE b/lib/generators/paper_trail/USAGE similarity index 52% rename from lib/generators/paper_trail/install/USAGE rename to lib/generators/paper_trail/USAGE index 302c668d1..e021be25c 100644 --- a/lib/generators/paper_trail/install/USAGE +++ b/lib/generators/paper_trail/USAGE @@ -1,3 +1,2 @@ Description: - Generates (but does not run) a migration to add a versions table. See section - 5.c. Generators in README.md for more information. + Generates (but does not run) a migration to add a versions table. diff --git a/lib/generators/paper_trail/install/install_generator.rb b/lib/generators/paper_trail/install_generator.rb similarity index 68% rename from lib/generators/paper_trail/install/install_generator.rb rename to lib/generators/paper_trail/install_generator.rb index 0b460f735..8e5786f47 100644 --- a/lib/generators/paper_trail/install/install_generator.rb +++ b/lib/generators/paper_trail/install_generator.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true -require_relative "../migration_generator" +require "rails/generators" +require "rails/generators/active_record" module PaperTrail # Installs PaperTrail in a rails app. - class InstallGenerator < MigrationGenerator + class InstallGenerator < ::Rails::Generators::Base + include ::Rails::Generators::Migration + # Class names of MySQL adapters. # - `MysqlAdapter` - Used by gems: `mysql`, `activerecord-jdbcmysql-adapter`. # - `Mysql2Adapter` - Used by `mysql2` gem. @@ -22,16 +25,34 @@ class InstallGenerator < MigrationGenerator ) desc "Generates (but does not run) a migration to add a versions table." \ - " Also generates an initializer file for configuring PaperTrail." \ - " See section 5.c. Generators in README.md for more information." + " Also generates an initializer file for configuring PaperTrail" def create_migration_file - add_paper_trail_migration("create_versions", - item_type_options: item_type_options, - versions_table_options: versions_table_options) + add_paper_trail_migration("create_versions") add_paper_trail_migration("add_object_changes_to_versions") if options.with_changes? end + def self.next_migration_number(dirname) + ::ActiveRecord::Generators::Base.next_migration_number(dirname) + end + + protected + + def add_paper_trail_migration(template) + migration_dir = File.expand_path("db/migrate") + if self.class.migration_exists?(migration_dir, template) + ::Kernel.warn "Migration already exists: #{template}" + else + migration_template( + "#{template}.rb.erb", + "db/migrate/#{template}.rb", + item_type_options: item_type_options, + migration_version: migration_version, + versions_table_options: versions_table_options + ) + end + end + private # MySQL 5.6 utf8mb4 limit is 191 chars for keys used in indexes. @@ -42,6 +63,13 @@ def item_type_options ", #{opt}" end + def migration_version + major = ActiveRecord::VERSION::MAJOR + if major >= 5 + "[#{major}.#{ActiveRecord::VERSION::MINOR}]" + end + end + def mysql? MYSQL_ADAPTERS.include?(ActiveRecord::Base.connection.class.name) end diff --git a/lib/generators/paper_trail/migration_generator.rb b/lib/generators/paper_trail/migration_generator.rb deleted file mode 100644 index ed6938dab..000000000 --- a/lib/generators/paper_trail/migration_generator.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require "rails/generators" -require "rails/generators/active_record" - -module PaperTrail - # Basic structure to support a generator that builds a migration - class MigrationGenerator < ::Rails::Generators::Base - include ::Rails::Generators::Migration - - def self.next_migration_number(dirname) - ::ActiveRecord::Generators::Base.next_migration_number(dirname) - end - - protected - - def add_paper_trail_migration(template, extra_options = {}) - migration_dir = File.expand_path("db/migrate") - if self.class.migration_exists?(migration_dir, template) - ::Kernel.warn "Migration already exists: #{template}" - else - migration_template( - "#{template}.rb.erb", - "db/migrate/#{template}.rb", - { migration_version: migration_version }.merge(extra_options) - ) - end - end - - def migration_version - major = ActiveRecord::VERSION::MAJOR - if major >= 5 - "[#{major}.#{ActiveRecord::VERSION::MINOR}]" - end - end - end -end diff --git a/lib/generators/paper_trail/install/templates/add_object_changes_to_versions.rb.erb b/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb.erb similarity index 100% rename from lib/generators/paper_trail/install/templates/add_object_changes_to_versions.rb.erb rename to lib/generators/paper_trail/templates/add_object_changes_to_versions.rb.erb diff --git a/lib/generators/paper_trail/install/templates/create_versions.rb.erb b/lib/generators/paper_trail/templates/create_versions.rb.erb similarity index 94% rename from lib/generators/paper_trail/install/templates/create_versions.rb.erb rename to lib/generators/paper_trail/templates/create_versions.rb.erb index a7e65027c..07c8c5a06 100644 --- a/lib/generators/paper_trail/install/templates/create_versions.rb.erb +++ b/lib/generators/paper_trail/templates/create_versions.rb.erb @@ -25,7 +25,7 @@ class CreateVersions < ActiveRecord::Migration<%= migration_version %> # the `created_at` column. # (https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html) # - # MySQL users should also upgrade to at least rails 4.2, which is the first + # MySQL users should also upgrade to rails 4.2, which is the first # version of ActiveRecord with support for fractional seconds in MySQL. # (https://github.com/rails/rails/pull/14359) # diff --git a/lib/generators/paper_trail/update_sti/USAGE b/lib/generators/paper_trail/update_sti/USAGE deleted file mode 100644 index 5183e27fb..000000000 --- a/lib/generators/paper_trail/update_sti/USAGE +++ /dev/null @@ -1,4 +0,0 @@ -Description: - Generates (but does not run) a migration to update item_type for STI entries - in an existing versions table. See section 5.c. Generators in README.md for - more information. diff --git a/lib/generators/paper_trail/update_sti/templates/update_versions_for_sti.rb.erb b/lib/generators/paper_trail/update_sti/templates/update_versions_for_sti.rb.erb deleted file mode 100644 index ebd1cb00c..000000000 --- a/lib/generators/paper_trail/update_sti/templates/update_versions_for_sti.rb.erb +++ /dev/null @@ -1,85 +0,0 @@ -# This migration updates existing `versions` that have `item_type` that refers to -# the base_class, and changes them to refer to the subclass instead. -class UpdateVersionsForSti < ActiveRecord::Migration<%= migration_version %> - include ActionView::Helpers::TextHelper - def up -<%= - # Returns class, column, range - def self.parse_custom_entry(text) - parts = text.split("):") - range = parts.last.split("..").map(&:to_i) - range = Range.new(range.first, range.last) - parts.first.split("(") + [range] - end - # Running: - # rails g paper_trail:update_sti Animal(species):1..4 Plant(genus):42..1337 - # results in: - # # Versions of item_type "Animal" with IDs between 1 and 4 will be updated based on `species` - # # Versions of item_type "Plant" with IDs between 42 and 1337 will be updated based on `genus` - # hints = {"Animal"=>{1..4=>"species"}, "Plant"=>{42..1337=>"genus"}} - hint_descriptions = "" - hints = args.inject(Hash.new{|h, k| h[k] = {}}) do |s, v| - klass, column, range = parse_custom_entry(v) - hint_descriptions << " # Versions of item_type \"#{klass}\" with IDs between #{ - range.first} and #{range.last} will be updated based on \`#{column}\`\n" - s[klass][range] = column - s - end - - unless hints.empty? - "#{hint_descriptions} hints = #{hints.inspect}\n" - end -%> - # Find all ActiveRecord models mentioned in existing versions - changes = Hash.new { |h, k| h[k] = [] } - model_names = PaperTrail::Version.select(:item_type).distinct - model_names.map(&:item_type).each do |model_name| - hint = hints[model_name] if defined?(hints) - begin - klass = model_name.constantize - # Actually implements an inheritance_column? (Usually "type") - has_inheritance_column = klass.columns.map(&:name).include?(klass.inheritance_column) - # Find domain of types stored in PaperTrail versions - PaperTrail::Version.where(item_type: model_name).select(:id, :object, :object_changes).each do |obj| - if (object_detail = PaperTrail.serializer.load(obj.object || obj.object_changes)) - is_found = false - type_name = nil - hint&.each do |k, v| - if k === obj.id && (type_name = object_detail[v]) - break - end - end - if type_name.nil? && has_inheritance_column - type_name = object_detail[klass.inheritance_column] - end - if type_name - type_name = type_name.last if type_name.is_a?(Array) - if type_name != model_name - changes[type_name] << obj.id - end - end - end - end - rescue NameError => ex - say "Skipping reference to #{model_name}", subitem: true - end - end - changes.each do |k, v| - # Update in blocks of up to 100 at a time - block_of_ids = [] - id_count = 0 - num_updated = 0 - v.sort.each do |id| - block_of_ids << id - if (id_count += 1) % 100 == 0 - num_updated += PaperTrail::Version.where(id: block_of_ids).update_all(item_type: k) - block_of_ids = [] - end - end - num_updated += PaperTrail::Version.where(id: block_of_ids).update_all(item_type: k) - if num_updated > 0 - say "Associated #{pluralize(num_updated, 'record')} to #{k}", subitem: true - end - end - end -end diff --git a/lib/generators/paper_trail/update_sti/update_sti_generator.rb b/lib/generators/paper_trail/update_sti/update_sti_generator.rb deleted file mode 100644 index 216e6ff82..000000000 --- a/lib/generators/paper_trail/update_sti/update_sti_generator.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require_relative "../migration_generator" - -module PaperTrail - # Updates STI entries for PaperTrail - class UpdateStiGenerator < MigrationGenerator - source_root File.expand_path("templates", __dir__) - - desc "Generates (but does not run) a migration to update item_type for STI entries in an "\ - "existing versions table. See section 5.c. Generators in README.md for more information." - - def create_migration_file - add_paper_trail_migration("update_versions_for_sti", sti_type_options: options) - end - end -end diff --git a/lib/paper_trail/config.rb b/lib/paper_trail/config.rb index 6eeb81730..347f18838 100644 --- a/lib/paper_trail/config.rb +++ b/lib/paper_trail/config.rb @@ -16,8 +16,6 @@ class Config attr_accessor( :association_reify_error_behaviour, - :classes_warned_about_sti_item_types, - :i_have_updated_my_existing_item_types, :object_changes_adapter, :serializer, :version_limit diff --git a/lib/paper_trail/events/base.rb b/lib/paper_trail/events/base.rb index d4692d5a7..7483b56a9 100644 --- a/lib/paper_trail/events/base.rb +++ b/lib/paper_trail/events/base.rb @@ -162,6 +162,15 @@ def ignored_attr_has_changed? ignored.any? && (changed_in_latest_version & ignored).any? end + # PT 10 has a new optional column, `item_subtype` + # + # @api private + def merge_item_subtype_into(data) + if @record.class.paper_trail.version_class.columns_hash.key?("item_subtype") + data.merge!(item_subtype: @record.class.name) + end + end + # Updates `data` from the model's `meta` option and from `controller_info`. # Metadata is always recorded; that means all three events (create, update, # destroy) and `update_columns`. diff --git a/lib/paper_trail/events/create.rb b/lib/paper_trail/events/create.rb index 76c014b42..a04adda68 100644 --- a/lib/paper_trail/events/create.rb +++ b/lib/paper_trail/events/create.rb @@ -23,6 +23,7 @@ def data changes = notable_changes data[:object_changes] = prepare_object_changes(changes) end + merge_item_subtype_into(data) merge_metadata_into(data) end end diff --git a/lib/paper_trail/events/destroy.rb b/lib/paper_trail/events/destroy.rb index a5367bf81..52ea401c7 100644 --- a/lib/paper_trail/events/destroy.rb +++ b/lib/paper_trail/events/destroy.rb @@ -14,7 +14,7 @@ class Destroy < Base def data data = { item_id: @record.id, - item_type: @record.class.name, + item_type: @record.class.base_class.name, event: @record.paper_trail_event || "destroy", whodunnit: PaperTrail.request.whodunnit } @@ -26,6 +26,7 @@ def data changes = @record.attributes.map { |attr, value| [attr, [value, nil]] }.to_h data[:object_changes] = prepare_object_changes(changes) end + merge_item_subtype_into(data) merge_metadata_into(data) end end diff --git a/lib/paper_trail/events/update.rb b/lib/paper_trail/events/update.rb index c2b59b6b7..01b3ecf3d 100644 --- a/lib/paper_trail/events/update.rb +++ b/lib/paper_trail/events/update.rb @@ -38,6 +38,7 @@ def data changes = @force_changes.nil? ? notable_changes : @force_changes data[:object_changes] = prepare_object_changes(changes) end + merge_item_subtype_into(data) merge_metadata_into(data) end diff --git a/lib/paper_trail/model_config.rb b/lib/paper_trail/model_config.rb index bf4c0bfe1..5c6334940 100644 --- a/lib/paper_trail/model_config.rb +++ b/lib/paper_trail/model_config.rb @@ -129,31 +129,6 @@ def cannot_record_after_destroy? ::ActiveRecord::Base.belongs_to_required_by_default end - # Define the association usually called `versions`. The name is configurable - # via `versions_association_name`. - # - # Single Table Inheritance (STI) is supported, with custom inheritance - # columns. Imagine a `version` whose `item_type` is "Animal". The `animals` - # table is an STI table (it has cats and dogs) and it has a custom - # inheritance column, `species`. If `attrs["species"]` is "Dog", `item_type` - # is "Dog". If `attrs["species"]` is blank, `item_type` is "Animal". See - # `spec/models/animal_spec.rb`. - def setup_versions_association(klass) - klass.has_many( - klass.versions_association_name, - lambda do |object| - relation = order(model.timestamp_sort_order) - item_type = object.paper_trail.versions_association_item_type - unless item_type.nil? - relation = relation.unscope(where: :item_type).where(item_type: item_type) - end - relation - end, - class_name: klass.version_class_name, - as: :item - ) - end - def setup_associations(options) @model_class.class_attribute :version_association_name @model_class.version_association_name = options[:version] || :version @@ -171,7 +146,12 @@ def setup_associations(options) assert_concrete_activerecord_class(@model_class.version_class_name) - setup_versions_association(@model_class) + @model_class.has_many( + @model_class.versions_association_name, + -> { order(model.timestamp_sort_order) }, + class_name: @model_class.version_class_name, + as: :item + ) end def setup_callbacks_from_options(options_on = []) diff --git a/lib/paper_trail/record_trail.rb b/lib/paper_trail/record_trail.rb index 761041ab3..a23713103 100644 --- a/lib/paper_trail/record_trail.rb +++ b/lib/paper_trail/record_trail.rb @@ -7,21 +7,10 @@ module PaperTrail # Represents the "paper trail" for a single record. class RecordTrail - E_STI_ITEM_TYPES_NOT_UPDATED = <<~STR.squish.freeze - It looks like %s is an STI subclass, and you have not yet updated your - `item_type`s. Starting with - [#1108](https://github.com/paper-trail-gem/paper_trail/pull/1108), we now - store the subclass name instead of the base_class. You must migrate - existing `versions` records if you use STI. A migration generator has been - provided. See section 5.c. Generators in the README for instructions. This - warning will continue until you have thoroughly read the instructions. - STR - RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1") def initialize(record) @record = record - assert_sti_item_type_updated end # Invoked after rollbacks to ensure versions records are not created for @@ -85,9 +74,7 @@ def record_create data = event.data.merge(data_for_create) versions_assoc = @record.send(@record.class.versions_association_name) - version = versions_assoc.new(data) - version.save! - version + versions_assoc.create!(data) end # PT-AT extends this method to add its transaction id. @@ -111,12 +98,12 @@ def record_destroy(recording_order) # `data_for_destroy` but PT-AT still does. data = event.data.merge(data_for_destroy) - version = @record.class.paper_trail.version_class.new(data) - if version.save + version = @record.class.paper_trail.version_class.create(data) + if version.errors.any? + log_version_errors(version, :destroy) + else assign_and_reset_version_association(version) version - else - log_version_errors(version, :destroy) end end @@ -140,11 +127,11 @@ def record_update(force:, in_after_callback:, is_touch:) data = event.data.merge(data_for_update) versions_assoc = @record.send(@record.class.versions_association_name) - version = versions_assoc.new(data) - if version.save - version - else + version = versions_assoc.create(data) + if version.errors.any? log_version_errors(version, :update) + else + version end end @@ -167,11 +154,11 @@ def record_update_columns(changes) data = event.data.merge(data_for_update_columns) versions_assoc = @record.send(@record.class.versions_association_name) - version = versions_assoc.new(data) - if version.save - version - else + version = versions_assoc.create(data) + if version.errors.any? log_version_errors(version, :update) + else + version end end @@ -240,20 +227,6 @@ def update_columns(attributes) record_update_columns(changes) end - # Given `@record`, when building the query for the `versions` association, - # what `item_type` (if any) should we use in our query. Returning nil - # indicates that rails should do whatever it normally does. - def versions_association_item_type - type_column = @record.class.inheritance_column - item_type = (respond_to?(type_column) ? send(type_column) : nil) || - @record.class.name - if item_type == @record.class.base_class.name - nil - else - item_type - end - end - # Returns the object (not a Version) as it was at the given timestamp. def version_at(timestamp, reify_options = {}) # Because a version stores how its object looked *before* the change, @@ -271,23 +244,6 @@ def versions_between(start_time, end_time) private - # @api private - def assert_sti_item_type_updated - # Does the user promise they have updated their `item_type`s? - return if ::PaperTrail.config.i_have_updated_my_existing_item_types - - # Is this class an STI subclass? - record_class = @record.class - return if record_class.descends_from_active_record? - - # Have we already issued this warning? - ::PaperTrail.config.classes_warned_about_sti_item_types ||= [] - return if ::PaperTrail.config.classes_warned_about_sti_item_types.include?(record_class) - - ::Kernel.warn(format(E_STI_ITEM_TYPES_NOT_UPDATED, record_class.name)) - ::PaperTrail.config.classes_warned_about_sti_item_types << record_class - end - # @api private def assign_and_reset_version_association(version) @record.send("#{@record.class.version_association_name}=", version) diff --git a/lib/paper_trail/reifier.rb b/lib/paper_trail/reifier.rb index 47011725f..d2a60bce4 100644 --- a/lib/paper_trail/reifier.rb +++ b/lib/paper_trail/reifier.rb @@ -48,9 +48,9 @@ def apply_defaults_to(options, version) # In this situation we constantize the `item_type` to get hold of the # class...except when the stored object's attributes include a `type` # key. If this is the case, the object we belong to is using single - # table inheritance (STI) and the `item_type` could be either the base - # class or the actual subclass. Either way, we can trust ActiveRecord - # to know what to do by providing data in the inheritance_column. + # table inheritance (STI) and the `item_type` will be the base class, + # not the actual subclass. If `type` is present but empty, the class is + # the base class. def init_model(attrs, options, version) if options[:dup] != true && version.item model = version.item @@ -58,12 +58,12 @@ def init_model(attrs, options, version) init_unversioned_attrs(attrs, model) end else - klass = version.item_type.constantize + klass = version_reification_class(version, attrs) # The `dup` option always returns a new object, otherwise we should # attempt to look for the item outside of default scope(s). find_cond = { klass.primary_key => version.item_id } if options[:dup] || (item_found = klass.unscoped.where(find_cond).first).nil? - model = version_reification_item(klass, attrs) + model = klass.new elsif options[:unversioned_attributes] == :nil model = item_found init_unversioned_attrs(attrs, model) @@ -110,17 +110,21 @@ def reify_attributes(model, version, attrs) end end - # Allow ActiveRecord to build the skeletal reified object to its liking. - # This method supports Single Table Inheritance (STI) with custom - # inheritance columns. - # @api private - def version_reification_item(klass, attrs) - new_attrs = {} - inheritance_column = klass.inheritance_column - if attrs.include?(inheritance_column) - new_attrs[inheritance_column] = attrs[inheritance_column] - end - klass.new(new_attrs) + # Given a `version`, return the class to reify. This method supports + # Single Table Inheritance (STI) with custom inheritance columns. + # + # For example, imagine a `version` whose `item_type` is "Animal". The + # `animals` table is an STI table (it has cats and dogs) and it has a + # custom inheritance column, `species`. If `attrs["species"]` is "Dog", + # this method returns the constant `Dog`. If `attrs["species"]` is blank, + # this method returns the constant `Animal`. You can see this particular + # example in action in `spec/models/animal_spec.rb`. + # + def version_reification_class(version, attrs) + inheritance_column_name = version.item_type.constantize.inheritance_column + inher_col_value = attrs[inheritance_column_name] + class_name = inher_col_value.blank? ? version.item_type : inher_col_value + class_name.constantize end end end diff --git a/spec/dummy_app/app/models/management.rb b/spec/dummy_app/app/models/management.rb new file mode 100644 index 000000000..3e983040c --- /dev/null +++ b/spec/dummy_app/app/models/management.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Note that there is no `type` column for this subclassed model, so changes to +# Management objects should result in Versions which have an item_type of +# Customer. +class Management < Customer +end diff --git a/spec/dummy_app/config/initializers/paper_trail.rb b/spec/dummy_app/config/initializers/paper_trail.rb index 274aa269b..648e1ada7 100644 --- a/spec/dummy_app/config/initializers/paper_trail.rb +++ b/spec/dummy_app/config/initializers/paper_trail.rb @@ -1,4 +1,3 @@ # frozen_string_literal: true ::PaperTrail.config.track_associations = true -::PaperTrail.config.i_have_updated_my_existing_item_types = true diff --git a/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb b/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb index d102aa90a..154de4756 100644 --- a/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb +++ b/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb @@ -78,9 +78,10 @@ def up end create_table :versions, versions_table_options do |t| - t.string :item_type, item_type_options - t.integer :item_id, null: false - t.string :event, null: false + t.string :item_type, item_type_options(null: false) + t.integer :item_id, null: false + t.string :item_subtype, item_type_options(null: true) + t.string :event, null: false t.string :whodunnit t.text :object, limit: TEXT_BYTES t.text :object_changes, limit: TEXT_BYTES @@ -377,8 +378,8 @@ def down private - def item_type_options - opt = { null: false } + def item_type_options(null:) + opt = { null: null } opt[:limit] = 191 if mysql? opt end diff --git a/spec/generators/paper_trail/install_generator_spec.rb b/spec/generators/paper_trail/install_generator_spec.rb index 84ef5aee2..85dbe5001 100644 --- a/spec/generators/paper_trail/install_generator_spec.rb +++ b/spec/generators/paper_trail/install_generator_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" require "generator_spec/test_case" -require File.expand_path("../../../lib/generators/paper_trail/install/install_generator", __dir__) +require File.expand_path("../../../lib/generators/paper_trail/install_generator", __dir__) RSpec.describe PaperTrail::InstallGenerator, type: :generator do include GeneratorSpec::TestCase @@ -48,15 +48,6 @@ } } ) - expect(destination_root).not_to( - have_structure { - directory("db") { - directory("migrate") { - migration("add_object_changes_to_versions") - } - } - } - ) end end diff --git a/spec/models/cat_spec.rb b/spec/models/cat_spec.rb index 22054e1e1..100e87d3c 100644 --- a/spec/models/cat_spec.rb +++ b/spec/models/cat_spec.rb @@ -8,22 +8,4 @@ expect(described_class.descends_from_active_record?).to eq(false) end end - - describe "#paper_trail" do - before do - ::PaperTrail.config.i_have_updated_my_existing_item_types = nil - end - - after do - ::PaperTrail.config.i_have_updated_my_existing_item_types = true - end - - it "warns that STI item_type has not been updated" do - expect { described_class.create }.to( - output( - /It looks like Cat is an STI subclass, and you have not yet updated/ - ).to_stderr - ) - end - end end diff --git a/spec/models/family/celebrity_family_spec.rb b/spec/models/family/celebrity_family_spec.rb index 3aa2d49c9..6d93f2c30 100644 --- a/spec/models/family/celebrity_family_spec.rb +++ b/spec/models/family/celebrity_family_spec.rb @@ -4,24 +4,33 @@ module Family RSpec.describe CelebrityFamily, type: :model, versioning: true do - it { is_expected.to be_versioned } + describe "#joins" do + it "works on an STI model" do + described_class.create! + result = described_class. + joins(:versions). + select("families.id, max(versions.event) as event"). + group("families.id"). + first + expect(result.event).to eq("create") + end + end describe "#create" do - # https://github.com/paper-trail-gem/paper_trail/pull/1108 - it "creates a version record with item_type == class.name, not base_class" do + it "creates version with item_subtype == class.name, not base_class" do carter = described_class.create( name: "Carter", path_to_stardom: "Mexican radio" ) v = carter.versions.last expect(v[:event]).to eq("create") - expect(v[:item_type]).to eq("Family::CelebrityFamily") + expect(v[:item_subtype]).to eq("Family::CelebrityFamily") end end describe "#reify" do context "belongs_to" do - it "uses the correct item_type in queries" do + it "uses the correct item_subtype" do parent = described_class.new(name: "Jermaine Jackson") parent.path_to_stardom = "Emulating Motown greats such as the Temptations and "\ "The Supremes" @@ -32,12 +41,13 @@ module Family name: "Hazel Gordy", children_attributes: { id: child1.id, name: "Jay Jackson" } ) - # We expect `reify` for all versions to have item_type 'Family::CelebrityFamily', - # not 'Family::Family'. See PR #1108 + expect(parent.versions.count).to eq(2) # A create and an update parent.versions.each do |parent_version| - expect(parent_version.item_type).to eq(parent.class.name) + expect(parent_version.item_type).to eq("Family::Family") + expect(parent_version.item_subtype).to eq("Family::CelebrityFamily") end + expect(parent.versions[1].reify).to be_a(::Family::CelebrityFamily) end end @@ -52,11 +62,10 @@ module Family parent.children.build(name: "Pugsley") parent.save! - # We expect `reify` for all versions to have item_type 'Family::CelebrityFamily', - # not 'Family::Family'. See PR #1108 expect(parent.versions.count).to eq(2) parent.versions.each do |parent_version| - expect(parent_version.item_type).to eq(parent.class.name) + expect(parent_version.item_type).to eq("Family::Family") + expect(parent_version.item_subtype).to eq("Family::CelebrityFamily") end end end @@ -73,11 +82,10 @@ module Family parent.grandsons.build(name: "Rodney") parent.save! - # We expect `reify` for all versions to have item_type 'Family::CelebrityFamily', - # not 'Family::Family'. See PR #1108 expect(parent.versions.count).to eq(2) parent.versions.each do |parent_version| - expect(parent_version.item_type).to eq(parent.class.name) + expect(parent_version.item_type).to eq("Family::Family") + expect(parent_version.item_subtype).to eq("Family::CelebrityFamily") end end end @@ -95,19 +103,17 @@ module Family mentee_attributes: { id: parent.mentee.id, name: "Al Shean" } ) - # We expect `reify` for all versions to have item_type 'Family::CelebrityFamily', - # not 'Family::Family'. See PR #1108 expect(parent.versions.count).to eq(2) parent.versions.each do |parent_version| - expect(parent_version.item_type).to eq(parent.class.name) + expect(parent_version.item_type).to eq("Family::Family") + expect(parent_version.item_subtype).to eq("Family::CelebrityFamily") end end end end describe "#update" do - # https://github.com/paper-trail-gem/paper_trail/pull/1108 - it "creates a version record with item_type == class.name, not base_class" do + it "creates version with item_subtype == class.name, not base_class" do carter = described_class.create( name: "Carter", path_to_stardom: "Mexican radio" @@ -115,7 +121,8 @@ module Family carter.update(path_to_stardom: "Johnny") v = carter.versions.last expect(v[:event]).to eq("update") - expect(v[:item_type]).to eq("Family::CelebrityFamily") + expect(v[:item_type]).to eq("Family::Family") + expect(v[:item_subtype]).to eq("Family::CelebrityFamily") end end end diff --git a/spec/models/management_spec.rb b/spec/models/management_spec.rb new file mode 100644 index 000000000..bf3cb9277 --- /dev/null +++ b/spec/models/management_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "spec_helper" + +::RSpec.describe(::Management, type: :model, versioning: true) do + it "utilises the base_class for STI classes having no type column" do + expect(Management.inheritance_column).to eq("type") + expect(Management.columns.map(&:name)).not_to include("type") + + # Create, update, and destroy a Management and a Customer + customer1 = Customer.create(name: "Cust 1") + customer2 = Management.create(name: "Cust 2") + customer1.update(name: "Cust 1a") + customer2.update(name: "Cust 2a") + customer1.destroy + customer2.destroy + + # All versions end up with an `item_type` of Customer + expect( + PaperTrail::Version.where(item_type: "Customer").count + ).to eq(6) + expect( + PaperTrail::Version.where(item_type: "Management").count + ).to eq(0) + + # The item_subtype, on the other hand, is 3 and 3 + expect( + PaperTrail::Version.where(item_subtype: "Customer").count + ).to eq(3) + expect( + PaperTrail::Version.where(item_subtype: "Management").count + ).to eq(3) + end +end diff --git a/spec/models/person_spec.rb b/spec/models/person_spec.rb index 1f3c8ff6c..fe45779c6 100644 --- a/spec/models/person_spec.rb +++ b/spec/models/person_spec.rb @@ -7,8 +7,6 @@ # - has a dozen associations of various types # - has a custom serializer, TimeZoneSerializer, for its `time_zone` attribute RSpec.describe Person, type: :model, versioning: true do - it { is_expected.to be_versioned } - describe "#time_zone" do it "returns an ActiveSupport::TimeZone" do person = Person.new(time_zone: "Samoa") @@ -168,7 +166,7 @@ expect(person.reload.versions.length).to(eq(3)) - # These will work when PT-AT has PR #5 merged: + # These will work when PT-AT adds support for the new `item_subtype` column # second_version = person.reload.versions.second.reify(has_one: true) # expect(second_version.car.name).to(eq("BMW 325")) # expect(second_version.bicycle.name).to(eq("BMX 1.0")) diff --git a/spec/models/pet_spec.rb b/spec/models/pet_spec.rb index 47ff4652c..0863e578c 100644 --- a/spec/models/pet_spec.rb +++ b/spec/models/pet_spec.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true require "spec_helper" -require "rails/generators" RSpec.describe Pet, type: :model, versioning: true do - it { is_expected.to be_versioned } + it "baseline test setup" do + expect(Pet.new).to be_versioned + end it "can be reified" do person = Person.create(name: "Frank") @@ -26,21 +27,10 @@ expect(second_version.animals.length).to(eq(2)) expect(second_version.animals.map { |a| a.class.name }).to(eq(%w[Dog Cat])) expect(second_version.pets.map { |p| p.animal.class.name }).to(eq(%w[Dog Cat])) - # (A fix in PT_AT to better reify STI tables and thus have these next four - # examples function is in the works. -- @LorinT) - - # As a side-effect to the fix for Issue #594, this errantly brings back Beethoven. - # expect(second_version.animals.first.name).to(eq("Snoopy")) - - # This will work when PT-AT has PR #5 merged: - # expect(second_version.dogs.first.name).to(eq("Snoopy")) - # (specifically needs the base_class removed in reifiers/has_many_through.rb) - - # As a side-effect to the fix for Issue #594, this errantly brings back Sylvester. - # expect(second_version.animals.second.name).to(eq("Garfield")) - - # This will work when PT-AT has PR #5 merged: - # expect(second_version.cats.first.name).to(eq("Garfield")) + expect(second_version.animals.first.name).to(eq("Snoopy")) + expect(second_version.dogs.first.name).to(eq("Snoopy")) + expect(second_version.animals.second.name).to(eq("Garfield")) + expect(second_version.cats.first.name).to(eq("Garfield")) last_version = person.reload.versions.last.reify(has_many: true) expect(last_version.pets.length).to(eq(2)) @@ -52,123 +42,4 @@ expect(last_version.animals.second.name).to(eq("Sylvester")) expect(last_version.cats.first.name).to(eq("Sylvester")) end - - context "Older version entry present where item_type refers to the base_class" do - let(:cat) { Cat.create(name: "Garfield") } # Index 0 - let(:animal) { Animal.create } # Index 4 - - before do - # This line runs the `let` for :cat, creating two entries - cat.update_attributes(name: "Sylvester") # Index 1 - second - cat.update_attributes(name: "Cheshire") # Index 2 - third - cat.destroy # Index 3 - fourth - # Prior to PR#1108 a subclassed version's item_type referred to the base_class, but - # now it refers to the class itself. In order to simulate an entry having been made - # in the old way, set one of our versions to be "Animal" instead of "Cat". - versions = PaperTrail::Version.order(:id) - versions.second.update(item_type: cat.class.base_class.name) - - # This line runs the `let` for :animal, creating two entries - animal.update(name: "Muppets Drummer") # Index 5 - animal.destroy # Index 6 - end - - it "can reify a subclassed item" do - versions = PaperTrail::Version.order(:id) - - # Still the reification process correctly brings back Cat since `species` is - # properly set to this sub-classed name. - expect(versions.second.reify).to be_a(Cat) # Sylvester - expect(versions.third.reify).to be_a(Cat) # Cheshire - expect(versions.fourth.reify).to be_a(Cat) # Cheshire that was destroyed - - # Creating an object from the base class is correctly identified as "Animal" - expect(versions[5].reify).to be_an(Animal) # Muppets Drummer - expect(versions[6].reify).to be_an(Animal) # Animal that was destroyed - end - - it "has a generator that builds migrations to upgrade older entries" do - # When using the has_many :versions association, it only finds versions in which - # item_type refers directly to the subclass name. - expect(cat.versions.count).to eq(3) - # To have has_many :versions work properly, you can generate and run a migration - # that examines all existing models to identify use of STI, then updates all older - # version entries that may refer to the base_class so they refer to the subclass. - # (This is the same as running: rails g paper_trail:update_sti; rails db:migrate) - migrator = ::PaperTrailSpecMigrator.new - expect { - migrator.generate_and_migrate("paper_trail:update_sti", []) - }.to output(/Associated 1 record to Cat/).to_stdout - # And now it finds all four changes - cat_versions = cat.versions.order(:id).to_a - expect(cat_versions.length).to eq(4) - expect(cat_versions.map(&:event)).to eq(%w[create update update destroy]) - - # And Animal is unaffected - animal_versions = animal.versions.order(:id).to_a - expect(animal_versions.length).to eq(3) - expect(animal_versions.map(&:event)).to eq(%w[create update destroy]) - end - - # After creating a bunch of records above, we change the inheritance_column - # so that we can demonstrate passing hints to the migration generator. - context "simulate a historical change to inheritance_column" do - before do - Animal.inheritance_column = "species_xyz" - end - - after do - # Clean up the temporary switch-up - Animal.inheritance_column = "species" - end - - it "no hints given to generator, does not generate the correct migration" do - # Because of the change to inheritance_column, the generator `rails g - # paper_trail:update_sti` would be unable to determine the previous - # inheritance_column, so a generated migration *with no hints* would - # accomplish nothing. - migrator = ::PaperTrailSpecMigrator.new - hints = [] - expect { - migrator.generate_and_migrate("paper_trail:update_sti", hints) - }.not_to output(/Associated 1 record to Cat/).to_stdout - - expect(cat.versions.length).to eq(3) - # And older Cat changes remain stored as Animal. - expect(PaperTrail::Version.where(item_type: "Animal", item_id: cat.id).count).to eq(1) - end - - it "giving hints to the generator, updates older entries in a custom way" do - # Pick up all version IDs regarding our single cat Garfield / Sylvester / Cheshire - cat_ids = PaperTrail::Version.where(item_type: %w[Animal Cat], item_id: cat.id). - order(:id).pluck(:id) - - # This time (as opposed to above example) we are going to provide hints - # to the generator. - # - # You can specify custom inheritance_column settings over a range of - # IDs so that the generated migration will properly update all your historic versions, - # having them now to refer to the proper subclass. - - # This is the same as running: - # rails g paper_trail:update_sti Animal(species):1..4; rails db:migrate - migrator = ::PaperTrailSpecMigrator.new - hints = ["Animal(species):#{cat_ids.first}..#{cat_ids.last}"] - expect { - migrator.generate_and_migrate("paper_trail:update_sti", hints) - }.to output(/Associated 1 record to Cat/).to_stdout - - # And now the has_many :versions properly finds all four changes - cat_versions = cat.versions.order(:id).to_a - - expect(cat_versions.length).to eq(4) - expect(cat_versions.map(&:event)).to eq(%w[create update update destroy]) - - # And Animal is still unaffected - animal_versions = animal.versions.order(:id).to_a - expect(animal_versions.length).to eq(3) - expect(animal_versions.map(&:event)).to eq(%w[create update destroy]) - end - end - end end diff --git a/spec/models/song_spec.rb b/spec/models/song_spec.rb new file mode 100644 index 000000000..91f44e951 --- /dev/null +++ b/spec/models/song_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "spec_helper" + +::RSpec.describe(::Song, type: :model, versioning: true) do + describe "#joins" do + it "works" do + described_class.create! + result = described_class. + joins(:versions). + select("songs.id, max(versions.event) as event"). + group("songs.id"). + first + expect(result.event).to eq("create") + end + end +end diff --git a/spec/paper_trail/events/destroy_spec.rb b/spec/paper_trail/events/destroy_spec.rb index aa65de721..fd89ed0ed 100644 --- a/spec/paper_trail/events/destroy_spec.rb +++ b/spec/paper_trail/events/destroy_spec.rb @@ -6,14 +6,14 @@ module PaperTrail module Events ::RSpec.describe Destroy do describe "#data", versioning: true do - # https://github.com/paper-trail-gem/paper_trail/pull/1108 - it "uses class.name for item_type, not base_class" do + it "includes correct item_subtype" do carter = Family::CelebrityFamily.new( name: "Carter", path_to_stardom: "Mexican radio" ) data = PaperTrail::Events::Destroy.new(carter, true).data - expect(data[:item_type]).to eq("Family::CelebrityFamily") + expect(data[:item_type]).to eq("Family::Family") + expect(data[:item_subtype]).to eq("Family::CelebrityFamily") end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index dc49df4ea..08d6f78e7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -50,7 +50,9 @@ def params_wrapper(args) # Migrate require_relative "support/paper_trail_spec_migrator" -::PaperTrailSpecMigrator.new.migrate +::PaperTrailSpecMigrator. + new(::File.expand_path("dummy_app/db/migrate/", __dir__)). + migrate RSpec.configure do |config| config.fixture_path = "#{::Rails.root}/spec/fixtures" diff --git a/spec/support/alt_db_init.rb b/spec/support/alt_db_init.rb index 0f71ae5af..f82887c39 100644 --- a/spec/support/alt_db_init.rb +++ b/spec/support/alt_db_init.rb @@ -40,7 +40,8 @@ class Document < Base Foo::Base.configurations = configs Foo::Base.establish_connection(:foo) ActiveRecord::Base.establish_connection(:foo) -::PaperTrailSpecMigrator.new.migrate +paper_trail_migrations_path = File.expand_path("#{db_directory}/migrate/", __FILE__) +::PaperTrailSpecMigrator.new(paper_trail_migrations_path).migrate module Bar class Base < ActiveRecord::Base @@ -59,4 +60,4 @@ class Document < Base Bar::Base.configurations = configs Bar::Base.establish_connection(:bar) ActiveRecord::Base.establish_connection(:bar) -::PaperTrailSpecMigrator.new.migrate +::PaperTrailSpecMigrator.new(paper_trail_migrations_path).migrate diff --git a/spec/support/paper_trail_spec_migrator.rb b/spec/support/paper_trail_spec_migrator.rb index b4783bd0f..57ffc0f39 100644 --- a/spec/support/paper_trail_spec_migrator.rb +++ b/spec/support/paper_trail_spec_migrator.rb @@ -1,15 +1,14 @@ # frozen_string_literal: true -# Manage migrations including running generators to build them, and cleaning up strays +# Looks like the API for programatically running migrations will change +# in rails 5.2. This is an undocumented change, AFAICT. Then again, +# how many people use the programmatic interface? Most people probably +# just use rake. Maybe we're doing it wrong. class PaperTrailSpecMigrator - def initialize - @migrations_path = dummy_app_migrations_dir + def initialize(migrations_path) + @migrations_path = migrations_path end - # Looks like the API for programatically running migrations will change - # in rails 5.2. This is an undocumented change, AFAICT. Then again, - # how many people use the programmatic interface? Most people probably - # just use rake. Maybe we're doing it wrong. def migrate if ::ActiveRecord.gem_version >= ::Gem::Version.new("5.2.0.rc1") ::ActiveRecord::MigrationContext.new(@migrations_path).migrate @@ -17,48 +16,4 @@ def migrate ::ActiveRecord::Migrator.migrate(@migrations_path) end end - - # Generate a migration, run it, and delete it. We use this for testing the - # UpdateStiGenerator. We delete the file because we don't want it to exist - # when we run migrations at the beginning of the next full test suite run. - # - # - generator [String] - name of generator, eg. "paper_trail:update_sti" - # - generator_invoke_args [Array] - arguments to `Generators#invoke` - def generate_and_migrate(generator, generator_invoke_args) - files = generate(generator, generator_invoke_args) - begin - migrate - ensure - files.each do |file| - File.delete(Rails.root.join(file)) - end - end - end - - private - - def dummy_app_migrations_dir - Pathname.new(File.expand_path("../dummy_app/db/migrate", __dir__)) - end - - # Run the specified migration generator. - # - # We sleep until the next whole second because that is the precision of the - # timestamp that rails puts in generator filenames. If we didn't sleep, - # there's a good chance two tests would run within the same second and - # generate the same exact migration filename. Then, even though we delete the - # generated migrations after running them, some form of caching (perhaps - # filesystem, perhaps rails) will run the cached migration file. - # - # - generator [String] - name of generator, eg. "paper_trail:update_sti" - # - generator_invoke_args [Array] - arguments to `Generators#invoke` - def generate(generator, generator_invoke_args) - sleep_until_the_next_whole_second - Rails::Generators.invoke(generator, generator_invoke_args, destination_root: Rails.root) - end - - def sleep_until_the_next_whole_second - t = Time.now.to_f - sleep((t.ceil - t).abs + 0.01) - end end