# TinyTDS is the FreeTDS binding Rails uses to reach SQL Server

> tiny_tds is a Ruby gem that talks to Microsoft SQL Server through the FreeTDS DB-Library API, giving native Ruby types, proper encodings and a three class API for connecting, querying and iterating results. It is a low level driver rather than an ORM adapter, and the install path differs sharply between precompiled gems, Homebrew and building FreeTDS yourself.

**rails-sqlserver/tiny_tds** — TinyTDS - Simple and fast FreeTDS bindings for Ruby using DB-Library.

- Repository: https://github.com/rails-sqlserver/tiny_tds
- Stars: 616 · Forks: 187
- Language: Ruby
- License: NOASSERTION
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/rails-sqlserver-tiny-tds

## Three classes, and nothing else in the way

The described API of TinyTDS is three classes, and the brevity is the design.

TinyTds::Client is the connection to the database. TinyTds::Result is returned from issuing an execute on the connection, and it includes Enumerable. TinyTds::Error is a wrapper for all FreeTDS exceptions.

The stated purpose is the extremely common use case of connecting, querying and iterating over results to Microsoft SQL Server from Ruby using the FreeTDS DB-Library API. That is a deliberately narrow sentence. TinyTDS is not an ActiveRecord adapter and does not pretend to be one; it is the layer underneath such things, and the repository lives under the rails-sqlserver organisation alongside the SQL Server adapter, which is where the gemspec points its homepage, issues and documentation links.

Two capabilities are called out as differentiators. The first is automatic casting to Ruby primitives with proper encoding support, converting SQL Server datatypes to native Ruby primitives while supporting either :utc or :local time zones for time-like types. The second is client encoding options, which the documentation says are unique among Ruby client libraries for SQL Server, defaulting to UTF-8. String and binary data is properly encoded on the way through.

So the value proposition is correctness at the boundary rather than convenience above it. If you need a query builder, an ActiveRecord adapter is a different project.

## The install path depends on who compiles the C

Installation is where the FreeTDS dependency becomes concrete, and the README splits it into three cases.

For Windows and Linux on 64-bit, the project precompiles tiny_tds with FreeTDS and supporting libraries, which are dynamically linked at runtime. In that case the install is one command, and the gem should find the platform specific gem:

```shell
gem install tiny_tds
```

If you would rather compile FreeTDS and the supporting libraries yourself, the same gem can be forced to the generic platform:

```shell
gem install tiny_tds --platform ruby
```

On Mac, FreeTDS comes from Homebrew, in two steps. The dependencies go in first:

```shell
brew install openssl@3 libiconv
brew install freetds
```

and then the gem itself with the usual command.

Everybody else is on the source path. The gem finds FreeTDS and the other libraries based on the compiler paths, and the README gives a Debian example that builds FreeTDS 1.4.23 from source with a prefix of /usr/local, TDS version 7.4, and ODBC disabled:

```shell
$ apt-get install wget
$ apt-get install build-essential
$ apt-get install libc6-dev

$ wget http://www.freetds.org/files/stable/freetds-1.4.23.tar.gz
$ tar -xzf freetds-1.4.23.tar.gz
$ cd freetds-1.4.23
$ ./configure --prefix=/usr/local --with-tdsver=7.4 --disable-odbc
$ make
$ make install
```

And if FreeTDS lives somewhere the compiler does not look, the gem accepts a flag pointing at it:

```shell
gem install tiny_tds -- --with-freetds-dir=/opt/freetds
```

The practical takeaway is that a precompiled gem exists for common Linux and Windows 64-bit setups, and everything else is a native build. The gem is tested with Ruby v2.7 and upwards.

## The TDS version decides whether dates arrive as dates

The data type section contains the detail that decides whether a query result is useful, and it comes down to the protocol version in use.

The goal stated is to support every SQL Server data type and convert it to a logical Ruby object. When dates or times are returned, they are instantiated to either :utc or :local time depending on the query options. All strings are associated with the connection's encoding, and all binary data types are associated with Ruby's ASCII-8BIT/BINARY encoding.

