Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add tinytds adapter, the best way to connect to MSSQL from a C based …

…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...
commit 7b645ed77da630e8cb71b521ec15367a38b9a0f2 1 parent b81bdca
@jeremyevans authored
View
4 CHANGELOG
@@ -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)
View
2  README.rdoc
@@ -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
View
28 doc/opening_databases.rdoc
@@ -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.
+
View
125 lib/sequel/adapters/tinytds.rb
@@ -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
View
2  lib/sequel/database/connecting.rb
@@ -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
View
2  spec/integration/prepared_statement_test.rb
@@ -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
View
4 spec/integration/timezone_test.rb
@@ -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
View
2  spec/integration/type_test.rb
@@ -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}]
Please sign in to comment.
Something went wrong with that request. Please try again.