ifad/chronomodel

View on GitHub
lib/chrono_model/adapter/upgrade.rb

Summary

Maintainability
A
2 hrs
Test Coverage
F
29%
# frozen_string_literal: true

module ChronoModel
  class Adapter < ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
    module Upgrade
      def chrono_upgrade!
        chrono_ensure_schemas

        chrono_upgrade_structure!
      end

      private

      # Locate tables needing a structure upgrade
      #
      def chrono_tables_needing_upgrade
        tables = {}

        on_temporal_schema { self.tables }.each do |table_name|
          next unless is_chrono?(table_name)

          metadata = chrono_metadata_for(table_name)
          version = metadata['chronomodel']

          if version.blank?
            tables[table_name] = { version: nil, priority: 'CRITICAL' }
          elsif version != VERSION
            tables[table_name] = { version: version, priority: 'LOW' }
          end
        end

        tables
      end

      # Emit a warning about tables needing an upgrade
      #
      def chrono_upgrade_warning
        upgrade = chrono_tables_needing_upgrade.map do |table, desc|
          "#{table} - priority: #{desc[:priority]}"
        end.join('; ')

        return if upgrade.empty?

        logger.warn 'ChronoModel: There are tables needing a structure upgrade, and ChronoModel structures need to be recreated.'
        logger.warn 'ChronoModel: Please run ChronoModel.upgrade! to attempt the upgrade. If you have dependant database objects'
        logger.warn 'ChronoModel: the upgrade will fail and you have to drop the dependent objects, run .upgrade! and create them'
        logger.warn 'ChronoModel: again. Sorry. Some features or the whole library may not work correctly until upgrade is complete.'
        logger.warn "ChronoModel: Tables pending upgrade: #{upgrade}"
      end

      # Upgrades existing structure for each table, if required.
      #
      def chrono_upgrade_structure!
        transaction do
          chrono_tables_needing_upgrade.each do |table_name, desc|
            if desc[:version].blank?
              logger.info "ChronoModel: Upgrading legacy PG 9.0 table #{table_name} to #{VERSION}"
              chrono_upgrade_from_postgres_v90(table_name)
              logger.info "ChronoModel: legacy #{table_name} upgrade complete"
            else
              logger.info "ChronoModel: upgrading #{table_name} from #{desc[:version]} to #{VERSION}"
              chrono_public_view_ddl(table_name)
              logger.info "ChronoModel: #{table_name} upgrade complete"
            end
          end
        end
      rescue StandardError => e
        message = "ChronoModel structure upgrade failed: #{e.message}. Please drop dependent objects first and then run ChronoModel.upgrade! again."

        # Quite important, output it also to stderr.
        #
        logger.error message
        warn message
      end

      def chrono_upgrade_from_postgres_v90(table_name)
        # roses are red
        # violets are blue
        # and this is the most boring piece of code ever
        history_table = "#{HISTORY_SCHEMA}.#{table_name}"
        p_pkey = primary_key(table_name)

        execute "ALTER TABLE #{history_table} ADD COLUMN validity tsrange;"
        execute <<-SQL.squish
            UPDATE #{history_table} SET validity = tsrange(valid_from,
                CASE WHEN extract(year from valid_to) = 9999 THEN NULL
                     ELSE valid_to
                END
            );
        SQL

        execute "DROP INDEX #{history_table}_temporal_on_valid_from;"
        execute "DROP INDEX #{history_table}_temporal_on_valid_from_and_valid_to;"
        execute "DROP INDEX #{history_table}_temporal_on_valid_to;"
        execute "DROP INDEX #{history_table}_inherit_pkey"
        execute "DROP INDEX #{history_table}_recorded_at"
        execute "DROP INDEX #{history_table}_instance_history"
        execute "ALTER TABLE #{history_table} DROP CONSTRAINT #{table_name}_valid_from_before_valid_to;"
        execute "ALTER TABLE #{history_table} DROP CONSTRAINT #{table_name}_timeline_consistency;"
        execute "DROP RULE #{table_name}_upd_first ON #{table_name};"
        execute "DROP RULE #{table_name}_upd_next ON #{table_name};"
        execute "DROP RULE #{table_name}_del ON #{table_name};"
        execute "DROP RULE #{table_name}_ins ON #{table_name};"
        execute "DROP TRIGGER history_ins ON #{TEMPORAL_SCHEMA}.#{table_name};"
        execute "DROP FUNCTION #{TEMPORAL_SCHEMA}.#{table_name}_ins();"
        execute "ALTER TABLE #{history_table} DROP COLUMN valid_from;"
        execute "ALTER TABLE #{history_table} DROP COLUMN valid_to;"

        execute 'CREATE EXTENSION IF NOT EXISTS btree_gist;'

        chrono_public_view_ddl(table_name)
        on_history_schema { add_history_validity_constraint(table_name, p_pkey) }
        on_history_schema { chrono_create_history_indexes_for(table_name, p_pkey) }
      end
      # private
    end
  end
end