The list of supported types is given for the 7.3 TDS protocol version, and it includes date, datetime2, datetimeoffset and time. The critical sentence is the next one: using a lower protocol version will result in these types being returned as strings.

That is the practical trap. The client option tds_version defaults to 7.3, so the default configuration gets proper date and time objects. If your freetds.conf or your server forces a lower TDS version, the same query returns strings that your application code has to parse, and the failure is silent in the sense that nothing errors, the values are just the wrong type.

The text also states that only datetimeoffset types are excluded, while the list of supported types includes datetimeoffset. Those two statements sit awkwardly together, and the safe reading for an adopter is to test the specific types the application depends on rather than assume either one.

## Connection options cover the awkward cases

The client takes a hash of options, and the documented set is worth reading in full because a few of them are the difference between working and not working.

username and password are the obvious ones. dataserver can be the name for a data server as defined in freetds.conf, and a raw hostname or hostname:port works too. The documentation notes that FreeTDS will accept a named instance such as localhost\SQLEXPRESS, but strongly suggests using the :host and :port options instead, with a pointer to how to find the host port for named instances. host is used when dataserver is blank, and port defaults to 1433 and is only used with host. database is the default database, and appname is a short string seen in the SQL Server process activity window.

Timeouts are separate. login_timeout is the seconds to wait for login, defaulting to 60, while timeout is the seconds to wait for a response to a SQL command, defaulting to 5. The documentation adds a specific caveat about that second one: timeouts caused by network failure will raise a timeout error one second after the configured timeout limit is hit, with a pull request reference for the details.

Three options handle the modern SQL Server deployment. azure signals a connection to Azure. contained signals a connection with a contained database user. use_utf16 defaults to true, and means UTF-16 is used for database wide character encoding instead of UCS-2, since newer Windows versions use that encoding.

The connection is created like this:

```ruby
client = TinyTds::Client.new username: 'sa', password: 'secret', host: 'mydb.host.net'
```

## Connection state has three separate questions

There is a small but instructive piece of API design in how connection health is exposed, and the documentation walks through it with a live example.

The #active? method determines if a connection is good, and the implementation may change but it should always guarantee that a connection is good. Currently it checks for either a closed or dead connection. Three methods, three states:

```ruby
client.dead?    # => false
client.closed?  # => false
client.active?  # => true
client.execute("SQL TO A DEAD SERVER")
client.dead?    # => true
client.closed?  # => false
client.active?  # => false
client.close
client.closed?  # => true
client.active?  # => false
```

The point of the example is that a dead connection is not a closed one. A connection that has failed at the network level is dead while still being open at the socket level, so closed? returns false and the connection looks alive to any code that only checks that. The active? method exists to give one call that answers the question you actually care about.

That is a small thing, but it is the kind of detail that a driver written by people who have been burned by it will have, and it is the reason to prefer #active? over a nil check on the socket.

## Results are lazy, and forgetting that breaks the next query

The result object is where a driver either behaves or does not, and the documentation is explicit about the contract.

A result object is returned by the client's execute command. It is important that you either return the data from the query, most likely with the #each method, or that you cancel the results before asking the client to execute another SQL batch. Failing to do so will yield an error.

Calling #each on the result lazily loads each row from the database.

So there are two ways to be correct. Consume the result with each, or cancel it. Execute a second query while a first result is still open and the client raises, which is the standard behaviour of a streaming protocol where the server is still sending rows on the same session.

The escape hatch is the message handler, which is a callable such as a Proc or a method that receives info messages from the database, with a single parameter which is a TinyTds::Error object representing the message. The documented example shows it being used to surface informational output:

```ruby
opts = ... # host, username, password, etc
opts[:message_handler] = Proc.new { |m| puts m.message }
client = TinyTds::Client.new opts
# => Changed database context to 'master'.
# => Changed language setting to us_english.
client.execute("print 'hello world!'").do
# => hello world!
```

Note the .do call on the result there. It is the explicit way to run a statement that returns no rows, and it avoids the case where a result is opened and never consumed.

## Where TinyTDS is the wrong tool

Three situations rule it out.

