Skip to content

Commit 9cc715c

Browse files
committed
Version bump to 8.0.3.odbc to support ODBC
- ActiveRecord SQLServer Adapter defaults to TinyTDS. And the support for ODBC has been removed. This is the effort to support Adapter with ODBC Driver again. - ruby-odbc is now pointed to https://github.com/cloudvolumes/ruby-odbc 0.103.cv
1 parent a29f432 commit 9cc715c

File tree

11 files changed

+346
-32
lines changed

11 files changed

+346
-32
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## v8.0.3.odbc
2+
3+
#### Added
4+
5+
- ODBC restoration.
6+
17
## v8.0.3
28

39
#### Fixed

Gemfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ group :tinytds do
5959
end
6060
# rubocop:enable Bundler/DuplicatedGem
6161

62+
group :odbc do
63+
gem 'ruby-odbc', :git => 'https://github.com/cloudvolumes/ruby-odbc.git', :tag => '0.103.cv'
64+
end
65+
6266
group :development do
6367
gem "minitest-spec-rails"
6468
gem "mocha"

Rakefile

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,29 @@ task test: ["test:dblib"]
99
task default: [:test]
1010

1111
namespace :test do
12-
ENV["ARCONN"] = "sqlserver"
13-
14-
%w(dblib).each do |mode|
12+
%w(dblib odbc).each do |mode|
1513
Rake::TestTask.new(mode) do |t|
1614
t.libs = ARTest::SQLServer.test_load_paths
1715
t.test_files = test_files
1816
t.warning = !!ENV["WARNING"]
1917
t.verbose = false
2018
end
2119
end
20+
21+
task "dblib:env" do
22+
ENV["ARCONN"] = "dblib"
23+
end
24+
25+
task 'odbc:env' do
26+
ENV['ARCONN'] = 'odbc'
27+
end
2228
end
2329

30+
task "test:dblib" => "test:dblib:env"
31+
task "test:odbc" => "test:odbc:env"
32+
2433
namespace :profile do
25-
["dblib"].each do |mode|
34+
["dblib", "odbc"].each do |mode|
2635
namespace mode.to_sym do
2736
Dir.glob("test/profile/*_profile_case.rb").sort.each do |test_file|
2837
profile_case = File.basename(test_file).sub("_profile_case.rb", "")

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
8.0.3
1+
8.0.3.odbc
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module ActiveRecord
2+
module ConnectionAdapters
3+
module SQLServer
4+
module CoreExt
5+
module ODBC
6+
module Statement
7+
def finished?
8+
connected?
9+
false
10+
rescue ::ODBC::Error
11+
true
12+
end
13+
end
14+
15+
module Database
16+
def run_block(*args)
17+
yield sth = run(*args)
18+
19+
sth.drop
20+
end
21+
end
22+
end
23+
end
24+
end
25+
end
26+
end
27+
28+
ODBC::Statement.send :include, ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::ODBC::Statement
29+
ODBC::Database.send :include, ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::ODBC::Database

lib/active_record/connection_adapters/sqlserver/database_statements.rb

Lines changed: 106 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ def cast_result(raw_result)
3939
end
4040

