Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## v8.0.3.odbc

#### Added

- ODBC restoration.

## v8.0.3

#### Fixed
Expand Down
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ group :tinytds do
end
# rubocop:enable Bundler/DuplicatedGem

group :odbc do
gem 'ruby-odbc', :git => 'https://github.com/cloudvolumes/ruby-odbc.git', :tag => '0.103.cv'
end

group :development do
gem "minitest-spec-rails"
gem "mocha"
Expand Down
17 changes: 13 additions & 4 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,29 @@ task test: ["test:dblib"]
task default: [:test]

namespace :test do
ENV["ARCONN"] = "sqlserver"

%w(dblib).each do |mode|
%w(dblib odbc).each do |mode|
Rake::TestTask.new(mode) do |t|
t.libs = ARTest::SQLServer.test_load_paths
t.test_files = test_files
t.warning = !!ENV["WARNING"]
t.verbose = false
end
end

task "dblib:env" do
ENV["ARCONN"] = "dblib"
end

task 'odbc:env' do
ENV['ARCONN'] = 'odbc'
end
end

task "test:dblib" => "test:dblib:env"
task "test:odbc" => "test:odbc:env"

namespace :profile do
["dblib"].each do |mode|
["dblib", "odbc"].each do |mode|
namespace mode.to_sym do
Dir.glob("test/profile/*_profile_case.rb").sort.each do |test_file|
profile_case = File.basename(test_file).sub("_profile_case.rb", "")
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8.0.3
8.0.3.odbc
1 change: 1 addition & 0 deletions activerecord-sqlserver-adapter.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ Gem::Specification.new do |spec|

spec.add_dependency "activerecord", "~> 8.0.0"
spec.add_dependency "tiny_tds"
Comment thread
jyothu marked this conversation as resolved.
spec.add_dependency "ruby-odbc"
end
29 changes: 29 additions & 0 deletions lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module ActiveRecord
module ConnectionAdapters
module SQLServer
module CoreExt
module ODBC
module Statement
def finished?
connected?
false
rescue ::ODBC::Error
true
end
end

module Database
def run_block(*args)
yield sth = run(*args)

sth.drop
end
end
end
end
end
end
end

ODBC::Statement.send :include, ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::ODBC::Statement
ODBC::Database.send :include, ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::ODBC::Database
143 changes: 119 additions & 24 deletions lib/active_record/connection_adapters/sqlserver/database_statements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def cast_result(raw_result)
end

def affected_rows(raw_result)
return if raw_result.blank?

column_name = lowercase_schema_reflection ? 'affectedrows' : 'AffectedRows'
raw_result.first[column_name]
end
Expand All @@ -53,20 +55,18 @@ def raw_execute(sql, name = nil, binds = [], prepare: false, async: false, allow
end

def internal_exec_sql_query(sql, conn)
handle = internal_raw_execute(sql, conn)
handle = raw_connection_run(sql, conn)
handle_to_names_and_values(handle, ar_result: true)
ensure
finish_statement_handle(handle)
end

def exec_delete(sql, name = nil, binds = [])
sql = sql.dup << "; SELECT @@ROWCOUNT AS AffectedRows"
super(sql, name, binds)
super || super("SELECT @@ROWCOUNT As AffectedRows", "", [])
end

def exec_update(sql, name = nil, binds = [])
sql = sql.dup << "; SELECT @@ROWCOUNT AS AffectedRows"
super(sql, name, binds)
super || super("SELECT @@ROWCOUNT As AffectedRows", "", [])
end

def begin_db_transaction
Expand Down Expand Up @@ -170,17 +170,8 @@ def execute_procedure(proc_name, *variables)

log(sql, "Execute Procedure") do |notification_payload|
with_raw_connection do |conn|
result = internal_raw_execute(sql, conn)
verified!
options = { as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc }

result.each(options) do |row|
r = row.with_indifferent_access
yield(r) if block_given?
end

result = result.each.map { |row| row.is_a?(Hash) ? row.with_indifferent_access : row }
notification_payload[:row_count] = result.count
result = send("execute_#{@config[:mode]}_procedure", sql, conn)
notification_payload[:row_count] = result&.count
result
end
end
Expand Down Expand Up @@ -280,10 +271,12 @@ def sql_for_insert(sql, pk, binds, returning)
}
end

