diff --git a/Gemfile b/Gemfile index bd91a5a41366e..29b52bbc282fe 100644 --- a/Gemfile +++ b/Gemfile @@ -149,6 +149,7 @@ platforms :ruby, :windows do group :db do gem "pg", "~> 1.3" gem "mysql2", "~> 0.5" + gem "trilogy", "~> 2.4" end end diff --git a/Gemfile.lock b/Gemfile.lock index 9039bb8adb956..7b7605f91919a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -507,6 +507,7 @@ GEM timeout (0.3.2) tomlrb (2.0.3) trailblazer-option (0.1.2) + trilogy (2.4.0) turbo-rails (1.3.2) actionpack (>= 6.0.0) activejob (>= 6.0.0) @@ -617,6 +618,7 @@ DEPENDENCIES sucker_punch tailwindcss-rails terser (>= 1.1.4) + trilogy (~> 2.4) turbo-rails tzinfo-data w3c_validators (~> 1.3.6) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 013b2ac5c8005..9f58d32d0cb99 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,23 @@ +* Introduce adapter for Trilogy database client + + Trilogy is a MySQL-compatible database client. Rails applications can use Trilogy + by configuring their `config/database.yml`: + + ```yaml + development: + adapter: trilogy + database: blog_development + pool: 5 + ``` + + Or by using the `DATABASE_URL` environment variable: + + ```ruby + ENV['DATABASE_URL'] # => "trilogy://localhost/blog_development?pool=5" + ``` + + *Adrianna Chang* + * `after_commit` callbacks defined on models now execute in the correct order. ```ruby diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc index 95a87807b8a69..a048463edd15a 100644 --- a/activerecord/RUNNING_UNIT_TESTS.rdoc +++ b/activerecord/RUNNING_UNIT_TESTS.rdoc @@ -21,6 +21,7 @@ example: Simply executing bundle exec rake test is equivalent to the following: $ bundle exec rake test:mysql2 + $ bundle exec rake test:trilogy $ bundle exec rake test:postgresql $ bundle exec rake test:sqlite3 diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 51a59f14e6eb6..fd9c0eafbc100 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -18,16 +18,16 @@ def run_without_aborting(*tasks) abort "Errors running #{errors.join(', ')}" if errors.any? end -desc "Run mysql2, sqlite, and postgresql tests by default" +desc "Run mysql2, trilogy, sqlite, and postgresql tests by default" task default: :test task :package -desc "Run mysql2, sqlite, and postgresql tests" +desc "Run mysql2, trilogy, sqlite, and postgresql tests" task :test do tasks = defined?(JRUBY_VERSION) ? %w(test_jdbcmysql test_jdbcsqlite3 test_jdbcpostgresql) : - %w(test_mysql2 test_sqlite3 test_postgresql) + %w(test_mysql2 test_trilogy test_sqlite3 test_postgresql) run_without_aborting(*tasks) end @@ -35,7 +35,7 @@ namespace :test do task :isolated do tasks = defined?(JRUBY_VERSION) ? %w(isolated_test_jdbcmysql isolated_test_jdbcsqlite3 isolated_test_jdbcpostgresql) : - %w(isolated_test_mysql2 isolated_test_sqlite3 isolated_test_postgresql) + %w(isolated_test_mysql2 isolated_test_trilogy isolated_test_sqlite3 isolated_test_postgresql) run_without_aborting(*tasks) end @@ -56,7 +56,7 @@ namespace :db do task drop: ["db:mysql:drop", "db:postgresql:drop"] end -%w( mysql2 postgresql sqlite3 sqlite3_mem oracle jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter| +%w( mysql2 trilogy postgresql sqlite3 sqlite3_mem oracle jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter| namespace :test do Rake::TestTask.new(adapter => "#{adapter}:env") do |t| adapter_short = adapter[/^[a-z0-9]+/] @@ -64,10 +64,11 @@ end files = (FileList["test/cases/**/*_test.rb"].reject { |x| x.include?("/adapters/") || x.include?("/encryption/performance") } + FileList["test/cases/adapters/#{adapter_short}/**/*_test.rb"]) - files = files + FileList["test/cases/adapters/abstract_mysql_adapter/**/*_test.rb"] if adapter == "mysql2" + files = files + FileList["test/cases/adapters/abstract_mysql_adapter/**/*_test.rb"] if ["mysql2", "trilogy"].include?(adapter) t.test_files = files + t.test_files = files t.warning = true t.verbose = true t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) diff --git a/activerecord/bin/test b/activerecord/bin/test index 0c911085d76cf..872f80fa99ff5 100755 --- a/activerecord/bin/test +++ b/activerecord/bin/test @@ -15,7 +15,7 @@ module Minitest opts.separator "" opts.separator "Active Record options:" opts.on("-a", "--adapter [ADAPTER]", - "Run tests using a specific adapter (sqlite3, sqlite3_mem, mysql2, postgresql)") do |adapter| + "Run tests using a specific adapter (sqlite3, sqlite3_mem, mysql2, trilogy, postgresql)") do |adapter| ENV["ARCONN"] = adapter.strip end diff --git a/activerecord/lib/active_record/connection_adapters/trilogy/errors.rb b/activerecord/lib/active_record/connection_adapters/trilogy/errors.rb new file mode 100644 index 0000000000000..dfd9ca3dcc677 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/trilogy/errors.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module ActiveRecord + module ConnectionAdapters + module Trilogy + module Errors + # ServerShutdown will be raised when the database server was shutdown. + class ServerShutdown < ActiveRecord::ConnectionFailed + end + + # ServerLost will be raised when the database connection was lost. + class ServerLost < ActiveRecord::ConnectionFailed + end + + # ServerGone will be raised when the database connection is gone. + class ServerGone < ActiveRecord::ConnectionFailed + end + + # BrokenPipe will be raised when a system process connection fails. + class BrokenPipe < ActiveRecord::ConnectionFailed + end + + # SocketError will be raised when Ruby encounters a network error. + class SocketError < ActiveRecord::ConnectionFailed + end + + # ConnectionResetByPeer will be raised when a network connection is closed + # outside the sytstem process. + class ConnectionResetByPeer < ActiveRecord::ConnectionFailed + end + + # ClosedConnection will be raised when the Trilogy encounters a closed + # connection. + class ClosedConnection < ActiveRecord::ConnectionFailed + end + + # InvalidSequenceId will be raised when Trilogy ecounters an invalid sequence + # id. + class InvalidSequenceId < ActiveRecord::ConnectionFailed + end + + # UnexpectedPacket will be raised when Trilogy ecounters an unexpected + # response packet. + class UnexpectedPacket < ActiveRecord::ConnectionFailed + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/trilogy/lost_connection_exception_translator.rb b/activerecord/lib/active_record/connection_adapters/trilogy/lost_connection_exception_translator.rb new file mode 100644 index 0000000000000..8c41f49133943 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/trilogy/lost_connection_exception_translator.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module ActiveRecord + module ConnectionAdapters + module Trilogy + class LostConnectionExceptionTranslator + attr_reader :exception, :message, :error_number + + def initialize(exception, message, error_number) + @exception = exception + @message = message + @error_number = error_number + end + + def translate + translate_database_exception || translate_ruby_exception || translate_trilogy_exception + end + + private + ER_SERVER_SHUTDOWN = 1053 + CR_SERVER_LOST = 2013 + CR_SERVER_LOST_EXTENDED = 2055 + CR_SERVER_GONE_ERROR = 2006 + + def translate_database_exception + case error_number + when ER_SERVER_SHUTDOWN + Errors::ServerShutdown.new(message) + when CR_SERVER_LOST, CR_SERVER_LOST_EXTENDED + Errors::ServerLost.new(message) + when CR_SERVER_GONE_ERROR + Errors::ServerGone.new(message) + end + end + + def translate_ruby_exception + case exception + when Errno::EPIPE + Errors::BrokenPipe.new(message) + when SocketError, IOError + Errors::SocketError.new(message) + when ::Trilogy::ConnectionError + if message.include?("Connection reset by peer") + Errors::ConnectionResetByPeer.new(message) + end + end + end + + def translate_trilogy_exception + return unless exception.is_a?(::Trilogy::Error) + + case message + when /TRILOGY_CLOSED_CONNECTION/ + Errors::ClosedConnection.new(message) + when /TRILOGY_INVALID_SEQUENCE_ID/ + Errors::InvalidSequenceId.new(message) + when /TRILOGY_UNEXPECTED_PACKET/ + Errors::UnexpectedPacket.new(message) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/trilogy_adapter.rb b/activerecord/lib/active_record/connection_adapters/trilogy_adapter.rb new file mode 100644 index 0000000000000..a8bdfe84fc55a --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/trilogy_adapter.rb @@ -0,0 +1,347 @@ +# frozen_string_literal: true + +require "active_record/connection_adapters/abstract_mysql_adapter" + +gem "trilogy", "~> 2.4" +require "trilogy" + +require "active_record/connection_adapters/trilogy/lost_connection_exception_translator" +require "active_record/connection_adapters/trilogy/errors" + +module ActiveRecord + module ConnectionHandling # :nodoc: + def trilogy_adapter_class + ConnectionAdapters::TrilogyAdapter + end + + # Establishes a connection to the database that's used by all Active Record objects. + def trilogy_connection(config) + configuration = config.dup + + # Set FOUND_ROWS capability on the connection so UPDATE queries returns number of rows + # matched rather than number of rows updated. + configuration[:found_rows] = true + + options = [ + configuration[:host], + configuration[:port], + configuration[:database], + configuration[:username], + configuration[:password], + configuration[:socket], + 0 + ] + + trilogy_adapter_class.new nil, logger, options, configuration + end + end + module ConnectionAdapters + class TrilogyAdapter < AbstractMysqlAdapter + module DatabaseStatements + READ_QUERY = AbstractAdapter.build_read_query_regexp( + :desc, :describe, :set, :show, :use + ) # :nodoc: + private_constant :READ_QUERY + + HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP(6)").freeze # :nodoc: + private_constant :HIGH_PRECISION_CURRENT_TIMESTAMP + + def select_all(*, **) # :nodoc: + result = nil + with_raw_connection do |conn| + result = super + conn.next_result while conn.more_results_exist? + end + result + end + + def write_query?(sql) # :nodoc: + !READ_QUERY.match?(sql) + rescue ArgumentError # Invalid encoding + !READ_QUERY.match?(sql.b) + end + + def explain(arel, binds = [], options = []) + sql = build_explain_clause(options) + " " + to_sql(arel, binds) + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + result = exec_query(sql, "EXPLAIN", binds) + elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start + + MySQL::ExplainPrettyPrinter.new.pp(result, elapsed) + end + + def exec_query(sql, name = "SQL", binds = [], prepare: false, async: false) + result = execute(sql, name, async: async) + ActiveRecord::Result.new(result.fields, result.to_a) + end + + alias exec_without_stmt exec_query + + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) + execute(to_sql(sql, binds), name) + end + + def exec_delete(sql, name = nil, binds = []) + result = execute(to_sql(sql, binds), name) + result.affected_rows + end + + alias :exec_update :exec_delete + + def high_precision_current_timestamp + HIGH_PRECISION_CURRENT_TIMESTAMP + end + + def build_explain_clause(options = []) + return "EXPLAIN" if options.empty? + + explain_clause = "EXPLAIN #{options.join(" ").upcase}" + + if analyze_without_explain? && explain_clause.include?("ANALYZE") + explain_clause.sub("EXPLAIN ", "") + else + explain_clause + end + end + + private + def last_inserted_id(result) + result.last_insert_id + end + end + + ER_BAD_DB_ERROR = 1049 + ER_DBACCESS_DENIED_ERROR = 1044 + ER_ACCESS_DENIED_ERROR = 1045 + + ADAPTER_NAME = "Trilogy" + + include DatabaseStatements + + SSL_MODES = { + SSL_MODE_DISABLED: ::Trilogy::SSL_DISABLED, + SSL_MODE_PREFERRED: ::Trilogy::SSL_PREFERRED_NOVERIFY, + SSL_MODE_REQUIRED: ::Trilogy::SSL_REQUIRED_NOVERIFY, + SSL_MODE_VERIFY_CA: ::Trilogy::SSL_VERIFY_CA, + SSL_MODE_VERIFY_IDENTITY: ::Trilogy::SSL_VERIFY_IDENTITY + }.freeze + + class << self + def new_client(config) + config[:ssl_mode] = parse_ssl_mode(config[:ssl_mode]) if config[:ssl_mode] + ::Trilogy.new(config) + rescue ::Trilogy::ConnectionError, ::Trilogy::ProtocolError => error + raise translate_connect_error(config, error) + end + + def parse_ssl_mode(mode) + return mode if mode.is_a? Integer + + m = mode.to_s.upcase + # enable Mysql2 client compatibility + m = "SSL_MODE_#{m}" unless m.start_with? "SSL_MODE_" + + SSL_MODES.fetch(m.to_sym, mode) + end + + def translate_connect_error(config, error) + case error.error_code + when ER_DBACCESS_DENIED_ERROR, ER_BAD_DB_ERROR + ActiveRecord::NoDatabaseError.db_error(config[:database]) + when ER_ACCESS_DENIED_ERROR + ActiveRecord::DatabaseConnectionError.username_error(config[:username]) + else + if error.message.include?(/TRILOGY_DNS_ERROR/) + ActiveRecord::DatabaseConnectionError.hostname_error(config[:host]) + else + ActiveRecord::ConnectionNotEstablished.new(error.message) + end + end + end + end + + def supports_json? + !mariadb? && database_version >= "5.7.8" + end + + def supports_comments? + true + end + + def supports_comments_in_create? + true + end + + def supports_savepoints? + true + end + + def savepoint_errors_invalidate_transactions? + true + end + + def supports_lazy_transactions? + true + end + + def quote_string(string) + with_raw_connection(allow_retry: true, uses_transaction: false) do |conn| + conn.escape(string) + end + end + + def active? + connection&.ping || false + rescue ::Trilogy::Error + false + end + + alias reset! reconnect! + + def disconnect! + super + unless connection.nil? + connection.close + self.connection = nil + end + end + + def discard! + self.connection = nil + end + + def each_hash(result) + return to_enum(:each_hash, result) unless block_given? + + keys = result.fields.map(&:to_sym) + result.rows.each do |row| + hash = {} + idx = 0 + row.each do |value| + hash[keys[idx]] = value + idx += 1 + end + yield hash + end + + nil + end + + def error_number(exception) + exception.error_code if exception.respond_to?(:error_code) + end + + private + def connection + @raw_connection + end + + def connection=(conn) + @raw_connection = conn + end + + def connect + self.connection = self.class.new_client(@config) + end + + def reconnect + connection&.close + self.connection = nil + connect + end + + def sync_timezone_changes(conn) + # Sync any changes since connection last established. + if default_timezone == :local + conn.query_flags |= ::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE + else + conn.query_flags &= ~::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE + end + end + + def execute_batch(statements, name = nil) + statements = statements.map { |sql| transform_query(sql) } + combine_multi_statements(statements).each do |statement| + with_raw_connection do |conn| + raw_execute(statement, name) + conn.next_result while conn.more_results_exist? + end + end + end + + def multi_statements_enabled? + !!@config[:multi_statement] + end + + def with_multi_statements + if multi_statements_enabled? + return yield + end + + with_raw_connection do |conn| + conn.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_ON) + + yield + ensure + conn.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_OFF) + end + end + + def combine_multi_statements(total_sql) + total_sql.each_with_object([]) do |sql, total_sql_chunks| + previous_packet = total_sql_chunks.last + if max_allowed_packet_reached?(sql, previous_packet) + total_sql_chunks << +sql + else + previous_packet << ";\n" + previous_packet << sql + end + end + end + + def max_allowed_packet_reached?(current_packet, previous_packet) + if current_packet.bytesize > max_allowed_packet + raise ActiveRecordError, + "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable." + elsif previous_packet.nil? + true + else + (current_packet.bytesize + previous_packet.bytesize + 2) > max_allowed_packet + end + end + + def max_allowed_packet + @max_allowed_packet ||= show_variable("max_allowed_packet") + end + + def full_version + schema_cache.database_version.full_version_string + end + + def get_full_version + with_raw_connection(allow_retry: true, uses_transaction: false) do |conn| + conn.server_info[:version] + end + end + + def translate_exception(exception, message:, sql:, binds:) + error_code = exception.error_code if exception.respond_to?(:error_code) + + Trilogy::LostConnectionExceptionTranslator.new(exception, message, error_code).translate || super + end + + def default_prepared_statements + false + end + + def default_insert_value(column) + super unless column.auto_increment? + end + + # https://mariadb.com/kb/en/analyze-statement/ + def analyze_without_explain? + mariadb? && database_version >= "10.1.0" + end + end + end +end diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index ab8433ad6159d..5693fa9f03275 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -121,7 +121,7 @@ def rename_table(table_name, new_name, **options) def change_column(table_name, column_name, type, **options) options[:_skip_validate_options] = true - if connection.adapter_name == "Mysql2" + if connection.adapter_name == "Mysql2" || connection.adapter_name == "Trilogy" options[:collation] = :no_collation end super @@ -372,7 +372,7 @@ def change_column(table_name, column_name, type, **options) end def create_table(table_name, **options) - if connection.adapter_name == "Mysql2" + if connection.adapter_name == "Mysql2" || connection.adapter_name == "Trilogy" super(table_name, options: "ENGINE=InnoDB", **options) else super @@ -404,7 +404,7 @@ def create_table(table_name, **options) end end - unless connection.adapter_name == "Mysql2" && options[:id] == :bigint + unless ["Mysql2", "Trilogy"].include?(connection.adapter_name) && options[:id] == :bigint if [:integer, :bigint].include?(options[:id]) && !options.key?(:default) options[:default] = nil end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 3ab9cfe395b94..3abfa4f615e08 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -74,6 +74,7 @@ def register_task(pattern, task) end register_task(/mysql/, "ActiveRecord::Tasks::MySQLDatabaseTasks") + register_task(/trilogy/, "ActiveRecord::Tasks::MySQLDatabaseTasks") register_task(/postgresql/, "ActiveRecord::Tasks::PostgreSQLDatabaseTasks") register_task(/sqlite/, "ActiveRecord::Tasks::SQLiteDatabaseTasks") diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 05a0092f759c3..6bc459ef18b4f 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -125,7 +125,7 @@ def test_exec_query_returns_an_empty_result assert_instance_of(ActiveRecord::Result, result) end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) def test_charset assert_not_nil @connection.charset assert_not_equal "character_set_database", @connection.charset diff --git a/activerecord/test/cases/adapters/abstract_mysql_adapter/adapter_prevent_writes_test.rb b/activerecord/test/cases/adapters/abstract_mysql_adapter/adapter_prevent_writes_test.rb index f6ca1503c1927..12aaedde0f319 100644 --- a/activerecord/test/cases/adapters/abstract_mysql_adapter/adapter_prevent_writes_test.rb +++ b/activerecord/test/cases/adapters/abstract_mysql_adapter/adapter_prevent_writes_test.rb @@ -64,7 +64,7 @@ def test_doesnt_error_when_a_show_query_is_called_while_preventing_writes def test_doesnt_error_when_a_set_query_is_called_while_preventing_writes ActiveRecord::Base.while_preventing_writes do - assert_nil @conn.execute("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci") + assert_nothing_raised { @conn.execute("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci") } end end @@ -91,7 +91,7 @@ def test_doesnt_error_when_a_read_query_with_leading_chars_is_called_while_preve def test_doesnt_error_when_a_use_query_is_called_while_preventing_writes ActiveRecord::Base.while_preventing_writes do db_name = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary").database - assert_nil @conn.execute("USE #{db_name}") + assert_nothing_raised { @conn.execute("USE #{db_name}") } end end diff --git a/activerecord/test/cases/adapters/abstract_mysql_adapter/connection_test.rb b/activerecord/test/cases/adapters/abstract_mysql_adapter/connection_test.rb index 343d4a401fe8c..523029b46ca82 100644 --- a/activerecord/test/cases/adapters/abstract_mysql_adapter/connection_test.rb +++ b/activerecord/test/cases/adapters/abstract_mysql_adapter/connection_test.rb @@ -24,7 +24,11 @@ def test_bad_connection assert_raise ActiveRecord::NoDatabaseError do db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary") configuration = db_config.configuration_hash.merge(database: "inexistent_activerecord_unittest") - connection = ActiveRecord::Base.mysql2_connection(configuration) + connection = if current_adapter?(:Mysql2Adapter) + ActiveRecord::Base.mysql2_connection(configuration) + else + ActiveRecord::Base.trilogy_connection(configuration) + end connection.drop_table "ex", if_exists: true end end @@ -139,17 +143,19 @@ def test_mysql_sql_mode_variable_overrides_strict_mode end end - def test_passing_arbitrary_flags_to_adapter - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge(flags: Mysql2::Client::COMPRESS)) - assert_equal (Mysql2::Client::COMPRESS | Mysql2::Client::FOUND_ROWS), ActiveRecord::Base.connection.raw_connection.query_options[:flags] + unless current_adapter?(:TrilogyAdapter) + def test_passing_arbitrary_flags_to_adapter + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge(flags: Mysql2::Client::COMPRESS)) + assert_equal (Mysql2::Client::COMPRESS | Mysql2::Client::FOUND_ROWS), ActiveRecord::Base.connection.raw_connection.query_options[:flags] + end end - end - def test_passing_flags_by_array_to_adapter - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge(flags: ["COMPRESS"])) - assert_equal ["COMPRESS", "FOUND_ROWS"], ActiveRecord::Base.connection.raw_connection.query_options[:flags] + def test_passing_flags_by_array_to_adapter + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge(flags: ["COMPRESS"])) + assert_equal ["COMPRESS", "FOUND_ROWS"], ActiveRecord::Base.connection.raw_connection.query_options[:flags] + end end end diff --git a/activerecord/test/cases/adapters/trilogy/dbconsole_test.rb b/activerecord/test/cases/adapters/trilogy/dbconsole_test.rb new file mode 100644 index 0000000000000..52b7cecf66b44 --- /dev/null +++ b/activerecord/test/cases/adapters/trilogy/dbconsole_test.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require "cases/helper" +require "active_support/testing/method_call_assertions" + +module ActiveRecord + module ConnectionAdapters + class TrilogyDbConsoleTest < ActiveRecord::TrilogyTestCase + include ActiveSupport::Testing::MethodCallAssertions + + def test_trilogy + config = make_db_config(adapter: "trilogy", database: "db") + + assert_find_cmd_and_exec_called_with([%w[mysql mysql5], "db"]) do + TrilogyAdapter.dbconsole(config) + end + end + + def test_mysql_full + config = make_db_config( + adapter: "trilogy", + database: "db", + host: "localhost", + port: 1234, + socket: "socket", + username: "user", + password: "qwerty", + encoding: "UTF-8", + sslca: "/path/to/ca-cert.pem", + sslcert: "/path/to/client-cert.pem", + sslcapath: "/path/to/cacerts", + sslcipher: "DHE-RSA-AES256-SHA", + sslkey: "/path/to/client-key.pem", + ssl_mode: "VERIFY_IDENTITY" + ) + + args = [ + %w[mysql mysql5], + "--host=localhost", + "--port=1234", + "--socket=socket", + "--user=user", + "--default-character-set=UTF-8", + "--ssl-ca=/path/to/ca-cert.pem", + "--ssl-cert=/path/to/client-cert.pem", + "--ssl-capath=/path/to/cacerts", + "--ssl-cipher=DHE-RSA-AES256-SHA", + "--ssl-key=/path/to/client-key.pem", + "--ssl-mode=VERIFY_IDENTITY", + "-p", "db" + ] + + assert_find_cmd_and_exec_called_with(args) do + TrilogyAdapter.dbconsole(config) + end + end + + def test_mysql_include_password + config = make_db_config(adapter: "trilogy", database: "db", username: "user", password: "qwerty") + + assert_find_cmd_and_exec_called_with([%w[mysql mysql5], "--user=user", "--password=qwerty", "db"]) do + TrilogyAdapter.dbconsole(config, include_password: true) + end + end + + private + def make_db_config(config) + ActiveRecord::DatabaseConfigurations::HashConfig.new("test", "primary", config) + end + + def assert_find_cmd_and_exec_called_with(args, &block) + assert_called_with(TrilogyAdapter, :find_cmd_and_exec, args, &block) + end + end + end +end diff --git a/activerecord/test/cases/adapters/trilogy/trilogy_adapter_test.rb b/activerecord/test/cases/adapters/trilogy/trilogy_adapter_test.rb new file mode 100644 index 0000000000000..aaf0e8c328a02 --- /dev/null +++ b/activerecord/test/cases/adapters/trilogy/trilogy_adapter_test.rb @@ -0,0 +1,888 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/ddl_helper" +require "models/book" +require "models/post" + +require "active_support/error_reporter/test_helper" + +class TrilogyAdapterTest < ActiveRecord::TrilogyTestCase + setup do + @configuration = { + adapter: "trilogy", + username: "rails", + database: "activerecord_unittest", + } + + @adapter = trilogy_adapter + @adapter.execute("TRUNCATE books") + @adapter.execute("TRUNCATE posts") + + db_config = ActiveRecord::DatabaseConfigurations.new({}).resolve(@configuration) + pool_config = ActiveRecord::ConnectionAdapters::PoolConfig.new(ActiveRecord::Base, db_config, :writing, :default) + @pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new(pool_config) + end + + teardown do + @adapter.disconnect! + end + + test "#explain for one query" do + explain = @adapter.explain("select * from posts") + assert_match %(possible_keys), explain + end + + test "#default_prepared_statements" do + assert_not_predicate @pool.connection, :prepared_statements? + end + + test "#adapter_name answers name" do + assert_equal "Trilogy", @adapter.adapter_name + end + + test "#supports_json answers true without Maria DB and greater version" do + assert @adapter.supports_json? + end + + test "#supports_json answers false without Maria DB and lesser version" do + database_version = @adapter.class::Version.new("5.0.0", nil) + + @adapter.stub(:database_version, database_version) do + assert_equal false, @adapter.supports_json? + end + end + + test "#supports_json answers false with Maria DB" do + @adapter.stub(:mariadb?, true) do + assert_equal false, @adapter.supports_json? + end + end + + test "#supports_comments? answers true" do + assert @adapter.supports_comments? + end + + test "#supports_comments_in_create? answers true" do + assert @adapter.supports_comments_in_create? + end + + test "#supports_savepoints? answers true" do + assert @adapter.supports_savepoints? + end + + test "#requires_reloading? answers false" do + assert_equal false, @adapter.requires_reloading? + end + + test "#native_database_types answers known types" do + assert_equal ActiveRecord::ConnectionAdapters::TrilogyAdapter::NATIVE_DATABASE_TYPES, @adapter.native_database_types + end + + test "#quote_column_name answers quoted string when not quoted" do + assert_equal "`test`", @adapter.quote_column_name("test") + end + + test "#quote_column_name answers triple quoted string when quoted" do + assert_equal "```test```", @adapter.quote_column_name("`test`") + end + + test "#quote_column_name answers quoted string for integer" do + assert_equal "`1`", @adapter.quote_column_name(1) + end + + test "#quote_string answers string with connection" do + assert_equal "\\\"test\\\"", @adapter.quote_string(%("test")) + end + + test "#quote_string works when the connection is known to be closed" do + adapter = trilogy_adapter + adapter.connect! + adapter.instance_variable_get(:@raw_connection).close + + assert_equal "\\\"test\\\"", adapter.quote_string(%("test")) + end + + test "#quoted_true answers TRUE" do + assert_equal "TRUE", @adapter.quoted_true + end + + test "#quoted_false answers FALSE" do + assert_equal "FALSE", @adapter.quoted_false + end + + test "#active? answers true with connection" do + assert @adapter.active? + end + + test "#active? answers false with connection and exception" do + @adapter.send(:connection).stub(:ping, -> { raise ::Trilogy::BaseError.new }) do + assert_equal false, @adapter.active? + end + end + + test "#active? answers false without connection" do + adapter = trilogy_adapter + assert_equal false, adapter.active? + end + + test "#reconnect closes connection with connection" do + connection = Minitest::Mock.new Trilogy.new(@configuration) + connection.expect :close, true + adapter = trilogy_adapter_with_connection(connection) + adapter.reconnect! + + assert connection.verify + end + + test "#reconnect doesn't retain old connection on failure" do + old_connection = Minitest::Mock.new Trilogy.new(@configuration) + old_connection.expect :close, true + + adapter = trilogy_adapter_with_connection(old_connection) + + begin + Trilogy.stub(:new, -> _ { raise Trilogy::BaseError.new }) do + adapter.reconnect! + end + rescue ActiveRecord::StatementInvalid => ex + assert_instance_of Trilogy::BaseError, ex.cause + else + flunk "Expected Trilogy::BaseError to be raised" + end + + assert_nil adapter.send(:connection) + end + + test "#reconnect answers new connection with existing connection" do + old_connection = @adapter.send(:connection) + @adapter.reconnect! + connection = @adapter.send(:connection) + + assert_instance_of Trilogy, connection + assert_not_equal old_connection, connection + end + + test "#reconnect answers new connection without existing connection" do + adapter = trilogy_adapter + adapter.reconnect! + assert_instance_of Trilogy, adapter.send(:connection) + end + + test "#reset closes connection with existing connection" do + connection = Minitest::Mock.new Trilogy.new(@configuration) + connection.expect :close, true + adapter = trilogy_adapter_with_connection(connection) + adapter.reset! + + assert connection.verify + end + + test "#reset answers new connection with existing connection" do + old_connection = @adapter.send(:connection) + @adapter.reset! + connection = @adapter.send(:connection) + + assert_instance_of Trilogy, connection + assert_not_equal old_connection, connection + end + + test "#reset answers new connection without existing connection" do + adapter = trilogy_adapter + adapter.reset! + assert_instance_of Trilogy, adapter.send(:connection) + end + + test "#disconnect closes connection with existing connection" do + connection = Minitest::Mock.new Trilogy.new(@configuration) + connection.expect :close, true + adapter = trilogy_adapter_with_connection(connection) + adapter.disconnect! + + assert connection.verify + end + + test "#disconnect makes adapter inactive with connection" do + @adapter.disconnect! + assert_equal false, @adapter.active? + end + + test "#disconnect answers nil with connection" do + assert_nil @adapter.disconnect! + end + + test "#disconnect answers nil without connection" do + adapter = trilogy_adapter + assert_nil adapter.disconnect! + end + + test "#disconnect leaves adapter inactive without connection" do + adapter = trilogy_adapter + adapter.disconnect! + + assert_equal false, adapter.active? + end + + test "#discard answers nil with connection" do + assert_nil @adapter.discard! + end + + test "#discard makes adapter inactive with connection" do + @adapter.discard! + assert_equal false, @adapter.active? + end + + test "#discard answers nil without connection" do + adapter = trilogy_adapter + assert_nil adapter.discard! + end + + test "#exec_query answers result with valid query" do + result = @adapter.exec_query "SELECT id, author_id, title, body FROM posts;" + + assert_equal %w[id author_id title body], result.columns + assert_equal [], result.rows + end + + test "#exec_query fails with invalid query" do + assert_raises_with_message ActiveRecord::StatementInvalid, /'activerecord_unittest.bogus' doesn't exist/ do + @adapter.exec_query "SELECT * FROM bogus;" + end + end + + test "#exec_insert inserts new row" do + @adapter.exec_insert "INSERT INTO posts (title, body) VALUES ('Test', 'example');", nil, nil + result = @adapter.execute "SELECT id, title, body FROM posts;" + + assert_equal [[1, "Test", "example"]], result.rows + end + + test "#exec_delete deletes existing row" do + @adapter.execute "INSERT INTO posts (title, body) VALUES ('Test', 'example');" + @adapter.exec_delete "DELETE FROM posts WHERE title = 'Test';", nil, nil + result = @adapter.execute "SELECT id, title, body FROM posts;" + + assert_equal [], result.rows + end + + test "#exec_update updates existing row" do + @adapter.execute "INSERT INTO posts (title, body) VALUES ('Test', 'example');" + @adapter.exec_update "UPDATE posts SET title = 'Test II' where body = 'example';", nil, nil + result = @adapter.execute "SELECT id, title, body FROM posts;" + + assert_equal [[1, "Test II", "example"]], result.rows + end + + test "default query flags set timezone to UTC" do + if ActiveRecord.respond_to?(:default_timezone) + assert_equal :utc, ActiveRecord.default_timezone + else + assert_equal :utc, ActiveRecord::Base.default_timezone + end + ruby_time = Time.utc(2019, 5, 31, 12, 52) + time = "2019-05-31 12:52:00" + + @adapter.execute("INSERT into books (name, format, created_at, updated_at) VALUES ('name', 'paperback', '#{time}', '#{time}');") + result = @adapter.execute("select * from books limit 1;") + + result.each_hash do |hsh| + assert_equal ruby_time, hsh["created_at"] + assert_equal ruby_time, hsh["updated_at"] + end + + assert_equal 1, @adapter.send(:connection).query_flags + end + + test "query flags for timezone can be set to local" do + if ActiveRecord.respond_to?(:default_timezone) + old_timezone, ActiveRecord.default_timezone = ActiveRecord.default_timezone, :local + assert_equal :local, ActiveRecord.default_timezone + else + old_timezone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, :local + assert_equal :local, ActiveRecord::Base.default_timezone + end + ruby_time = Time.local(2019, 5, 31, 12, 52) + time = "2019-05-31 12:52:00" + + @adapter.execute("INSERT into books (name, format, created_at, updated_at) VALUES ('name', 'paperback', '#{time}', '#{time}');") + result = @adapter.execute("select * from books limit 1;") + + result.each_hash do |hsh| + assert_equal ruby_time, hsh["created_at"] + assert_equal ruby_time, hsh["updated_at"] + end + + assert_equal 5, @adapter.send(:connection).query_flags + ensure + if ActiveRecord.respond_to?(:default_timezone) + ActiveRecord.default_timezone = old_timezone + else + ActiveRecord::Base.default_timezone = old_timezone + end + end + + test "query flags for timezone can be set to local and reset to utc" do + if ActiveRecord.respond_to?(:default_timezone) + old_timezone, ActiveRecord.default_timezone = ActiveRecord.default_timezone, :local + assert_equal :local, ActiveRecord.default_timezone + else + old_timezone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, :local + assert_equal :local, ActiveRecord::Base.default_timezone + end + ruby_time = Time.local(2019, 5, 31, 12, 52) + time = "2019-05-31 12:52:00" + + @adapter.execute("INSERT into books (name, format, created_at, updated_at) VALUES ('name', 'paperback', '#{time}', '#{time}');") + result = @adapter.execute("select * from books limit 1;") + + result.each_hash do |hsh| + assert_equal ruby_time, hsh["created_at"] + assert_equal ruby_time, hsh["updated_at"] + end + + assert_equal 5, @adapter.send(:connection).query_flags + + if ActiveRecord.respond_to?(:default_timezone) + ActiveRecord.default_timezone = :utc + else + ActiveRecord::Base.default_timezone = :utc + end + + ruby_utc_time = Time.utc(2019, 5, 31, 12, 52) + utc_result = @adapter.execute("select * from books limit 1;") + + utc_result.each_hash do |hsh| + assert_equal ruby_utc_time, hsh["created_at"] + assert_equal ruby_utc_time, hsh["updated_at"] + end + + assert_equal 1, @adapter.send(:connection).query_flags + ensure + if ActiveRecord.respond_to?(:default_timezone) + ActiveRecord.default_timezone = old_timezone + else + ActiveRecord::Base.default_timezone = old_timezone + end + end + + test "#execute answers results for valid query" do + result = @adapter.execute "SELECT id, author_id, title, body FROM posts;" + assert_equal %w[id author_id title body], result.fields + end + + test "#execute answers results for valid query after reconnect" do + mock_connection = Minitest::Mock.new Trilogy.new(@configuration) + adapter = trilogy_adapter_with_connection(mock_connection) + + # Cause an ER_SERVER_SHUTDOWN error (code 1053) after the session is + # set. On reconnect, the adapter will get a real, working connection. + server_shutdown_error = Trilogy::ProtocolError.new + server_shutdown_error.instance_variable_set(:@error_code, 1053) + mock_connection.expect(:query, nil) { raise server_shutdown_error } + + assert_raises(ActiveRecord::ConnectionFailed) do + adapter.execute "SELECT * FROM posts;" + end + + adapter.reconnect! + result = adapter.execute "SELECT id, author_id, title, body FROM posts;" + + assert_equal %w[id author_id title body], result.fields + assert mock_connection.verify + mock_connection.close + end + + test "#execute fails with invalid query" do + assert_raises_with_message ActiveRecord::StatementInvalid, /Table 'activerecord_unittest.bogus' doesn't exist/ do + @adapter.execute "SELECT * FROM bogus;" + end + end + + test "#execute fails with invalid SQL" do + assert_raises(ActiveRecord::StatementInvalid) do + @adapter.execute "SELECT bogus FROM posts;" + end + end + + test "#execute answers results for valid query after losing connection unexpectedly" do + connection = Trilogy.new(@configuration.merge(read_timeout: 1)) + + adapter = trilogy_adapter_with_connection(connection) + assert adapter.active? + + # Make connection lost for future queries by exceeding the read timeout + assert_raises(Trilogy::TimeoutError) do + connection.query "SELECT sleep(2);" + end + assert_not adapter.active? + + # The adapter believes the connection is verified, so it will run the + # following query immediately. It will fail, and as the query's not + # retryable, the adapter will raise an error. + + # The next query fails because the connection is lost + assert_raises(ActiveRecord::ConnectionFailed) do + adapter.execute "SELECT COUNT(*) FROM posts;" + end + assert_not adapter.active? + + # The adapter now knows the connection is lost, so it will re-verify (and + # ultimately reconnect) before running another query. + + # This query triggers a reconnect + result = adapter.execute "SELECT COUNT(*) FROM posts;" + assert_equal [[0]], result.rows + assert adapter.active? + end + + test "#execute answers results for valid query after losing connection" do + connection = Trilogy.new(@configuration.merge(read_timeout: 1)) + + adapter = trilogy_adapter_with_connection(connection) + assert adapter.active? + + # Make connection lost for future queries by exceeding the read timeout + assert_raises(ActiveRecord::StatementInvalid) do + adapter.execute "SELECT sleep(2);" + end + assert_not adapter.active? + + # The above failure has not yet caused a reconnect, but the adapter has + # lost confidence in the connection, so it will re-verify before running + # the next query -- which means it will succeed. + + # This query triggers a reconnect + result = adapter.execute "SELECT COUNT(*) FROM posts;" + assert_equal [[0]], result.rows + assert adapter.active? + end + + test "#execute fails if the connection is closed" do + connection = ::Trilogy.new(@configuration.merge(read_timeout: 1)) + + adapter = trilogy_adapter_with_connection(connection) + adapter.pool = @pool + + assert_raises ActiveRecord::ConnectionFailed do + adapter.transaction do + # Make connection lost for future queries by exceeding the read timeout + assert_raises(ActiveRecord::StatementInvalid) do + adapter.execute "SELECT sleep(2);" + end + assert_not adapter.active? + + adapter.execute "SELECT COUNT(*) FROM posts;" + end + end + + assert_not adapter.active? + + # This query triggers a reconnect + result = adapter.execute "SELECT COUNT(*) FROM posts;" + assert_equal [[0]], result.rows + end + + test "can reconnect after failing to rollback" do + connection = ::Trilogy.new(@configuration.merge(read_timeout: 1)) + + adapter = trilogy_adapter_with_connection(connection) + adapter.pool = @pool + + adapter.transaction do + adapter.execute("SELECT 1") + + # Cause the client to disconnect without the adapter's awareness + assert_raises ::Trilogy::TimeoutError do + adapter.send(:connection).query("SELECT sleep(2)") + end + + raise ActiveRecord::Rollback + end + + result = adapter.execute("SELECT 1") + assert_equal [[1]], result.rows + end + + test "can reconnect after failing to commit" do + connection = Trilogy.new(@configuration.merge(read_timeout: 1)) + + adapter = trilogy_adapter_with_connection(connection) + adapter.pool = @pool + + assert_raises ActiveRecord::ConnectionFailed do + adapter.transaction do + adapter.execute("SELECT 1") + + # Cause the client to disconnect without the adapter's awareness + assert_raises Trilogy::TimeoutError do + adapter.send(:connection).query("SELECT sleep(2)") + end + end + end + + result = adapter.execute("SELECT 1") + assert_equal [[1]], result.rows + end + + test "#execute fails with deadlock error" do + adapter = trilogy_adapter + + new_connection = Trilogy.new(@configuration) + + deadlocking_adapter = trilogy_adapter_with_connection(new_connection) + + # Add seed data + adapter.insert("INSERT INTO posts (title, body) VALUES('Setup', 'Content')") + + adapter.transaction do + adapter.execute( + "UPDATE posts SET title = 'Connection 1' WHERE title != 'Connection 1';" + ) + + # Decrease the lock wait timeout in this session + deadlocking_adapter.execute("SET innodb_lock_wait_timeout = 1") + + assert_raises(ActiveRecord::LockWaitTimeout) do + deadlocking_adapter.execute( + "UPDATE posts SET title = 'Connection 2' WHERE title != 'Connection 2';" + ) + end + end + end + + test "#execute fails with unknown error" do + assert_raises_with_message(ActiveRecord::StatementInvalid, /A random error/) do + connection = Minitest::Mock.new Trilogy.new(@configuration) + connection.expect(:query, nil) { raise Trilogy::ProtocolError, "A random error." } + adapter = trilogy_adapter_with_connection(connection) + + adapter.execute "SELECT * FROM posts;" + end + end + + test "#select_all when query cache is enabled fires the same notification payload for uncached and cached queries" do + @adapter.cache do + event_fired = false + subscription = ->(name, start, finish, id, payload) { + event_fired = true + + # First, we test keys that are defined by default by the AbstractAdapter + assert_includes payload, :sql + assert_equal "SELECT * FROM posts", payload[:sql] + + assert_includes payload, :name + assert_equal "uncached query", payload[:name] + + assert_includes payload, :connection + assert_equal @adapter, payload[:connection] + + assert_includes payload, :binds + assert_equal [], payload[:binds] + + assert_includes payload, :type_casted_binds + assert_equal [], payload[:type_casted_binds] + + # :stament_name is always nil and never set 🤷‍♂️ + assert_includes payload, :statement_name + assert_nil payload[:statement_name] + + assert_not_includes payload, :cached + } + ActiveSupport::Notifications.subscribed(subscription, "sql.active_record") do + @adapter.select_all "SELECT * FROM posts", "uncached query" + end + assert event_fired + + event_fired = false + subscription = ->(name, start, finish, id, payload) { + event_fired = true + + # First, we test keys that are defined by default by the AbstractAdapter + assert_includes payload, :sql + assert_equal "SELECT * FROM posts", payload[:sql] + + assert_includes payload, :name + assert_equal "cached query", payload[:name] + + assert_includes payload, :connection + assert_equal @adapter, payload[:connection] + + assert_includes payload, :binds + assert_equal [], payload[:binds] + + assert_includes payload, :type_casted_binds + assert_equal [], payload[:type_casted_binds].is_a?(Proc) ? payload[:type_casted_binds].call : payload[:type_casted_binds] + + # Rails does not include :stament_name for cached queries 🤷‍♂️ + assert_not_includes payload, :statement_name + + assert_includes payload, :cached + assert_equal true, payload[:cached] + } + ActiveSupport::Notifications.subscribed(subscription, "sql.active_record") do + @adapter.select_all "SELECT * FROM posts", "cached query" + end + assert event_fired + end + end + + test "#execute answers result with valid SQL" do + result = @adapter.execute "SELECT id, author_id, title FROM posts;" + + assert_equal %w[id author_id title], result.fields + assert_equal [], result.rows + end + + test "#execute emits a query notification" do + assert_notification("sql.active_record") do + @adapter.execute "SELECT * FROM posts;" + end + end + + test "#indexes answers indexes with existing indexes" do + proof = [{ + table: "posts", + name: "index_posts_on_author_id", + unique: false, + columns: ["author_id"], + lengths: {}, + orders: {}, + opclasses: {}, + where: nil, + type: nil, + using: :btree, + comment: nil + }] + + indexes = @adapter.indexes("posts").map do |index| + { + table: index.table, + name: index.name, + unique: index.unique, + columns: index.columns, + lengths: index.lengths, + orders: index.orders, + opclasses: index.opclasses, + where: index.where, + type: index.type, + using: index.using, + comment: index.comment + } + end + + assert_equal proof, indexes + end + + test "#indexes answers empty array with no indexes" do + assert_equal [], @adapter.indexes("users") + end + + test "#begin_db_transaction answers empty result" do + result = @adapter.begin_db_transaction + assert_equal [], result.rows + + # rollback transaction so it doesn't bleed into other tests + @adapter.rollback_db_transaction + end + + test "#begin_db_transaction raises error" do + error = Class.new(Exception) + assert_raises error do + @adapter.stub(:raw_execute, -> (*) { raise error }) do + @adapter.begin_db_transaction + end + end + + # rollback transaction so it doesn't bleed into other tests + @adapter.rollback_db_transaction + end + + test "#commit_db_transaction answers empty result" do + result = @adapter.commit_db_transaction + assert_equal [], result.rows + end + + test "#commit_db_transaction raises error" do + error = Class.new(Exception) + assert_raises error do + @adapter.stub(:raw_execute, -> (*) { raise error }) do + @adapter.commit_db_transaction + end + end + end + + test "#rollback_db_transaction raises error" do + error = Class.new(Exception) + assert_raises error do + @adapter.stub(:raw_execute, -> (*) { raise error }) do + @adapter.rollback_db_transaction + end + end + end + + test "#insert answers ID with ID" do + assert_equal 5, @adapter.insert("INSERT INTO posts (title, body) VALUES ('test', 'content');", "test", nil, 5) + end + + test "#insert answers last ID without ID" do + assert_equal 1, @adapter.insert("INSERT INTO posts (title, body) VALUES ('test', 'content');", "test") + end + + test "#insert answers incremented last ID without ID" do + @adapter.insert("INSERT INTO posts (title, body) VALUES ('test', 'content');", "test") + assert_equal 2, @adapter.insert("INSERT INTO posts (title, body) VALUES ('test', 'content');", "test") + end + + test "#update answers affected row count when updatable" do + @adapter.insert("INSERT INTO posts (title, body) VALUES ('test', 'content');") + assert_equal 1, @adapter.update("UPDATE posts SET title = 'Test' WHERE id = 1;") + end + + test "#update answers zero affected rows when not updatable" do + assert_equal 0, @adapter.update("UPDATE posts SET title = 'Test' WHERE id = 1;") + end + + test "strict mode can be disabled" do + adapter = trilogy_adapter(strict: false) + + adapter.execute "INSERT INTO posts (title) VALUES ('test');" + result = adapter.execute "SELECT * FROM posts;" + assert_equal [[1, nil, "test", "", nil, 0, 0, 0, 0, 0, 0, 0]], result.rows + end + + test "#select_value returns a single value" do + assert_equal 123, @adapter.select_value("SELECT 123") + end + + test "#each_hash yields symbolized result rows" do + @adapter.execute "INSERT INTO posts (title, body) VALUES ('test', 'content');" + result = @adapter.execute "SELECT title, body FROM posts;" + + @adapter.each_hash(result) do |row| + assert_equal "test", row[:title] + end + end + + test "#each_hash returns an enumarator of symbolized result rows when no block is given" do + @adapter.execute "INSERT INTO posts (title, body) VALUES ('test', 'content');" + result = @adapter.execute "SELECT * FROM posts;" + rows_enum = @adapter.each_hash result + + assert_equal "test", rows_enum.next[:title] + end + + test "#each_hash returns empty array when results is empty" do + result = @adapter.execute "SELECT * FROM posts;" + rows = @adapter.each_hash result + + assert_empty rows.to_a + end + + test "#error_number answers number for exception" do + exception = Minitest::Mock.new + exception.expect :error_code, 123 + + assert_equal 123, @adapter.error_number(exception) + end + + # We only want to test if QueryLogs functionality is available + if ActiveRecord.respond_to?(:query_transformers) + test "execute uses AbstractAdapter#transform_query when available" do + # Add custom query transformer + old_query_transformers = ActiveRecord.query_transformers + ActiveRecord.query_transformers = [-> (sql, _adapter) { sql + " /* it works */" }] + + sql = "SELECT * FROM posts;" + + mock_connection = Minitest::Mock.new Trilogy.new(@configuration) + adapter = trilogy_adapter_with_connection(mock_connection) + mock_connection.expect :query, nil, [sql + " /* it works */"] + + adapter.execute sql + + assert mock_connection.verify + ensure + # Teardown custom query transformers + ActiveRecord.query_transformers = old_query_transformers + end + end + + test "parses ssl_mode as int" do + adapter = trilogy_adapter(ssl_mode: 0) + adapter.connect! + + assert adapter.active? + end + + test "parses ssl_mode as string" do + adapter = trilogy_adapter(ssl_mode: "disabled") + adapter.connect! + + assert adapter.active? + end + + test "parses ssl_mode as string prefixed" do + adapter = trilogy_adapter(ssl_mode: "SSL_MODE_DISABLED") + adapter.connect! + + assert adapter.active? + end + + def trilogy_adapter_with_connection(connection, **config_overrides) + ActiveRecord::ConnectionAdapters::TrilogyAdapter + .new(connection, nil, {}, @configuration.merge(config_overrides)) + .tap { |conn| conn.execute("SELECT 1") } + end + + def trilogy_adapter(**config_overrides) + ActiveRecord::ConnectionAdapters::TrilogyAdapter + .new(@configuration.merge(config_overrides)) + end + + def assert_raises_with_message(exception, message, &block) + block.call + rescue exception => error + assert_match message, error.message + else + fail %(Expected #{exception} with message "#{message}" but nothing failed.) + end + + # Create a temporary subscription to verify notification is sent. + # Optionally verify the notification payload includes expected types. + def assert_notification(notification, expected_payload = {}, &block) + notification_sent = false + + subscription = lambda do |*args| + notification_sent = true + event = ActiveSupport::Notifications::Event.new(*args) + + expected_payload.each do |key, value| + assert( + value === event.payload[key], + "Expected notification payload[:#{key}] to match #{value.inspect}, but got #{event.payload[key].inspect}." + ) + end + end + + ActiveSupport::Notifications.subscribed(subscription, notification) do + block.call if block_given? + end + + assert notification_sent, "#{notification} notification was not sent" + end + + # Create a temporary subscription to verify notification was not sent. + def assert_no_notification(notification, &block) + notification_sent = false + + subscription = lambda do |*args| + notification_sent = true + end + + ActiveSupport::Notifications.subscribed(subscription, notification) do + block.call if block_given? + end + + assert_not notification_sent, "#{notification} notification was sent" + end +end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 7a7a08c2ccd9a..928c8d5d31bd1 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -93,7 +93,7 @@ def test_belongs_to_with_primary_key def test_belongs_to_with_primary_key_joins_on_correct_column sql = Client.joins(:firm_with_primary_key).to_sql - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_no_match(/`firm_with_primary_keys_companies`\.`id`/, sql) assert_match(/`firm_with_primary_keys_companies`\.`name`/, sql) elsif current_adapter?(:OracleAdapter) diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 38d220bd9190c..7a21f3c3ccf31 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -2282,7 +2282,7 @@ def test_calling_first_nth_or_last_on_existing_record_with_build_should_load_ass assert_not_predicate author.topics_without_type, :loaded? assert_queries(1) do - if current_adapter?(:Mysql2Adapter, :SQLite3Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter, :SQLite3Adapter) assert_equal fourth, author.topics_without_type.first assert_equal third, author.topics_without_type.second end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index e3125c5af6c6a..aa775a0b1f92a 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -193,7 +193,7 @@ def setup assert_equal category_attrs, category.attributes_before_type_cast end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) test "read attributes_before_type_cast on a boolean" do bool = Boolean.create!("value" => false) assert_equal 0, bool.reload.attributes_before_type_cast["value"] diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 0b03f9b765873..1850ffc022447 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -143,6 +143,7 @@ def test_column_names_are_escaped badchar = { "SQLite3Adapter" => '"', "Mysql2Adapter" => "`", + "TrilogyAdapter" => "`", "PostgreSQLAdapter" => '"', "OracleAdapter" => '"', }.fetch(classname) { @@ -878,7 +879,7 @@ def test_unicode_column_name assert_equal "たこ焼き仮面", weird.なまえ end - unless current_adapter?(:PostgreSQLAdapter) + unless current_adapter?(:PostgreSQLAdapter) || current_adapter?(:TrilogyAdapter) def test_respect_internal_encoding old_default_internal = Encoding.default_internal silence_warnings { Encoding.default_internal = "EUC-JP" } @@ -1112,7 +1113,7 @@ def test_bignum_pk assert_equal company, Company.find(company.id) end - if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter, :SQLite3Adapter) + if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter, :TrilogyAdapter, :SQLite3Adapter) def test_default_char_types default = Default.new @@ -1120,7 +1121,7 @@ def test_default_char_types assert_equal "a varchar field", default.char2 # Mysql text type can't have default value - unless current_adapter?(:Mysql2Adapter) + unless current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_equal "a text field", default.char3 end end diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb index 8d18a12adf79d..4e4e0bbd80e70 100644 --- a/activerecord/test/cases/cache_key_test.rb +++ b/activerecord/test/cases/cache_key_test.rb @@ -51,7 +51,7 @@ class CacheMeWithVersion < ActiveRecord::Base end test "cache_version is the same when it comes from the DB or from the user" do - skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + skip("Mysql2, Trilogy, and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :TrilogyAdapter, :PostgreSQLAdapter) record = CacheMeWithVersion.create record_from_db = CacheMeWithVersion.find(record.id) @@ -63,7 +63,7 @@ class CacheMeWithVersion < ActiveRecord::Base end test "cache_version does not truncate zeros when timestamp ends in zeros" do - skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + skip("Mysql2, Trilogy, and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :TrilogyAdapter, :PostgreSQLAdapter) travel_to Time.now.beginning_of_day do record = CacheMeWithVersion.create @@ -84,7 +84,7 @@ class CacheMeWithVersion < ActiveRecord::Base end test "cache_version does NOT call updated_at when value is from the database" do - skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + skip("Mysql2, Trilogy, and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :TrilogyAdapter, :PostgreSQLAdapter) record = CacheMeWithVersion.create record_from_db = CacheMeWithVersion.find(record.id) diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 5660b530337f6..0fb21dce85e5b 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -400,7 +400,7 @@ def test_should_group_by_summed_field_having_condition end def test_should_group_by_summed_field_having_condition_from_select - skip unless current_adapter?(:Mysql2Adapter, :SQLite3Adapter) + skip unless current_adapter?(:Mysql2Adapter, :TrilogyAdapter, :SQLite3Adapter) c = Account.select("MIN(credit_limit) AS min_credit_limit").group(:firm_id).having("min_credit_limit > 50").sum(:credit_limit) assert_nil c[1] assert_equal 60, c[2] diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb index 1bea9d1dcd580..58a0243fbd9f3 100644 --- a/activerecord/test/cases/comment_test.rb +++ b/activerecord/test/cases/comment_test.rb @@ -182,7 +182,7 @@ def test_change_column_comment column = Commented.columns_hash["id"] assert_equal "Edited column comment", column.comment - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert column.auto_increment? end end diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb index db4d197104459..14a128499d0e4 100644 --- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb @@ -6,7 +6,7 @@ module ActiveRecord module ConnectionAdapters class MysqlTypeLookupTest < ActiveRecord::TestCase - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) include ConnectionHelper setup do diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index 543cb9e6f4455..3fa4b5def8332 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -170,7 +170,7 @@ def test_caches_database_version assert_no_queries do assert_equal @database_version.to_s, @cache.database_version.to_s - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_not_nil @cache.database_version.full_version_string end end diff --git a/activerecord/test/cases/custom_locking_test.rb b/activerecord/test/cases/custom_locking_test.rb index f52b26e9ec8b8..f5f41e75f3876 100644 --- a/activerecord/test/cases/custom_locking_test.rb +++ b/activerecord/test/cases/custom_locking_test.rb @@ -8,7 +8,7 @@ class CustomLockingTest < ActiveRecord::TestCase fixtures :people def test_custom_lock - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_match "SHARE MODE", Person.lock("LOCK IN SHARE MODE").to_sql assert_sql(/LOCK IN SHARE MODE/) do Person.all.merge!(lock: "LOCK IN SHARE MODE").find(1) diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index c3fe8f9f64768..cedc2734191bf 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -45,7 +45,7 @@ def test_datetime_precision_is_truncated_on_assignment assert_equal 123456000, foo.updated_at.nsec end - unless current_adapter?(:Mysql2Adapter) + unless current_adapter?(:Mysql2Adapter, :TrilogyAdapter) def test_no_datetime_precision_isnt_truncated_on_assignment @connection.create_table(:foos, force: true) @connection.add_column :foos, :created_at, :datetime, precision: nil diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 04195fe4f4a7f..948da822eee89 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -100,7 +100,7 @@ def test_default_varbinary_string assert_equal "varbinary_default", DefaultBinary.new.varbinary_col end - if current_adapter?(:Mysql2Adapter) && !ActiveRecord::Base.connection.mariadb? + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) && !ActiveRecord::Base.connection.mariadb? def test_default_binary_string assert_equal "binary_default", DefaultBinary.new.binary_col end @@ -165,7 +165,7 @@ class PostgresqlDefaultExpressionTest < ActiveRecord::TestCase end class MysqlDefaultExpressionTest < ActiveRecord::TestCase - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) include SchemaDumpingHelper if supports_default_expression? @@ -215,7 +215,7 @@ class MysqlDefaultExpressionTest < ActiveRecord::TestCase end class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) # ActiveRecord::Base#create! (and #save and other related methods) will # open a new transaction. When in transactional tests mode, this will # cause Active Record to create a new savepoint. However, since MySQL doesn't diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index af50e6907c855..e7a4af5eca84e 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -82,7 +82,7 @@ def call(_, _, _, _, values) end end - if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter, :PostgreSQLAdapter) def test_bulk_insert subscriber = InsertQuerySubscriber.new subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber) @@ -145,12 +145,20 @@ def test_bulk_insert_with_a_multi_statement_query_in_a_nested_transaction end end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) def test_bulk_insert_with_multi_statements_enabled + orig_connection_class = ActiveRecord::Base.connection.class run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection( - orig_connection.merge(flags: %w[MULTI_STATEMENTS]) - ) + case orig_connection_class::ADAPTER_NAME + when "Trilogy" + ActiveRecord::Base.establish_connection( + orig_connection.merge(multi_statement: true) + ) + else + ActiveRecord::Base.establish_connection( + orig_connection.merge(flags: %w[MULTI_STATEMENTS]) + ) + end fixtures = { "traffic_lights" => [ @@ -161,7 +169,12 @@ def test_bulk_insert_with_multi_statements_enabled assert_nothing_raised do conn = ActiveRecord::Base.connection conn.execute("SELECT 1; SELECT 2;") - conn.raw_connection.abandon_results! + case orig_connection_class::ADAPTER_NAME + when "Trilogy" + conn.raw_connection.next_result while conn.raw_connection.more_results_exist? + else + conn.raw_connection.abandon_results! + end end assert_difference "TrafficLight.count" do @@ -176,16 +189,29 @@ def test_bulk_insert_with_multi_statements_enabled assert_nothing_raised do conn = ActiveRecord::Base.connection conn.execute("SELECT 1; SELECT 2;") - conn.raw_connection.abandon_results! + case orig_connection_class::ADAPTER_NAME + when "Trilogy" + conn.raw_connection.next_result while conn.raw_connection.more_results_exist? + else + conn.raw_connection.abandon_results! + end end end end def test_bulk_insert_with_multi_statements_disabled + orig_connection_class = ActiveRecord::Base.connection.class run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection( - orig_connection.merge(flags: []) - ) + case orig_connection_class::ADAPTER_NAME + when "Trilogy" + ActiveRecord::Base.establish_connection( + orig_connection.merge(multi_statement: false) + ) + else + ActiveRecord::Base.establish_connection( + orig_connection.merge(flags: []) + ) + end fixtures = { "traffic_lights" => [ @@ -196,7 +222,12 @@ def test_bulk_insert_with_multi_statements_disabled assert_raises(ActiveRecord::StatementInvalid) do conn = ActiveRecord::Base.connection conn.execute("SELECT 1; SELECT 2;") - conn.raw_connection.abandon_results! + case orig_connection_class::ADAPTER_NAME + when "Trilogy" + conn.raw_connection.next_result while conn.raw_connection.more_results_exist? + else + conn.raw_connection.abandon_results! + end end assert_difference "TrafficLight.count" do @@ -207,7 +238,12 @@ def test_bulk_insert_with_multi_statements_disabled assert_raises(ActiveRecord::StatementInvalid) do conn = ActiveRecord::Base.connection conn.execute("SELECT 1; SELECT 2;") - conn.raw_connection.abandon_results! + case orig_connection_class::ADAPTER_NAME + when "Trilogy" + conn.raw_connection.next_result while conn.raw_connection.more_results_exist? + else + conn.raw_connection.abandon_results! + end end end end diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb index 51a320e048d32..02450364bdf3c 100644 --- a/activerecord/test/cases/invalid_connection_test.rb +++ b/activerecord/test/cases/invalid_connection_test.rb @@ -3,7 +3,7 @@ require "cases/helper" class TestAdapterWithInvalidConnection < ActiveRecord::TestCase - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) self.use_transactional_tests = false class Bird < ActiveRecord::Base diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index f97727e590feb..11507518d0296 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -478,7 +478,7 @@ def test_migrations_can_handle_foreign_keys_to_specific_tables end # MySQL 5.7 and Oracle do not allow to create duplicate indexes on the same columns - unless current_adapter?(:Mysql2Adapter, :OracleAdapter) + unless current_adapter?(:Mysql2Adapter, :TrilogyAdapter, :OracleAdapter) def test_migrate_revert_add_index_with_name RevertNamedIndexMigration1.new.migrate(:up) RevertNamedIndexMigration2.new.migrate(:up) diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 2f047d638e8df..6e50f481d0782 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -52,7 +52,7 @@ def test_create_table_with_not_null_column def test_create_table_with_defaults # MySQL doesn't allow defaults on TEXT or BLOB columns. - mysql = current_adapter?(:Mysql2Adapter) + mysql = current_adapter?(:Mysql2Adapter, :TrilogyAdapter) connection.create_table :testings do |t| t.column :one, :string, default: "hello" @@ -143,7 +143,7 @@ def test_create_table_with_limits assert_equal "smallint", one.sql_type assert_equal "integer", four.sql_type assert_equal "bigint", eight.sql_type - elsif current_adapter?(:Mysql2Adapter) + elsif current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_match %r/\Aint/, default.sql_type assert_match %r/\Atinyint/, one.sql_type assert_match %r/\Aint/, four.sql_type @@ -281,7 +281,7 @@ def test_add_column_with_timestamp_type if current_adapter?(:PostgreSQLAdapter) assert_equal "timestamp without time zone", column.sql_type - elsif current_adapter?(:Mysql2Adapter) + elsif current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_equal "timestamp", column.sql_type elsif current_adapter?(:OracleAdapter) assert_equal "TIMESTAMP(6)", column.sql_type @@ -301,7 +301,7 @@ def test_add_column_with_postgresql_datetime_type if current_adapter?(:PostgreSQLAdapter) assert_equal "timestamp(6) without time zone", column.sql_type - elsif current_adapter?(:Mysql2Adapter) + elsif current_adapter?(:Mysql2Adapter, :TrilogyAdapter) sql_type = supports_datetime_with_precision? ? "datetime(6)" : "datetime" assert_equal sql_type, column.sql_type else @@ -337,7 +337,7 @@ def test_change_column_with_timestamp_type if current_adapter?(:PostgreSQLAdapter) assert_equal "timestamp without time zone", column.sql_type - elsif current_adapter?(:Mysql2Adapter) + elsif current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_equal "timestamp", column.sql_type elsif current_adapter?(:OracleAdapter) assert_equal "TIMESTAMP(6)", column.sql_type @@ -518,7 +518,7 @@ class ChangeSchemaWithDependentObjectsTest < ActiveRecord::TestCase end def test_create_table_with_force_cascade_drops_dependent_objects - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" elsif current_adapter?(:SQLite3Adapter) skip "SQLite3 does not support DROP TABLE CASCADE syntax" diff --git a/activerecord/test/cases/migration/check_constraint_test.rb b/activerecord/test/cases/migration/check_constraint_test.rb index 5ac1500990208..4bbf4ffce8a82 100644 --- a/activerecord/test/cases/migration/check_constraint_test.rb +++ b/activerecord/test/cases/migration/check_constraint_test.rb @@ -48,7 +48,7 @@ def test_check_constraints assert_equal "products", constraint.table_name assert_equal "products_price_check", constraint.name - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_equal "`price` > `discounted_price`", constraint.expression else assert_equal "price > discounted_price", constraint.expression @@ -116,7 +116,7 @@ def test_add_check_constraint assert_equal "trades", constraint.table_name assert_equal "chk_rails_2189e9f96c", constraint.name - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_equal "`quantity` > 0", constraint.expression else assert_equal "quantity > 0", constraint.expression @@ -246,7 +246,7 @@ def test_remove_check_constraint assert_equal "trades", constraint.table_name assert_equal "price_check", constraint.name - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_equal "`price` > 0", constraint.expression else assert_equal "price > 0", constraint.expression diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index b6064500ee04d..d52c143cd9c0f 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -39,13 +39,13 @@ def test_add_remove_single_field_using_symbol_arguments def test_add_column_without_limit # TODO: limit: nil should work with all adapters. - skip "MySQL wrongly enforces a limit of 255" if current_adapter?(:Mysql2Adapter) + skip "MySQL wrongly enforces a limit of 255" if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) add_column :test_models, :description, :string, limit: nil TestModel.reset_column_information assert_nil TestModel.columns_hash["description"].limit end - if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter, :PostgreSQLAdapter) def test_unabstracted_database_dependent_types add_column :test_models, :intelligence_quotient, :smallint TestModel.reset_column_information @@ -174,7 +174,7 @@ def test_native_types end end - if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter, :PostgreSQLAdapter) def test_out_of_range_limit_should_raise assert_raise(ArgumentError) { add_column :test_models, :integer_too_big, :integer, limit: 10 } assert_raise(ArgumentError) { add_column :test_models, :text_too_big, :text, limit: 0xfffffffff } diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb index 1c62a68cf9cbd..5bec98860acf9 100644 --- a/activerecord/test/cases/migration/column_positioning_test.rb +++ b/activerecord/test/cases/migration/column_positioning_test.rb @@ -25,7 +25,7 @@ def setup ActiveRecord::Base.primary_key_prefix_type = nil end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) def test_column_positioning assert_equal %w(first second third), conn.columns(:testings).map(&:name) end diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 4cd88754ef728..784c95d3a0845 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -64,7 +64,7 @@ def test_rename_column_preserves_default_value_not_null assert_equal "70000", default_after end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) def test_mysql_rename_column_preserves_auto_increment rename_column "test_models", "id", "id_test" assert_predicate connection.columns("test_models").find { |c| c.name == "id_test" }, :auto_increment? @@ -136,7 +136,7 @@ def test_remove_column_with_index def test_remove_column_with_multi_column_index # MariaDB starting with 10.2.8 # Dropping a column that is part of a multi-column UNIQUE constraint is not permitted. - skip if current_adapter?(:Mysql2Adapter) && connection.mariadb? && connection.database_version >= "10.2.8" + skip if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) && connection.mariadb? && connection.database_version >= "10.2.8" add_column "test_models", :hat_size, :integer add_column "test_models", :hat_style, :string, limit: 100 diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 5ab2b0d470671..f20168d404721 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -646,7 +646,7 @@ def migrate(x) end }.new - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) # MySQL does not allow to create table names longer than limit error = assert_raises(StandardError) do ActiveRecord::Migrator.new(:up, [migration], @schema_migration, @internal_metadata).migrate @@ -676,7 +676,7 @@ def migrate(x) end }.new - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) # MySQL does not allow to create table names longer than limit error = assert_raises(StandardError) do ActiveRecord::Migrator.new(:up, [migration], @schema_migration, @internal_metadata).migrate @@ -758,7 +758,7 @@ def up end end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) def test_change_column_on_7_0 migration = Class.new(ActiveRecord::Migration[7.0]) do def up @@ -774,7 +774,7 @@ def up private def precision_implicit_default - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) { precision: 0 } else { precision: nil } @@ -1054,7 +1054,7 @@ def change assert_match %r{bigint "banana_id", null: false}, schema end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) def test_legacy_bigint_primary_key_should_be_auto_incremented @migration = Class.new(migration_class) { def change @@ -1101,7 +1101,7 @@ def assert_legacy_primary_key assert_not_predicate legacy_pk, :bigint? assert_not legacy_pk.null - if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter, :PostgreSQLAdapter) schema = dump_table_schema "legacy_primary_keys" assert_match %r{create_table "legacy_primary_keys", id: :(?:integer|serial), (?!default: nil)}, schema end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index cd64df50606f5..15817382c2ea5 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -93,7 +93,7 @@ def test_rename_column_of_child_table end def test_rename_reference_column_of_child_table - if current_adapter?(:Mysql2Adapter) && !@connection.send(:supports_rename_index?) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) && !@connection.send(:supports_rename_index?) skip "Cannot drop index, needed in a foreign key constraint" end @@ -271,7 +271,7 @@ def test_add_on_delete_restrict_foreign_key assert_equal 1, foreign_keys.size fk = foreign_keys.first - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) # ON DELETE RESTRICT is the default on MySQL assert_nil fk.on_delete else @@ -748,7 +748,7 @@ def test_add_foreign_key_with_if_not_exists_not_set @connection.add_foreign_key :astronauts, :rockets end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) if ActiveRecord::Base.connection.mariadb? assert_match(/Duplicate key on write or update/, error.message) elsif ActiveRecord::Base.connection.database_version < "5.6" diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index 0733140a69f1b..e23665d5fcd69 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -255,7 +255,7 @@ def test_add_index connection.remove_index("testings", name: "named_admin") # Selected adapters support index sort order - if current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter) + if current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :TrilogyAdapter, :PostgreSQLAdapter) connection.add_index("testings", ["last_name"], order: { last_name: :desc }) connection.remove_index("testings", ["last_name"]) connection.add_index("testings", ["last_name", "first_name"], order: { last_name: :desc }) diff --git a/activerecord/test/cases/migration/invalid_options_test.rb b/activerecord/test/cases/migration/invalid_options_test.rb index 1f509ff549a92..2ae37887d5bb3 100644 --- a/activerecord/test/cases/migration/invalid_options_test.rb +++ b/activerecord/test/cases/migration/invalid_options_test.rb @@ -10,7 +10,7 @@ class InvalidOptionsTest < ActiveRecord::TestCase def invalid_add_column_option_exception_message(key) default_keys = [":limit", ":precision", ":scale", ":default", ":null", ":collation", ":comment", ":primary_key", ":if_exists", ":if_not_exists"] - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) default_keys.concat([":auto_increment", ":charset", ":as", ":size", ":unsigned", ":first", ":after", ":type", ":stored"]) elsif current_adapter?(:PostgreSQLAdapter) default_keys.concat([":array", ":using", ":cast_as", ":as", ":type", ":enum_type", ":stored"]) @@ -27,7 +27,7 @@ def invalid_create_table_option_exception_message(key) table_keys = [":temporary", ":if_not_exists", ":options", ":as", ":comment", ":charset", ":collation"] primary_keys = [":limit", ":default", ":precision"] - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) primary_keys.concat([":unsigned"]) elsif current_adapter?(:SQLite3Adapter) table_keys.concat([":rename"]) @@ -95,7 +95,7 @@ def test_add_index_with_invalid_options ) end - if current_adapter?(:Mysql2Adapter) || current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter, :PostgreSQLAdapter) def test_change_column_with_invalid_options exception = assert_raises(ArgumentError) do change_column "posts", "title", :text, liimit: true diff --git a/activerecord/test/cases/migration/schema_definitions_test.rb b/activerecord/test/cases/migration/schema_definitions_test.rb index dee7d508eb443..57ee9fabdbc7f 100644 --- a/activerecord/test/cases/migration/schema_definitions_test.rb +++ b/activerecord/test/cases/migration/schema_definitions_test.rb @@ -63,7 +63,7 @@ def test_build_create_index_definition connection.drop_table(:test) if connection.table_exists?(:test) end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) def test_build_create_index_definition_for_existing_index connection.create_table(:test) do |t| t.column :foo, :string diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index f2b6af005b66b..971acf15b2e8d 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -284,7 +284,7 @@ def migrate(x) migrator.migrate end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) if ActiveRecord::Base.connection.mariadb? assert_match(/Can't DROP COLUMN `last_name`; check that it exists/, error.message) else @@ -958,7 +958,7 @@ def test_decimal_scale_without_precision_should_raise Person.connection.drop_table :test_decimal_scales, if_exists: true end - if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter, :PostgreSQLAdapter) def test_out_of_range_integer_limit_should_raise e = assert_raise(ArgumentError) do Person.connection.create_table :test_integer_limits, force: true do |t| @@ -996,7 +996,7 @@ def test_out_of_range_binary_limit_should_raise end end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) def test_invalid_text_size_should_raise e = assert_raise(ArgumentError) do Person.connection.create_table :test_text_sizes, force: true do |t| @@ -1232,6 +1232,7 @@ def test_adding_multiple_columns classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] expected_query_count = { "Mysql2Adapter" => 1, + "TrilogyAdapter" => 1, "PostgreSQLAdapter" => 2, # one for bulk change, one for comment }.fetch(classname) { raise "need an expected query count for #{classname}" @@ -1334,6 +1335,7 @@ def test_adding_indexes classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] expected_query_count = { "Mysql2Adapter" => 1, # mysql2 supports creating two indexes using one statement + "TrilogyAdapter" => 1, # trilogy supports creating two indexes using one statement "PostgreSQLAdapter" => 3, }.fetch(classname) { raise "need an expected query count for #{classname}" @@ -1367,6 +1369,7 @@ def test_removing_index classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] expected_query_count = { "Mysql2Adapter" => 1, # mysql2 supports dropping and creating two indexes using one statement + "TrilogyAdapter" => 1, # trilogy supports dropping and creating two indexes using one statement "PostgreSQLAdapter" => 2, }.fetch(classname) { raise "need an expected query count for #{classname}" @@ -1397,6 +1400,7 @@ def test_changing_columns classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] expected_query_count = { "Mysql2Adapter" => 3, # one query for columns, one query for primary key, one query to do the bulk change + "TrilogyAdapter" => 3, # one query for columns, one query for primary key, one query to do the bulk change "PostgreSQLAdapter" => 3, # one query for columns, one for bulk change, one for comment }.fetch(classname) { raise "need an expected query count for #{classname}" @@ -1427,6 +1431,7 @@ def test_changing_column_null_with_default classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] expected_query_count = { "Mysql2Adapter" => 7, # four queries to retrieve schema info, one for bulk change, one for UPDATE, one for NOT NULL + "TrilogyAdapter" => 7, # four queries to retrieve schema info, one for bulk change, one for UPDATE, one for NOT NULL "PostgreSQLAdapter" => 5, # two queries for columns, one for bulk change, one for UPDATE, one for NOT NULL }.fetch(classname) { raise "need an expected query count for #{classname}" @@ -1471,7 +1476,7 @@ def test_default_functions_on_columns end end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) def test_updating_auto_increment with_bulk_change_table do |t| t.change :id, :bigint, auto_increment: true @@ -1498,6 +1503,7 @@ def test_changing_index classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] expected_query_count = { "Mysql2Adapter" => 1, # mysql2 supports dropping and creating two indexes using one statement + "TrilogyAdapter" => 1, # trilogy supports dropping and creating two indexes using one statement "PostgreSQLAdapter" => 2, }.fetch(classname) { raise "need an expected query count for #{classname}" diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 03851abc2e5da..09e4061391725 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -342,7 +342,7 @@ def test_any_type_primary_key assert_no_match %r{t\.index \["code"\]}, schema end - if current_adapter?(:Mysql2Adapter) && supports_datetime_with_precision? + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) && supports_datetime_with_precision? test "schema typed primary key column" do @connection.create_table(:scheduled_logs, id: :timestamp, precision: 6, force: true) schema = dump_table_schema("scheduled_logs") @@ -483,7 +483,7 @@ def test_schema_dump_primary_key_bigint_with_default_nil end class PrimaryKeyIntegerTest < ActiveRecord::TestCase - if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) + if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter, :TrilogyAdapter) include SchemaDumpingHelper self.use_transactional_tests = false @@ -519,7 +519,7 @@ class Widget < ActiveRecord::Base assert_match %r{create_table "widgets", id: :#{@pk_type}, }, schema end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) test "primary key column type with options" do @connection.create_table(:widgets, id: :primary_key, limit: 4, unsigned: true, force: true) column = @connection.columns(:widgets).find { |c| c.name == "id" } diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 9b72b7c402859..6d9cc8d68e432 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -207,7 +207,7 @@ def test_type_cast_symbol def test_type_cast_date date = Date.today - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) expected = date else expected = @conn.quoted_date(date) @@ -217,7 +217,7 @@ def test_type_cast_date def test_type_cast_time time = Time.now - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) expected = time else expected = @conn.quoted_date(time) diff --git a/activerecord/test/cases/relation/delete_all_test.rb b/activerecord/test/cases/relation/delete_all_test.rb index 022a2d10be849..e449a967feeb2 100644 --- a/activerecord/test/cases/relation/delete_all_test.rb +++ b/activerecord/test/cases/relation/delete_all_test.rb @@ -81,7 +81,7 @@ def test_delete_all_with_joins_and_where_part_is_hash assert_equal pets.count, pets.delete_all end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_no_match %r/SELECT DISTINCT #{Regexp.escape(Pet.connection.quote_table_name("pets.pet_id"))}/, sqls.last else assert_match %r/SELECT #{Regexp.escape(Pet.connection.quote_table_name("pets.pet_id"))}/, sqls.last diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index bd5e200149651..751748d6db68b 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -168,7 +168,7 @@ def test_merge_doesnt_duplicate_same_clauses only_david = Author.where("#{author_id} IN (?)", david) - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_sql(/WHERE \(#{Regexp.escape(author_id)} IN \('1'\)\)\z/) do assert_equal [david], only_david.merge(only_david) end diff --git a/activerecord/test/cases/relation/update_all_test.rb b/activerecord/test/cases/relation/update_all_test.rb index 583c3090a3ae7..f1db13a190d7d 100644 --- a/activerecord/test/cases/relation/update_all_test.rb +++ b/activerecord/test/cases/relation/update_all_test.rb @@ -66,7 +66,7 @@ def test_update_all_with_joins assert_equal pets.count, pets.update_all(name: "Bob") end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_no_match %r/SELECT DISTINCT #{Regexp.escape(Pet.connection.quote_table_name("pets.pet_id"))}/, sqls.last else assert_match %r/SELECT #{Regexp.escape(Pet.connection.quote_table_name("pets.pet_id"))}/, sqls.last diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index c77b0786f4ad6..8b210df44b6e6 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -475,7 +475,7 @@ def test_finding_with_complex_order def test_finding_with_sanitized_order query = Tag.order([Arel.sql("field(id, ?)"), [1, 3, 2]]).to_sql - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_match(/field\(id, '1','3','2'\)/, query) else assert_match(/field\(id, 1,3,2\)/, query) @@ -490,7 +490,7 @@ def test_finding_with_sanitized_order def test_finding_with_arel_sql_order query = Tag.order(Arel.sql("field(id, ?)", [1, 3, 2])).to_sql - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_match(/field\(id, '1', '3', '2'\)/, query) else assert_match(/field\(id, 1, 3, 2\)/, query) diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 1075e8b1c0e2f..cb32141c9f790 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -31,7 +31,7 @@ def test_sanitize_sql_array_handles_bind_variables def test_sanitize_sql_array_handles_named_bind_variables quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=:name", name: "Bambi"]) - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_equal "name=#{quoted_bambi} AND id='1'", Binary.sanitize_sql_array(["name=:name AND id=:id", name: "Bambi", id: 1]) else assert_equal "name=#{quoted_bambi} AND id=1", Binary.sanitize_sql_array(["name=:name AND id=:id", name: "Bambi", id: 1]) @@ -118,7 +118,7 @@ def test_bind_arity end def test_named_bind_variables - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_equal "'1'", bind(":a", a: 1) # ' ruby-mode assert_equal "'1' '1'", bind(":a :a", a: 1) # ' ruby-mode else @@ -150,28 +150,28 @@ def each(&b) def test_bind_enumerable quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')}) - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_equal "'1','2','3'", bind("?", [1, 2, 3]) else assert_equal "1,2,3", bind("?", [1, 2, 3]) end assert_equal quoted_abc, bind("?", %w(a b c)) - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_equal "'1','2','3'", bind(":a", a: [1, 2, 3]) else assert_equal "1,2,3", bind(":a", a: [1, 2, 3]) end assert_equal quoted_abc, bind(":a", a: %w(a b c)) # ' - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_equal "'1','2','3'", bind("?", SimpleEnumerable.new([1, 2, 3])) else assert_equal "1,2,3", bind("?", SimpleEnumerable.new([1, 2, 3])) end assert_equal quoted_abc, bind("?", SimpleEnumerable.new(%w(a b c))) - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_equal "'1','2','3'", bind(":a", a: SimpleEnumerable.new([1, 2, 3])) else assert_equal "1,2,3", bind(":a", a: SimpleEnumerable.new([1, 2, 3])) @@ -188,7 +188,7 @@ def test_bind_empty_enumerable def test_bind_range quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')}) - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_equal "'0'", bind("?", 0..0) assert_equal "'1','2','3'", bind("?", 1..3) else diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 945ed2401fee5..5998ecbb258af 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -125,7 +125,7 @@ def test_schema_dump_includes_limit_constraint_for_integer_columns # int 3 is 4 bytes in postgresql assert_match %r{"c_int_3"(?!.*limit)}, output assert_match %r{"c_int_4"(?!.*limit)}, output - elsif current_adapter?(:Mysql2Adapter) + elsif current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_match %r{c_int_1.*limit: 1}, output assert_match %r{c_int_2.*limit: 2}, output assert_match %r{c_int_3.*limit: 3}, output @@ -169,7 +169,7 @@ def test_schema_dump_with_regexp_ignored_table def test_schema_dumps_index_columns_in_right_order index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_index/).first.strip - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) if ActiveRecord::Base.connection.supports_index_sort_order? assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }, order: { rating: :desc }', index_definition else @@ -202,7 +202,7 @@ def test_schema_dumps_index_sort_order def test_schema_dumps_index_length index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*_name_and_description/).first.strip - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_equal 't.index ["name", "description"], name: "index_companies_on_name_and_description", length: 10', index_definition else assert_equal 't.index ["name", "description"], name: "index_companies_on_name_and_description"', index_definition @@ -212,7 +212,7 @@ def test_schema_dumps_index_length if ActiveRecord::Base.connection.supports_check_constraints? def test_schema_dumps_check_constraints constraint_definition = dump_table_schema("products").split(/\n/).grep(/t.check_constraint.*products_price_check/).first.strip - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_equal 't.check_constraint "`price` > `discounted_price`", name: "products_price_check"', constraint_definition else assert_equal 't.check_constraint "price > discounted_price", name: "products_price_check"', constraint_definition @@ -291,7 +291,7 @@ def test_schema_dump_expression_indices if current_adapter?(:PostgreSQLAdapter) assert_match %r{CASE.+lower\(\(name\)::text\).+END\) DESC"\z}i, index_definition - elsif current_adapter?(:Mysql2Adapter) + elsif current_adapter?(:Mysql2Adapter, :TrilogyAdapter) assert_match %r{CASE.+lower\(`name`\).+END\) DESC"\z}i, index_definition elsif current_adapter?(:SQLite3Adapter) assert_match %r{CASE.+lower\(name\).+END\) DESC"\z}i, index_definition @@ -301,7 +301,7 @@ def test_schema_dump_expression_indices end end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) def test_schema_dump_includes_length_for_mysql_binary_fields output = dump_table_schema "binary_fields" assert_match %r{t\.binary\s+"var_binary",\s+limit: 255$}, output diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index c2869f806ee5e..8ef49bd47ee6d 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -358,7 +358,7 @@ def test_newly_emptied_serialized_hash_is_changed assert_equal({}, topic.content) end - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) def test_is_not_changed_when_stored_in_mysql_blob value = %w(Fée) model = BinaryField.create!(normal_blob: value, normal_text: value) diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index 0f4b94c01fc20..ab095fd9b2d2a 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -52,6 +52,7 @@ def assert_called_for_configs(method_name, configs, &block) ADAPTERS_TASKS = { mysql2: :mysql_tasks, + trilogy: :mysql_tasks, postgresql: :postgresql_tasks, sqlite3: :sqlite_tasks } diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index d75bccd2cdcd8..518e13aeac6c8 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -255,7 +255,7 @@ def self.run(*args) class AbstractMysqlTestCase < TestCase def self.run(*args) - super if current_adapter?(:Mysql2Adapter) + super if current_adapter?(:Mysql2Adapter) || current_adapter?(:TrilogyAdapter) end end @@ -265,6 +265,13 @@ def self.run(*args) end end + class TrilogyTestCase < TestCase + def self.run(*args) + super if current_adapter?(:TrilogyAdapter) + end + end + + class SQLite3TestCase < TestCase def self.run(*args) super if current_adapter?(:SQLite3Adapter) diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index e3c4e1e78558b..8e7298dd336d4 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -45,7 +45,7 @@ def test_time_precision_is_truncated_on_assignment assert_equal 123456000, foo.finish.nsec end - unless current_adapter?(:Mysql2Adapter) + unless current_adapter?(:Mysql2Adapter, :TrilogyAdapter) def test_no_time_precision_isnt_truncated_on_assignment @connection.create_table(:foos, force: true) @connection.add_column :foos, :start, :time diff --git a/activerecord/test/cases/unsafe_raw_sql_test.rb b/activerecord/test/cases/unsafe_raw_sql_test.rb index 46e3c640db0ee..8367fb9ba96ac 100644 --- a/activerecord/test/cases/unsafe_raw_sql_test.rb +++ b/activerecord/test/cases/unsafe_raw_sql_test.rb @@ -170,6 +170,7 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase collation_name = { "PostgreSQL" => "C", "Mysql2" => "utf8mb4_bin", + "Trilogy" => "utf8mb4_bin", "SQLite" => "binary" }[ActiveRecord::Base.connection.adapter_name] diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 88d1909f70c17..118c66320004b 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -359,7 +359,7 @@ def test_validate_uniqueness_by_default_database_collation assert_not topic1.valid? assert_not topic1.save - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) # Case insensitive collation (utf8mb4_0900_ai_ci) by default. # Should not allow "David" if "david" exists. assert_not topic2.valid? @@ -440,7 +440,7 @@ def test_validate_uniqueness_with_limit e2 = Event.create(title: "abcdefgh") assert_not e2.valid?, "Created an event whose title is not unique" - elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter) + elsif current_adapter?(:Mysql2Adapter, :TrilogyAdapter, :PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter) assert_raise(ActiveRecord::ValueTooLong) do Event.create(title: "abcdefgh") end @@ -459,7 +459,7 @@ def test_validate_uniqueness_with_limit_and_utf8 e2 = Event.create(title: "一二三四五六七八") assert_not e2.valid?, "Created an event whose title is not unique" - elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter) + elsif current_adapter?(:Mysql2Adapter, :TrilogyAdapter, :PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter) assert_raise(ActiveRecord::ValueTooLong) do Event.create(title: "一二三四五六七八") end diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index 4a1afd1199c72..33a6980521f52 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -158,7 +158,7 @@ def test_does_not_dump_view_as_table class UpdateableViewTest < ActiveRecord::TestCase # SQLite does not support CREATE, INSERT, and DELETE for VIEW - if current_adapter?(:Mysql2Adapter, :SQLServerAdapter, :PostgreSQLAdapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter, :SQLServerAdapter, :PostgreSQLAdapter) self.use_transactional_tests = false fixtures :books @@ -202,7 +202,7 @@ def test_update_record_to_fail_view_conditions book.reload end end - end # end of `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter)` + end # end of `if current_adapter?(:Mysql2Adapter, :TrilogyAdapter, :PostgreSQLAdapter, :SQLServerAdapter)` end end # end of `if ActiveRecord::Base.connection.supports_views?` diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml index 09aeb917f8b00..f290a0b2eedc9 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -1,5 +1,37 @@ default_connection: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %> +mysql: &mysql + arunit: + username: rails + encoding: utf8mb4 + collation: utf8mb4_unicode_ci + <% if ENV['MYSQL_PREPARED_STATEMENTS'] %> + prepared_statements: true + <% else %> + prepared_statements: false + <% end %> + <% if ENV['MYSQL_HOST'] %> + host: <%= ENV['MYSQL_HOST'] %> + <% end %> + <% if ENV['MYSQL_SOCK'] %> + socket: "<%= ENV['MYSQL_SOCK'] %>" + <% end %> + arunit2: + username: rails + encoding: utf8mb4 + collation: utf8mb4_general_ci + <% if ENV['MYSQL_PREPARED_STATEMENTS'] %> + prepared_statements: true + <% else %> + prepared_statements: false + <% end %> + <% if ENV['MYSQL_HOST'] %> + host: <%= ENV['MYSQL_HOST'] %> + <% end %> + <% if ENV['MYSQL_SOCK'] %> + socket: "<%= ENV['MYSQL_SOCK'] %>" + <% end %> + connections: jdbcderby: arunit: activerecord_unittest @@ -36,36 +68,7 @@ connections: timeout: 5000 mysql2: - arunit: - username: rails - encoding: utf8mb4 - collation: utf8mb4_unicode_ci -<% if ENV['MYSQL_PREPARED_STATEMENTS'] %> - prepared_statements: true -<% else %> - prepared_statements: false -<% end %> -<% if ENV['MYSQL_HOST'] %> - host: <%= ENV['MYSQL_HOST'] %> -<% end %> -<% if ENV['MYSQL_SOCK'] %> - socket: "<%= ENV['MYSQL_SOCK'] %>" -<% end %> - arunit2: - username: rails - encoding: utf8mb4 - collation: utf8mb4_general_ci -<% if ENV['MYSQL_PREPARED_STATEMENTS'] %> - prepared_statements: true -<% else %> - prepared_statements: false -<% end %> -<% if ENV['MYSQL_HOST'] %> - host: <%= ENV['MYSQL_HOST'] %> -<% end %> -<% if ENV['MYSQL_SOCK'] %> - socket: "<%= ENV['MYSQL_SOCK'] %>" -<% end %> + <<: *mysql oracle: arunit: @@ -107,3 +110,6 @@ connections: arunit2: adapter: sqlite3 database: ':memory:' + + trilogy: + <<: *mysql diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 7e6ae87e5ad51..0ba4e10fbd400 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -205,7 +205,7 @@ create_table :carriers, force: true create_table :carts, force: true, primary_key: [:shop_id, :id] do |t| - if ActiveRecord::TestCase.current_adapter?(:Mysql2Adapter) + if ActiveRecord::TestCase.current_adapter?(:Mysql2Adapter, :TrilogyAdapter) t.bigint :id, index: true, auto_increment: true, null: false else t.bigint :id, index: true, null: false diff --git a/activerecord/test/support/adapter_helper.rb b/activerecord/test/support/adapter_helper.rb index b09053011fd0d..2396df12b5129 100644 --- a/activerecord/test/support/adapter_helper.rb +++ b/activerecord/test/support/adapter_helper.rb @@ -14,20 +14,20 @@ def in_memory_db? end def mysql_enforcing_gtid_consistency? - current_adapter?(:Mysql2Adapter) && "ON" == ActiveRecord::Base.connection.show_variable("enforce_gtid_consistency") + current_adapter?(:Mysql2Adapter, :TrilogyAdapter) && "ON" == ActiveRecord::Base.connection.show_variable("enforce_gtid_consistency") end def supports_default_expression? if current_adapter?(:PostgreSQLAdapter) true - elsif current_adapter?(:Mysql2Adapter) + elsif current_adapter?(:Mysql2Adapter, :TrilogyAdapter) conn = ActiveRecord::Base.connection !conn.mariadb? && conn.database_version >= "8.0.13" end end def supports_non_unique_constraint_name? - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) conn = ActiveRecord::Base.connection conn.mariadb? else @@ -36,7 +36,7 @@ def supports_non_unique_constraint_name? end def supports_text_column_with_default? - if current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter, :TrilogyAdapter) conn = ActiveRecord::Base.connection conn.mariadb? && conn.database_version >= "10.2.1" else diff --git a/activerecord/test/support/marshal_compatibility_fixtures/Trilogy/rails_6_1_topic.dump b/activerecord/test/support/marshal_compatibility_fixtures/Trilogy/rails_6_1_topic.dump new file mode 100644 index 0000000000000..7803d413e73fc Binary files /dev/null and b/activerecord/test/support/marshal_compatibility_fixtures/Trilogy/rails_6_1_topic.dump differ diff --git a/activerecord/test/support/marshal_compatibility_fixtures/Trilogy/rails_6_1_topic_associations.dump b/activerecord/test/support/marshal_compatibility_fixtures/Trilogy/rails_6_1_topic_associations.dump new file mode 100644 index 0000000000000..e9f49fd9a310d Binary files /dev/null and b/activerecord/test/support/marshal_compatibility_fixtures/Trilogy/rails_6_1_topic_associations.dump differ diff --git a/activerecord/test/support/marshal_compatibility_fixtures/Trilogy/rails_7_1_topic.dump b/activerecord/test/support/marshal_compatibility_fixtures/Trilogy/rails_7_1_topic.dump new file mode 100644 index 0000000000000..7eae10a1b9f9f Binary files /dev/null and b/activerecord/test/support/marshal_compatibility_fixtures/Trilogy/rails_7_1_topic.dump differ diff --git a/activerecord/test/support/marshal_compatibility_fixtures/Trilogy/rails_7_1_topic_associations.dump b/activerecord/test/support/marshal_compatibility_fixtures/Trilogy/rails_7_1_topic_associations.dump new file mode 100644 index 0000000000000..c483498306ba6 Binary files /dev/null and b/activerecord/test/support/marshal_compatibility_fixtures/Trilogy/rails_7_1_topic_associations.dump differ diff --git a/railties/lib/rails/generators/database.rb b/railties/lib/rails/generators/database.rb index 2dbdd7d09384c..c1f80591aabea 100644 --- a/railties/lib/rails/generators/database.rb +++ b/railties/lib/rails/generators/database.rb @@ -4,7 +4,7 @@ module Rails module Generators module Database # :nodoc: JDBC_DATABASES = %w( jdbcmysql jdbcsqlite3 jdbcpostgresql jdbc ) - DATABASES = %w( mysql postgresql sqlite3 oracle sqlserver ) + JDBC_DATABASES + DATABASES = %w( mysql trilogy postgresql sqlite3 oracle sqlserver ) + JDBC_DATABASES def initialize(*) super @@ -14,6 +14,7 @@ def initialize(*) def gem_for_database(database = options[:database]) case database when "mysql" then ["mysql2", ["~> 0.5"]] + when "trilogy" then ["trilogy", ["~> 2.4"]] when "postgresql" then ["pg", ["~> 1.1"]] when "sqlite3" then ["sqlite3", ["~> 1.4"]] when "oracle" then ["activerecord-oracle_enhanced-adapter", nil] diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/trilogy.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/trilogy.yml.tt new file mode 100644 index 0000000000000..a22ccca2acbfe --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/trilogy.yml.tt @@ -0,0 +1,59 @@ +# MySQL. Versions 5.5.8 and up are supported. +# +# Install the MySQL driver +# gem install trilogy +# +# Ensure the MySQL gem is defined in your Gemfile +# gem "trilogy" +# +# And be sure to use new-style password hashing: +# https://dev.mysql.com/doc/refman/5.7/en/password-hashing.html +# +default: &default + adapter: trilogy + encoding: utf8mb4 + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + username: root + password: +<% if mysql_socket -%> + socket: <%= mysql_socket %> +<% else -%> + host: localhost +<% end -%> + +development: + <<: *default + database: <%= app_name %>_development + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: <%= app_name %>_test + +# As with config/credentials.yml, you never want to store sensitive information, +# like your database password, in your source code. If your source code is +# ever seen by anyone, they now have access to your database. +# +# Instead, provide the password or a full connection URL as an environment +# variable when you boot the app. For example: +# +# DATABASE_URL="trilogy://myuser:mypass@localhost/somedatabase" +# +# If the connection URL is provided in the special DATABASE_URL environment +# variable, Rails will automatically merge its configuration values on top of +# the values provided in this file. Alternatively, you can specify a connection +# URL environment variable explicitly: +# +# production: +# url: <%%= ENV["MY_APP_DATABASE_URL"] %> +# +# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database +# for a full overview on how database connection configuration can be specified. +# +production: + <<: *default + database: <%= app_name %>_production + username: <%= app_name %> + password: <%%= ENV["<%= app_name.upcase %>_DATABASE_PASSWORD"] %> diff --git a/railties/test/commands/db_system_change_test.rb b/railties/test/commands/db_system_change_test.rb index 273203653b2c0..55caddb85b23c 100644 --- a/railties/test/commands/db_system_change_test.rb +++ b/railties/test/commands/db_system_change_test.rb @@ -25,9 +25,9 @@ class Rails::Command::DbSystemChangeTest < ActiveSupport::TestCase assert_match <<~MSG.squish, output Invalid value for --to option. Supported preconfigurations are: - mysql, postgresql, sqlite3, oracle, - sqlserver, jdbcmysql, jdbcsqlite3, - jdbcpostgresql, jdbc. + mysql, trilogy, postgresql, sqlite3, + oracle, sqlserver, jdbcmysql, + jdbcsqlite3, jdbcpostgresql, jdbc. MSG end diff --git a/railties/test/generators/db_system_change_generator_test.rb b/railties/test/generators/db_system_change_generator_test.rb index e19de3081a844..f664c79e707cb 100644 --- a/railties/test/generators/db_system_change_generator_test.rb +++ b/railties/test/generators/db_system_change_generator_test.rb @@ -25,9 +25,9 @@ class ChangeGeneratorTest < Rails::Generators::TestCase assert_match <<~MSG.squish, output Invalid value for --to option. Supported preconfigurations are: - mysql, postgresql, sqlite3, oracle, - sqlserver, jdbcmysql, jdbcsqlite3, - jdbcpostgresql, jdbc. + mysql, trilogy, postgresql, sqlite3, + oracle, sqlserver, jdbcmysql, + jdbcsqlite3, jdbcpostgresql, jdbc. MSG end