Introduce adapter for Trilogy, a MySQL-compatible DB client

The [Trilogy database client][trilogy-client] and corresponding
[Active Record adapter][ar-adapter] were both open sourced by GitHub last year.

Shopify has recently taken the plunge and successfully adopted Trilogy in their Rails monolith.
With two major Rails applications running Trilogy successfully, we'd like to propose upstreaming the adapter
to Rails as a MySQL-compatible alternative to Mysql2Adapter.

[trilogy-client]: https://github.com/github/trilogy
[ar-adapter]: https://github.com/github/activerecord-trilogy-adapter

Co-authored-by: Aaron Patterson <tenderlove@github.com>
Co-authored-by: Adam Roben <adam@roben.org>
Co-authored-by: Ali Ibrahim <aibrahim2k2@gmail.com>
Co-authored-by: Aman Gupta <aman@tmm1.net>
Co-authored-by: Arthur Nogueira Neves <github@arthurnn.com>
Co-authored-by: Arthur Schreiber <arthurschreiber@github.com>
Co-authored-by: Ashe Connor <kivikakk@github.com>
Co-authored-by: Brandon Keepers <brandon@opensoul.org>
Co-authored-by: Brian Lopez <seniorlopez@gmail.com>
Co-authored-by: Brooke Kuhlmann <brooke@testdouble.com>
Co-authored-by: Bryana Knight <bryanaknight@github.com>
Co-authored-by: Carl Brasic <brasic@github.com>
Co-authored-by: Chris Bloom <chrisbloom7@github.com>
Co-authored-by: Cliff Pruitt <cliff.pruitt@cliffpruitt.com>
Co-authored-by: Daniel Colson <composerinteralia@github.com>
Co-authored-by: David Calavera <david.calavera@gmail.com>
Co-authored-by: David Celis <davidcelis@github.com>
Co-authored-by: David Ratajczak <david@mockra.com>
Co-authored-by: Dirkjan Bussink <d.bussink@gmail.com>
Co-authored-by: Eileen Uchitelle <eileencodes@gmail.com>
Co-authored-by: Enrique Gonzalez <enriikke@gmail.com>
Co-authored-by: Garrett Bjerkhoel <garrett@github.com>
Co-authored-by: Georgi Knox <georgicodes@github.com>
Co-authored-by: HParker <HParker@github.com>
Co-authored-by: Hailey Somerville <hailey@hailey.lol>
Co-authored-by: James Dennes <jdennes@gmail.com>
Co-authored-by: Jane Sternbach <janester@github.com>
Co-authored-by: Jess Bees <toomanybees@github.com>
Co-authored-by: Jesse Toth <jesse.toth@github.com>
Co-authored-by: Joel Hawksley <joelhawksley@github.com>
Co-authored-by: John Barnette <jbarnette@github.com>
Co-authored-by: John Crepezzi <john.crepezzi@gmail.com>
Co-authored-by: John Hawthorn <john@hawthorn.email>
Co-authored-by: John Nunemaker <nunemaker@gmail.com>
Co-authored-by: Jonathan Hoyt <hoyt@github.com>
Co-authored-by: Katrina Owen <kytrinyx@github.com>
Co-authored-by: Keeran Raj Hawoldar <keeran@gmail.com>
Co-authored-by: Kevin Solorio <soloriok@gmail.com>
Co-authored-by: Leo Correa <lcorr005@gmail.com>
Co-authored-by: Lizz Hale <lizzhale@github.com>
Co-authored-by: Lorin Thwaits <lorint@gmail.com>
Co-authored-by: Matt Jones <al2o3cr@gmail.com>
Co-authored-by: Matthew Draper <matthewd@github.com>
Co-authored-by: Max Veytsman <mveytsman@github.com>
Co-authored-by: Nathan Witmer <nathan@zerowidth.com>
Co-authored-by: Nick Holden <nick.r.holden@gmail.com>
Co-authored-by: Paarth Madan <paarth.madan@shopify.com>
Co-authored-by: Patrick Reynolds <patrick.reynolds@github.com>
Co-authored-by: Rob Sanheim <rsanheim@gmail.com>
Co-authored-by: Rocio Delgado <rocio@github.com>
Co-authored-by: Sam Lambert <sam.lambert@github.com>
Co-authored-by: Shay Frendt <shay@github.com>
Co-authored-by: Shlomi Noach <shlomi-noach@github.com>
Co-authored-by: Sophie Haskins <sophaskins@github.com>
Co-authored-by: Thomas Maurer <tma@github.com>
Co-authored-by: Tim Pease <tim.pease@gmail.com>
Co-authored-by: Yossef Mendelssohn <ymendel@pobox.com>
Co-authored-by: Zack Koppert <zkoppert@github.com>
Co-authored-by: Zhongying Qiao <cryptoque@users.noreply.github.com>
This commit is contained in:
Adrianna Chang 2023-03-20 16:17:09 -04:00
parent bd8aeead92
commit 5ed3f60df6
No known key found for this signature in database
GPG Key ID: 6816AFC60CB96DE5
68 changed files with 1735 additions and 161 deletions

@ -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

@ -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)

@ -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

@ -21,6 +21,7 @@ example:
Simply executing <tt>bundle exec rake test</tt> 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

@ -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)

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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")

@ -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

@ -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

@ -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,6 +143,7 @@ def test_mysql_sql_mode_variable_overrides_strict_mode
end
end
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))
@ -152,6 +157,7 @@ def test_passing_flags_by_array_to_adapter
assert_equal ["COMPRESS", "FOUND_ROWS"], ActiveRecord::Base.connection.raw_connection.query_options[:flags]
end
end
end
def test_mysql_set_session_variable
run_without_connection do |orig_connection|

@ -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

@ -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

@ -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)

@ -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

@ -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"]

@ -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

@ -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)

@ -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]

@ -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

@ -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

@ -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

@ -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)

@ -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

@ -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

@ -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|
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,8 +169,13 @@ def test_bulk_insert_with_multi_statements_enabled
assert_nothing_raised do
conn = ActiveRecord::Base.connection
conn.execute("SELECT 1; SELECT 2;")
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
ActiveRecord::Base.transaction 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;")
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|
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,8 +222,13 @@ def test_bulk_insert_with_multi_statements_disabled
assert_raises(ActiveRecord::StatementInvalid) do
conn = ActiveRecord::Base.connection
conn.execute("SELECT 1; SELECT 2;")
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
conn = ActiveRecord::Base.connection
@ -207,10 +238,15 @@ def test_bulk_insert_with_multi_statements_disabled
assert_raises(ActiveRecord::StatementInvalid) do
conn = ActiveRecord::Base.connection
conn.execute("SELECT 1; SELECT 2;")
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_insert_fixtures_set_raises_an_error_when_max_allowed_packet_is_smaller_than_fixtures_set_size
conn = ActiveRecord::Base.connection

@ -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

@ -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)

@ -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"

@ -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

@ -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 }

@ -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

@ -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

@ -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

@ -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"

@ -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 })

@ -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

@ -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

@ -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}"

@ -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" }

@ -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)

@ -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

@ -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

@ -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

@ -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)

@ -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

@ -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

@ -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)

@ -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
}

@ -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)

@ -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

@ -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]

@ -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

@ -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?`

@ -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

@ -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

@ -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

@ -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]

@ -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"] %>

@ -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

@ -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