noteflakes

Extralite - a new Ruby gem for working with SQLite databases

15·12·2021

In the last year I’ve been working a lot with SQLite databases. I started by using the popular sqlite3-ruby Ruby gem, but quickly noticed that for my usage there were a few things missing in the gem’s API. Being a tinkerer, and having had some experience writing C-extensions, I had a look at the SQLite C API and decided to try to write my own Ruby bindings for SQLite. Thus Extralite was born.

Extralite is an extra-lightweight (less than 460 lines of C-code) SQLite3 wrapper for Ruby. It provides a single class with a [minimal set of methods] for interacting with an SQLite3 database. Extralite provides the following improvements over the sqlite3-ruby gem:

Concurrency

One of the most important limitations of the sqlite3-ruby gem is that it doesn’t release the GVL while running queries and fetching rows from the database. This means that if any query takes a significant amount of time to execute, other threads will also be blocked while the query is running.

There has been some discussion on the sqlite3-ruby repository as to why this is. Basically, since developers can define their own SQLite functions, aggregates and collations using Ruby, the gem needs to hold on to the GVL while running queries, since those might call back into Ruby code.

Extralite does release the GVL when running queries, which makes it much more friendly to multithreaded code. While Extralite is busy fetching a row, other threads can continue running. If your program has multiple threads accessing SQLite databases at the same time, you’ll get much better usage out of your multicore machine.

Performance

Preliminary benchmarks show Extralite to be significantly faster than sqlite3-ruby. The benchmark included in the Extralite repository creates a database with varying number of rows, then meausres the time it takes for sqlite3-ruby and Extralite to fetch those rows. The performance advantage becomes more pronounced as the number of rows is increased:

Row count sqlite3-ruby Extralite Relative
10 56.48K rows/s 91.52K rows/s 1.62x
1K 256.3K rows/s 1758K rows/s 6.87x
100K 176.5K rows/s 2323.6K rows/s 13.17x

These results surprised me quite a bit, since Extralite does release the GVL on each fetched row, but I guess this is more than made up for by the fact that Extralite makes a minimum of allocations and offers a significantly smaller API surface area, compared with sqlite3-ruby.

Other features

Extralite provides a variety of ways to get query results: rows as hashes, rows as arrays, a single column, a single row, or a single value. While sqlite3-ruby has most of these (except for iterating over a single column), you need to set a mode (SQLite3::Database#results_as_hash) if you want to fetch rows as hashes.

Another important feature of Extralite is that it automatically executes SQL strings containing multiple SQL statements (separated with a semicolon.) In sqlite3-ruby you need to use a separate API (#execute_batch) in order to do that.

Other features, such as binding indexed and named parameters, getting the last inserted rowid, getting the number of changes made in the last query or loading extensions, are available in both gems.

What’s missing

Extralite is notably missing the ability to define custom functions, aggregates and collations using Ruby. If you rely on that feature, you’ll need to use sqlite3-ruby.

Usage with ORMs

Extralite includes an adapter for Sequel. If you wish to switch from sqlite3-ruby to Extralite, you can just add extralite to your Gemfile, and then change your database URLs to use the extralite schema instead of sqlite:

DB = Sequel.connect('extralite://my.db')

What about ActiveRecord? Well, I tried, but after spending a few hours looking at the ActiveRecord SQLite adapter code and trying to make sense out of that, all I got was cryptic error messages. I finally decided to abandon the effort. If you have experience writing ActiveRecord database adapters, I’ll greatly appreciate your contribution. Let me know on the Extralite repository.

Future directions

Extralite is pretty much feature-complete as far as I’m concerned, apart from missing an ActiveRecord adapter. I’m currently thinking about how to adapt the different abstractions I came up with while working with SQLite databases, but those will be published in a separate project.

In the meanwhile, if you have suggestions for improving Extralite, or wish to contribute, please let me know. I’ll gladly accept issues and PRs! The documentation for the Extralite gem can be found here.