From f08e8c4bb703c7f0e84da51ccd70bbd9c203e80d Mon Sep 17 00:00:00 2001
From: Igor Drozdov <idrozdov@gitlab.com>
Date: Mon, 12 Aug 2024 19:46:32 +0000
Subject: [PATCH] Add ActiveRecord::Enum patch

It removes newly introduced check that doesn't allow defining
an enum without backed column.

A migration job fails because it:

- Performs database migration on a particular version
- The version does not contain email_confirmation_setting column
- Performs another migration that initializes Rails env
- Initializers call Gitlab::CurrentSetting
- ApplicationSetting tries to define email_confirmation_setting enum
- Fails because the enum is not backed by a column
---
 config/initializers/00_active_record_enum.rb  | 31 +++++++++++
 .../database/migrations/pg_backend_pid.rb     | 19 ++++++-
 .../postgresql_adapter/type_map_cache.rb      |  8 ++-
 .../00_active_record_enum_spec.rb             | 22 ++++++++
 .../migrations/pg_backend_pid_spec.rb         | 53 +++++++++++++++++--
 .../postgresql_adapter/type_map_cache_spec.rb |  2 +
 6 files changed, 128 insertions(+), 7 deletions(-)
 create mode 100644 config/initializers/00_active_record_enum.rb
 create mode 100644 spec/initializers/00_active_record_enum_spec.rb

diff --git a/config/initializers/00_active_record_enum.rb b/config/initializers/00_active_record_enum.rb
new file mode 100644
index 0000000000000..d25651154e503
--- /dev/null
+++ b/config/initializers/00_active_record_enum.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+# Override ActiveRecord::Enum.load_schema! to remove the check that verifies that
+# defined enum attributes are backed by a database column.
+#
+# The check has been introduced in Rails 7.1: https://github.com/rails/rails/pull/45734
+#
+# However, there are valid use cases when the enum definition should not raise an error
+# when the column does not exist in the column list:
+#
+# - Perform database migration on a particular version, the one that doesn't contain the column
+# - Try performing another operation that loads Rails environment (for example, another migration):
+#   - Rails environment is being initialized and configured
+#   - Gitlab::CurrentSetting is being called to customize the configuration
+#   - ApplicationSetting model tries to define a enum attribute that is not backed by a column on that patricular
+#   migration version
+#   - An error is raised
+
+module ActiveRecordAttributesPatch
+  def attribute(name, *args, **options)
+    return super unless defined_enums.key?(name)
+    return unless block_given?
+
+    super do |subtype|
+      subtype = subtype.subtype if ActiveRecord::Enum::EnumType === subtype
+      ActiveRecord::Enum::EnumType.new(name, defined_enums[name], subtype)
+    end
+  end
+end
+
+ActiveRecord::Attributes::ClassMethods.prepend(ActiveRecordAttributesPatch)
diff --git a/lib/gitlab/database/migrations/pg_backend_pid.rb b/lib/gitlab/database/migrations/pg_backend_pid.rb
index 52f309e405821..c4dea00cc278f 100644
--- a/lib/gitlab/database/migrations/pg_backend_pid.rb
+++ b/lib/gitlab/database/migrations/pg_backend_pid.rb
@@ -7,6 +7,19 @@ module PgBackendPid
         module MigratorPgBackendPid
           extend ::Gitlab::Utils::Override
 
+          override :with_advisory_lock
+          def with_advisory_lock
+            Gitlab::Database::Migrations::PgBackendPid.say(connection)
+
+            super
+          ensure
+            Gitlab::Database::Migrations::PgBackendPid.say(connection)
+          end
+        end
+
+        module OldMigratorPgBackendPid
+          extend ::Gitlab::Utils::Override
+
           override :with_advisory_lock_connection
           def with_advisory_lock_connection
             super do |conn|
@@ -20,7 +33,11 @@ def with_advisory_lock_connection
         end
 
         def self.patch!
-          ActiveRecord::Migrator.prepend(MigratorPgBackendPid)
+          if ::Gitlab.next_rails?
+            ActiveRecord::Migrator.prepend(MigratorPgBackendPid)
+          else
+            ActiveRecord::Migrator.prepend(OldMigratorPgBackendPid)
+          end
         end
 
         def self.say(conn)
diff --git a/lib/gitlab/database/postgresql_adapter/type_map_cache.rb b/lib/gitlab/database/postgresql_adapter/type_map_cache.rb
index ff66d9115ab1f..0e6cbded10ab8 100644
--- a/lib/gitlab/database/postgresql_adapter/type_map_cache.rb
+++ b/lib/gitlab/database/postgresql_adapter/type_map_cache.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 # Caches loading of additional types from the DB
-# https://github.com/rails/rails/blob/v6.0.3.2/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L521-L589
+# https://github.com/rails/rails/blob/v7.1.3.4/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L997
 
 # rubocop:disable Gitlab/ModuleWithInstanceVariables
 
@@ -32,6 +32,12 @@ def initialize_type_map(map = type_map)
         end
 
         def reload_type_map