4141
def affected_rows(raw_result)
42+
return if raw_result.blank?
43+
4244
column_name = lowercase_schema_reflection ? 'affectedrows' : 'AffectedRows'
4345
raw_result.first[column_name]
4446
end
@@ -53,18 +55,18 @@ def raw_execute(sql, name = nil, binds = [], prepare: false, async: false, allow
5355
end
5456

5557
def internal_exec_sql_query(sql, conn)
56-
handle = internal_raw_execute(sql, conn)
58+
handle = raw_connection_run(sql, conn)
5759
handle_to_names_and_values(handle, ar_result: true)
5860
ensure
5961
finish_statement_handle(handle)
6062
end
6163

6264
def exec_delete(sql, name = nil, binds = [])
63-
super(sql, name, binds) || super("SELECT @@ROWCOUNT As AffectedRows", "", [])
65+
super || super("SELECT @@ROWCOUNT As AffectedRows", "", [])
6466
end
6567

6668
def exec_update(sql, name = nil, binds = [])
67-
super(sql, name, binds) || super("SELECT @@ROWCOUNT As AffectedRows", "", [])
69+
super || super("SELECT @@ROWCOUNT As AffectedRows", "", [])
6870
end
6971

7072
def begin_db_transaction
@@ -168,17 +170,8 @@ def execute_procedure(proc_name, *variables)
168170

169171
log(sql, "Execute Procedure") do |notification_payload|
170172
with_raw_connection do |conn|
171-
result = internal_raw_execute(sql, conn)
172-
verified!
173-
options = { as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc }
174-
175-
result.each(options) do |row|
176-
r = row.with_indifferent_access
177-
yield(r) if block_given?
178-
end
179-
180-
result = result.each.map { |row| row.is_a?(Hash) ? row.with_indifferent_access : row }
181-
notification_payload[:row_count] = result.count
173+
result = send("execute_#{@config[:mode]}_procedure", sql, conn)
174+
notification_payload[:row_count] = result&.count
182175
result
183176
end
184177
end
@@ -312,7 +305,11 @@ def sql_for_insert(sql, pk, binds, returning)
312305
# === SQLServer Specific ======================================== #
313306

314307
def set_identity_insert(table_name, conn, enable)
315-
internal_raw_execute("SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}", conn , perform_do: true)
308+
if @config[:mode].to_sym == :dblib
309+
internal_raw_execute("SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}", conn , perform_do: true)
310+
else
311+
internal_raw_execute_odbc("SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}", conn , perform_do: true)
312+
end
316313
rescue Exception
317314
raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
318315
end
@@ -345,7 +342,12 @@ def sp_executesql_sql_type(attr)
345342
value = active_model_attribute?(attr) ? attr.value_for_database : attr
346343

347344
if value.is_a?(Numeric)
348-
value > 2_147_483_647 ? "bigint".freeze : "int".freeze
345+
if value.is_a?(Integer)
346+
value > 2_147_483_647 ? "bigint".freeze : "int".freeze
347+
else
348+
# For Float, BigDecimal, Rational etc.
349+
value.is_a?(BigDecimal) ? "decimal(18,6)".freeze : "float".freeze
350+
end
349351
else
350352
"nvarchar(max)".freeze
351353
end
@@ -427,13 +429,26 @@ def identity_columns(table_name)
427429
# === SQLServer Specific (Selecting) ============================ #
428430

429431
def _raw_select(sql, conn)
430-
handle = internal_raw_execute(sql, conn)
432+
handle = raw_connection_run(sql, conn)
431433
handle_to_names_and_values(handle, fetch: :rows)
432434
ensure
433435
finish_statement_handle(handle)
434436
end
435437

438+
def raw_connection_run(sql, conn, perform_do: false)
439+
case @config[:mode].to_sym
440+
when :dblib
441+
internal_raw_execute(sql, conn, perform_do: perform_do)
442+
when :odbc
443+
internal_raw_execute_odbc(sql, conn, perform_do: perform_do)
444+
end
445+
end
446+
436447
def handle_to_names_and_values(handle, options = {})
448+
send("handle_to_names_and_values_#{@config[:mode]}", handle, options)
449+
end
450+
451+
def handle_to_names_and_values_dblib(handle, options = {})
437452
query_options = {}.tap do |qo|
438453
qo[:timezone] = ActiveRecord.default_timezone || :utc
439454
qo[:as] = (options[:ar_result] || options[:fetch] == :rows) ? :array : :hash
@@ -448,8 +463,33 @@ def handle_to_names_and_values(handle, options = {})
448463
options[:ar_result] ? ActiveRecord::Result.new(columns, results) : results
449464
end
450465

466+
def handle_to_names_and_values_odbc(handle, options = {})
467+
@raw_connection.use_utc = ActiveRecord.default_timezone || :utc
468+
469+
if options[:ar_result]
470+
columns = lowercase_schema_reflection ? handle.columns(true).map { |c| c.name.downcase } : handle.columns(true).map { |c| c.name }
471+
rows = handle.fetch_all || []
472+
ActiveRecord::Result.new(columns, rows)
473+
else
474+
case options[:fetch]
475+
when :all
476+
handle.each_hash || []
477+
when :rows
478+
handle.fetch_all || []
479+
end
480+
end
481+
end
482+
451483
def finish_statement_handle(handle)
452-
handle.cancel if handle
484+
return unless handle
485+
486+
case @config[:mode].to_sym
487+
when :dblib
488+
handle.cancel
489+
when :odbc
490+
handle.drop if handle.respond_to?(:drop) && !handle.finished?
491+
end
492+
453493
handle
454494
end
455495

@@ -462,6 +502,54 @@ def internal_raw_execute(sql, raw_connection, perform_do: false)
462502

463503
perform_do ? result.do : result
464504
end
505+
506+
# Executing SQL for ODBC mode
507+
def internal_raw_execute_odbc(sql, raw_connection, perform_do: false)
508+
return raw_connection.do(sql) if perform_do
509+
510+
block_given? ? raw_connection.run_block(sql) { |handle| yield(handle) } : raw_connection.run(sql)
511+
end
512+
513+
private
514+
515+
def execute_dblib_procedure(sql, conn)
516+
result = internal_raw_execute(sql, conn)
517+
verified!
518+
options = { as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc }
519+
520+
raw_rows = result.each(options).map do |row|
521+
row = row.with_indifferent_access
522+
yield(row) if block_given?
523+
row
524+
end
525+
526+
raw_rows.map { |row| row.is_a?(Hash) ? row.with_indifferent_access : row }
527+
end
528+
529+
def execute_odbc_procedure(sql, conn)
530+
results = []
531+
532+
internal_raw_execute_odbc(sql, conn) do |handle|
533+
get_rows = lambda do
534+
rows = handle_to_names_and_values handle, fetch: :all
535+
results << rows.map!(&:with_indifferent_access)
536+
end
537+
538+
get_rows.call
539+
get_rows.call while handle_more_results?(handle)
540+
end
541+
542+
results.many? ? results : results.first
543+
end
544+
545+
546+
def handle_more_results?(handle)
547+
case @config[:mode].to_sym
548+
when :dblib
549+
when :odbc
550+
handle.more_results
551+
end
552+
end
465553
end
466554
end
467555
end

0 commit comments

Comments
 (0)