Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 64 additions & 43 deletions lib/active_record/connection_adapters/odbc_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
19 changes: 15 additions & 4 deletions lib/odbc_adapter/database_statements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
56 changes: 29 additions & 27 deletions lib/odbc_adapter/quoting.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down