Skip to content

Commit ee32304

Browse files
authored
Merge pull request #19 from cloudvolumes/topic/jyothish/8-0-stable-with-odbc-AVM-38723
[AVM-38723] Bring back ODBC support for 8-0-stable branch
2 parents 3960253 + 9cc715c commit ee32304

File tree

17 files changed

+393
-55
lines changed

17 files changed

+393
-55
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

activerecord-sqlserver-adapter.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ Gem::Specification.new do |spec|
2929

3030
spec.add_dependency "activerecord", "~> 8.0.0"
3131
spec.add_dependency "tiny_tds"
32+
spec.add_dependency "ruby-odbc"
3233
end
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: 119 additions & 24 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,20 +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-
sql = sql.dup << "; SELECT @@ROWCOUNT AS AffectedRows"
64-
super(sql, name, binds)
65+
super || super("SELECT @@ROWCOUNT As AffectedRows", "", [])
6566
end
6667

6768
def exec_update(sql, name = nil, binds = [])
68-
sql = sql.dup << "; SELECT @@ROWCOUNT AS AffectedRows"
69-
super(sql, name, binds)
69+
super || super("SELECT @@ROWCOUNT As AffectedRows", "", [])
7070
end
7171

7272
def begin_db_transaction
@@ -170,17 +170,8 @@ def execute_procedure(proc_name, *variables)
170170

171171
log(sql, "Execute Procedure") do |notification_payload|
172172
with_raw_connection do |conn|
173-
result = internal_raw_execute(sql, conn)
174-
verified!
175-
options = { as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc }
176-
177-
result.each(options) do |row|
178-
r = row.with_indifferent_access
179-
yield(r) if block_given?
180-
end
181-
182-
result = result.each.map { |row| row.is_a?(Hash) ? row.with_indifferent_access : row }
183-
notification_payload[:row_count] = result.count
173+
result = send("execute_#{@config[:mode]}_procedure", sql, conn)
174+
notification_payload[:row_count] = result&.count
184175
result
185176
end
186177
end
@@ -280,10 +271,12 @@ def sql_for_insert(sql, pk, binds, returning)
280271
}
281272
end
282273

283-
<<~SQL.squish
274+
<<-SQL.strip_heredoc
275+
SET NOCOUNT ON
284276
DECLARE @ssaIdInsertTable table (#{pk_and_types.map { |pk_and_type| "#{pk_and_type[:quoted]} #{pk_and_type[:id_sql_type]}"}.join(", ") });
285-
#{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT #{ pk_and_types.map { |pk_and_type| "INSERTED.#{pk_and_type[:quoted]}" }.join(", ") } INTO @ssaIdInsertTable"}
286-
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
277+
#{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT #{ pk_and_types.map { |pk_and_type| "INSERTED.#{pk_and_type[:quoted]}" }.join(", ") } INTO @ssaIdInsertTable"}
278+
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;
279+
SET NOCOUNT OFF
287280
SQL
288281
else
289282
returning_columns = returning || Array(pk)
@@ -296,7 +289,14 @@ def sql_for_insert(sql, pk, binds, returning)
296289
end
297290
end
298291
else
299-
"#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"
292+
table = get_table_name(sql)
293+
id_column = identity_columns(table.to_s.strip).first
294+
295+
if id_column.present?
296+
sql.sub(/\s*VALUES\s*\(/, " OUTPUT INSERTED.#{id_column.name} VALUES (")
297+
else
298+
sql.sub(/\s*VALUES\s*\(/, " OUTPUT CAST(SCOPE_IDENTITY() AS bigint) AS Ident VALUES (")
299+
end
300300
end
301301

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

307307
def set_identity_insert(table_name, conn, enable)
308-
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
309313
rescue Exception
310314
raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
311315
end
@@ -338,7 +342,12 @@ def sp_executesql_sql_type(attr)
338342
value = active_model_attribute?(attr) ? attr.value_for_database : attr
339343

340344
if value.is_a?(Numeric)
341-
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
342351
else
343352
"nvarchar(max)".freeze
344353
end
@@ -420,13 +429,26 @@ def identity_columns(table_name)
420429
# === SQLServer Specific (Selecting) ============================ #
421430

422431
def _raw_select(sql, conn)
423-
handle = internal_raw_execute(sql, conn)
432+
handle = raw_connection_run(sql, conn)
424433
handle_to_names_and_values(handle, fetch: :rows)
425434
ensure
426435
finish_statement_handle(handle)
427436
end
428437

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+
429447
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 = {})
430452
query_options = {}.tap do |qo|
431453
qo[:timezone] = ActiveRecord.default_timezone || :utc
432454
qo[:as] = (options[:ar_result] || options[:fetch] == :rows) ? :array : :hash
@@ -441,8 +463,33 @@ def handle_to_names_and_values(handle, options = {})
441463
options[:ar_result] ? ActiveRecord::Result.new(columns, results) : results
442464
end
443465

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+
444483
def finish_statement_handle(handle)
445-
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+
446493
handle
447494
end
448495

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

456503
perform_do ? result.do : result
457504
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
458553
end
459554
end
460555
end

lib/active_record/connection_adapters/sqlserver/type/binary.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ module ConnectionAdapters
55
module SQLServer
66
module Type
77
class Binary < ActiveRecord::Type::Binary
8+
9+
def cast_value(value)
10+
if value.class.to_s == 'String' and !value.frozen?
11+
value.force_encoding(Encoding::BINARY) =~ /[^[:xdigit:]]/ ? value : [value].pack('H*')
12+
else
13+
value
14+
end
15+
end
16+
817
def type
918
:binary_basic
1019
end

0 commit comments

Comments
 (0)