Open-source project
github/github-ds avatar
github/github-ds

github-ds and the case for putting a Result object between you and the database

A collection of Ruby libraries for working with SQL on top of ActiveRecord's connection

700 stars29 forksRubyMIT

At a glance

What is it?
github-ds is a small set of Ruby libraries GitHub uses to run SQL and store key-value data on top of an ActiveRecord connection, with the KV and Result APIs returning a Result object instead of raising. It is not a query builder, and the design choice it does make, error handling as a return value, changes how the calling code reads.
Who is it for?
github-ds suits a Rails application that already has a MySQL connection and wants raw SQL with proper bind values, a key-value store without adding a second datastore, and error handling that does not unwind the stack. It does not suit a team that wants a query builder or an ORM, because the SQL is yours to write, and it does not suit a project that expects a maintained community roadmap, because the stated plan is continued maintenance only.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Three libraries, one ActiveRecord connection

github-ds is a collection of Ruby libraries for working with SQL on top of ActiveRecord's connection, and it is three libraries rather than one.

GitHub::KV is a key/value data store backed by MySQL. GitHub::SQL is for building and executing a SQL query, and it uses ActiveRecord's connection class while providing a better API for bind values and raw data access. GitHub::Result makes it easier to bake in resiliency by using a Result object instead of raising exceptions.

That last one is the thread through all three. The other two could have been separate libraries, but the reason they work together is that a key-value miss and a failed query both come back as the same kind of value.

The status note explains the trade in plain terms. The project is used in production extensively at GitHub, and because of that all changes will be thoroughly vetted, which could slow the process of contributing. The project says it will do its best to actively communicate the status of pull requests, and asks that anyone with substantial changes open an issue first to discuss.

The licence is MIT, the primary language is Ruby, and the last push to the repository was on 2026-10-16.

The Result object is the reason to look at this at all

The Result class is a small type, and the examples show both of its modes.

A successful call gives you a value and no exception:

ruby
result = GitHub::Result.new { do_something }
p result.ok? # => true
p result.value! # => 1

A failing one gives you the error instead:

ruby
result = GitHub::Result.new { do_something_that_errors }
p result.ok? # => false
p result.value { "default when error happens" } # => "default when error happens"
begin
  result.value! # raises exception because error happened
rescue => error
  p result.error
  p error
end

So there are three ways to read the outcome. ok? is a predicate with no side effect. value with a block gives you a fallback without an exception. value! raises, so it behaves like the call would have without the wrapper.

The key-value API uses the first form throughout. A get that finds nothing returns a Result whose value is nil rather than raising, and while the set and delete operations return nil directly. That consistency is what lets the library say it is used in production extensively without every caller writing a begin block.

The then method is the other half, and it is where the resiliency claim gets tested:

ruby
result = GitHub::Result.new {
  GitHub::Result.new { puts "Step 1: success!" }
}.then { |value|
  GitHub::Result.new { puts "Step 2: success!" }
}.then { |value|
  GitHub::Result.new { puts "Step 3: success!" }
}

When a step in the middle raises, the chain stops there and ok? is false, and the later step never runs. That is short-circuiting behaviour for a sequence of fallible operations, without exceptions crossing the chain.

GitHub::SQL is a query builder that only builds SQL

The SQL class is a thin layer, and the example makes the shape clear.

There are three static entry points, results, run and value, each taking a SQL string and optionally bind values:

ruby
GitHub::SQL.results <<-SQL
  SELECT * FROM example_key_values
SQL

GitHub::SQL.run <<-SQL, key: "foo", value: "bar"
  INSERT INTO example_key_values (`key`, `value`)
  VALUES (:key, :value)
SQL

The bind values are what make it more than a string wrapper. In the run example the values are passed as named parameters and bound with the :key and :value placeholders, which means the value does not get interpolated into the SQL text.

The instance form exists for the case where the query is built up conditionally, and the add method appends fragments to an existing query:

ruby
sql = GitHub::SQL.new <<-SQL
  SELECT `value` FROM example_key_values
SQL

key = ENV["KEY"]
unless key.nil?
  sql.add <<-SQL, key: key
    WHERE `key` = :key
  SQL
end

Note what this class is not. There is no schema abstraction, no relation, no lazy loading, no chainable where clause. It builds a SQL string with bound values and runs it on the ActiveRecord connection. The comparison with ActiveRecord's own query interface is therefore not a competition, and if you want the ORM, this is not it.

The examples directory holds separate files for the plain query, the add method, the key-value store, the Result type and running with an explicit connection, so the intended usage is documented per feature rather than in one narrative.

GitHub::KV is a MySQL key-value store with a schema you migrate yourself

The key-value store needs a table, and the library ships a Rails migration generator for it:

code
rails generate github:ds:active_record
rails db:migrate

The table name is configurable, and the README shows configuring it before the migration runs:

code
GitHub::KV.configure do |config|
  config.table_name = "new_key_values_table"
end

