From cb3be837000466e5f23c631a64ad497edf4bfc81 Mon Sep 17 00:00:00 2001 From: Justis Blasco Date: Thu, 24 Oct 2024 11:38:31 -0400 Subject: [PATCH 01/10] Update adapter initializer --- .../connection_adapters/odbc_adapter.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/active_record/connection_adapters/odbc_adapter.rb b/lib/active_record/connection_adapters/odbc_adapter.rb index cee68573..29e997c9 100644 --- a/lib/active_record/connection_adapters/odbc_adapter.rb +++ b/lib/active_record/connection_adapters/odbc_adapter.rb @@ -88,10 +88,14 @@ 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) + super + else + configure_time_options(config_or_deprecated_connection) + super(config_or_deprecated_connection, deprecated_logger, deprecated_config) + @database_metadata = deprecated_database_metadata + end end # Returns the human-readable name of the adapter. From 5bfe6e5418b6bff67be2c3e2e0db0982494f3dd2 Mon Sep 17 00:00:00 2001 From: Justis Blasco Date: Thu, 24 Oct 2024 12:00:39 -0400 Subject: [PATCH 02/10] Fully set up connection in initializer --- .../connection_adapters/odbc_adapter.rb | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/active_record/connection_adapters/odbc_adapter.rb b/lib/active_record/connection_adapters/odbc_adapter.rb index 29e997c9..c76dccd0 100644 --- a/lib/active_record/connection_adapters/odbc_adapter.rb +++ b/lib/active_record/connection_adapters/odbc_adapter.rb @@ -90,10 +90,21 @@ class ODBCAdapter < AbstractAdapter def initialize(config_or_deprecated_connection, deprecated_logger = nil, deprecated_config = nil, deprecated_database_metadata = nil) if config_or_deprecated_connection.is_a?(Hash) - super + 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 - configure_time_options(config_or_deprecated_connection) - super(config_or_deprecated_connection, deprecated_logger, deprecated_config) + connection = config_or_deprecated_connection + configure_time_options(connection) + super(connection, deprecated_logger, deprecated_config) @database_metadata = deprecated_database_metadata end end From 32a6ab36fa1700a4fcf612a04561655a2a91c742 Mon Sep 17 00:00:00 2001 From: Justis Blasco Date: Tue, 10 Dec 2024 10:48:44 -0500 Subject: [PATCH 03/10] Adjust Quoting namespace --- lib/odbc_adapter/quoting.rb | 60 ++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/lib/odbc_adapter/quoting.rb b/lib/odbc_adapter/quoting.rb index a3bd0546..4f4a7307 100644 --- a/lib/odbc_adapter/quoting.rb +++ b/lib/odbc_adapter/quoting.rb @@ -1,41 +1,45 @@ module ODBCAdapter module Quoting - # Quotes a string, escaping any ' (single quote) characters. - def quote_string(string) - string.gsub(/\'/, "''") - end + extend ActiveSupport::Concern - # 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 + module ClassMethods + # Quotes a string, escaping any ' (single quote) characters. + def quote_string(string) + string.gsub(/\'/, "''") + end - return name if quote_char.length.zero? - quote_char = quote_char[0] + # 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 - # Avoid quoting any already quoted name - return name if name[0] == quote_char && name[-1] == quote_char + return name if quote_char.length.zero? + quote_char = quote_char[0] - # If upcase identifiers, only quote mixed case names. - if database_metadata.upcase_identifiers? - return name unless name =~ /([A-Z]+[a-z])|([a-z]+[A-Z])/ - end + # Avoid quoting any already quoted name + return name if name[0] == quote_char && name[-1] == quote_char - "#{quote_char.chr}#{name}#{quote_char.chr}" - end + # If upcase identifiers, only quote mixed case names. + if database_metadata.upcase_identifiers? + return name unless name =~ /([A-Z]+[a-z])|([a-z]+[A-Z])/ + 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 From b11e2e5b2a564f8df4ea6a257b5aaa04b64e7069 Mon Sep 17 00:00:00 2001 From: Justis Blasco Date: Wed, 11 Dec 2024 01:10:10 -0500 Subject: [PATCH 04/10] Hardcode quoting config to avoid DB connection --- lib/odbc_adapter/quoting.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/odbc_adapter/quoting.rb b/lib/odbc_adapter/quoting.rb index 4f4a7307..d3b83b68 100644 --- a/lib/odbc_adapter/quoting.rb +++ b/lib/odbc_adapter/quoting.rb @@ -11,7 +11,7 @@ def quote_string(string) # 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 + quote_char = "\"" return name if quote_char.length.zero? quote_char = quote_char[0] @@ -19,10 +19,8 @@ def quote_column_name(name) # 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? - return name unless name =~ /([A-Z]+[a-z])|([a-z]+[A-Z])/ - end + # Upcase identifiers only quote mixed case names + return name unless name =~ /([A-Z]+[a-z])|([a-z]+[A-Z])/ "#{quote_char.chr}#{name}#{quote_char.chr}" end From 26a44ae7832247d9b34186d07348e841336d70c4 Mon Sep 17 00:00:00 2001 From: Justis Blasco Date: Wed, 11 Dec 2024 01:19:38 -0500 Subject: [PATCH 05/10] Add allow_retry arg --- lib/odbc_adapter/database_statements.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/odbc_adapter/database_statements.rb b/lib/odbc_adapter/database_statements.rb index acbcda3b..5893ce04 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 From 71267c7260ace77e7a7969b5f64882c4a4e70ca9 Mon Sep 17 00:00:00 2001 From: Justis Blasco Date: Wed, 11 Dec 2024 01:30:03 -0500 Subject: [PATCH 06/10] Rename type_cast --- lib/odbc_adapter/database_statements.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/odbc_adapter/database_statements.rb b/lib/odbc_adapter/database_statements.rb index 5893ce04..e051f759 100644 --- a/lib/odbc_adapter/database_statements.rb +++ b/lib/odbc_adapter/database_statements.rb @@ -135,7 +135,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 From cfd8fbf362e570c0ed8c842d54f533a955a80fa7 Mon Sep 17 00:00:00 2001 From: Justis Blasco Date: Fri, 17 Jan 2025 11:35:36 -0500 Subject: [PATCH 07/10] Move initialize_type_map to class method --- .../connection_adapters/odbc_adapter.rb | 84 ++++++++++--------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/lib/active_record/connection_adapters/odbc_adapter.rb b/lib/active_record/connection_adapters/odbc_adapter.rb index c76dccd0..84f536be 100644 --- a/lib/active_record/connection_adapters/odbc_adapter.rb +++ b/lib/active_record/connection_adapters/odbc_adapter.rb @@ -162,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 @@ -217,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 From c8ddeff19672acb1f8ef0099d5da59b3d2de3f13 Mon Sep 17 00:00:00 2001 From: Justis Blasco Date: Fri, 28 Mar 2025 15:01:26 -0400 Subject: [PATCH 08/10] Manually map Date/TimeStamp results to raw string --- lib/odbc_adapter/database_statements.rb | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/odbc_adapter/database_statements.rb b/lib/odbc_adapter/database_statements.rb index e051f759..2c4e646a 100644 --- a/lib/odbc_adapter/database_statements.rb +++ b/lib/odbc_adapter/database_statements.rb @@ -79,8 +79,17 @@ 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, ODBC::TimeStamp + value.to_s + else + value + end + end + end end # Assume received identifier is in DBMS's data dictionary case. From 25f2dc7c7dd73eb6e6c66a2f2508f2aa3cfbc695 Mon Sep 17 00:00:00 2001 From: pm-malasampath Date: Wed, 17 Sep 2025 16:59:40 -0400 Subject: [PATCH 09/10] type cast changes for date and datetime --- lib/odbc_adapter/database_statements.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/odbc_adapter/database_statements.rb b/lib/odbc_adapter/database_statements.rb index 2c4e646a..f55c2394 100644 --- a/lib/odbc_adapter/database_statements.rb +++ b/lib/odbc_adapter/database_statements.rb @@ -83,8 +83,10 @@ def dbms_type_cast(_columns, rows) rows.map do |values| values.map do |value| case value - when ODBC::Date, ODBC::TimeStamp - value.to_s + when ODBC::Date + value.to_date + when ODBC::TimeStamp + value.to_datetime else value end From 812a01e7eed696b5de57bc58ab6fa71d98db1b80 Mon Sep 17 00:00:00 2001 From: pm-malasampath Date: Thu, 18 Sep 2025 11:44:09 -0400 Subject: [PATCH 10/10] convert to string before date conversion --- lib/odbc_adapter/database_statements.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/odbc_adapter/database_statements.rb b/lib/odbc_adapter/database_statements.rb index f55c2394..0603e8b5 100644 --- a/lib/odbc_adapter/database_statements.rb +++ b/lib/odbc_adapter/database_statements.rb @@ -84,9 +84,9 @@ def dbms_type_cast(_columns, rows) values.map do |value| case value when ODBC::Date - value.to_date + value.to_s.to_date when ODBC::TimeStamp - value.to_datetime + value.to_s.to_datetime else value end