<<~SQL.squish
<<-SQL.strip_heredoc
SET NOCOUNT ON
DECLARE @ssaIdInsertTable table (#{pk_and_types.map { |pk_and_type| "#{pk_and_type[:quoted]} #{pk_and_type[:id_sql_type]}"}.join(", ") });
#{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT #{ pk_and_types.map { |pk_and_type| "INSERTED.#{pk_and_type[:quoted]}" }.join(", ") } INTO @ssaIdInsertTable"}
SELECT #{pk_and_types.map {|pk_and_type| "CAST(#{pk_and_type[:quoted]} AS #{pk_and_type[:id_sql_type]}) #{pk_and_type[:quoted]}"}.join(", ")} FROM @ssaIdInsertTable
#{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT #{ pk_and_types.map { |pk_and_type| "INSERTED.#{pk_and_type[:quoted]}" }.join(", ") } INTO @ssaIdInsertTable"}
SELECT #{pk_and_types.map {|pk_and_type| "CAST(#{pk_and_type[:quoted]} AS #{pk_and_type[:id_sql_type]}) #{pk_and_type[:quoted]}"}.join(", ")} FROM @ssaIdInsertTable;
SET NOCOUNT OFF
SQL
else
returning_columns = returning || Array(pk)
Expand All @@ -296,7 +289,14 @@ def sql_for_insert(sql, pk, binds, returning)
end
end
else
"#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"
table = get_table_name(sql)
id_column = identity_columns(table.to_s.strip).first

if id_column.present?
sql.sub(/\s*VALUES\s*\(/, " OUTPUT INSERTED.#{id_column.name} VALUES (")
else
sql.sub(/\s*VALUES\s*\(/, " OUTPUT CAST(SCOPE_IDENTITY() AS bigint) AS Ident VALUES (")
end
end

[sql, binds]
Expand All @@ -305,7 +305,11 @@ def sql_for_insert(sql, pk, binds, returning)
# === SQLServer Specific ======================================== #

def set_identity_insert(table_name, conn, enable)
internal_raw_execute("SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}", conn , perform_do: true)
if @config[:mode].to_sym == :dblib
internal_raw_execute("SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}", conn , perform_do: true)
else
internal_raw_execute_odbc("SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}", conn , perform_do: true)
end
rescue Exception
raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
end
Expand Down Expand Up @@ -338,7 +342,12 @@ def sp_executesql_sql_type(attr)
value = active_model_attribute?(attr) ? attr.value_for_database : attr

if value.is_a?(Numeric)
value > 2_147_483_647 ? "bigint".freeze : "int".freeze
if value.is_a?(Integer)
value > 2_147_483_647 ? "bigint".freeze : "int".freeze
else
# For Float, BigDecimal, Rational etc.
value.is_a?(BigDecimal) ? "decimal(18,6)".freeze : "float".freeze
end
else
"nvarchar(max)".freeze
end
Expand Down Expand Up @@ -420,13 +429,26 @@ def identity_columns(table_name)
# === SQLServer Specific (Selecting) ============================ #

def _raw_select(sql, conn)
handle = internal_raw_execute(sql, conn)
handle = raw_connection_run(sql, conn)
handle_to_names_and_values(handle, fetch: :rows)
ensure
finish_statement_handle(handle)
end

def raw_connection_run(sql, conn, perform_do: false)
case @config[:mode].to_sym
when :dblib
internal_raw_execute(sql, conn, perform_do: perform_do)
when :odbc
internal_raw_execute_odbc(sql, conn, perform_do: perform_do)
end
end

def handle_to_names_and_values(handle, options = {})
send("handle_to_names_and_values_#{@config[:mode]}", handle, options)
end

def handle_to_names_and_values_dblib(handle, options = {})
query_options = {}.tap do |qo|
qo[:timezone] = ActiveRecord.default_timezone || :utc
qo[:as] = (options[:ar_result] || options[:fetch] == :rows) ? :array : :hash
Expand All @@ -441,8 +463,33 @@ def handle_to_names_and_values(handle, options = {})
options[:ar_result] ? ActiveRecord::Result.new(columns, results) : results
end

def handle_to_names_and_values_odbc(handle, options = {})
@raw_connection.use_utc = ActiveRecord.default_timezone || :utc

if options[:ar_result]
columns = lowercase_schema_reflection ? handle.columns(true).map { |c| c.name.downcase } : handle.columns(true).map { |c| c.name }
rows = handle.fetch_all || []
ActiveRecord::Result.new(columns, rows)
else
case options[:fetch]
when :all
handle.each_hash || []
when :rows
handle.fetch_all || []
end
end
end

def finish_statement_handle(handle)
handle.cancel if handle
return unless handle

case @config[:mode].to_sym
when :dblib
handle.cancel
when :odbc
handle.drop if handle.respond_to?(:drop) && !handle.finished?
end

handle
end

Expand All @@ -455,6 +502,54 @@ def internal_raw_execute(sql, raw_connection, perform_do: false)

perform_do ? result.do : result
end

# Executing SQL for ODBC mode
def internal_raw_execute_odbc(sql, raw_connection, perform_do: false)
return raw_connection.do(sql) if perform_do

block_given? ? raw_connection.run_block(sql) { |handle| yield(handle) } : raw_connection.run(sql)
end

private

def execute_dblib_procedure(sql, conn)
result = internal_raw_execute(sql, conn)
verified!
options = { as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc }

raw_rows = result.each(options).map do |row|
row = row.with_indifferent_access
yield(row) if block_given?
row
end

raw_rows.map { |row| row.is_a?(Hash) ? row.with_indifferent_access : row }
end

def execute_odbc_procedure(sql, conn)
results = []

internal_raw_execute_odbc(sql, conn) do |handle|
get_rows = lambda do
rows = handle_to_names_and_values handle, fetch: :all
results << rows.map!(&:with_indifferent_access)
end

get_rows.call
get_rows.call while handle_more_results?(handle)
end

results.many? ? results : results.first
end


def handle_more_results?(handle)
case @config[:mode].to_sym
when :dblib
when :odbc
handle.more_results
end
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ module ConnectionAdapters
module SQLServer
module Type
class Binary < ActiveRecord::Type::Binary

def cast_value(value)
if value.class.to_s == 'String' and !value.frozen?
value.force_encoding(Encoding::BINARY) =~ /[^[:xdigit:]]/ ? value : [value].pack('H*')
else
value
end
end

def type
:binary_basic
end
Expand Down
Loading
Loading