Skip to content

Merge commits made to 5-0-stable-with-odbc branch with 5-1-stable-odbc #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 16, 2019
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: 5 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ require 'openssl'
source 'https://rubygems.org'
gemspec

gem 'sqlite3'
gem 'sqlite3', '< 1.4'
gem 'minitest', '< 5.3.4'
gem 'bcrypt'
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
Expand Down Expand Up @@ -51,6 +51,10 @@ group :tinytds do
end
end

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

group :development do
gem 'byebug'
gem 'mocha'
Expand Down
9 changes: 7 additions & 2 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ task default: [:test]

namespace :test do

%w(dblib).each do |mode|
%w(dblib odbc).each do |mode|

Rake::TestTask.new(mode) do |t|
t.libs = ARTest::SQLServer.test_load_paths
Expand All @@ -23,12 +23,17 @@ namespace :test 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 activerecord-sqlserver-adapter.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ Gem::Specification.new do |spec|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ['lib']
spec.add_dependency 'activerecord', '~> 5.1.0'
spec.add_dependency 'tiny_tds'
spec.add_dependency 'ruby-odbc'
end
34 changes: 34 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,34 @@
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,11 @@ def exec_insert(sql, name = nil, binds = [], pk = nil, _sequence_name = nil)
end

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

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

def supports_statement_cache?
Expand Down Expand Up @@ -108,6 +106,18 @@ def execute_procedure(proc_name, *variables)
yield(r) if block_given?
end
result.each.map { |row| row.is_a?(Hash) ? row.with_indifferent_access : row }
when :odbc
results = []
raw_connection_run(sql) do |handle|
get_rows = lambda do
rows = handle_to_names_and_values handle, fetch: :all
rows.each_with_index { |r, i| rows[i] = r.with_indifferent_access }
results << rows
end
get_rows.call
get_rows.call while handle_more_results?(handle)
end
results.many? ? results : results.first
end
end
end
Expand Down Expand Up @@ -206,7 +216,13 @@ def sql_for_insert(sql, pk, id_value, sequence_name, binds)
sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{quoted_pk}"
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.blank?
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
super
end
Expand Down Expand Up @@ -282,6 +298,8 @@ def raw_connection_do(sql)
case @connection_options[:mode]
when :dblib
@connection.execute(sql).do
when :odbc
@connection.do(sql)
end
ensure
@update_sql = false
Expand Down Expand Up @@ -344,19 +362,25 @@ def raw_connection_run(sql)
case @connection_options[:mode]
when :dblib
@connection.execute(sql)
when :odbc
block_given? ? @connection.run_block(sql) { |handle| yield(handle) } : @connection.run(sql)
end
end

def handle_more_results?(handle)
case @connection_options[:mode]
when :dblib
when :odbc
handle.more_results
end
end

def handle_to_names_and_values(handle, options = {})
case @connection_options[:mode]
when :dblib
handle_to_names_and_values_dblib(handle, options)
when :odbc
handle_to_names_and_values_odbc(handle, options)
end
end

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

def handle_to_names_and_values_odbc(handle, options = {})
@connection.use_utc = ActiveRecord::Base.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)
case @connection_options[:mode]
when :dblib
handle.cancel if handle
when :odbc
handle.drop if handle && handle.respond_to?(:drop) && !handle.finished?
end
handle
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ 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
25 changes: 25 additions & 0 deletions lib/active_record/connection_adapters/sqlserver_adapter.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'base64'
require 'active_record'
require 'odbc_utf8'
require 'arel_sqlserver'
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/connection_adapters/sqlserver/core_ext/active_record'
Expand Down Expand Up @@ -169,6 +170,8 @@ def disconnect!
case @connection_options[:mode]
when :dblib
@connection.close rescue nil
when :odbc
@connection.disconnect rescue nil
end
@connection = nil
@spid = nil
Expand Down Expand Up @@ -356,6 +359,8 @@ def connect
@connection = case config[:mode]
when :dblib
dblib_connect(config)
when :odbc
odbc_connect(config)
end
@spid = _raw_select('SELECT @@SPID', fetch: :rows).first.first
@version_year = version_year
Expand All @@ -365,6 +370,7 @@ def connect
def connection_errors
@connection_errors ||= [].tap do |errors|
errors << TinyTds::Error if defined?(TinyTds::Error)
errors << ODBC::Error if defined?(ODBC::Error)
end
end

