Skip to content

Commit 1dc97fc

Browse files
authored
Merge pull request #11 from lranj/5-1-stable-with-odbc
Merge commits made to 5-0-stable-with-odbc branch with 5-1-stable-odbc
2 parents ed8b170 + 22c9325 commit 1dc97fc

16 files changed

+231
-23
lines changed

Gemfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ require 'openssl'
22
source 'https://rubygems.org'
33
gemspec
44

5-
gem 'sqlite3'
5+
gem 'sqlite3', '< 1.4'
66
gem 'minitest', '< 5.3.4'
77
gem 'bcrypt'
88
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
@@ -51,6 +51,10 @@ group :tinytds do
5151
end
5252
end
5353

54+
group :odbc do
55+
gem 'ruby-odbc', :git => 'https://github.com/cloudvolumes/ruby-odbc.git', :tag => '0.101.cv'
56+
end
57+
5458
group :development do
5559
gem 'byebug'
5660
gem 'mocha'

Rakefile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ task default: [:test]
88

99
namespace :test do
1010

11-
%w(dblib).each do |mode|
11+
%w(dblib odbc).each do |mode|
1212

1313
Rake::TestTask.new(mode) do |t|
1414
t.libs = ARTest::SQLServer.test_load_paths
@@ -23,12 +23,17 @@ namespace :test do
2323
ENV['ARCONN'] = 'dblib'
2424
end
2525

26+
task 'odbc:env' do
27+
ENV['ARCONN'] = 'odbc'
28+
end
29+
2630
end
2731

2832
task 'test:dblib' => 'test:dblib:env'
33+
task 'test:odbc' => 'test:odbc:env'
2934

3035
namespace :profile do
31-
['dblib'].each do |mode|
36+
['dblib', 'odbc'].each do |mode|
3237
namespace mode.to_sym do
3338
Dir.glob('test/profile/*_profile_case.rb').sort.each do |test_file|
3439
profile_case = File.basename(test_file).sub('_profile_case.rb', '')

activerecord-sqlserver-adapter.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ Gem::Specification.new do |spec|
1717
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
1818
spec.require_paths = ['lib']
1919
spec.add_dependency 'activerecord', '~> 5.1.0'
20-
spec.add_dependency 'tiny_tds'
20+
spec.add_dependency 'ruby-odbc'
2121
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
module ActiveRecord
2+
module ConnectionAdapters
3+
module SQLServer
4+
module CoreExt
5+
module ODBC
6+
7+
module Statement
8+
9+
def finished?
10+
connected?
11+
false
12+
rescue ::ODBC::Error
13+
true
14+
end
15+
16+
end
17+
18+
module Database
19+
20+
def run_block(*args)
21+
yield sth = run(*args)
22+
sth.drop
23+
end
24+
25+
end
26+
27+
end
28+
end
29+
end
30+
end
31+
end
32+
33+
ODBC::Statement.send :include, ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::ODBC::Statement
34+
ODBC::Database.send :include, ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::ODBC::Database

lib/active_record/connection_adapters/sqlserver/database_statements.rb

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,11 @@ def exec_insert(sql, name = nil, binds = [], pk = nil, _sequence_name = nil)
2424
end
2525

2626
def exec_delete(sql, name, binds)
27-
sql = sql.dup << '; SELECT @@ROWCOUNT AS AffectedRows'
28-
super(sql, name, binds).rows.first.first
27+
super.rows.first.try(:first) || super("SELECT @@ROWCOUNT As AffectedRows", "", []).rows.first.try(:first)
2928
end
3029

3130
def exec_update(sql, name, binds)
32-
sql = sql.dup << '; SELECT @@ROWCOUNT AS AffectedRows'
33-
super(sql, name, binds).rows.first.first
31+
super.rows.first.try(:first) || super("SELECT @@ROWCOUNT As AffectedRows", "", []).rows.first.try(:first)
3432
end
3533

