diff --git a/README.md b/README.md index bd5eef134..8fba8ad71 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ adapters are available: ```yml development: - adapter: mysql2 # or mysql + adapter: mysql2 database: blog_development username: blog password: 1234 @@ -80,7 +80,7 @@ or preferably using the *properties:* syntax: ```yml production: - adapter: mysql + adapter: mysql2 username: blog password: blog url: "jdbc:mysql://localhost:3306/blog?profileSQL=true" diff --git a/lib/arjdbc/abstract/core.rb b/lib/arjdbc/abstract/core.rb index eb0f7f3b7..cccfcbc59 100644 --- a/lib/arjdbc/abstract/core.rb +++ b/lib/arjdbc/abstract/core.rb @@ -60,7 +60,16 @@ def translate_exception(exception, message:, sql:, binds:) # this version of log() automatically fills type_casted_binds from binds if necessary def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil, async: false) if binds.any? && (type_casted_binds.nil? || type_casted_binds.empty?) - type_casted_binds = ->{ binds.map(&:value_for_database) } # extract_raw_bind_values + type_casted_binds = lambda { + # extract_raw_bind_values + binds.map do |bind| + if bind.respond_to?(:value_for_database) + bind.value_for_database + else + bind + end + end + } end super end diff --git a/lib/arjdbc/abstract/transaction_support.rb b/lib/arjdbc/abstract/transaction_support.rb index bed6efce2..d513230c3 100644 --- a/lib/arjdbc/abstract/transaction_support.rb +++ b/lib/arjdbc/abstract/transaction_support.rb @@ -26,7 +26,9 @@ def supports_transaction_isolation? def begin_db_transaction log('BEGIN', 'TRANSACTION') do with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn| - conn.begin + result = conn.begin + verified! + result end end end diff --git a/lib/arjdbc/mysql/adapter_hash_config.rb b/lib/arjdbc/mysql/adapter_hash_config.rb index f1e6dd719..16ac6a896 100644 --- a/lib/arjdbc/mysql/adapter_hash_config.rb +++ b/lib/arjdbc/mysql/adapter_hash_config.rb @@ -7,7 +7,9 @@ def build_connection_config(config) load_jdbc_driver - config[:driver] ||= database_driver_name + # don't set driver if it's explicitly set to false + # allow Java's service discovery mechanism (with connector/j 8.0) + config[:driver] ||= database_driver_name if config[:driver] != false host = (config[:host] ||= "localhost") port = (config[:port] ||= 3306) @@ -40,7 +42,7 @@ def database_driver_name def build_properties(config) properties = config[:properties] || {} - properties["zeroDateTimeBehavior"] ||= "CONVERT_TO_NULL" + properties["zeroDateTimeBehavior"] ||= default_zero_date_time_behavior(config[:driver]) properties["jdbcCompliantTruncation"] ||= false @@ -88,6 +90,14 @@ def build_properties(config) properties end + def default_zero_date_time_behavior(driver) + return "CONVERT_TO_NULL" if driver == false + + return "CONVERT_TO_NULL" if driver.start_with?("com.mysql.cj.") + + "convertToNull" + end + # See https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-charsets.html # to charset-name (characterEncoding=...) def convert_mysql_encoding(config) diff --git a/lib/arjdbc/postgresql/adapter.rb b/lib/arjdbc/postgresql/adapter.rb index ae077ce14..7b01309f1 100644 --- a/lib/arjdbc/postgresql/adapter.rb +++ b/lib/arjdbc/postgresql/adapter.rb @@ -345,7 +345,7 @@ def enum_types type.typname AS name, type.OID AS oid, n.nspname AS schema, - string_agg(enum.enumlabel, ',' ORDER BY enum.enumsortorder) AS value + array_agg(enum.enumlabel ORDER BY enum.enumsortorder) AS value FROM pg_enum AS enum JOIN pg_type AS type ON (type.oid = enum.enumtypid) JOIN pg_namespace n ON type.typnamespace = n.oid @@ -842,6 +842,15 @@ class PostgreSQLAdapter < AbstractAdapter # setting, you should immediately run bin/rails db:migrate to update the types in your schema.rb. class_attribute :datetime_type, default: :timestamp + ## + # :singleton-method: + # Toggles automatic decoding of date columns. + # + # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.select_value("select '2024-01-01'::date").class #=> String + # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.decode_dates = true + # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.select_value("select '2024-01-01'::date").class #=> Date + class_attribute :decode_dates, default: false + # Try to use as much of the built in postgres logic as possible # maybe someday we can extend the actual adapter include ActiveRecord::ConnectionAdapters::PostgreSQL::ReferentialIntegrity @@ -855,9 +864,12 @@ class PostgreSQLAdapter < AbstractAdapter include ArJdbc::Abstract::DatabaseStatements include ArJdbc::Abstract::StatementCache include ArJdbc::Abstract::TransactionSupport - include ArJdbc::PostgreSQL include ArJdbc::PostgreSQLConfig + # NOTE: after AR refactor quote_column_name became class and instance method + include ArJdbc::PostgreSQL + extend ArJdbc::PostgreSQL + require 'arjdbc/postgresql/oid_types' include ::ArJdbc::PostgreSQL::OIDTypes include ::ArJdbc::PostgreSQL::DatabaseStatements diff --git a/lib/arjdbc/postgresql/adapter_hash_config.rb b/lib/arjdbc/postgresql/adapter_hash_config.rb index d59fd6e82..bc042ac1f 100644 --- a/lib/arjdbc/postgresql/adapter_hash_config.rb +++ b/lib/arjdbc/postgresql/adapter_hash_config.rb @@ -13,7 +13,13 @@ def build_connection_config(config) port = (config[:port] ||= ENV["PGPORT"] || 5432) database = config[:database] || config[:dbname] || ENV["PGDATABASE"] - config[:url] ||= "jdbc:postgresql://#{host}:#{port}/#{database}" + app = config[:application_name] || config[:appname] || config[:application] + + config[:url] ||= if app + "jdbc:postgresql://#{host}:#{port}/#{database}?ApplicationName=#{app}" + else + "jdbc:postgresql://#{host}:#{port}/#{database}" + end config[:url] << config[:pg_params] if config[:pg_params] diff --git a/lib/arjdbc/sqlite3/adapter.rb b/lib/arjdbc/sqlite3/adapter.rb index 1852054a3..964d99b9f 100644 --- a/lib/arjdbc/sqlite3/adapter.rb +++ b/lib/arjdbc/sqlite3/adapter.rb @@ -18,6 +18,7 @@ require "active_support/core_ext/class/attribute" require "arjdbc/sqlite3/column" require "arjdbc/sqlite3/adapter_hash_config" +require "arjdbc/sqlite3/pragmas" require "arjdbc/abstract/relation_query_attribute_monkey_patch" @@ -59,6 +60,7 @@ module SQLite3 # DIFFERENCE: Some common constant names to reduce differences in rest of this module from AR5 version ConnectionAdapters = ::ActiveRecord::ConnectionAdapters IndexDefinition = ::ActiveRecord::ConnectionAdapters::IndexDefinition + ForeignKeyDefinition = ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition Quoting = ::ActiveRecord::ConnectionAdapters::SQLite3::Quoting RecordNotUnique = ::ActiveRecord::RecordNotUnique SchemaCreation = ConnectionAdapters::SQLite3::SchemaCreation @@ -79,6 +81,15 @@ module SQLite3 json: { name: "json" }, } + DEFAULT_PRAGMAS = { + "foreign_keys" => true, + "journal_mode" => :wal, + "synchronous" => :normal, + "mmap_size" => 134217728, # 128 megabytes + "journal_size_limit" => 67108864, # 64 megabytes + "cache_size" => 2000 + } + class StatementPool < ConnectionAdapters::StatementPool # :nodoc: private def dealloc(stmt) @@ -154,8 +165,23 @@ def supports_concurrent_connections? !@memory_database end + def supports_virtual_columns? + database_version >= "3.31.0" + end + + def connected? + !(@raw_connection.nil? || @raw_connection.closed?) + end + def active? - @raw_connection && !@raw_connection.closed? + if connected? + @lock.synchronize do + if @raw_connection&.active? + verified! + true + end + end + end || false end def return_value_after_insert?(column) # :nodoc: @@ -167,10 +193,11 @@ def return_value_after_insert?(column) # :nodoc: # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! - super - - @raw_connection&.close rescue nil - @raw_connection = nil + @lock.synchronize do + super + @raw_connection&.close rescue nil + @raw_connection = nil + end end def supports_index_sort_order? @@ -235,7 +262,6 @@ def remove_index(table_name, column_name = nil, **options) # :nodoc: internal_exec_query "DROP INDEX #{quote_column_name(index_name)}" end - # Renames a table. # # Example: @@ -324,15 +350,31 @@ def add_reference(table_name, ref_name, **options) # :nodoc: end alias :add_belongs_to :add_reference + FK_REGEX = /.*FOREIGN KEY\s+\("([^"]+)"\)\s+REFERENCES\s+"(\w+)"\s+\("(\w+)"\)/ + DEFERRABLE_REGEX = /DEFERRABLE INITIALLY (\w+)/ def foreign_keys(table_name) # SQLite returns 1 row for each column of composite foreign keys. fk_info = internal_exec_query("PRAGMA foreign_key_list(#{quote(table_name)})", "SCHEMA") + # Deferred or immediate foreign keys can only be seen in the CREATE TABLE sql + fk_defs = table_structure_sql(table_name) + .select do |column_string| + column_string.start_with?("CONSTRAINT") && + column_string.include?("FOREIGN KEY") + end + .to_h do |fk_string| + _, from, table, to = fk_string.match(FK_REGEX).to_a + _, mode = fk_string.match(DEFERRABLE_REGEX).to_a + deferred = mode&.downcase&.to_sym || false + [[table, from, to], deferred] + end + grouped_fk = fk_info.group_by { |row| row["id"] }.values.each { |group| group.sort_by! { |row| row["seq"] } } grouped_fk.map do |group| row = group.first options = { on_delete: extract_foreign_key_action(row["on_delete"]), - on_update: extract_foreign_key_action(row["on_update"]) + on_update: extract_foreign_key_action(row["on_update"]), + deferrable: fk_defs[[row["table"], row["from"], row["to"]]] } if group.one? @@ -342,8 +384,7 @@ def foreign_keys(table_name) options[:column] = group.map { |row| row["from"] } options[:primary_key] = group.map { |row| row["to"] } end - # DIFFERENCE: FQN - ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name, row["table"], options) + ForeignKeyDefinition.new(table_name, row["table"], options) end end @@ -390,7 +431,14 @@ def new_column_from_field(table_name, field, definitions) type_metadata = fetch_type_metadata(field["type"]) default_value = extract_value_from_default(default) - default_function = extract_default_function(default_value, default) + generated_type = extract_generated_type(field) + + if generated_type.present? + default_function = default + else + default_function = extract_default_function(default_value, default) + end + rowid = is_column_the_rowid?(field, definitions) ActiveRecord::ConnectionAdapters::SQLite3Column.new( @@ -401,7 +449,8 @@ def new_column_from_field(table_name, field, definitions) default_function, collation: field["collation"], auto_increment: field["auto_increment"], - rowid: rowid + rowid: rowid, + generated_type: generated_type ) end @@ -413,7 +462,12 @@ def bind_params_length end def table_structure(table_name) - structure = internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA") + structure = if supports_virtual_columns? + internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA") + else + internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA") + end + raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? table_structure_with_collation(table_name, structure) end @@ -453,8 +507,9 @@ def has_default_function?(default_value, default) # See: https://www.sqlite.org/lang_altertable.html # SQLite has an additional restriction on the ALTER TABLE statement def invalid_alter_table_type?(type, options) - type.to_sym == :primary_key || options[:primary_key] || - options[:null] == false && options[:default].nil? + type == :primary_key || options[:primary_key] || + options[:null] == false && options[:default].nil? || + (type == :virtual && options[:stored]) end def alter_table( @@ -510,12 +565,6 @@ def copy_table(from, to, options = {}) options[:rename][column.name.to_sym] || column.name) : column.name - if column.has_default? - type = lookup_cast_type_from_column(column) - default = type.deserialize(column.default) - default = -> { column.default_function } if default.nil? - end - column_options = { limit: column.limit, precision: column.precision, @@ -525,19 +574,31 @@ def copy_table(from, to, options = {}) primary_key: column_name == from_primary_key } - unless column.auto_increment? - column_options[:default] = default + if column.virtual? + column_options[:as] = column.default_function + column_options[:stored] = column.virtual_stored? + column_options[:type] = column.type + elsif column.has_default? + type = lookup_cast_type_from_column(column) + default = type.deserialize(column.default) + default = -> { column.default_function } if default.nil? + + unless column.auto_increment? + column_options[:default] = default + end end - column_type = column.bigint? ? :bigint : column.type + column_type = column.virtual? ? :virtual : (column.bigint? ? :bigint : column.type) @definition.column(column_name, column_type, **column_options) end yield @definition if block_given? end copy_table_indexes(from, to, options[:rename] || {}) + + columns_to_copy = @definition.columns.reject { |col| col.options.key?(:as) }.map(&:name) copy_table_contents(from, to, - @definition.columns.map(&:name), + columns_to_copy, options[:rename] || {}) end @@ -611,32 +672,22 @@ def translate_exception(exception, message:, sql:, binds:) COLLATE_REGEX = /.*\"(\w+)\".*collate\s+\"(\w+)\".*/i.freeze PRIMARY_KEY_AUTOINCREMENT_REGEX = /.*\"(\w+)\".+PRIMARY KEY AUTOINCREMENT/i + GENERATED_ALWAYS_AS_REGEX = /.*"(\w+)".+GENERATED ALWAYS AS \((.+)\) (?:STORED|VIRTUAL)/i def table_structure_with_collation(table_name, basic_structure) collation_hash = {} auto_increments = {} - sql = <<~SQL - SELECT sql FROM - (SELECT * FROM sqlite_master UNION ALL - SELECT * FROM sqlite_temp_master) - WHERE type = 'table' AND name = #{quote(table_name)} - SQL - - # Result will have following sample string - # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - # "password_digest" varchar COLLATE "NOCASE"); - result = query_value(sql, "SCHEMA") + generated_columns = {} - if result - # Splitting with left parentheses and discarding the first part will return all - # columns separated with comma(,). - columns_string = result.split("(", 2).last + column_strings = table_structure_sql(table_name, basic_structure.map { |column| column["name"] }) - columns_string.split(",").each do |column_string| + if column_strings.any? + column_strings.each do |column_string| # This regex will match the column name and collation type and will save # the value in $1 and $2 respectively. collation_hash[$1] = $2 if COLLATE_REGEX =~ column_string auto_increments[$1] = true if PRIMARY_KEY_AUTOINCREMENT_REGEX =~ column_string + generated_columns[$1] = $2 if GENERATED_ALWAYS_AS_REGEX =~ column_string end basic_structure.map do |column| @@ -650,6 +701,10 @@ def table_structure_with_collation(table_name, basic_structure) column["auto_increment"] = true end + if generated_columns.has_key?(column_name) + column["dflt_value"] = generated_columns[column_name] + end + column end else @@ -657,6 +712,50 @@ def table_structure_with_collation(table_name, basic_structure) end end + UNQUOTED_OPEN_PARENS_REGEX = /\((?![^'"]*['"][^'"]*$)/ + FINAL_CLOSE_PARENS_REGEX = /\);*\z/ + + def table_structure_sql(table_name, column_names = nil) + unless column_names + column_info = table_info(table_name) + column_names = column_info.map { |column| column["name"] } + end + + sql = <<~SQL + SELECT sql FROM + (SELECT * FROM sqlite_master UNION ALL + SELECT * FROM sqlite_temp_master) + WHERE type = 'table' AND name = #{quote(table_name)} + SQL + + # Result will have following sample string + # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + # "password_digest" varchar COLLATE "NOCASE", + # "o_id" integer, + # CONSTRAINT "fk_rails_78146ddd2e" FOREIGN KEY ("o_id") REFERENCES "os" ("id")); + result = query_value(sql, "SCHEMA") + + return [] unless result + + # Splitting with left parentheses and discarding the first part will return all + # columns separated with comma(,). + result.partition(UNQUOTED_OPEN_PARENS_REGEX) + .last + .sub(FINAL_CLOSE_PARENS_REGEX, "") + # column definitions can have a comma in them, so split on commas followed + # by a space and a column name in quotes or followed by the keyword CONSTRAINT + .split(/,(?=\s(?:CONSTRAINT|"(?:#{Regexp.union(column_names).source})"))/i) + .map(&:strip) + end + + def table_info(table_name) + if supports_virtual_columns? + internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA") + else + internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA") + end + end + def arel_visitor Arel::Visitors::SQLite.new(self) end @@ -686,29 +785,17 @@ def configure_connection end end - # Enforce foreign key constraints - # https://www.sqlite.org/pragma.html#pragma_foreign_keys - # https://www.sqlite.org/foreignkeys.html - raw_execute("PRAGMA foreign_keys = ON", "SCHEMA") - unless @memory_database - # Journal mode WAL allows for greater concurrency (many readers + one writer) - # https://www.sqlite.org/pragma.html#pragma_journal_mode - raw_execute("PRAGMA journal_mode = WAL", "SCHEMA") - # Set more relaxed level of database durability - # 2 = "FULL" (sync on every write), 1 = "NORMAL" (sync every 1000 written pages) and 0 = "NONE" - # https://www.sqlite.org/pragma.html#pragma_synchronous - raw_execute("PRAGMA synchronous = NORMAL", "SCHEMA") - # Set the global memory map so all processes can share some data - # https://www.sqlite.org/pragma.html#pragma_mmap_size - # https://www.sqlite.org/mmap.html - raw_execute("PRAGMA mmap_size = #{128.megabytes}", "SCHEMA") - end - # Impose a limit on the WAL file to prevent unlimited growth - # https://www.sqlite.org/pragma.html#pragma_journal_size_limit - raw_execute("PRAGMA journal_size_limit = #{64.megabytes}", "SCHEMA") - # Set the local connection cache to 2000 pages - # https://www.sqlite.org/pragma.html#pragma_cache_size - raw_execute("PRAGMA cache_size = 2000", "SCHEMA") + super + + pragmas = @config.fetch(:pragmas, {}).stringify_keys + DEFAULT_PRAGMAS.merge(pragmas).each do |pragma, value| + if ::SQLite3::Pragmas.respond_to?(pragma) + stmt = ::SQLite3::Pragmas.public_send(pragma, value) + raw_execute(stmt, "SCHEMA") + else + warn "Unknown SQLite pragma: #{pragma}" + end + end end end # DIFFERENCE: A registration here is moved down to concrete class so we are not registering part of an adapter. @@ -774,6 +861,14 @@ def jdbc_connection_class def initialize(...) super + @memory_database = false + case @config[:database].to_s + when "" + raise ArgumentError, "No database file specified. Missing argument: database" + when ":memory:" + @memory_database = true + end + # assign arjdbc extra connection params conn_params = build_connection_config(@config.compact) diff --git a/lib/arjdbc/sqlite3/column.rb b/lib/arjdbc/sqlite3/column.rb index 2a45d6de2..5191118c5 100644 --- a/lib/arjdbc/sqlite3/column.rb +++ b/lib/arjdbc/sqlite3/column.rb @@ -4,12 +4,14 @@ module ActiveRecord::ConnectionAdapters class SQLite3Column < JdbcColumn attr_reader :rowid - - def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, auto_increment: nil, rowid: false, **) + + def initialize(*, auto_increment: nil, rowid: false, generated_type: nil, **) super + @auto_increment = auto_increment @default = nil if default =~ /NULL/ @rowid = rowid + @generated_type = generated_type end def self.string_to_binary(value) @@ -39,6 +41,18 @@ def auto_incremented_by_db? auto_increment? || rowid end + def virtual? + !@generated_type.nil? + end + + def virtual_stored? + virtual? && @generated_type == :stored + end + + def has_default? + super && !virtual? + end + def init_with(coder) @auto_increment = coder["auto_increment"] super @@ -100,4 +114,4 @@ def extract_scale(sql_type) end end end -end \ No newline at end of file +end diff --git a/lib/arjdbc/sqlite3/pragmas.rb b/lib/arjdbc/sqlite3/pragmas.rb new file mode 100644 index 000000000..7ac03ff55 --- /dev/null +++ b/lib/arjdbc/sqlite3/pragmas.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +module SQLite3 + # defines methods to de generate pragma statements + module Pragmas + class << self + # The enumeration of valid synchronous modes. + SYNCHRONOUS_MODES = [["full", 2], ["normal", 1], ["off", 0]].freeze + + # The enumeration of valid temp store modes. + TEMP_STORE_MODES = [["default", 0], ["file", 1], ["memory", 2]].freeze + + # The enumeration of valid auto vacuum modes. + AUTO_VACUUM_MODES = [["none", 0], ["full", 1], ["incremental", 2]].freeze + + # The list of valid journaling modes. + JOURNAL_MODES = [["delete"], ["truncate"], ["persist"], ["memory"], ["wal"], ["off"]].freeze + + # The list of valid locking modes. + LOCKING_MODES = [["normal"], ["exclusive"]].freeze + + # The list of valid encodings. + ENCODINGS = [["utf-8"], ["utf-16"], ["utf-16le"], ["utf-16be"]].freeze + + # The list of valid WAL checkpoints. + WAL_CHECKPOINTS = [["passive"], ["full"], ["restart"], ["truncate"]].freeze + + # Enforce foreign key constraints + # https://www.sqlite.org/pragma.html#pragma_foreign_keys + # https://www.sqlite.org/foreignkeys.html + def foreign_keys(value) + gen_boolean_pragma(:foreign_keys, value) + end + + # Journal mode WAL allows for greater concurrency (many readers + one writer) + # https://www.sqlite.org/pragma.html#pragma_journal_mode + def journal_mode(value) + gen_enum_pragma(:journal_mode, value, JOURNAL_MODES) + end + + # Set more relaxed level of database durability + # 2 = "FULL" (sync on every write), 1 = "NORMAL" (sync every 1000 written pages) and 0 = "NONE" + # https://www.sqlite.org/pragma.html#pragma_synchronous + def synchronous(value) + gen_enum_pragma(:synchronous, value, SYNCHRONOUS_MODES) + end + + def temp_store(value) + gen_enum_pragma(:temp_store, value, TEMP_STORE_MODES) + end + + # Set the global memory map so all processes can share some data + # https://www.sqlite.org/pragma.html#pragma_mmap_size + # https://www.sqlite.org/mmap.html + def mmap_size(value) + "PRAGMA mmap_size = #{value.to_i}" + end + + # Impose a limit on the WAL file to prevent unlimited growth + # https://www.sqlite.org/pragma.html#pragma_journal_size_limit + def journal_size_limit(value) + "PRAGMA journal_size_limit = #{value.to_i}" + end + + # Set the local connection cache to 2000 pages + # https://www.sqlite.org/pragma.html#pragma_cache_size + def cache_size(value) + "PRAGMA cache_size = #{value.to_i}" + end + + private + + def gen_boolean_pragma(name, mode) + case mode + when String + case mode.downcase + when "on", "yes", "true", "y", "t" then mode = "'ON'" + when "off", "no", "false", "n", "f" then mode = "'OFF'" + else + raise ActiveRecord::JDBCError, "unrecognized pragma parameter #{mode.inspect}" + end + when true, 1 + mode = "ON" + when false, 0, nil + mode = "OFF" + else + raise ActiveRecord::JDBCError, "unrecognized pragma parameter #{mode.inspect}" + end + + "PRAGMA #{name} = #{mode}" + end + + def gen_enum_pragma(name, mode, enums) + match = enums.find { |p| p.find { |i| i.to_s.downcase == mode.to_s.downcase } } + + unless match + # Unknown pragma value + raise ActiveRecord::JDBCError, "unrecognized #{name} #{mode.inspect}" + end + + "PRAGMA #{name} = '#{match.first.upcase}'" + end + end + end +end diff --git a/test/db/mysql/simple_test.rb b/test/db/mysql/simple_test.rb index 4a73cb1a8..0bab4ecb5 100644 --- a/test/db/mysql/simple_test.rb +++ b/test/db/mysql/simple_test.rb @@ -262,33 +262,6 @@ def test_quoting_braces end end if defined? JRUBY_VERSION - test "config :host" do - skip unless MYSQL_CONFIG[:database] # JDBC :url defined instead - skip if mariadb_driver? - - begin - config = { :adapter => 'mysql', :port => 3306 } - config[:username] = MYSQL_CONFIG[:username] - config[:password] = MYSQL_CONFIG[:password] - config[:database] = MYSQL_CONFIG[:database] - config[:properties] = MYSQL_CONFIG[:properties].dup - with_connection(config) do |connection| - conf = connection.instance_variable_get('@config') - assert_match(/^jdbc:mysql:\/\/:\d*\//, conf[:url]) - end - - ActiveRecord::Base.connection.disconnect! - - host = [ MYSQL_CONFIG[:host] || 'localhost', '127.0.0.1' ] # fail-over - with_connection(config.merge :host => host, :port => nil) do |connection| - conf = connection.instance_variable_get('@config') - assert_match(/^jdbc:mysql:\/\/.*?127.0.0.1\//, conf[:url]) - end - ensure - ActiveRecord::Base.establish_connection(MYSQL_CONFIG) - end - end if defined? JRUBY_VERSION - test 'bulk change table' do assert ActiveRecord::Base.connection.supports_bulk_alter? diff --git a/test/db/mysql/unit_test.rb b/test/db/mysql/unit_test.rb index c6ee69232..4a8bdcc11 100644 --- a/test/db/mysql/unit_test.rb +++ b/test/db/mysql/unit_test.rb @@ -1,4 +1,4 @@ -# encoding: ASCII-8BIT +require 'db/mysql' require 'test_helper' class MySQLUnitTest < Test::Unit::TestCase @@ -93,11 +93,11 @@ def initialize; end context 'connection' do test 'jndi configuration' do + skip "mysql_connection was removed, find ways to integrate jndi if needed since AR 7.1 & 7.2 changed so much" connection_handler = connection_handler_stub config = { :jndi => 'jdbc/TestDS' } connection_handler.expects(:jndi_connection).with() { |c| config = c } - connection_handler.mysql_connection config # we do not complete username/database etc : assert_nil config[:username] @@ -109,48 +109,49 @@ def initialize; end assert config[:adapter_class] end - test 'configuration attempts to load MySQL driver by default' do - skip 'jdbc/mysql not available' if load_jdbc_mysql.nil? + test "configuration attempts to load MySQL driver by default" do + skip "jdbc/mysql not available" if load_jdbc_mysql.nil? - connection_handler = connection_handler_stub + config_hash = { adapter: "mysql2", database: "MyDB" } - config = { database: 'MyDB' } - connection_handler.expects(:jdbc_connection) ::Jdbc::MySQL.expects(:load_driver).with(:require) - connection_handler.mysql_connection config + + connection_handler(config_hash) end - test 'configuration uses driver_name from Jdbc::MySQL' do - skip 'jdbc/mysql not available' if load_jdbc_mysql.nil? + test "configuration uses driver_name from Jdbc::MySQL" do + skip "jdbc/mysql not available" if load_jdbc_mysql.nil? - connection_handler = connection_handler_stub + config_hash = { adapter: "mysql2", database: "MyDB" } - config = { database: 'MyDB' } - connection_handler.expects(:jdbc_connection).with() { |c| config = c } - ::Jdbc::MySQL.expects(:driver_name).returns('com.mysql.CustomDriver') - connection_handler.mysql_connection config - assert_equal 'com.mysql.CustomDriver', config[:driver] + ::Jdbc::MySQL.expects(:driver_name).returns("com.mysql.CustomDriver") + + conn = connection_handler(config_hash) + + config = conn.instance_variable_get("@connection_parameters") + assert_equal "com.mysql.CustomDriver", config[:driver] end - test 'configuration sets up properties according to connector/j driver (>= 8.0)' do - skip 'jdbc/mysql not available' if load_jdbc_mysql.nil? + test "configuration sets up properties according to connector/j driver (>= 8.0)" do + skip "jdbc/mysql not available" if load_jdbc_mysql.nil? - connection_handler = connection_handler_stub + config_hash = { adapter: "mysql2", database: "MyDB" } - config = { database: 'MyDB' } - connection_handler.expects(:jdbc_connection).with() { |c| config = c } - ::Jdbc::MySQL.expects(:driver_name).returns('com.mysql.cj.jdbc.Driver') - connection_handler.mysql_connection config - assert_equal 'com.mysql.cj.jdbc.Driver', config[:driver] - assert_equal 'CONVERT_TO_NULL', config[:properties]['zeroDateTimeBehavior'] - assert_equal false, config[:properties]['useLegacyDatetimeCode'] - assert_equal false, config[:properties]['jdbcCompliantTruncation'] - assert_equal false, config[:properties]['useSSL'] + ::Jdbc::MySQL.expects(:driver_name).returns("com.mysql.cj.jdbc.Driver") + + conn = connection_handler(config_hash) + config = conn.instance_variable_get("@connection_parameters") + + assert_equal "com.mysql.cj.jdbc.Driver", config[:driver] + assert_equal "CONVERT_TO_NULL", config[:properties]["zeroDateTimeBehavior"] + assert_equal false, config[:properties]["useLegacyDatetimeCode"] + assert_equal false, config[:properties]["jdbcCompliantTruncation"] + assert_equal false, config[:properties]["useSSL"] end end - context 'connection (Jdbc::MySQL missing)' do + context "connection (Jdbc::MySQL missing)" do module ::Jdbc; end @@ -165,58 +166,35 @@ def teardown ::Jdbc.const_set :MySQL, @@jdbc_mysql if @@jdbc_mysql end - test 'configuration sets url and properties assuming mysql driver (<= 5.1)' do - connection_handler = connection_handler_stub - - config = { host: '127.0.0.1', database: 'MyDB' } - connection_handler.expects(:jdbc_connection).with() { |c| config = c } - connection_handler.mysql_connection config - - # we do not complete username/database etc : - assert_equal 'root', config[:username] - assert_equal 'com.mysql.jdbc.Driver', config[:driver] - assert_equal 'jdbc:mysql://127.0.0.1/MyDB', config[:url] - assert_equal 'UTF-8', config[:properties]['characterEncoding'] - assert_equal 'convertToNull', config[:properties]['zeroDateTimeBehavior'] - assert_equal false, config[:properties]['useLegacyDatetimeCode'] - assert_equal false, config[:properties]['jdbcCompliantTruncation'] - assert_equal false, config[:properties]['useSSL'] - end - - test 'configuration attempts to load MySQL driver by default' do - connection_handler = connection_handler_stub + test "configuration sets url and properties assuming mysql driver <= 5.1" do + config_hash = { adapter: "mysql2", host: "127.0.0.1", database: "MyDB" } - config = { database: 'MyDB' } - connection_handler.expects(:jdbc_connection) - connection_handler.expects(:require).with('jdbc/mysql') - connection_handler.mysql_connection config + conn = connection_handler(config_hash) + config = conn.instance_variable_get("@connection_parameters") + # we do not complete username, port etc : + assert_equal nil, config[:username] + assert_equal "com.mysql.jdbc.Driver", config[:driver] + assert_equal "jdbc:mysql://127.0.0.1:3306/MyDB", config[:url] + assert_equal "UTF-8", config[:properties]["characterEncoding"] + assert_equal "convertToNull", config[:properties]["zeroDateTimeBehavior"] + assert_equal false, config[:properties]["useLegacyDatetimeCode"] + assert_equal false, config[:properties]["jdbcCompliantTruncation"] + assert_equal false, config[:properties]["useSSL"] end - test 'configuration allows to skip driver loading' do - connection_handler = connection_handler_stub + test "configuration allows to skip driver loading" do + config_hash = { adapter: "mysql2", database: "MyDB", driver: false } - config = { database: 'MyDB', driver: false } - connection_handler.expects(:jdbc_connection).with() { |c| config = c } - connection_handler.expects(:require).never - connection_handler.mysql_connection config - assert_not config[:driver] # allow Java's service discovery mechanism (with connector/j 8.0) - end + conn = connection_handler(config_hash) + config = conn.instance_variable_get("@connection_parameters") - test 'configuration works with MariaDB driver specified' do - connection_handler = connection_handler_stub - - config = { database: 'MyDB', driver: 'org.mariadb.jdbc.Driver' } - connection_handler.expects(:jdbc_connection).with() { |c| config = c } - connection_handler.mysql_connection config - - # we do not complete username/database etc : - assert_equal 'root', config[:username] - assert_equal 'org.mariadb.jdbc.Driver', config[:driver] - assert_equal 'jdbc:mysql://localhost/MyDB', config[:url] - assert_equal false, config[:properties]['useLegacyDatetimeCode'] - assert_equal false, config[:properties]['useSsl'] + # allow Java's service discovery mechanism (with connector/j 8.0) + assert_not config[:driver] end + end + def connection_handler(config) + ActiveRecord::ConnectionAdapters::Mysql2Adapter.new(config) end def load_jdbc_mysql diff --git a/test/db/sqlite3.rb b/test/db/sqlite3.rb index 57859d7c4..429e25a80 100644 --- a/test/db/sqlite3.rb +++ b/test/db/sqlite3.rb @@ -10,4 +10,8 @@ end ActiveRecord::Base.establish_connection(SQLITE3_CONFIG) -at_exit { Dir['*test.sqlite3-*'].each { |f| File.delete(f) } } +at_exit do + Dir["*test.sqlite3*"].each do |sqlite_file| + File.delete(sqlite_file) + end +end