Expand Down Expand Up @@ -400,6 +406,25 @@ def dblib_connect(config)
end
end

def odbc_connect(config)
if config[:dsn].include?(';')
driver = ODBC::Driver.new.tap do |d|
d.name = config[:dsn_name] || 'Driver1'
d.attrs = config[:dsn].split(';').map { |atr| atr.split('=') }.reject { |kv| kv.size != 2 }.reduce({}) { |a, e| k, v = e ; a[k] = v ; a }
end
ODBC::Database.new.drvconnect(driver)
else
ODBC.connect config[:dsn], config[:username], config[:password]
end.tap do |c|
begin
c.use_time = true
c.use_utc = ActiveRecord::Base.default_timezone == :utc
rescue Exception
warn 'Ruby ODBC v0.99992 or higher is required.'
end
end
end

def config_appname(config)
config[:appname] || configure_application_name || Rails.application.class.name.split('::').first rescue nil
end
Expand Down
4 changes: 4 additions & 0 deletions lib/active_record/sqlserver_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ def sqlserver_connection(config) #:nodoc:
case mode
when :dblib
require 'tiny_tds'
when :odbc
raise ArgumentError, 'Missing :dsn configuration.' unless config.key?(:dsn)
require 'odbc'
require 'active_record/connection_adapters/sqlserver/core_ext/odbc'
else
raise ArgumentError, "Unknown connection mode in #{config.inspect}."
end
Expand Down
2 changes: 1 addition & 1 deletion test/cases/adapter_test_sqlserver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
string = connection.inspect
string.must_match %r{ActiveRecord::ConnectionAdapters::SQLServerAdapter}
string.must_match %r{version\: \d.\d}
string.must_match %r{mode: dblib}
string.must_match %r{mode: (dblib|odbc)}
string.must_match %r{azure: (true|false)}
string.wont_match %r{host}
string.wont_match %r{password}
Expand Down
20 changes: 10 additions & 10 deletions test/cases/column_test_sqlserver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -273,28 +273,28 @@ def assert_obj_set_and_save(attribute, value)
col.sql_type.must_equal 'date'
col.type.must_equal :date
col.null.must_equal true
col.default.must_equal connection_dblib_73? ? Date.civil(0001, 1, 1) : '0001-01-01'
obj.date.must_equal Date.civil(0001, 1, 1)
col.default.must_equal Date.civil(1900, 1, 1)
obj.date.must_equal Date.civil(1900, 1, 1)
col.default_function.must_be_nil
type = connection.lookup_cast_type_from_column(col)
type.must_be_instance_of Type::Date
type.limit.must_be_nil
type.precision.must_be_nil
type.scale.must_be_nil
# Can cast strings. SQL Server format.
obj.date = '04-01-0001'
obj.date.must_equal Date.civil(0001, 4, 1)
obj.date = '04-01-1900'
obj.date.must_equal Date.civil(1900, 4, 1)
obj.save!
obj.date.must_equal Date.civil(0001, 4, 1)
obj.date.must_equal Date.civil(1900, 4, 1)
obj.reload
obj.date.must_equal Date.civil(0001, 4, 1)
obj.date.must_equal Date.civil(1900, 4, 1)
# Can cast strings. ISO format.
obj.date = '0001-04-01'
obj.date.must_equal Date.civil(0001, 4, 1)
obj.date = '1900-04-01'
obj.date.must_equal Date.civil(1900, 4, 1)
obj.save!
obj.date.must_equal Date.civil(0001, 4, 1)
obj.date.must_equal Date.civil(1900, 4, 1)
obj.reload
obj.date.must_equal Date.civil(0001, 4, 1)
obj.date.must_equal Date.civil(1900, 4, 1)
# Can keep and return assigned date.
assert_obj_set_and_save :date, Date.civil(1972, 04, 14)
# Can accept and cast time objects.
Expand Down
Loading