+          if ::Gitlab.next_rails? && @type_map.blank?
+            @type_map = ActiveRecord::Type::HashLookupTypeMap.new
+
+            return initialize_type_map
+          end
+
           TYPE_MAP_CACHE_MONITOR.synchronize do
             self.class.type_map_cache[@connection_parameters.hash] = nil
           end
diff --git a/spec/initializers/00_active_record_enum_spec.rb b/spec/initializers/00_active_record_enum_spec.rb
new file mode 100644
index 0000000000000..f413d14e642e3
--- /dev/null
+++ b/spec/initializers/00_active_record_enum_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# Adds a missing test to provide full coverage for the patch
+RSpec.describe 'ActiveRecord::Enum Patch', feature_category: :database do
+  context 'when enum is not backed by a database column' do
+    let(:klass) do
+      Class.new(ActiveRecord::Base) do
+        self.table_name = :projects
+
+        enum values_without_backed_column: [:one, :two]
+      end
+    end
+
+    it 'does not raise an error' do
+      expect do
+        klass.new.values_without_backed_column
+      end.not_to raise_error
+    end
+  end
+end
diff --git a/spec/lib/gitlab/database/migrations/pg_backend_pid_spec.rb b/spec/lib/gitlab/database/migrations/pg_backend_pid_spec.rb
index a9ef28a4b5189..6c2ecb0b1f097 100644
--- a/spec/lib/gitlab/database/migrations/pg_backend_pid_spec.rb
+++ b/spec/lib/gitlab/database/migrations/pg_backend_pid_spec.rb
@@ -4,6 +4,47 @@
 
 RSpec.describe Gitlab::Database::Migrations::PgBackendPid, feature_category: :database do
   describe Gitlab::Database::Migrations::PgBackendPid::MigratorPgBackendPid do
+    let(:block) { -> {} }
+
+    let(:klass) do
+      Class.new do
+        def initialize(block)
+          @block = block
+        end
+
+        def with_advisory_lock
+          @block.call
+        end
+
+        def connection
+          ApplicationRecord.connection
+        end
+      end
+    end
+
+    let(:patched_instance) { klass.prepend(described_class).new(block) }
+
+    it 'wraps the method execution with calls to .say' do
+      expect(Gitlab::Database::Migrations::PgBackendPid).to receive(:say).twice
+      expect(block).to receive(:call)
+
+      patched_instance.with_advisory_lock
+    end
+
+    context 'when an error is raised' do
+      let(:block) { -> { raise ActiveRecord::ConcurrentMigrationError, 'test' } }
+
+      it 'wraps the method execution with calls to .say' do
+        expect(Gitlab::Database::Migrations::PgBackendPid).to receive(:say).twice
+
+        expect do
+          patched_instance.with_advisory_lock
+        end.to raise_error ActiveRecord::ConcurrentMigrationError
+      end
+    end
+  end
+
+  describe Gitlab::Database::Migrations::PgBackendPid::OldMigratorPgBackendPid do
     let(:klass) do
       Class.new do
         def with_advisory_lock_connection
@@ -33,16 +74,20 @@ def with_advisory_lock_connection
 
   describe '.patch!' do
     it 'patches ActiveRecord::Migrator' do
-      expect(ActiveRecord::Migrator).to receive(:prepend).with(described_class::MigratorPgBackendPid)
+      if ::Gitlab.next_rails?
+        expect(ActiveRecord::Migrator).to receive(:prepend).with(described_class::MigratorPgBackendPid)
+      else
+        expect(ActiveRecord::Migrator).to receive(:prepend).with(described_class::OldMigratorPgBackendPid)
+      end
 
       described_class.patch!
     end
   end
 
   describe '.say' do
-    it 'outputs the connection information' do
-      conn = ActiveRecord::Base.connection
+    let(:conn) { ActiveRecord::Base.connection }
 
+    it 'outputs the connection information' do
       expect(conn).to receive(:object_id).and_return(9876)
       expect(conn).to receive(:select_value).with('SELECT pg_backend_pid()').and_return(12345)
       expect(Gitlab::Database).to receive(:db_config_name).with(conn).and_return('main')
@@ -53,8 +98,6 @@ def with_advisory_lock_connection
     end
 
     it 'outputs nothing if ActiveRecord::Migration.verbose is false' do
-      conn = ActiveRecord::Base.connection
-
       allow(ActiveRecord::Migration).to receive(:verbose).and_return(false)
 
       expect { described_class.say(conn) }.not_to output.to_stdout
diff --git a/spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb b/spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb
index 75c3a3650d71d..7e355c84e385c 100644
--- a/spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb
+++ b/spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb
@@ -50,6 +50,8 @@
 
   # Based on https://github.com/rails/rails/blob/v6.1.3.2/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L36-L41
   def initialize_connection(config = db_config)
+    return adapter_class.new(config).connect! if ::Gitlab.next_rails?
+
     conn_params = config.symbolize_keys.compact
 
     conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
-- 
GitLab