diff --git a/lib/active_record/connection_adapters/odbc_adapter.rb b/lib/active_record/connection_adapters/odbc_adapter.rb index cee68573..84f536be 100644 --- a/lib/active_record/connection_adapters/odbc_adapter.rb +++ b/lib/active_record/connection_adapters/odbc_adapter.rb @@ -88,10 +88,25 @@ class ODBCAdapter < AbstractAdapter # when a connection is first established. attr_reader :database_metadata - def initialize(connection, logger, config, database_metadata) - configure_time_options(connection) - super(connection, logger, config) - @database_metadata = database_metadata + def initialize(config_or_deprecated_connection, deprecated_logger = nil, deprecated_config = nil, deprecated_database_metadata = nil) + if config_or_deprecated_connection.is_a?(Hash) + config = config_or_deprecated_connection + connection, config = + if config.key?(:dsn) + ActiveRecord::Base.send(:odbc_dsn_connection, config) + elsif config.key?(:conn_str) + ActiveRecord::Base.send(:odbc_conn_str_connection, config) + else + raise ArgumentError, 'No data source name (:dsn) or connection string (:conn_str) specified.' + end + super(connection, Rails.logger, config) + @database_metadata = ::ODBCAdapter::DatabaseMetadata.new(connection, config[:encoding_bug]) + else + connection = config_or_deprecated_connection + configure_time_options(connection) + super(connection, deprecated_logger, deprecated_config) + @database_metadata = deprecated_database_metadata + end end # Returns the human-readable name of the adapter. @@ -147,37 +162,48 @@ def new_column(name, default, sql_type_metadata, null, table_name, native_type = # Build the type map for ActiveRecord # Here, ODBC and ODBC_UTF8 constants are interchangeable - def initialize_type_map(map) - map.register_type 'boolean', Type::Boolean.new - map.register_type 'json', Type::Json.new - map.register_type ODBC::SQL_CHAR, Type::String.new - map.register_type ODBC::SQL_LONGVARCHAR, Type::Text.new - map.register_type ODBC::SQL_TINYINT, Type::Integer.new(limit: 4) - map.register_type ODBC::SQL_SMALLINT, Type::Integer.new(limit: 8) - map.register_type ODBC::SQL_INTEGER, Type::Integer.new(limit: 16) - map.register_type ODBC::SQL_BIGINT, Type::BigInteger.new(limit: 32) - map.register_type ODBC::SQL_REAL, Type::Float.new(limit: 24) - map.register_type ODBC::SQL_FLOAT, Type::Float.new - map.register_type ODBC::SQL_DOUBLE, Type::Float.new(limit: 53) - map.register_type ODBC::SQL_DECIMAL, Type::Float.new - map.register_type ODBC::SQL_NUMERIC, Type::Integer.new - map.register_type ODBC::SQL_BINARY, Type::Binary.new - map.register_type ODBC::SQL_DATE, Type::Date.new - map.register_type ODBC::SQL_DATETIME, Type::DateTime.new - map.register_type ODBC::SQL_TIME, Type::Time.new - map.register_type ODBC::SQL_TIMESTAMP, Type::DateTime.new - map.register_type ODBC::SQL_GUID, Type::String.new - - alias_type map, ODBC::SQL_BIT, 'boolean' - alias_type map, ODBC::SQL_VARCHAR, ODBC::SQL_CHAR - alias_type map, ODBC::SQL_WCHAR, ODBC::SQL_CHAR - alias_type map, ODBC::SQL_WVARCHAR, ODBC::SQL_CHAR - alias_type map, ODBC::SQL_WLONGVARCHAR, ODBC::SQL_LONGVARCHAR - alias_type map, ODBC::SQL_VARBINARY, ODBC::SQL_BINARY - alias_type map, ODBC::SQL_LONGVARBINARY, ODBC::SQL_BINARY - alias_type map, ODBC::SQL_TYPE_DATE, ODBC::SQL_DATE - alias_type map, ODBC::SQL_TYPE_TIME, ODBC::SQL_TIME - alias_type map, ODBC::SQL_TYPE_TIMESTAMP, ODBC::SQL_TIMESTAMP + class << self + def initialize_type_map(map) + map.register_type 'boolean', Type::Boolean.new + map.register_type 'json', Type::Json.new + map.register_type ODBC::SQL_CHAR, Type::String.new + map.register_type ODBC::SQL_LONGVARCHAR, Type::Text.new + map.register_type ODBC::SQL_TINYINT, Type::Integer.new(limit: 4) + map.register_type ODBC::SQL_SMALLINT, Type::Integer.new(limit: 8) + map.register_type ODBC::SQL_INTEGER, Type::Integer.new(limit: 16) + map.register_type ODBC::SQL_BIGINT, Type::BigInteger.new(limit: 32) + map.register_type ODBC::SQL_REAL, Type::Float.new(limit: 24) + map.register_type ODBC::SQL_FLOAT, Type::Float.new + map.register_type ODBC::SQL_DOUBLE, Type::Float.new(limit: 53) + map.register_type ODBC::SQL_DECIMAL, Type::Float.new + map.register_type ODBC::SQL_NUMERIC, Type::Integer.new + map.register_type ODBC::SQL_BINARY, Type::Binary.new + map.register_type ODBC::SQL_DATE, Type::Date.new + map.register_type ODBC::SQL_DATETIME, Type::DateTime.new + map.register_type ODBC::SQL_TIME, Type::Time.new + map.register_type ODBC::SQL_TIMESTAMP, Type::DateTime.new + map.register_type ODBC::SQL_GUID, Type::String.new + + alias_type map, ODBC::SQL_BIT, 'boolean' + alias_type map, ODBC::SQL_VARCHAR, ODBC::SQL_CHAR + alias_type map, ODBC::SQL_WCHAR, ODBC::SQL_CHAR + alias_type map, ODBC::SQL_WVARCHAR, ODBC::SQL_CHAR + alias_type map, ODBC::SQL_WLONGVARCHAR, ODBC::SQL_LONGVARCHAR + alias_type map, ODBC::SQL_VARBINARY, ODBC::SQL_BINARY + alias_type map, ODBC::SQL_LONGVARBINARY, ODBC::SQL_BINARY + alias_type map, ODBC::SQL_TYPE_DATE, ODBC::SQL_DATE + alias_type map, ODBC::SQL_TYPE_TIME, ODBC::SQL_TIME + alias_type map, ODBC::SQL_TYPE_TIMESTAMP, ODBC::SQL_TIMESTAMP + end + + # Can't use the built-in ActiveRecord map#alias_type because it doesn't + # work with non-string keys, and in our case the keys are (almost) all + # numeric + def alias_type(map, new_type, old_type) + map.register_type(new_type) do |_, *args| + map.lookup(old_type, *args) + end + end end # Translate an exception from the native DBMS to something usable by @@ -202,14 +228,9 @@ def translate_exception(exception, message) end private - - # Can't use the built-in ActiveRecord map#alias_type because it doesn't - # work with non-string keys, and in our case the keys are (almost) all - # numeric - def alias_type(map, new_type, old_type) - map.register_type(new_type) do |_, *args| - map.lookup(old_type, *args) - end + + def initialize_type_map(map) + self.class.initialize_type_map(map) end # Ensure ODBC is mapping time-based fields to native ruby objects diff --git a/lib/odbc_adapter/database_statements.rb b/lib/odbc_adapter/database_statements.rb index acbcda3b..0603e8b5 100644 --- a/lib/odbc_adapter/database_statements.rb +++ b/lib/odbc_adapter/database_statements.rb @@ -20,7 +20,7 @@ def execute(sql, name = nil, binds = []) # Executes +sql+ statement in the context of this connection using # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. - def exec_query(sql, name = 'SQL', binds = [], prepare: false) # rubocop:disable Lint/UnusedMethodArgument + def exec_query(sql, name = 'SQL', binds = [], allow_retry: false, prepare: false) # rubocop:disable Lint/UnusedMethodArgument log(sql, name) do stmt = if prepared_statements @@ -79,8 +79,19 @@ def default_sequence_name(table, _column) # A custom hook to allow end users to overwrite the type casting before it # is returned to ActiveRecord. Useful before a full adapter has made its way # back into this repository. - def dbms_type_cast(_columns, values) - values + def dbms_type_cast(_columns, rows) + rows.map do |values| + values.map do |value| + case value + when ODBC::Date + value.to_s.to_date + when ODBC::TimeStamp + value.to_s.to_datetime + else + value + end + end + end end # Assume received identifier is in DBMS's data dictionary case. @@ -135,7 +146,7 @@ def prepare_statement_sub(sql) end def prepared_binds(binds) - binds.map(&:value_for_database).map { |bind| _type_cast(bind) } + binds.map(&:value_for_database).map { |bind| type_cast(bind) } end end end diff --git a/lib/odbc_adapter/quoting.rb b/lib/odbc_adapter/quoting.rb index a3bd0546..d3b83b68 100644 --- a/lib/odbc_adapter/quoting.rb +++ b/lib/odbc_adapter/quoting.rb @@ -1,41 +1,43 @@ module ODBCAdapter module Quoting - # Quotes a string, escaping any ' (single quote) characters. - def quote_string(string) - string.gsub(/\'/, "''") - end + extend ActiveSupport::Concern + + module ClassMethods + # Quotes a string, escaping any ' (single quote) characters. + def quote_string(string) + string.gsub(/\'/, "''") + end - # Returns a quoted form of the column name. - def quote_column_name(name) - name = name.to_s - quote_char = database_metadata.identifier_quote_char.to_s.strip + # Returns a quoted form of the column name. + def quote_column_name(name) + name = name.to_s + quote_char = "\"" - return name if quote_char.length.zero? - quote_char = quote_char[0] + return name if quote_char.length.zero? + quote_char = quote_char[0] - # Avoid quoting any already quoted name - return name if name[0] == quote_char && name[-1] == quote_char + # Avoid quoting any already quoted name + return name if name[0] == quote_char && name[-1] == quote_char - # If upcase identifiers, only quote mixed case names. - if database_metadata.upcase_identifiers? + # Upcase identifiers only quote mixed case names return name unless name =~ /([A-Z]+[a-z])|([a-z]+[A-Z])/ - end - "#{quote_char.chr}#{name}#{quote_char.chr}" - end + "#{quote_char.chr}#{name}#{quote_char.chr}" + end - # Ideally, we'd return an ODBC date or timestamp literal escape - # sequence, but not all ODBC drivers support them. - def quoted_date(value) - if value.acts_like?(:time) - zone_conversion_method = ActiveRecord.default_timezone == :utc ? :getutc : :getlocal + # Ideally, we'd return an ODBC date or timestamp literal escape + # sequence, but not all ODBC drivers support them. + def quoted_date(value) + if value.acts_like?(:time) + zone_conversion_method = ActiveRecord.default_timezone == :utc ? :getutc : :getlocal - if value.respond_to?(zone_conversion_method) - value = value.send(zone_conversion_method) + if value.respond_to?(zone_conversion_method) + value = value.send(zone_conversion_method) + end + value.strftime('%Y-%m-%d %H:%M:%S') # Time, DateTime + else + value.strftime('%Y-%m-%d') # Date end - value.strftime('%Y-%m-%d %H:%M:%S') # Time, DateTime - else - value.strftime('%Y-%m-%d') # Date end end end