The first is when you want an ORM. TinyTDS gives you a client, a result and an error. There is no query builder, no migration tool, no model layer, no connection pool in the gem, no schema reflection. If you want ActiveRecord semantics on SQL Server, the ActiveRecord adapter is the thing to install, and it will pull TinyTDS in underneath. Choosing the driver directly means accepting that you are writing the data access layer yourself.

The second is a platform where you cannot install system libraries. The gem is a native extension linked against FreeTDS, so a container image needs FreeTDS present, a Lambda style function needs a layer or a custom build, and a locked down machine needs the precompiled gem to match its platform exactly. The README covers Windows and Linux 64-bit with precompiled binaries, and Mac through Homebrew, but there is no path described for a musl based container or an unusual architecture.

The third is a workload that needs features FreeTDS does not carry. The driver inherits the limits of the DB-Library API it wraps, including the TDS protocol versions it negotiates, and the FreeTDS version you compile against determines the data types available. A modern SQL Server feature exposed only through the newer TDS versions or through a proprietary extension is not reachable through this layer.

The alternative worth naming is the official Microsoft ODBC Driver for SQL Server with the ruby-odbc gem, or the older ADO path through the adonet-sqlserver provider, both of which use ODBC rather than DB-Library. The trade is real: ODBC adds a configuration layer and a driver dependency, but it is the vendor's own implementation with a broader feature surface, whereas TinyTDS is smaller, has fewer moving parts, and is the path the Ruby on Rails SQL Server adapter itself takes.

## Conclusion

TinyTDS fits a Ruby service or script that needs SQL Server without pulling in ActiveRecord, particularly one where type casting and encoding correctness matter more than a rich query builder, and it is the layer the Rails SQL Server adapter is built on. It is the wrong choice if you want an ORM, since it gives you a client, a result and an error and nothing more, and it is the wrong choice on a platform where you cannot install system libraries, because the gem needs FreeTDS either precompiled or built from source. Before adopting it, check three things: that the precompiled gem covers your platform and architecture, that the FreeTDS protocol version in your freetds.conf matches the 7.3 default so that date and time types arrive as Ruby objects rather than strings, and that the licence in the repository matches your use, since the project records a custom licence classification while shipping an MIT-LICENSE file.

## FAQ

### What is TinyTDS and what is it used for?

tiny_tds is a Ruby gem that provides simple and fast FreeTDS bindings using the DB-Library API, meant for connecting, querying and iterating over results from Microsoft SQL Server. Its API is three classes: TinyTds::Client, TinyTds::Result which includes Enumerable, and TinyTds::Error which wraps all FreeTDS exceptions.

### How do I install the tiny_tds gem on Linux?

On 64-bit Windows and Linux, precompiled gems exist with FreeTDS and the supporting libraries linked at runtime, so gem install tiny_tds is enough. To compile FreeTDS yourself, use gem install tiny_tds --platform ruby, or point the gem at an existing installation with gem install tiny_tds -- --with-freetds-dir=/opt/freetds.

### Why are my date and time columns coming back as strings?

The documented type conversions for date, datetime2, datetimeoffset and time apply when using the 7.3 TDS protocol version, and the client option tds_version defaults to 7.3. Using a lower protocol version results in these types being returned as strings instead of Ruby date and time objects.

### How do I check whether a TinyTDS connection is still usable?

Use the #active? method, which currently checks for either a closed or dead connection and is intended to always report whether the connection is good. A dead connection is not necessarily a closed one, so closed? alone is not a sufficient health check.

### Why does my second query fail after the first one?

A result object is returned by execute, and the documentation requires that you either return the data from the query with the #each method or cancel the results before executing another SQL batch. Failing to do so yields an error, because the rows are loaded lazily while the connection is still busy.

## Sources

- [Issues](https://github.com/rails-sqlserver/tiny_tds/issues)
- [rails-sqlserver/tiny_tds on GitHub](https://github.com/rails-sqlserver/tiny_tds)
- [README](https://github.com/rails-sqlserver/tiny_tds/blob/master/README.md)
- [Releases](https://github.com/rails-sqlserver/tiny_tds/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/rails-sqlserver-tiny-tds