After that, the API is the one you would expect from a key-value store. There is get, set, mget for several keys at once, exists, mexists, setnx to set only if the key is absent, del, and mdel. The m-prefixed forms are batch variants, and in the example a get of a missing key returns a Result whose value is nil rather than raising.

The instance is created with an ActiveRecord connection, and the example passes the default one:

ruby
kv = GitHub::KV.new { ActiveRecord::Base.connection }

There is one behavioural quirk worth knowing before you build on it. Due to MySQL's default collation, KV keys are case-insensitive. So a key set as Foo and a key looked up as foo are the same key, which is fine for most uses and quietly wrong for the ones where case is meaningful.

Expiration is honoured but not cleaned up

The caveats section is short and it covers exactly one thing, and the thing is a real operational detail.

KV supports expiring keys and obeys expiration when performing operations, but it does not actually purge the expired rows. At GitHub, the team uses pt-archiver from the Percona toolkit to nibble the expired rows away, configured with a replica lag check and a specific set of options: the index name set to the index on the key values table for expires_at, a limit of 1000, and a where clause of expires_at less than or equal to the current time.

The shape of this is worth noticing. The library enforces the semantic, so a get on an expired key behaves as if it is gone, and the physical cleanup is left to a separate archival tool. That is a reasonable division of responsibility, since a library that deleted rows during reads would turn every read into a write.

The cost is that adopting KV means adopting pt-archiver too, or accepting a table that grows forever. The key_values table needs an index on expires_at for the archiver to work efficiently, and the configuration assumes one exists.

The index name in the configuration is specific enough to be a hint about the schema, since it names the key values table and the expires_at column, and the where clause is written against the current time in SQL rather than passed in from Ruby.

Local development, the MySQL requirement, and a project that plans to stay put

The development instructions are conventional Ruby, and the one requirement is the one that trips people up.

After checking out the repository you run script/bootstrap to install dependencies, script/test to run the tests, and script/console for an interactive prompt to experiment in. To install the gem locally you run script/install, and to release a new version you update the number in version.rb, commit, and run script/release, which creates a git tag, pushes the commits and tags, and pushes the gem file to rubygems.org.

The test requirement is specific: you need a MySQL database with no password set for the root user, and running docker-compose up boots just that. The project says this functionality is not currently used by GitHub and was from a contributor, so it asks you to say if it does not work or gets out of date.

The compose file is minimal. It is a version 3 file with a single mysql service on the 5.6 image, with the empty root password allowed through an environment variable and port 3306 published.

The roadmap section is the most telling part of the whole document. Nothing is currently on the radar other than continued maintenance, and the project invites you to open an issue if you have a big idea.

The maintainers table names four people, and the contributing section asks contributors to adhere to the Contributor Covenant and to read the contributing guide. Bug reports and pull requests are welcome on GitHub, and the project describes itself as intended to be a safe, welcoming space for collaboration.

Editorial conclusion

github-ds suits a Rails application that already has a MySQL connection and wants raw SQL with proper bind values, a key-value store without adding a second datastore, and error handling that does not unwind the stack. It does not suit a team that wants a query builder or an ORM, because the SQL is yours to write, and it does not suit a project that expects a maintained community roadmap, because the stated plan is continued maintenance only. Before adopting it, look at the KV expiration behaviour, since expired rows are honoured but never purged by the library itself, and note that the last push was on 2026-10-16 with the project publishing no GitHub releases.

Frequently asked questions

What is github-ds?

It is a collection of Ruby libraries for working with SQL on top of an ActiveRecord connection. GitHub::SQL builds and executes queries with proper bind values, GitHub::KV is a key-value store backed by MySQL, and GitHub::Result returns a Result object instead of raising exceptions.

How do I install github-ds?

Add gem 'github-ds' to your Gemfile and run bundle, or install it yourself with gem install github-ds. To use the key-value store you also need to create the key_values table with the included Rails migration generator, rails generate github:ds:active_record followed by rails db:migrate.

Does github-ds expire keys in GitHub::KV, and are expired rows deleted?

KV honours expiration when performing operations, so an expired key behaves as if it is gone, but it does not purge the expired rows. The project handles that at GitHub with pt-archiver, configured to check replica lag and delete up to 1000 rows at a time where expires_at is less than or equal to the current time.

What does GitHub::Result give me compared to raising?

Three ways to read the outcome. ok? is a predicate with no side effect, value with a block returns a fallback without raising, and value! raises so it behaves like the unwrapped call. The then method chains fallible steps and short-circuits when one raises, leaving the later steps unrun.

Is github-ds an ORM or a query builder?

Neither. GitHub::SQL builds a SQL string with bound values and runs it on the ActiveRecord connection, with no schema abstraction, relations, lazy loading or chainable where clause. If you want ActiveRecord's own query interface, this library is not a replacement for it.

Official sources

  1. github/github-ds on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/github-github-ds.svg)](https://hysenlabs.com/projects/github-github-ds)

Was this useful?