Persistant Sets of Structured Data

db-struct is a Ruby gem that provides class DBStruct, a class that is similar to Ruby’s built-in Struct class but stores its data in a SQLite database. In addition, each subclass also provides access to the database via an interface that closely mimics a Ruby Hash, including support for enumeration.

It is currently at the “experimental toy” stage of development.

Installation

Simply install it via gem:

gem install --prerelease db-struct

Note that you will also need to install SQLite3 separately. On stock Ruby, that’s:

gem install sqlite3

while with JRuby, it’s:

gem install jdbc-sqlite3

It uses Sequel to do the heavy lifting. This is installed as a dependency, but you’ll need to know how to open a database with it.

Overview

Let’s start with a simple example, a table of books. The first thing we need to do is create a database connection:

DB = Sequel.sqlite("books.sqlite3")

We can then define the structure and the underlying table:

Book = DBStruct.with(DB, :books) do
  field :title,     String
  field :author,    String
  field :date,      Time
  field :edition,   Integer
end

Book is now a subclass of DBStruct; creating it will also create a table named :books if it doesn’t exist.

We can create an instance of Book like this:

b1 = Book.new(title:   "Diseases of the Dragon",
              author:  "Lady Sybil Ramkin",
              date:    Time.new(1989, 11, 1),
              edition: 1)

The contents are immediately written to the database, but this object behaves more or less like a Ruby Struct:

puts b1.title
b1.author = "Lady Sybil Ramkin-Vimes"
puts b1.author

Note that these are not part of a transaction. If you need that (and you probably will), you can the transaction method:

Book.transaction {
  puts b1.title
  b1.author = "Lady Sybil Ramkin-Vimes"
  puts b1.author
}

This starts a transaction, evaluates the block and commits. If there is an exception inside the block, the transaction is rolled back instead. Transactions can be safely nested. (DBStruct#transaction is simply a thin wrapper around Sequel::Database#transaction.)

The class method items returns a DBStruct::BogoHash, which behaves like a Hash mapping numeric row IDs to corresponding Book objects:

puts Book.items[b1.rowid].title

The usual enumeration operations are also available:

Book.items.values.each{|book|
    puts "  #{book.title} by #{book.author}"
}

first_editions = Book.items
  .select{|id, book| book.edition == 1}
  .map{|id, _| id}

You can also add a special field type called a group. This can be used to subdivide the table:

Book = DBStruct.with(DB, :books) do
  group :category,  String
  field :title,     String
  field :author,    String
  field :date,      Time
  field :edition,   Integer
end

b1 = Book.new(category:"non-fiction",
              title:   "Diseases of the Dragon",
              author:  "Lady Sybil Ramkin",
              date:    Time.new(1989, 11, 1),
              edition: 1)

A group is just an ordinary field except that Books.items will filter by them:

non_fiction = Book.items("non-fiction")

Multiple groups are allowed and nil can be used as a wildcard when selection them.

Alternately, you can filter using a Sequel where clause:

dragon_books = Book.where(Sequel.like(:title, "%Dragon%"))

but if you need that often, you may well be better off dealing with Sequel directly.