3634
def supports_statement_cache?
@@ -108,6 +106,18 @@ def execute_procedure(proc_name, *variables)
108106
yield(r) if block_given?
109107
end
110108
result.each.map { |row| row.is_a?(Hash) ? row.with_indifferent_access : row }
109+
when :odbc
110+
results = []
111+
raw_connection_run(sql) do |handle|
112+
get_rows = lambda do
113+
rows = handle_to_names_and_values handle, fetch: :all
114+
rows.each_with_index { |r, i| rows[i] = r.with_indifferent_access }
115+
results << rows
116+
end
117+
get_rows.call
118+
get_rows.call while handle_more_results?(handle)
119+
end
120+
results.many? ? results : results.first
111121
end
112122
end
113123
end
@@ -206,7 +216,13 @@ def sql_for_insert(sql, pk, id_value, sequence_name, binds)
206216
sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{quoted_pk}"
207217
end
208218
else
209-
"#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"
219+
table = get_table_name(sql)
220+
id_column = identity_columns(table.to_s.strip).first
221+
if !id_column.blank?
222+
sql.sub(/\s*VALUES\s*\(/, " OUTPUT INSERTED.#{id_column.name} VALUES (")
223+
else
224+
sql.sub(/\s*VALUES\s*\(/, " OUTPUT CAST(SCOPE_IDENTITY() AS bigint) AS Ident VALUES (")
225+
end
210226
end
211227
super
212228
end
@@ -282,6 +298,8 @@ def raw_connection_do(sql)
282298
case @connection_options[:mode]
283299
when :dblib
284300
@connection.execute(sql).do
301+
when :odbc
302+
@connection.do(sql)
285303
end
286304
ensure
287305
@update_sql = false
@@ -344,19 +362,25 @@ def raw_connection_run(sql)
344362
case @connection_options[:mode]
345363
when :dblib
346364
@connection.execute(sql)
365+
when :odbc
366+
block_given? ? @connection.run_block(sql) { |handle| yield(handle) } : @connection.run(sql)
347367
end
348368
end
349369

350370
def handle_more_results?(handle)
351371
case @connection_options[:mode]
352372
when :dblib
373+
when :odbc
374+
handle.more_results
353375
end
354376
end
355377

356378
def handle_to_names_and_values(handle, options = {})
357379
case @connection_options[:mode]
358380
when :dblib
359381
handle_to_names_and_values_dblib(handle, options)
382+
when :odbc
383+
handle_to_names_and_values_odbc(handle, options)
360384
end
361385
end
362386

@@ -370,10 +394,28 @@ def handle_to_names_and_values_dblib(handle, options = {})
370394
options[:ar_result] ? ActiveRecord::Result.new(columns, results) : results
371395
end
372396

397+
def handle_to_names_and_values_odbc(handle, options = {})
398+
@connection.use_utc = ActiveRecord::Base.default_timezone == :utc
399+
if options[:ar_result]
400+
columns = lowercase_schema_reflection ? handle.columns(true).map { |c| c.name.downcase } : handle.columns(true).map { |c| c.name }
401+
rows = handle.fetch_all || []
402+
ActiveRecord::Result.new(columns, rows)
403+
else
404+
case options[:fetch]
405+
when :all
406+
handle.each_hash || []
407+
when :rows
408+
handle.fetch_all || []
409+
end
410+
end
411+
end
412+
373413
def finish_statement_handle(handle)
374414
case @connection_options[:mode]
375415
when :dblib
376416
handle.cancel if handle
417+
when :odbc
418+
handle.drop if handle && handle.respond_to?(:drop) && !handle.finished?
377419
end
378420
handle
379421
end

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ module SQLServer
44
module Type
55
class Binary < ActiveRecord::Type::Binary
66

7+
def cast_value(value)
8+
if value.class.to_s == 'String' and !value.frozen?
9+
value.force_encoding(Encoding::BINARY) =~ /[^[:xdigit:]]/ ? value : [value].pack('H*')
10+
else
11+
value
12+
end
13+
end
14+
715
def type
816
:binary_basic
917
end

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'base64'
22
require 'active_record'
3+
require 'odbc_utf8'
34
require 'arel_sqlserver'
45
require 'active_record/connection_adapters/abstract_adapter'
56
require 'active_record/connection_adapters/sqlserver/core_ext/active_record'
@@ -169,6 +170,8 @@ def disconnect!
169170
case @connection_options[:mode]
170171
when :dblib
171172
@connection.close rescue nil
173+
when :odbc
174+
@connection.disconnect rescue nil
172175
end
173176
@connection = nil
174177
@spid = nil
@@ -356,6 +359,8 @@ def connect
356359
@connection = case config[:mode]
357360
when :dblib
358361
dblib_connect(config)
362+
when :odbc
363+
odbc_connect(config)
359364
end
360365
@spid = _raw_select('SELECT @@SPID', fetch: :rows).first.first
361366
@version_year = version_year
@@ -365,6 +370,7 @@ def connect
365370
def connection_errors
366371
@connection_errors ||= [].tap do |errors|
367372
errors << TinyTds::Error if defined?(TinyTds::Error)
373+
errors << ODBC::Error if defined?(ODBC::Error)
368374
end
369375
end
370376

@@ -400,6 +406,25 @@ def dblib_connect(config)
400406
end
401407
end
402408

409+
def odbc_connect(config)
410+
if config[:dsn].include?(';')
411+
driver = ODBC::Driver.new.tap do |d|
412+
d.name = config[:dsn_name] || 'Driver1'
413+
d.attrs = config[:dsn].split(';').map { |atr| atr.split('=') }.reject { |kv| kv.size != 2 }.reduce({}) { |a, e| k, v = e ; a[k] = v ; a }
414+
end
415+
ODBC::Database.new.drvconnect(driver)
416+
else
417+
ODBC.connect config[:dsn], config[:username], config[:password]
418+
end.tap do |c|
419+
begin
420+
c.use_time = true
421+
c.use_utc = ActiveRecord::Base.default_timezone == :utc
422+
rescue Exception
423+
warn 'Ruby ODBC v0.99992 or higher is required.'
424+
end
425+
end
426+
end
427+
403428
def config_appname(config)
404429
config[:appname] || configure_application_name || Rails.application.class.name.split('::').first rescue nil
405430
end

lib/active_record/sqlserver_base.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ def sqlserver_connection(config) #:nodoc:
77
case mode
88
when :dblib
99
require 'tiny_tds'
10+
when :odbc
11+
raise ArgumentError, 'Missing :dsn configuration.' unless config.key?(:dsn)
12+
require 'odbc'
13+
require 'active_record/connection_adapters/sqlserver/core_ext/odbc'
1014
else
1115
raise ArgumentError, "Unknown connection mode in #{config.inspect}."
1216
end

test/cases/adapter_test_sqlserver.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
1717
string = connection.inspect
1818
string.must_match %r{ActiveRecord::ConnectionAdapters::SQLServerAdapter}
1919
string.must_match %r{version\: \d.\d}
20-
string.must_match %r{mode: dblib}
20+
string.must_match %r{mode: (dblib|odbc)}
2121
string.must_match %r{azure: (true|false)}
2222
string.wont_match %r{host}
2323
string.wont_match %r{password}

test/cases/column_test_sqlserver.rb

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -273,28 +273,28 @@ def assert_obj_set_and_save(attribute, value)
273273
col.sql_type.must_equal 'date'
274274
col.type.must_equal :date
275275
col.null.must_equal true
276-
col.default.must_equal connection_dblib_73? ? Date.civil(0001, 1, 1) : '0001-01-01'
277-
obj.date.must_equal Date.civil(0001, 1, 1)
276+
col.default.must_equal Date.civil(1900, 1, 1)
277+
obj.date.must_equal Date.civil(1900, 1, 1)
278278
col.default_function.must_be_nil
279279
type = connection.lookup_cast_type_from_column(col)
280280
type.must_be_instance_of Type::Date
281281
type.limit.must_be_nil
282282
type.precision.must_be_nil
283283
type.scale.must_be_nil
284284
# Can cast strings. SQL Server format.
285-
obj.date = '04-01-0001'
286-
obj.date.must_equal Date.civil(0001, 4, 1)
285+
obj.date = '04-01-1900'
286+
obj.date.must_equal Date.civil(1900, 4, 1)
287287
obj.save!
288-
obj.date.must_equal Date.civil(0001, 4, 1)
288+
obj.date.must_equal Date.civil(1900, 4, 1)
289289
obj.reload
290-
obj.date.must_equal Date.civil(0001, 4, 1)
290+
obj.date.must_equal Date.civil(1900, 4, 1)
291291
# Can cast strings. ISO format.
292-
obj.date = '0001-04-01'
293-
obj.date.must_equal Date.civil(0001, 4, 1)
292+
obj.date = '1900-04-01'
293+
obj.date.must_equal Date.civil(1900, 4, 1)
294294
obj.save!
295-
obj.date.must_equal Date.civil(0001, 4, 1)
295+
obj.date.must_equal Date.civil(1900, 4, 1)
296296
obj.reload
297-
obj.date.must_equal Date.civil(0001, 4, 1)
297+
obj.date.must_equal Date.civil(1900, 4, 1)
298298
# Can keep and return assigned date.
299299
assert_obj_set_and_save :date, Date.civil(1972, 04, 14)
300300
# Can accept and cast time objects.

0 commit comments

Comments
 (0)