diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9acb3e94..f3bc8a33 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -3,7 +3,7 @@ name: Test
on: [push, pull_request]
jobs:
- test:
+ test_mysql:
strategy:
fail-fast: false
matrix:
@@ -36,6 +36,43 @@ jobs:
run: sudo systemctl start mysql.service
- run: bin/setup
- run: bundle exec rake
+
+ test_trilogy:
+ strategy:
+ fail-fast: false
+ matrix:
+ ruby:
+ - 3.2
+ - 3.3
+ - 3.4
+ gemfile:
+ - gemfiles/rails_8_1.gemfile
+ env:
+ DB_ADAPTER: "trilogy"
+ PERCONA_DB_USER: root
+ PERCONA_DB_PASSWORD: root
+ BUNDLE_GEMFILE: ${{ matrix.gemfile }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ matrix.ruby }}
+ bundler-cache: true
+ - name: "Add Percona GPG key"
+ run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 9334A25F8507EFA5
+ - name: "Add Percona APT repository"
+ run: echo "deb http://repo.percona.com/apt `lsb_release -cs` main" | sudo tee -a /etc/apt/sources.list
+ - run: sudo apt-get update -qq
+ - run: sudo apt-get install percona-toolkit
+ - name: Start MySQL server
+ run: sudo systemctl start mysql.service
+ - name: Configure MySQL for Trilogy
+ run: |
+ sudo mysql -u root -proot -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root'; FLUSH PRIVILEGES;"
+ - run: bin/setup
+ - run: bundle exec rake
+
lint:
strategy:
fail-fast: false
diff --git a/.rubocop.yml b/.rubocop.yml
index 6a8b14af..7c8d1ead 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -28,6 +28,8 @@ Layout/LineLength:
Exclude:
- 'departure.gemspec'
- 'test_database.rb'
+ - 'spec/*'
+ - 'spec/**/*'
Metrics/MethodLength:
Max: 30
diff --git a/Appraisals b/Appraisals
index 164058de..2da80096 100644
--- a/Appraisals
+++ b/Appraisals
@@ -10,4 +10,5 @@ end
appraise 'rails-8-1' do
gem 'rails', '8.1.1'
+ gem 'trilogy'
end
diff --git a/README.md b/README.md
index 913fc8e5..9bca27ff 100644
--- a/README.md
+++ b/README.md
@@ -151,9 +151,16 @@ It's strongly recommended to name it after this gems name, such as
All configuration options are configurable from the `Departure.configure` block example below
-|Option|Default|What it Controls|
-|---|---|---|
-|disable_rails_advisory_lock_patch|false|When truthy, disables a patch in at least rails 7.1 and 7.2 where rails throws ConcurrentMigrationErrors due to the inability to release the advisory lock in migrations|
+| Option | Default | What it Controls |
+|-----------------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| disable_rails_advisory_lock_patch | false | When truthy, disables a patch in at least rails 7.1 and 7.2 where rails throws ConcurrentMigrationErrors due to the inability to release the advisory lock in migrations |
+
+### Trilogy Adapter Support
+
+Starting in Rails 8.1 we add support for the use of the trilogy database adapter gem. Logic for selecting an adapter follows this logic
+
+1. If the database configuration specifies 'trilogy' use the trilogy adapter
+2. Default to mysql2
### Disable on per-migration basis
@@ -226,7 +233,7 @@ adapters.
%% Core Departure Components
subgraph "Departure System"
RailsAdapter["RailsAdapter
(Version Detection)"]
- DepartureAdapter["Rails81DepartureAdapter
(Connection Adapter)"]
+ DepartureAdapter["Rails81MysqlAdapter
(Connection Adapter)"]
Runner["Runner
(Query Interceptor)"]
Command["Command
(Process Executor)"]
CliGenerator["CliGenerator
(Command Builder)"]
diff --git a/gemfiles/rails_7_2.gemfile b/gemfiles/rails_7_2.gemfile
index a4240263..1264792b 100644
--- a/gemfiles/rails_7_2.gemfile
+++ b/gemfiles/rails_7_2.gemfile
@@ -1,16 +1,16 @@
# This file was generated by Appraisal
-source "https://rubygems.org"
+source 'https://rubygems.org'
-gem "base64"
-gem "codeclimate-test-reporter", "~> 1.0.3", group: :test, require: nil
-gem "lhm"
-gem "logger"
-gem "mutex_m", require: false
-gem "rubocop", "~> 1.74.0", require: false
-gem "rubocop-performance", "~> 1.20.2", require: false
-gem "zeitwerk", "< 2.7.0"
-gem "bigdecimal"
-gem "rails", "7.2.2.1"
+gem 'base64'
+gem 'bigdecimal'
+gem 'codeclimate-test-reporter', '~> 1.0.3', group: :test, require: nil
+gem 'lhm'
+gem 'logger'
+gem 'mutex_m', require: false
+gem 'rails', '7.2.2.1'
+gem 'rubocop', '~> 1.74.0', require: false
+gem 'rubocop-performance', '~> 1.20.2', require: false
+gem 'zeitwerk', '< 2.7.0'
-gemspec path: "../"
+gemspec path: '../'
diff --git a/gemfiles/rails_8_0.gemfile b/gemfiles/rails_8_0.gemfile
index 742c791d..79cd0c9b 100644
--- a/gemfiles/rails_8_0.gemfile
+++ b/gemfiles/rails_8_0.gemfile
@@ -1,16 +1,16 @@
# This file was generated by Appraisal
-source "https://rubygems.org"
+source 'https://rubygems.org'
-gem "base64"
-gem "codeclimate-test-reporter", "~> 1.0.3", group: :test, require: nil
-gem "lhm"
-gem "logger"
-gem "mutex_m", require: false
-gem "rubocop", "~> 1.74.0", require: false
-gem "rubocop-performance", "~> 1.20.2", require: false
-gem "zeitwerk", "< 2.7.0"
-gem "bigdecimal"
-gem "rails", "8.0.2.1"
+gem 'base64'
+gem 'bigdecimal'
+gem 'codeclimate-test-reporter', '~> 1.0.3', group: :test, require: nil
+gem 'lhm'
+gem 'logger'
+gem 'mutex_m', require: false
+gem 'rails', '8.0.2.1'
+gem 'rubocop', '~> 1.74.0', require: false
+gem 'rubocop-performance', '~> 1.20.2', require: false
+gem 'zeitwerk', '< 2.7.0'
-gemspec path: "../"
+gemspec path: '../'
diff --git a/gemfiles/rails_8_1.gemfile b/gemfiles/rails_8_1.gemfile
index 07e7cc31..21b127d7 100644
--- a/gemfiles/rails_8_1.gemfile
+++ b/gemfiles/rails_8_1.gemfile
@@ -1,15 +1,16 @@
# This file was generated by Appraisal
-source "https://rubygems.org"
+source 'https://rubygems.org'
-gem "base64"
-gem "codeclimate-test-reporter", "~> 1.0.3", group: :test, require: nil
-gem "lhm"
-gem "logger"
-gem "mutex_m", require: false
-gem "rubocop", "~> 1.74.0", require: false
-gem "rubocop-performance", "~> 1.20.2", require: false
-gem "zeitwerk", "< 2.7.0"
-gem "rails", "8.1.1"
+gem 'base64'
+gem 'codeclimate-test-reporter', '~> 1.0.3', group: :test, require: nil
+gem 'lhm'
+gem 'logger'
+gem 'mutex_m', require: false
+gem 'rails', '8.1.1'
+gem 'rubocop', '~> 1.74.0', require: false
+gem 'rubocop-performance', '~> 1.20.2', require: false
+gem 'trilogy'
+gem 'zeitwerk', '< 2.7.0'
-gemspec path: "../"
+gemspec path: '../'
diff --git a/gemfiles/rails_8_1.gemfile.lock b/gemfiles/rails_8_1.gemfile.lock
index e0420011..2adfcf9a 100644
--- a/gemfiles/rails_8_1.gemfile.lock
+++ b/gemfiles/rails_8_1.gemfile.lock
@@ -270,6 +270,7 @@ GEM
stringio (3.1.7)
thor (1.4.0)
timeout (0.4.4)
+ trilogy (2.9.0)
tsort (0.2.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
@@ -310,6 +311,7 @@ DEPENDENCIES
rspec-its (~> 1.2)
rubocop (~> 1.74.0)
rubocop-performance (~> 1.20.2)
+ trilogy
zeitwerk (< 2.7.0)
BUNDLED WITH
diff --git a/lib/active_record/connection_adapters/rails_8_1_departure_adapter.rb b/lib/active_record/connection_adapters/rails_8_1_mysql2_adapter.rb
similarity index 95%
rename from lib/active_record/connection_adapters/rails_8_1_departure_adapter.rb
rename to lib/active_record/connection_adapters/rails_8_1_mysql2_adapter.rb
index 886eff87..3fd94b77 100644
--- a/lib/active_record/connection_adapters/rails_8_1_departure_adapter.rb
+++ b/lib/active_record/connection_adapters/rails_8_1_mysql2_adapter.rb
@@ -6,12 +6,12 @@
module ActiveRecord
module ConnectionAdapters
- class Rails81DepartureAdapter < ActiveRecord::ConnectionAdapters::Mysql2Adapter
+ class Rails81Mysql2Adapter < ActiveRecord::ConnectionAdapters::Mysql2Adapter
TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) } if defined?(initialize_type_map)
class Column < ActiveRecord::ConnectionAdapters::MySQL::Column
def adapter
- Rails81DepartureAdapter
+ Rails81Mysql2Adapter
end
end
@@ -33,8 +33,6 @@ def visit_DropForeignKey(name) # rubocop:disable Naming/MethodName
end
end
- extend Forwardable
-
include ForAlterStatements unless method_defined?(:change_column_for_alter)
ADAPTER_NAME = 'Percona'.freeze
diff --git a/lib/active_record/connection_adapters/rails_8_1_trilogy_adapter.rb b/lib/active_record/connection_adapters/rails_8_1_trilogy_adapter.rb
new file mode 100644
index 00000000..a22670a1
--- /dev/null
+++ b/lib/active_record/connection_adapters/rails_8_1_trilogy_adapter.rb
@@ -0,0 +1,77 @@
+require 'active_record/connection_adapters/abstract_mysql_adapter'
+require 'active_record/connection_adapters/trilogy_adapter'
+require 'active_record/connection_adapters/patch_connection_handling'
+require 'departure'
+require 'forwardable'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class Rails81TrilogyAdapter < ActiveRecord::ConnectionAdapters::TrilogyAdapter
+ class Column < ActiveRecord::ConnectionAdapters::MySQL::Column
+ def adapter
+ Rails81TrilogyAdapter
+ end
+ end
+
+ # https://github.com/departurerb/departure/commit/f178ca26cd3befa1c68301d3b57810f8cdcff9eb
+ # For `DROP FOREIGN KEY constraint_name` with pt-online-schema-change requires specifying `_constraint_name`
+ # rather than the real constraint_name due to to a limitation in MySQL
+ # pt-online-schema-change adds a leading underscore to foreign key constraint names when creating the new table.
+ # https://www.percona.com/blog/2017/03/21/dropping-foreign-key-constraint-using-pt-online-schema-change-2/
+ class SchemaCreation < ActiveRecord::ConnectionAdapters::MySQL::SchemaCreation
+ def visit_DropForeignKey(name) # rubocop:disable Naming/MethodName
+ fk_name =
+ if name =~ /^__(.+)/
+ Regexp.last_match(1)
+ else
+ "_#{name}"
+ end
+
+ "DROP FOREIGN KEY #{fk_name}"
+ end
+ end
+
+ include ForAlterStatements unless method_defined?(:change_column_for_alter)
+
+ ADAPTER_NAME = 'Percona'.freeze
+
+ def self.new_client(config)
+ original_client = super
+
+ Departure::DbClient.new(config, original_client)
+ end
+
+ # add_index is modified from the underlying mysql adapter implementation to ensure we add ALTER TABLE to it
+ def add_index(table_name, column_name, options = {})
+ index_definition, = add_index_options(table_name, column_name, **options)
+ execute <<-SQL.squish
+ ALTER TABLE #{quote_table_name(index_definition.table)}
+ ADD #{schema_creation.accept(index_definition)}
+ SQL
+ end
+
+ # remove_index is modified from the underlying mysql adapter implementation to ensure we add ALTER TABLE to it
+ def remove_index(table_name, column_name = nil, **options)
+ return if options[:if_exists] && !index_exists?(table_name, column_name, **options)
+
+ index_name = index_name_for_remove(table_name, column_name, options)
+
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP INDEX #{quote_column_name(index_name)}"
+ end
+
+ def schema_creation
+ SchemaCreation.new(self)
+ end
+
+ private
+
+ # rubocop:disable Metrics/ParameterLists
+ def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false)
+ return raw_connection.send_to_pt_online_schema_change(sql) if raw_connection.alter_statement?(sql)
+
+ super
+ end
+ # rubocop:enable Metrics/ParameterLists
+ end
+ end
+end
diff --git a/lib/departure.rb b/lib/departure.rb
index f75c2d36..cfff97bd 100644
--- a/lib/departure.rb
+++ b/lib/departure.rb
@@ -23,8 +23,6 @@
# We need the OS not to buffer the IO to see pt-osc's output while migrating
$stdout.sync = true
-Departure::RailsAdapter.for_current.register_integrations
-
module Departure
class << self
attr_accessor :configuration
diff --git a/lib/departure/configuration.rb b/lib/departure/configuration.rb
index a6d94830..356e9054 100644
--- a/lib/departure/configuration.rb
+++ b/lib/departure/configuration.rb
@@ -1,7 +1,7 @@
module Departure
class Configuration
attr_accessor :tmp_path, :global_percona_args, :enabled_by_default, :redirect_stderr,
- :disable_rails_advisory_lock_patch
+ :disable_rails_advisory_lock_patch, :db_adapter_name
def initialize
@tmp_path = '.'.freeze
@@ -9,6 +9,7 @@ def initialize
@global_percona_args = nil
@enabled_by_default = true
@redirect_stderr = true
+ @db_adapter_name = nil
end
def error_log_path
diff --git a/lib/departure/rails_adapter.rb b/lib/departure/rails_adapter.rb
index 1fb7f4cb..a3a872f5 100644
--- a/lib/departure/rails_adapter.rb
+++ b/lib/departure/rails_adapter.rb
@@ -10,7 +10,13 @@ class UnsupportedRailsVersionError < StandardError; end
class MustImplementError < StandardError; end
class << self
+ def register_integrations(**args)
+ for_current(**args).register_integrations
+ end
+
def version_matches?(version_string, compatibility_string = current_version::STRING)
+ raise "Invalid Gem Version: '#{version_string}'" unless Gem::Version.correct?(version_string)
+
requirement = Gem::Requirement.new(compatibility_string)
requirement.satisfied_by?(Gem::Version.new(version_string))
end
@@ -19,13 +25,19 @@ def current_version
ActiveRecord::VERSION
end
- def for_current
- self.for(current_version)
+ def for_current(**args)
+ self.for(current_version, **args)
end
- def for(ar_version)
+ # rubocop:disable Metrics/PerceivedComplexity
+ def for(ar_version, db_connection_adapter: nil)
+ # rubocop:enable Metrics/PerceivedComplexity
if ar_version::MAJOR == 8 && ar_version::MINOR.positive?
- V8_1_Adapter
+ if db_connection_adapter == 'trilogy'
+ V8_1_TrilogyAdapter
+ else
+ V8_1_Mysql2Adapter
+ end
elsif ar_version::MAJOR == 8
V8_0_Adapter
elsif ar_version::MAJOR >= 7 && ar_version::MINOR >= 2
@@ -69,14 +81,12 @@ def register_integrations
require 'active_record/connection_adapters/rails_7_2_departure_adapter'
require 'departure/rails_patches/active_record_migrator_with_advisory_lock_patch'
- ActiveSupport.on_load(:active_record) do
- ActiveRecord::Migration.class_eval do
- include Departure::Migration
- end
-
- ActiveRecord::Migrator.prepend Departure::RailsPatches::ActiveRecordMigratorWithAdvisoryLockPatch
+ ActiveRecord::Migration.class_eval do
+ include Departure::Migration
end
+ ActiveRecord::Migrator.prepend Departure::RailsPatches::ActiveRecordMigratorWithAdvisoryLockPatch
+
ActiveRecord::ConnectionAdapters.register 'percona',
'ActiveRecord::ConnectionAdapters::Rails72DepartureAdapter',
'active_record/connection_adapters/rails_7_2_departure_adapter'
@@ -98,14 +108,12 @@ def register_integrations
require 'active_record/connection_adapters/rails_8_0_departure_adapter'
require 'departure/rails_patches/active_record_migrator_with_advisory_lock_patch'
- ActiveSupport.on_load(:active_record) do
- ActiveRecord::Migration.class_eval do
- include Departure::Migration
- end
-
- ActiveRecord::Migrator.prepend Departure::RailsPatches::ActiveRecordMigratorWithAdvisoryLockPatch
+ ActiveRecord::Migration.class_eval do
+ include Departure::Migration
end
+ ActiveRecord::Migrator.prepend Departure::RailsPatches::ActiveRecordMigratorWithAdvisoryLockPatch
+
ActiveRecord::ConnectionAdapters.register 'percona',
'ActiveRecord::ConnectionAdapters::Rails80DepartureAdapter',
'active_record/connection_adapters/rails_8_0_departure_adapter'
@@ -121,27 +129,25 @@ def sql_column
end
end
- class V8_1_Adapter < BaseAdapter # rubocop:disable Naming/ClassAndModuleCamelCase
+ class V8_1_Mysql2Adapter < BaseAdapter # rubocop:disable Naming/ClassAndModuleCamelCase
class << self
def register_integrations
- require 'active_record/connection_adapters/rails_8_1_departure_adapter'
+ require 'active_record/connection_adapters/rails_8_1_mysql2_adapter'
require 'departure/rails_patches/active_record_migrator_with_advisory_lock_patch'
- ActiveSupport.on_load(:active_record) do
- ActiveRecord::Migration.class_eval do
- include Departure::Migration
- end
-
- ActiveRecord::Migrator.prepend Departure::RailsPatches::ActiveRecordMigratorWithAdvisoryLockPatch
+ ActiveRecord::Migration.class_eval do
+ include Departure::Migration
end
+ ActiveRecord::Migrator.prepend Departure::RailsPatches::ActiveRecordMigratorWithAdvisoryLockPatch
+
ActiveRecord::ConnectionAdapters.register 'percona',
- 'ActiveRecord::ConnectionAdapters::Rails81DepartureAdapter',
- 'active_record/connection_adapters/rails_8_1_departure_adapter'
+ 'ActiveRecord::ConnectionAdapters::Rails81Mysql2Adapter',
+ 'active_record/connection_adapters/rails_8_1_mysql2_adapter'
end
def create_connection_adapter(**config)
- ActiveRecord::ConnectionAdapters::Rails81DepartureAdapter.new(config)
+ ActiveRecord::ConnectionAdapters::Rails81Mysql2Adapter.new(config)
end
# rubocop:disable Metrics/ParameterLists
@@ -162,5 +168,28 @@ def sql_column
end
end
end
+
+ class V8_1_TrilogyAdapter < V8_1_Mysql2Adapter # rubocop:disable Naming/ClassAndModuleCamelCase
+ class << self
+ def register_integrations
+ require 'active_record/connection_adapters/rails_8_1_trilogy_adapter'
+ require 'departure/rails_patches/active_record_migrator_with_advisory_lock_patch'
+
+ ActiveRecord::Migration.class_eval do
+ include Departure::Migration
+ end
+
+ ActiveRecord::Migrator.prepend Departure::RailsPatches::ActiveRecordMigratorWithAdvisoryLockPatch
+
+ ActiveRecord::ConnectionAdapters.register 'percona',
+ 'ActiveRecord::ConnectionAdapters::Rails81TrilogyAdapter',
+ 'active_record/connection_adapters/rails_8_1_trilogy_adapter'
+ end
+
+ def create_connection_adapter(**config)
+ ActiveRecord::ConnectionAdapters::Rails81TrilogyAdapter.new(config)
+ end
+ end
+ end
end
end
diff --git a/lib/departure/railtie.rb b/lib/departure/railtie.rb
index 2faec9af..0e9cd4bb 100644
--- a/lib/departure/railtie.rb
+++ b/lib/departure/railtie.rb
@@ -10,6 +10,8 @@ class Railtie < Rails::Railtie
Departure.configure do |config|
config.tmp_path = app.paths['tmp'].first
end
+
+ Departure::RailsAdapter.register_integrations
end
config.after_initialize do
diff --git a/spec/active_record/connection_adapters/rails_8_0_departure_adapter_spec.rb b/spec/active_record/connection_adapters/rails_8_0_departure_adapter_spec.rb
new file mode 100644
index 00000000..19335994
--- /dev/null
+++ b/spec/active_record/connection_adapters/rails_8_0_departure_adapter_spec.rb
@@ -0,0 +1,472 @@
+require 'spec_helper'
+
+if rails_version_under_test_matches?(RAILS_8_0, __FILE__)
+ require 'active_record/connection_adapters/rails_8_0_departure_adapter'
+
+ describe ActiveRecord::ConnectionAdapters::Rails80DepartureAdapter, activerecord_compatibility: RAILS_8_0 do
+ describe ActiveRecord::ConnectionAdapters::Rails80DepartureAdapter::Column do
+ let(:field) { double(:field) }
+ let(:default) { double(:default) }
+ let(:cast_type) do
+ if defined?(ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::MysqlString)
+ ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::MysqlString.new
+ else
+ ActiveRecord::Type.lookup(:string, adapter: :mysql2)
+ end
+ end
+ let(:metadata) do
+ ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
+ type: cast_type.type,
+ sql_type: type,
+ limit: cast_type.limit
+ )
+ end
+ let(:mysql_metadata) do
+ ActiveRecord::ConnectionAdapters::MySQL::TypeMetadata.new(metadata)
+ end
+ let(:type) { 'VARCHAR' }
+ let(:null) { double(:null) }
+ let(:collation) { double(:collation) }
+
+ let(:column) do
+ described_class.new('field', 'default', mysql_metadata, null, collation: 'collation')
+ end
+
+ describe '#adapter' do
+ subject { column.adapter }
+ it do
+ is_expected.to eq(
+ ActiveRecord::ConnectionAdapters::Rails80DepartureAdapter
+ )
+ end
+ end
+ end
+
+ let(:config) do
+ {
+ prepared_statements: '',
+ username: 'root',
+ password: 'password',
+ database: 'some_test_db'
+ }
+ end
+
+ let(:internal_added_config) do
+ {
+ adapter: 'mysql2',
+ flags: anything
+ }
+ end
+
+ let(:database_version) { double(full_version_string: '8.0.01') }
+ let(:mysql_adapter) do
+ instance_double(ActiveRecord::ConnectionAdapters::Mysql2Adapter, get_database_version: database_version)
+ end
+ let(:logger) { double(:logger, puts: true) }
+ let(:query_options) { { database_timezone: :utc } }
+ let(:runner) do
+ instance_double(Departure::Runner).tap do |r|
+ allow(r).to receive(:database_adapter).and_return(mysql_adapter)
+ allow(r).to receive(:query_options).and_return(query_options)
+ allow(r).to receive(:close).and_return(true)
+ allow(r).to receive(:abandon_results!).and_return(true)
+ allow(r).to receive(:affected_rows).and_return(1)
+ allow(r).to receive(:query).and_return(nil)
+ allow(r).to receive(:execute).with('percona command').and_return(true)
+ end
+ end
+ let(:cli_generator) { instance_double(Departure::CliGenerator, generate: 'percona command') }
+ let(:adapter) { described_class.new(config).tap { |adapter| adapter.send(:connect) } }
+ let(:mysql_client) { double(:mysql_client) }
+
+ before do
+ allow(mysql_client).to receive(:server_info).and_return(version: '8.0.19')
+ allow(mysql_adapter).to receive(:raw_connection).and_return(mysql_client)
+ allow(Departure::LoggerFactory).to receive(:build) { logger }
+
+ # Add a default stub for Mysql2Adapter.new to handle any config
+ allow(ActiveRecord::ConnectionAdapters::Mysql2Adapter).to receive(:new).and_return(mysql_adapter)
+
+ allow(Departure::CliGenerator).to(
+ receive(:new).and_return(cli_generator)
+ )
+ allow(Departure::Runner).to(
+ receive(:new).with(logger, cli_generator, mysql_adapter)
+ ).and_return(runner)
+ end
+
+ it '#supports_migrations?' do
+ expect(adapter.supports_migrations?).to eql(true)
+ end
+
+ describe '#new_column' do
+ let(:field) { double(:field) }
+ let(:default) { double(:default) }
+ let(:type) { double(:type) }
+ let(:null) { double(:null) }
+ let(:collation) { double(:collation) }
+ let(:table_name) { double(:table_name) }
+ let(:default_function) { double(:default_function) }
+ let(:comment) { double(:comment) }
+
+ it do
+ expect(ActiveRecord::ConnectionAdapters::Rails80DepartureAdapter::Column).to receive(:new)
+ adapter.new_column(field, default, type, null, table_name, default_function, collation, comment)
+ end
+ end
+
+ describe 'schema statements' do
+ describe '#add_index' do
+ let(:table_name) { :foo }
+ let(:column_name) { :bar_id }
+ let(:index_name) { 'index_name' }
+ let(:options) { { type: 'index_type' } }
+ let(:index_type) { options[:type].upcase }
+ let(:sql) { 'ADD index_type INDEX `index_name` (bar_id)' }
+ let(:index_options) do
+ [
+ ActiveRecord::ConnectionAdapters::IndexDefinition.new(
+ table_name,
+ index_name,
+ nil,
+ [column_name],
+ **options
+ ),
+ nil,
+ false
+ ]
+ end
+
+ let(:expected_sql) do
+ "ALTER TABLE `#{table_name}` ADD #{index_type} INDEX `#{index_name}` (`#{column_name}`)"
+ end
+
+ before do
+ allow(adapter).to(
+ receive(:add_index_options)
+ .with(table_name, column_name, options)
+ .and_return(index_options)
+ )
+ end
+
+ it 'passes the built SQL to #execute' do
+ allow(runner).to receive(:query).with(anything)
+ expect(runner).to receive(:close)
+ expect(adapter).to receive(:execute).with(expected_sql)
+ adapter.add_index(table_name, column_name, options)
+ end
+ end
+
+ describe '#remove_index' do
+ let(:table_name) { :foo }
+ let(:options) { { column: :bar_id } }
+ let(:sql) { 'DROP INDEX `index_name`' }
+
+ before do
+ allow(adapter).to(
+ receive(:index_name_for_remove)
+ .with(table_name, options)
+ .and_return('index_name')
+ )
+ allow(adapter).to(
+ receive(:index_name_for_remove)
+ .with(table_name, nil, options)
+ .and_return('index_name')
+ )
+ end
+
+ it 'passes the built SQL to #execute' do
+ expect(adapter).to(
+ receive(:execute)
+ .with("ALTER TABLE `#{table_name}` DROP INDEX `index_name`")
+ )
+ adapter.remove_index(table_name, **options)
+ end
+ end
+ end
+
+ describe '#exec_delete' do
+ let(:sql) { 'DELETE FROM comments WHERE id = 1' }
+ let(:affected_rows) { 1 }
+ let(:name) { nil }
+ let(:binds) { nil }
+
+ before do
+ allow(runner).to receive(:query).with(anything)
+ allow(mysql_client).to receive(:affected_rows).and_return(affected_rows)
+ end
+
+ it 'executes the sql' do
+ expect(runner).to receive(:affected_rows)
+ expect(adapter).to(receive(:execute).with(sql, name))
+ adapter.exec_delete(sql, name, binds)
+ end
+
+ it 'returns the number of affected rows' do
+ expect(runner).to receive(:close)
+ expect(runner).to receive(:affected_rows) { affected_rows }
+ expect(adapter.exec_delete(sql, name, binds)).to eq(affected_rows)
+ end
+ end
+
+ describe '#exec_insert' do
+ let(:sql) { 'INSERT INTO comments (id) VALUES (20)' }
+ let(:name) { nil }
+ let(:binds) { nil }
+
+ it 'executes the sql' do
+ expect(adapter).to(receive(:execute).with(sql, name))
+ adapter.exec_insert(sql, name, binds)
+ end
+ end
+
+ describe '#exec_query' do
+ let(:sql) { 'SELECT * FROM comments' }
+ let(:name) { nil }
+ let(:binds) { nil }
+
+ before do
+ allow(runner).to receive(:query).with(sql)
+ allow(adapter).to(
+ receive(:execute).with(sql, name).and_return(result_set)
+ )
+ end
+
+ context 'when the adapter returns results' do
+ let(:result_set) { double(fields: ['id'], to_a: [1]) }
+
+ it 'executes the sql' do
+ expect(adapter).to(
+ receive(:execute).with(sql, name)
+ ).and_return(result_set)
+
+ adapter.exec_query(sql, name, binds)
+ end
+
+ it 'returns an ActiveRecord::Result' do
+ expect(ActiveRecord::Result).to(
+ receive(:new).with(result_set.fields, result_set.to_a)
+ )
+ adapter.exec_query(sql, name, binds)
+ end
+ end
+
+ context 'when the adapter returns nil' do
+ let(:result_set) { nil }
+
+ it 'executes the sql' do
+ expect(adapter).to(
+ receive(:execute).with(sql, name)
+ ).and_return(result_set)
+
+ adapter.exec_query(sql, name, binds)
+ end
+
+ it 'returns an ActiveRecord::Result' do
+ expect(ActiveRecord::Result).to(
+ receive(:new).with([], [])
+ )
+ adapter.exec_query(sql, name, binds)
+ end
+ end
+ end
+
+ describe '#last_inserted_id' do
+ let(:result) { double(:result) }
+
+ it 'delegates to the mysql adapter' do
+ expect(mysql_adapter).to(
+ receive(:last_inserted_id).with(result)
+ )
+ adapter.last_inserted_id(result)
+ end
+ end
+
+ describe '#select_rows' do
+ subject { adapter.select_rows(sql, name) }
+
+ let(:sql) { 'SELECT id, body FROM comments' }
+ let(:name) { nil }
+
+ let(:array_of_rows) { [%w[1 body], %w[2 body]] }
+ let(:mysql2_result) do
+ # rubocop:disable Style/WordArray
+ instance_double(Mysql2::Result, to_a: array_of_rows, fields: ['id', 'body'])
+ # rubocop:enable Style/WordArray
+ end
+
+ before do
+ allow(adapter).to(
+ receive(:execute).with(sql, name)
+ ).and_return(mysql2_result)
+ end
+
+ it { is_expected.to match_array(array_of_rows) }
+ end
+
+ describe '#select' do
+ subject { adapter.select(sql, name) }
+
+ let(:sql) { 'SELECT id, body FROM comments' }
+ let(:name) { nil }
+
+ let(:array_of_rows) { [%w[1 body], %w[2 body]] }
+ let(:mysql2_result) do
+ instance_double(Mysql2::Result, fields: %w[id body], to_a: array_of_rows)
+ end
+
+ before do
+ allow(adapter).to(
+ receive(:execute).with(sql, name)
+ ).and_return(mysql2_result)
+ end
+
+ it do
+ is_expected.to match_array(
+ [
+ { 'id' => '1', 'body' => 'body' },
+ { 'id' => '2', 'body' => 'body' }
+ ]
+ )
+ end
+ end
+
+ describe '#write_query?' do
+ it 'identifies write queries correctly' do
+ expect(adapter.write_query?('INSERT INTO comments (id) VALUES (1)')).to be true
+ expect(adapter.write_query?('UPDATE comments SET body = "test"')).to be true
+ expect(adapter.write_query?('DELETE FROM comments WHERE id = 1')).to be true
+ expect(adapter.write_query?('ALTER TABLE comments ADD COLUMN test VARCHAR(255)')).to be true
+ end
+
+ it 'identifies read queries correctly' do
+ expect(adapter.write_query?('SELECT * FROM comments')).to be false
+ expect(adapter.write_query?('SHOW TABLES')).to be false
+ expect(adapter.write_query?('DESCRIBE comments')).to be false
+ expect(adapter.write_query?('DESC comments')).to be false
+ end
+ end
+
+ describe '#full_version' do
+ it 'returns the database version' do
+ expect(adapter.full_version).to eq('8.0.01')
+ end
+ end
+
+ describe '#get_full_version' do
+ it 'caches the version after first call' do
+ version = adapter.get_full_version
+ expect(version).to eq('8.0.01')
+
+ # Call again to test caching
+ expect(mysql_adapter).not_to receive(:get_database_version)
+ second_version = adapter.get_full_version
+ expect(second_version).to eq('8.0.01')
+ end
+ end
+
+ describe '#schema_creation' do
+ it 'returns a SchemaCreation instance' do
+ expect(adapter.schema_creation).to be_a(
+ ActiveRecord::ConnectionAdapters::Rails80DepartureAdapter::SchemaCreation
+ )
+ end
+ end
+
+ describe ActiveRecord::ConnectionAdapters::Rails80DepartureAdapter::SchemaCreation do
+ let(:adapter) { instance_double(ActiveRecord::ConnectionAdapters::Rails80DepartureAdapter) }
+ let(:schema_creation) { described_class.new(adapter) }
+
+ describe '#visit_DropForeignKey' do
+ context 'when the foreign key name has double underscore prefix' do
+ it 'removes the double underscore prefix' do
+ result = schema_creation.visit_DropForeignKey('__fk_constraint_name')
+ expect(result).to eq('DROP FOREIGN KEY fk_constraint_name')
+ end
+ end
+
+ context 'when the foreign key name does not have double underscore prefix' do
+ it 'adds a single underscore prefix' do
+ result = schema_creation.visit_DropForeignKey('fk_constraint_name')
+ expect(result).to eq('DROP FOREIGN KEY _fk_constraint_name')
+ end
+ end
+ end
+ end
+
+ describe '.new_client' do
+ before do
+ # For .new_client tests, we need to stub Mysql2Adapter without the flags requirement
+ # since new_client doesn't go through the initialization that adds flags
+ allow(ActiveRecord::ConnectionAdapters::Mysql2Adapter).to receive(:new)
+ .with(hash_including(adapter: 'mysql2')).and_return(mysql_adapter)
+
+ # Stub ConnectionDetails which is used by new_client
+ allow(Departure::ConnectionDetails).to receive(:new).with(config).and_return(
+ double('ConnectionDetails',
+ password: 'password',
+ username: 'root',
+ hostname: 'localhost',
+ database: 'some_test_db',
+ port: 3306)
+ )
+ end
+
+ it 'creates a new Departure::Runner instance' do
+ # Allow real Runner creation for this test
+ allow(Departure::Runner).to receive(:new).and_call_original
+
+ client = described_class.new_client(config)
+ expect(client).to be_a(Departure::Runner)
+ end
+
+ it 'configures the runner with proper dependencies' do
+ expect(Departure::LoggerFactory).to receive(:build).and_return(logger)
+ expect(Departure::CliGenerator).to receive(:new).and_return(cli_generator)
+ expect(Departure::Runner).to receive(:new).with(logger, cli_generator, anything)
+
+ described_class.new_client(config)
+ end
+ end
+
+ describe '#change_table' do
+ let(:table_name) { :test_table }
+ let(:recorder) { instance_double(ActiveRecord::Migration::CommandRecorder, commands: []) }
+
+ before do
+ allow(ActiveRecord::Migration::CommandRecorder).to receive(:new).and_return(recorder)
+ allow(adapter).to receive(:update_table_definition).and_return(double)
+ allow(adapter).to receive(:bulk_change_table)
+ end
+
+ it 'uses a CommandRecorder to track changes' do
+ expect(ActiveRecord::Migration::CommandRecorder).to receive(:new).with(adapter)
+ adapter.change_table(table_name) { |_t| }
+ end
+
+ it 'calls bulk_change_table with the recorded commands' do
+ expect(adapter).to receive(:bulk_change_table).with(table_name, [])
+ adapter.change_table(table_name) { |_t| }
+ end
+ end
+
+ describe 'initialization' do
+ it 'sets prepared_statements to false' do
+ new_adapter = described_class.new(config)
+ expect(new_adapter.instance_variable_get(:@prepared_statements)).to be false
+ end
+
+ it 'configures flags for FOUND_ROWS when flags is a number' do
+ config_with_flags = config.merge(flags: 0)
+ new_adapter = described_class.new(config_with_flags)
+ expect(new_adapter.instance_variable_get(:@config)[:flags]).to eq(Mysql2::Client::FOUND_ROWS)
+ end
+
+ it 'adds FOUND_ROWS to flags array when flags is an array' do
+ config_with_flags = config.merge(flags: [])
+ new_adapter = described_class.new(config_with_flags)
+ expect(new_adapter.instance_variable_get(:@config)[:flags]).to include('FOUND_ROWS')
+ end
+ end
+ end
+end
diff --git a/spec/active_record/connection_adapters/rails_8_1_departure_adapter_spec.rb b/spec/active_record/connection_adapters/rails_8_1_departure_adapter_spec.rb
deleted file mode 100644
index 26497d75..00000000
--- a/spec/active_record/connection_adapters/rails_8_1_departure_adapter_spec.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-require 'spec_helper'
-require 'active_record/connection_adapters/rails_8_1_departure_adapter'
-
-describe ActiveRecord::ConnectionAdapters::Rails81DepartureAdapter, activerecord_compatibility: RAILS_8_1 do
- let(:adapter) { described_class.new(db_config_for(adapter: 'mysql2')) }
- let(:client) { described_class.new_client(db_config_for(adapter: 'mysql2')) }
-
- describe '#new_client' do
- it 'wraps the underlying db_client and exposes a mysql_client' do
- expect(client).to be_a(Departure::DbClient)
- expect(client.database_client).to be_a(Mysql2::Client)
- end
- end
-end
diff --git a/spec/active_record/connection_adapters/rails_8_1_mysql2_adapter_spec.rb b/spec/active_record/connection_adapters/rails_8_1_mysql2_adapter_spec.rb
new file mode 100644
index 00000000..178b5087
--- /dev/null
+++ b/spec/active_record/connection_adapters/rails_8_1_mysql2_adapter_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+if rails_version_under_test_matches?(RAILS_8_1, __FILE__)
+ require 'active_record/connection_adapters/rails_8_1_mysql2_adapter'
+
+ describe ActiveRecord::ConnectionAdapters::Rails81Mysql2Adapter, activerecord_compatibility: RAILS_8_1 do
+ let(:adapter) { described_class.new(db_config_for(adapter: 'mysql2')) }
+ let(:client) { described_class.new_client(db_config_for(adapter: 'mysql2')) }
+
+ describe '#new_client' do
+ it 'wraps the underlying db_client and exposes a mysql_client' do
+ expect(client).to be_a(Departure::DbClient)
+ expect(client.database_client).to be_a(Mysql2::Client)
+ end
+ end
+
+ describe 'database_statements' do
+ let(:table_name) { :foo }
+ let(:column_name) { :bar_id }
+ let(:index_name) { 'index_name' }
+ let(:options) { { type: 'index_type' } }
+
+ describe '#add_index' do
+ let(:index_definition) do
+ ActiveRecord::ConnectionAdapters::IndexDefinition.new(
+ table_name,
+ index_name,
+ nil,
+ [column_name],
+ **options
+ )
+ end
+
+ let(:index_options) { [index_definition, nil, false] }
+ let(:index_type) { options[:type].upcase }
+ let(:schema_creation_double) { instance_double(described_class::SchemaCreation) }
+
+ it 'passes the built ALTER TABLE SQL to #execute' do
+ allow(adapter).to receive(:shard) { :default }
+ allow(adapter).to receive(:role) { :writing }
+
+ expect(schema_creation_double).to receive(:accept).with(index_definition) {
+ "INDEX_TYPE INDEX `#{index_name}` (`#{column_name}`)"
+ }
+ expect(adapter).to receive(:schema_creation) { schema_creation_double }
+
+ expect(adapter).to receive(:add_index_options).with(table_name, column_name,
+ options).and_return(index_options)
+ execute_sql = "ALTER TABLE `#{table_name}` ADD #{index_type} INDEX `#{index_name}` (`#{column_name}`)"
+ expect(adapter).to receive(:execute).with(execute_sql).and_return(true)
+
+ adapter.add_index(table_name, column_name, options)
+ end
+ end
+
+ describe '#remove_index' do
+ let(:options) { { column: column_name } }
+ let(:sql) { "DROP INDEX `#{index_name}`" }
+
+ it 'passes the built ALTER TABLE SQL to #execute' do
+ allow(adapter).to receive(:shard) { :default }
+ allow(adapter).to receive(:role) { :writing }
+ expect(adapter).to receive(:index_name_for_remove).with(table_name, nil, options).and_return(index_name.to_s)
+ execute_sql = "ALTER TABLE `#{table_name}` DROP INDEX `#{index_name}`"
+ expect(adapter).to receive(:execute).with(execute_sql).and_return(true)
+
+ adapter.remove_index(table_name, **options)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/active_record/connection_adapters/rails_8_1_trilogy_adapter_spec.rb b/spec/active_record/connection_adapters/rails_8_1_trilogy_adapter_spec.rb
new file mode 100644
index 00000000..ca1b5feb
--- /dev/null
+++ b/spec/active_record/connection_adapters/rails_8_1_trilogy_adapter_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe 'ActiveRecord::ConnectionAdapters::Rails81TrilogyAdapter', activerecord_compatibility: RAILS_8_1 do
+ let(:described_class) do
+ # has to be required here because trilogy doesn't exist in older versions of rails
+ require 'active_record/connection_adapters/rails_8_1_trilogy_adapter'
+
+ ActiveRecord::ConnectionAdapters::Rails81TrilogyAdapter
+ end
+
+ let(:adapter) { described_class.new(db_config_for(adapter: 'trilogy')) }
+ let(:client) { described_class.new_client(db_config_for(adapter: 'trilogy')) }
+ let(:trilogy_double) { instance_double(::Trilogy) }
+
+ describe '#new_client' do
+ it 'wraps the underlying db_client and exposes a mysql_client' do
+ expect_any_instance_of(::Trilogy).to receive(:_connect) { trilogy_double }
+
+ expect(client).to be_a(Departure::DbClient)
+ expect(client.database_client).to be_a(::Trilogy)
+ end
+ end
+
+ describe 'database_statements' do
+ let(:table_name) { :foo }
+ let(:column_name) { :bar_id }
+ let(:index_name) { 'index_name' }
+ let(:options) { { type: 'index_type' } }
+
+ describe '#add_index' do
+ let(:index_definition) do
+ ActiveRecord::ConnectionAdapters::IndexDefinition.new(
+ table_name,
+ index_name,
+ nil,
+ [column_name],
+ **options
+ )
+ end
+
+ let(:index_options) { [index_definition, nil, false] }
+ let(:index_type) { options[:type].upcase }
+ let(:schema_creation_double) { instance_double(described_class::SchemaCreation) }
+
+ it 'passes the built ALTER TABLE SQL to #execute' do
+ allow(adapter).to receive(:shard) { :default }
+ allow(adapter).to receive(:role) { :writing }
+
+ expect(schema_creation_double).to receive(:accept).with(index_definition) {
+ "INDEX_TYPE INDEX `#{index_name}` (`#{column_name}`)"
+ }
+ expect(adapter).to receive(:schema_creation) { schema_creation_double }
+
+ expect(adapter).to receive(:add_index_options).with(table_name, column_name, options).and_return(index_options)
+ execute_sql = "ALTER TABLE `#{table_name}` ADD #{index_type} INDEX `#{index_name}` (`#{column_name}`)"
+ expect(adapter).to receive(:execute).with(execute_sql).and_return(true)
+
+ adapter.add_index(table_name, column_name, options)
+ end
+ end
+
+ describe '#remove_index' do
+ let(:options) { { column: column_name } }
+ let(:sql) { "DROP INDEX `#{index_name}`" }
+
+ it 'passes the built ALTER TABLE SQL to #execute' do
+ allow(adapter).to receive(:shard) { :default }
+ allow(adapter).to receive(:role) { :writing }
+ expect(adapter).to receive(:index_name_for_remove).with(table_name, nil, options).and_return(index_name.to_s)
+ execute_sql = "ALTER TABLE `#{table_name}` DROP INDEX `#{index_name}`"
+ expect(adapter).to receive(:execute).with(execute_sql).and_return(true)
+
+ adapter.remove_index(table_name, **options)
+ end
+ end
+ end
+end
diff --git a/spec/departure/ci_context.rb b/spec/departure/ci_context.rb
new file mode 100644
index 00000000..378c31ce
--- /dev/null
+++ b/spec/departure/ci_context.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe 'CI Context' do
+ it 'uses the proper runner in integration specs', integration: true, activerecord_compatibility: RAILS_8_1 do
+ establish_default_database_connection
+
+ case ENV['DB_ADAPTER']
+ when 'trilogy'
+ expect(ActiveRecord::Base.connection.adapter_name).to eql('Trilogy')
+ when 'mysql2'
+ expect(ActiveRecord::Base.connection.adapter_name).to eql('Mysql2')
+ else
+ raise StandardError, 'Your test is not specifying a DB_ADAPTER of mysql2 or trilogy'
+ end
+ end
+end
diff --git a/spec/departure/configuration_spec.rb b/spec/departure/configuration_spec.rb
index 984c279c..db084e8a 100644
--- a/spec/departure/configuration_spec.rb
+++ b/spec/departure/configuration_spec.rb
@@ -4,6 +4,12 @@
describe '#initialize' do
its(:tmp_path) { is_expected.to eq('.') }
its(:error_log_filename) { is_expected.to eq('departure_error.log') }
+ its(:db_adapter_name) { is_expected.to be_nil }
+ end
+
+ describe '#db_adapter_name=' do
+ subject { described_class.new.tmp_path = 'trilogy' }
+ it { is_expected.to eq('trilogy') }
end
describe '#tmp_path' do
diff --git a/spec/departure/migration_spec.rb b/spec/departure/migration_spec.rb
index 122f5b15..8d3aa351 100644
--- a/spec/departure/migration_spec.rb
+++ b/spec/departure/migration_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Departure::Migration do
+ before { setup_departure_integrations }
+
let(:base) do
Class.new do
attr_accessor :migrated_direction
diff --git a/spec/integration/indexes_spec.rb b/spec/integration/indexes_spec.rb
index 271137f5..e2256bd5 100644
--- a/spec/integration/indexes_spec.rb
+++ b/spec/integration/indexes_spec.rb
@@ -64,7 +64,7 @@ class Comment < ActiveRecord::Base; end
it 'executes the percona command' do
if ActiveRecord::Base.connection.send(:supports_rename_index?)
- expect_percona_command('RENAME INDEX `index_comments_on_some_id_field` TO `new_index_comments_on_some_id_field`') # rubocop:disable Metrics/LineLength
+ expect_percona_command('RENAME INDEX `index_comments_on_some_id_field` TO `new_index_comments_on_some_id_field`')
else
expect_percona_command('ADD INDEX `new_index_comments_on_some_id_field` (`some_id_field`)')
expect_percona_command('DROP INDEX `index_comments_on_some_id_field`')
diff --git a/spec/integration/rails_adapter_spec.rb b/spec/integration/rails_adapter_spec.rb
index 97b5b79f..4588ead9 100644
--- a/spec/integration/rails_adapter_spec.rb
+++ b/spec/integration/rails_adapter_spec.rb
@@ -14,21 +14,42 @@ def gem_version_for(string)
end
end
+ def instance_for(version, db_connection_adapter = 'mysql2')
+ described_class.for(gem_version_for(version), db_connection_adapter:)
+ end
+
+ context 'rails 8.1 adapter' do
+ describe 'returns trilogy adapter' do
+ it 'when the config specifies an adapter of trilogy' do
+ expect(instance_for('8.1.0', 'trilogy')).to be(Departure::RailsAdapter::V8_1_TrilogyAdapter)
+ end
+ end
+
+ describe 'returns mysql2 adapter' do
+ it 'by default' do
+ expect(instance_for('8.1.0')).to be(Departure::RailsAdapter::V8_1_Mysql2Adapter)
+ expect(instance_for('8.1.0.beta1')).to be(Departure::RailsAdapter::V8_1_Mysql2Adapter)
+ end
+
+ it 'when the config specifies an adapter of mysql2' do
+ expect(instance_for('8.1.0', 'mysql2')).to be(Departure::RailsAdapter::V8_1_Mysql2Adapter)
+ end
+
+ it 'when the config specifies anything else' do
+ expect(instance_for('8.1.0', 'percona')).to be(Departure::RailsAdapter::V8_1_Mysql2Adapter)
+ end
+ end
+ end
+
it 'returns the correct adapater based on the gem version' do
- expect(described_class.for(gem_version_for('8.1.0'))).to be(Departure::RailsAdapter::V8_1_Adapter)
- expect(described_class.for(gem_version_for('8.1.0.beta1'))).to be(Departure::RailsAdapter::V8_1_Adapter)
- expect(described_class.for(gem_version_for('8.0.1'))).to be(Departure::RailsAdapter::V8_0_Adapter)
- expect(described_class.for(gem_version_for('8.0.0'))).to be(Departure::RailsAdapter::V8_0_Adapter)
- expect(described_class.for(gem_version_for('7.2.0'))).to be(Departure::RailsAdapter::V7_2_Adapter)
+ expect(instance_for('8.0.1')).to be(Departure::RailsAdapter::V8_0_Adapter)
+ expect(instance_for('8.0.0')).to be(Departure::RailsAdapter::V8_0_Adapter)
+ expect(instance_for('7.2.0')).to be(Departure::RailsAdapter::V7_2_Adapter)
end
- it 'raises an exception on before 7.2' do
- expect do
- described_class.for(gem_version_for('7.1.0'))
- end.to raise_error(described_class::UnsupportedRailsVersionError)
- expect do
- described_class.for(gem_version_for('6.1.0'))
- end.to raise_error(described_class::UnsupportedRailsVersionError)
+ it 'raises an exception for older versiosn of rails' do
+ expect { instance_for('7.1.0') }.to raise_error(Departure::RailsAdapter::UnsupportedRailsVersionError)
+ expect { instance_for('6.1.0') }.to raise_error(Departure::RailsAdapter::UnsupportedRailsVersionError)
end
end
@@ -66,6 +87,82 @@ def gem_version_for(string)
end
end
+ describe '.register_integrations' do
+ it 'delegates to the adapter for the current Rails version' do
+ adapter_class = described_class.for_current
+ expect(adapter_class).to receive(:register_integrations)
+
+ described_class.register_integrations
+ end
+
+ it 'passes through keyword arguments to the adapter' do
+ expect(described_class).to receive(:for_current).with(db_connection_adapter: 'trilogy').and_call_original
+ adapter_class = described_class.for_current(db_connection_adapter: 'trilogy')
+ allow(described_class).to receive(:for_current).with(db_connection_adapter: 'trilogy').and_return(adapter_class)
+ expect(adapter_class).to receive(:register_integrations)
+
+ described_class.register_integrations(db_connection_adapter: 'trilogy')
+ end
+
+ context 'when called on specific adapters' do
+ it 'requires the correct adapter file and registers components for V7_2_Adapter' do
+ expect(described_class::V7_2_Adapter).to receive(:require).with('active_record/connection_adapters/rails_7_2_departure_adapter')
+ expect(described_class::V7_2_Adapter).to receive(:require).with('departure/rails_patches/active_record_migrator_with_advisory_lock_patch')
+ expect(ActiveRecord::Migration).to receive(:class_eval)
+ expect(ActiveRecord::Migrator).to receive(:prepend).with(Departure::RailsPatches::ActiveRecordMigratorWithAdvisoryLockPatch)
+ expect(ActiveRecord::ConnectionAdapters).to receive(:register).with(
+ 'percona',
+ 'ActiveRecord::ConnectionAdapters::Rails72DepartureAdapter',
+ 'active_record/connection_adapters/rails_7_2_departure_adapter'
+ )
+
+ described_class::V7_2_Adapter.register_integrations
+ end
+
+ it 'requires the correct adapter file and registers components for V8_0_Adapter' do
+ expect(described_class::V8_0_Adapter).to receive(:require).with('active_record/connection_adapters/rails_8_0_departure_adapter')
+ expect(described_class::V8_0_Adapter).to receive(:require).with('departure/rails_patches/active_record_migrator_with_advisory_lock_patch')
+ expect(ActiveRecord::Migration).to receive(:class_eval)
+ expect(ActiveRecord::Migrator).to receive(:prepend).with(Departure::RailsPatches::ActiveRecordMigratorWithAdvisoryLockPatch)
+ expect(ActiveRecord::ConnectionAdapters).to receive(:register).with(
+ 'percona',
+ 'ActiveRecord::ConnectionAdapters::Rails80DepartureAdapter',
+ 'active_record/connection_adapters/rails_8_0_departure_adapter'
+ )
+
+ described_class::V8_0_Adapter.register_integrations
+ end
+
+ it 'requires the correct adapter file and registers components for V8_1_Mysql2Adapter' do
+ expect(described_class::V8_1_Mysql2Adapter).to receive(:require).with('active_record/connection_adapters/rails_8_1_mysql2_adapter')
+ expect(described_class::V8_1_Mysql2Adapter).to receive(:require).with('departure/rails_patches/active_record_migrator_with_advisory_lock_patch')
+ expect(ActiveRecord::Migration).to receive(:class_eval)
+ expect(ActiveRecord::Migrator).to receive(:prepend).with(Departure::RailsPatches::ActiveRecordMigratorWithAdvisoryLockPatch)
+ expect(ActiveRecord::ConnectionAdapters).to receive(:register).with(
+ 'percona',
+ 'ActiveRecord::ConnectionAdapters::Rails81Mysql2Adapter',
+ 'active_record/connection_adapters/rails_8_1_mysql2_adapter'
+ )
+
+ described_class::V8_1_Mysql2Adapter.register_integrations
+ end
+
+ it 'requires the correct adapter file and registers components for V8_1_TrilogyAdapter' do
+ expect(described_class::V8_1_TrilogyAdapter).to receive(:require).with('active_record/connection_adapters/rails_8_1_trilogy_adapter')
+ expect(described_class::V8_1_TrilogyAdapter).to receive(:require).with('departure/rails_patches/active_record_migrator_with_advisory_lock_patch')
+ expect(ActiveRecord::Migration).to receive(:class_eval)
+ expect(ActiveRecord::Migrator).to receive(:prepend).with(Departure::RailsPatches::ActiveRecordMigratorWithAdvisoryLockPatch)
+ expect(ActiveRecord::ConnectionAdapters).to receive(:register).with(
+ 'percona',
+ 'ActiveRecord::ConnectionAdapters::Rails81TrilogyAdapter',
+ 'active_record/connection_adapters/rails_8_1_trilogy_adapter'
+ )
+
+ described_class::V8_1_TrilogyAdapter.register_integrations
+ end
+ end
+ end
+
describe 'advisory_lock patch' do
it 'runs migrations without throwing an ActiveRecord::ConcurrentMigration Error' do
expect { run_a_migration(:up, 1) }.not_to raise_error
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 8065619c..a0930343 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -29,8 +29,6 @@
test_database = TestDatabase.new(db_config)
-Departure::RailsAdapter.for_current.register_integrations
-
RSpec.configure do |config|
config.include TableMethods
config.filter_run_when_matching :focus
@@ -52,9 +50,13 @@
# Cleans up the database before each example, so the current example doesn't
# see the state of the previous one
config.before(:each) do |example|
- establish_mysql_connection
+ establish_default_database_connection
+
+ if example.metadata[:integration]
+ test_database.setup
- test_database.setup if example.metadata[:integration]
+ Departure::RailsAdapter.for_current.register_integrations
+ end
end
config.order = :random
@@ -74,3 +76,16 @@ def initialize(migrations_paths, schema_migration = nil)
if ActiveRecord::VERSION::STRING >= '7.1'
ActiveRecord::MigrationContext.send :prepend, Rails7Compatibility::MigrationContext
end
+
+def rails_version_under_test_matches?(version_string, file)
+ Departure::RailsAdapter.version_matches?(ActiveRecord::VERSION::STRING, version_string).tap do |result|
+ unless result
+ error = "Skip #{file} test - current '#{version_string}' not matching version #{ActiveRecord::VERSION::STRING}"
+
+ puts ''
+ puts '-- *** INFO ****'
+ puts "-- #{error}"
+ puts ''
+ end
+ end
+end
diff --git a/spec/support/database_helpers.rb b/spec/support/database_helpers.rb
index 887f3f22..130a5af6 100644
--- a/spec/support/database_helpers.rb
+++ b/spec/support/database_helpers.rb
@@ -1,21 +1,55 @@
MIGRATION_FIXTURES = File.expand_path('../dummy/db/migrate', __dir__)
-def db_config_for(adapter:, **kwargs)
+def db_config_for(adapter:)
db_config = Configuration.new
-
{
adapter:,
- **db_config.config,
- **kwargs
+ **db_config.config
+ }
+end
+
+def establish_default_database_connection(**config, &block)
+ case ENV['DB_ADAPTER']
+ when 'trilogy'
+ establish_trilogy_connection(**config, &block)
+ when 'percona'
+ establish_percona_connection(**config, &block)
+ else
+ establish_mysql_connection(**config, &block)
+ end
+end
+
+def build_connection_config(adapter, **config)
+ c = {
+ **db_config_for(adapter: adapter),
+ **config
}
+
+ yield c if block_given?
+
+ c
+end
+
+def establish_trilogy_connection(**config, &block)
+ c = build_connection_config('trilogy', **config, &block)
+
+ ActiveRecord::Base.establish_connection(c)
end
-def establish_percona_connection(**kwargs)
- ActiveRecord::Base.establish_connection(**db_config_for(adapter: 'percona', **kwargs))
+def establish_percona_connection(**config, &block)
+ c = build_connection_config('percona', **config, &block)
+
+ ActiveRecord::Base.establish_connection(c)
+end
+
+def establish_mysql_connection(**config, &block)
+ c = build_connection_config('mysql2', **config, &block)
+
+ ActiveRecord::Base.establish_connection(c)
end
-def establish_mysql_connection(**kwargs)
- ActiveRecord::Base.establish_connection(**db_config_for(adapter: 'mysql2', **kwargs))
+def setup_departure_integrations
+ Departure::RailsAdapter.for_current.register_integrations
end
def disable_departure_rails_advisory_lock_patch