Skip to content

Commit

Permalink
Add tinytds adapter, the best way to connect to MSSQL from a C based …
Browse files Browse the repository at this point in the history
…ruby running on *nix

Before, the only real way to connect to MSSQL from a C based ruby
on *nix was to use the ODBC adapter with unixodbc and freetds.
I've heard it's a big pain to set up, and never attempted to do
so myself.  Fortunately, tiny_tds was released recently and makes
it simple.

Thanks to metaskills from the tiny_tds project for making changes
to tiny_tds to better support Sequel.
  • Loading branch information
jeremyevans committed Feb 14, 2011
1 parent b81bdca commit 7b645ed
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 7 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
=== HEAD

* Recognize bigint unsigned as a Bignum type in the schema dumper (gamespy-tech)
* Add tinytds adapter, the best way to connect to MSSQL from a C based ruby running on *nix (jeremyevans)

* Recognize bigint unsigned as a Bignum type in the schema dumper (gamespy-tech) (#327)

* Add Dataset#calc_found_rows for MySQL datasets (macks)

Expand Down
2 changes: 1 addition & 1 deletion README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ toolkit for Ruby.
configurations, and database sharding.
* Sequel currently has adapters for ADO, Amalgalite, DataObjects,
DB2, DBI, Firebird, Informix, JDBC, MySQL, Mysql2, ODBC, OpenBase,
Oracle, PostgreSQL, SQLite3, and Swift.
Oracle, PostgreSQL, SQLite3, Swift, and TinyTDS.

== Resources

Expand Down
28 changes: 28 additions & 0 deletions doc/opening_databases.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,31 @@ Examples:
The following additional options are supported:

* :timeout - the busy timeout to use in milliseconds (default: 5000).

=== swift

swift is a ruby 1.9 only library, so you'll need to be running ruby 1.9. It
can connect to SQLite, MySQL, and PostgreSQL, and you must specify which
database using the db_type option.

Examples:

swift:///?database=:memory:&db_type=sqlite
swift://root:root@localhost/test?db_type=mysql
swift://root:root@localhost/test?db_type=postgres

=== tinytds

Because the underscore is not a valid character in a URI schema, the adapter
is named tinytds instead of tiny_tds. The connection options are passed directly
to tiny_tds, except that the tiny_tds :dataserver and :username options are set to
the Sequel :host and :user options. The :host option should be an entry in the
freetds.conf file, it's not currently possible to a host not present in the
freetds.conf file. Some options that you may want to set are
:login_timeout, :timeout, :appname, and :encoding, see the tiny_tds README for details.
For highest performance, you should disable any identifier output method when
using the tinytds adapter, which probably means disabling any identifier input method
as well. The default for Microsoft SQL Server is to :downcase identifiers on output
and :upcase them on input, so the highest performance will require changing the setting
from the default.

125 changes: 125 additions & 0 deletions lib/sequel/adapters/tinytds.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
require 'tiny_tds'
Sequel.require 'adapters/shared/mssql'

module Sequel
module TinyTDS
class Database < Sequel::Database
include Sequel::MSSQL::DatabaseMethods
set_adapter_scheme :tinytds

# Transfer the :host and :user options to the
# :dataserver and :username options.
def connect(server)
opts = server_opts(server)
opts[:dataserver] = opts[:host]
opts[:username] = opts[:user]
TinyTds::Client.new(opts)
end

# Return instance of Sequel::TinyTDS::Dataset with the given options.
def dataset(opts = nil)
TinyTDS::Dataset.new(self, opts)
end

# Execute the given +sql+ on the server. If the :return option
# is present, its value should be a method symbol that is called
# on the TinyTds::Result object returned from executing the
# +sql+. The value of such a method is returned to the caller.
# Otherwise, if a block is given, it is yielded the result object.
# If no block is given and a :return is not present, +nil+ is returned.
def execute(sql, opts={})
synchronize(opts[:server]) do |c|
begin
m = opts[:return]
r = nil
log_yield(sql) do
r = c.execute(sql)
return r.send(m) if m
end
yield(r) if block_given?
rescue TinyTds::Error => e
raise_error(e)
ensure
r.cancel if r && c.sqlsent?
end
end
end

# Return the number of rows modified by the given +sql+.
def execute_dui(sql, opts={})
execute(sql, opts.merge(:return=>:do))
end

# Return the value of the autogenerated primary key (if any)
# for the row inserted by the given +sql+.
def execute_insert(sql, opts={})
execute(sql, opts.merge(:return=>:insert))
end

# Execute the DDL +sql+ on the database and return nil.
def execute_ddl(sql, opts={})
execute(sql, opts.merge(:return=>:each))
nil
end

private

# For some reason, unless you specify a column can be
# NULL, it assumes NOT NULL, so turn NULL on by default unless
# the column is a primary key column.
def column_list_sql(g)
pks = []
g.constraints.each{|c| pks = c[:columns] if c[:type] == :primary_key}
g.columns.each{|c| c[:null] = true if !pks.include?(c[:name]) && !c[:primary_key] && !c.has_key?(:null) && !c.has_key?(:allow_null)}
super
end

# Close the TinyTds::Client object.
def disconnect_connection(c)
c.close
end
end

class Dataset < Sequel::Dataset
include Sequel::MSSQL::DatasetMethods

# Yield hashes with symbol keys, attempting to optimize for
# various cases.
def fetch_rows(sql)
execute(sql) do |result|
each_opts = {:cache_rows=>false}
each_opts[:timezone] = :utc if Sequel.database_timezone == :utc
offset = @opts[:offset]
@columns = cols = result.fields.map{|c| output_identifier(c)}
if identifier_output_method
each_opts[:as] = :array
result.each(each_opts) do |r|
h = {}
cols.zip(r).each{|k, v| h[k] = v}
h.delete(row_number_column) if offset
yield h
end
else
each_opts[:symbolize_keys] = true
if offset
result.each(each_opts) do |r|
r.delete(row_number_column) if offset
yield r
end
else
result.each(each_opts, &block)
end
end
end
self
end

private

# Properly escape the given string +v+.
def literal_string(v)
db.synchronize{|c| "N'#{c.escape(v)}'"}
end
end
end
end
2 changes: 1 addition & 1 deletion lib/sequel/database/connecting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class Database
# ---------------------

# Array of supported database adapters
ADAPTERS = %w'ado amalgalite db2 dbi do firebird informix jdbc mysql mysql2 odbc openbase oracle postgres sqlite swift'.collect{|x| x.to_sym}
ADAPTERS = %w'ado amalgalite db2 dbi do firebird informix jdbc mysql mysql2 odbc openbase oracle postgres sqlite swift tinytds'.collect{|x| x.to_sym}

# Whether to use the single threaded connection pool by default
@@single_threaded = false
Expand Down
2 changes: 1 addition & 1 deletion spec/integration/prepared_statement_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@
@ds.filter(:d=>@ds.ba(:$x, :date)).prepare(:first, :ps_date).call(:x=>@vs[:d])[:d].should == @vs[:d]
end

cspecify "should handle datetime type", [:do], [:mysql2], [:swift], [:jdbc, :sqlite] do
cspecify "should handle datetime type", [:do], [:mysql2], [:swift], [:jdbc, :sqlite], [:tinytds] do
Sequel.datetime_class = DateTime
@ds.filter(:dt=>@ds.ba(:$x, :timestamp)).prepare(:first, :ps_datetime).call(:x=>@vs[:dt])[:dt].should == @vs[:dt]
end
Expand Down
4 changes: 2 additions & 2 deletions spec/integration/timezone_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ def test_timezone
Sequel.datetime_class = Time
end

cspecify "should support using UTC for database storage and local time for the application", [:swift], [:do, proc{|db| db.database_type != :sqlite}] do
cspecify "should support using UTC for database storage and local time for the application", [:swift], [:tinytds], [:do, proc{|db| db.database_type != :sqlite}] do
Sequel.database_timezone = :utc
Sequel.application_timezone = :local
test_timezone
end

cspecify "should support using local time for database storage and UTC for the application", [:swift], [:do, proc{|db| db.database_type != :sqlite}] do
cspecify "should support using local time for database storage and UTC for the application", [:swift], [:tinytds], [:do, proc{|db| db.database_type != :sqlite}] do
Sequel.database_timezone = :local
Sequel.application_timezone = :utc
test_timezone
Expand Down
2 changes: 1 addition & 1 deletion spec/integration/type_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def create_items_table_with_column(name, type, opts={})
ds.first[:tim].strftime('%Y%m%d%H%M%S').should == t.strftime('%Y%m%d%H%M%S')
end

cspecify "should support generic file type", [:do], [:odbc, :mssql], [:mysql2], [:swift] do
cspecify "should support generic file type", [:do], [:odbc, :mssql], [:mysql2], [:swift], [:tinytds] do
ds = create_items_table_with_column(:name, File)
ds.insert(:name => ("a\0"*300).to_sequel_blob)
ds.all.should == [{:name=>("a\0"*300).to_sequel_blob}]
Expand Down

0 comments on commit 7b645ed

Please sign in to comment.