This post is part of Develop Single-Machine Rails Applications with LiteStack Series
- 1An Introduction to LiteStack for Ruby on Rails
- 2A Deep Dive Into LiteDB for Ruby on Rails
- 3Handle Incoming Webhooks with LiteJob for Ruby on Rails
- 4Stream Updates to Your Users with LiteCable for Ruby on Rails
- 5Speed Up Your Ruby on Rails Application with LiteCache
- 6Full-Text Search for Ruby on Rails with Litesearch
In the second post of our series covering LiteStack (an alternative way to build Rails applications entirely based on SQLite), we'll explore the database's concepts of flexible typing and type affinity.
We'll not only discover how SQLite's data handling differs from other SQL databases, but also how we efficiently process and store binary data, like images, directly in a database column.
Note: LiteDB is essentially SQLite, but fine-tuned for usage in Rails. We'll use LiteDB and SQLite interchangeably in this post.
Flexible Typing In LiteDB for Ruby on Rails
The first thing to always keep in mind is that SQLite is flexibly typed. This means that, in contrast to most other SQL databases (where the data type of a value is defined by its container — the column data type), SQLite is very forgiving. Or, as the SQLite docs state more formally:
Datatype of a value is associated with the value itself, not with its container.
This means, for example, that you can store strings in INTEGER
columns, or exceed the length defined by VARCHAR(n)
. In the development process, this is usually not an issue. It boils down to a bit of a vendor lock-in, though: Using an SQLite database in development and a PostgreSQL one in production can become a nightmare.
Note: SQLite version 3.37.0 introduced the option of STRICT tables.
SQLite for Rails: Type Affinity
Type affinity in SQLite is a concept that determines how data is stored and converted. Before explaining what this means, let's take a look at SQLite's storage classes. These are:
NULL
INTEGER
(7 different data types depending on size)REAL
(any floating point value)TEXT
(any string)BLOB
(raw binary data)
These are equivalent to data types for the purpose of a general discussion, but have a broader scope. When you create a table, you do not declare storage classes for each column, but type affinities.
A certain type affinity determines what storage class(es) will be used to store a certain column. Because this is a rather elusive topic to discuss theoretically, let's look at an example. Say we create the following table:
INTEGER
, TEXT
, and NUMERIC
are the columns' type affinities. For example, suppose I enter an integer number into the department
column. It will be converted into TEXT
, because the TEXT
affinity only uses the TEXT
, NULL
, and BLOG
storage classes:
On the other hand, if I enter the string "67000"
as salary
, the NUMERIC
affinity will convert it to INTEGER
, because it is a well formed number literal.
If I were to enter "eighty-five thousand"
, it would just default to the TEXT
storage class.
The rules are rather complex, so check out the SQLite official docs on datatypes. The main takeaway here is that, somewhat counterintuitively, the column type affinities you declare need not reflect the actual data types of the stored values.
SQLite also lacks boolean, datetime, and JSONB datatypes, and UUIDs. Let's take a look at these now.
No Boolean Datatype
Boolean values are commonly represented as 0
and 1
instead of TRUE
and FALSE
.
No Datetime Datatype
The SQLite docs recommend using the built-in conversion functions and that you store dates and times:
- As a TEXT string in the ISO-8601 format. Example: '2018-04-02 12:13:46'.
- As an INTEGER number of seconds since 1970 (also known as "unix time").
- As a REAL value that is the fractional Julian day number.
No JSONB Datatype
Especially for developers relying on the convenience of the PostgreSQL JSONB datatype, it's important to point out that SQLite doesn't use a binary format to store JSON:
Experiments have been unable to find a binary encoding that is smaller or faster than a plain text encoding.
Most notably, SQLite's JSON implementation doesn't support BLOB storage.
No Universally Unique Identifiers (UUIDs)
Another characteristic that comes as a surprise to most developers is that SQLite lacks the concept of UUIDs. Not completely, as they can be compiled and loaded as an extension, but they are not built into the database per default.
If you are interested in exploring the advantages and tradeoffs of flexible database types further, the SQLite docs have a decent write-up.
Enough with the theoretical deliberations and dwelling on what SQLite doesn't have, though. Let's get back to building! Let's see how we can use SQLite to store image prompts.
Storing Image Prompts Directly in SQLite
The SQLite documentation claims that the database is excellently suited to store small blobs of binary data (e.g., thumbnails). We probably still would use a CDN in production, but it's interesting enough to give it a try.
For starters, let's add a content_type
column to our prompts
table. It will come in handy later:
With this in place, the transcoding into a data URL can be done in the model itself:
We can now start to simplify our controller code a bit. If we assign both prompt_image
and content_type
to @prompt
from the uploaded file, we can scrap the helper methods prompt_image
and prompt_image_data_url
:
All that's left to do now is to actually display the image in our view. For this, we can use the same data_url
helper method from the model:
There is a tiny blemish here, though. We are potentially storing and transferring way too much image data. StableDiffusion needs only a 768 pixel wide (or long) image to work, so we will scale it down beforehand. To accomplish this, we'll install the ImageProcessing library and use libvips to transform our image.
Note: you have to install libvips on your operating system for this to work. Instructions are available in the README.
The image_processing
gem is already installed in an off-the-shelf Rails 7 app, so we can start writing our transformation logic. We'll do this in a before_save
callback in our Prompt
model:
The resize_to_fit
method will scale an image to fit the specified dimensions while preserving the aspect ratio. For example, if we upload an image with 1024x768 pixels, it will be scaled down to 768x576.
This completes the preprocessing logic of our image generation pipeline.
Overall Notes on SQLite for Rails and Its Performance
Although some traditional data types like boolean and datetime are absent, SQLite excels in handling binary data, making it an excellent choice for certain applications. If you are interested in how SQLite compares to other Relational Database Management Systems (RDBMS's) in terms of performance, have a look at the benchmarks.
A Quick Note On Litestream
As a final side-note, I would like to touch on Litestream, a performant and safe way to replicate your SQLite databases to S3-compatible object storage in a streaming manner. Not only does it provide a very cost-effective backup solution, but it is also a very simple way to restore your database.
Up Next: Asynchronously Handling Image Predictions with LiteJob
In this article, we've delved into SQLite's flexible typing and type affinity, harnessing its power to process and store binary data. We have looked into optimizing image storage and leveraged SQLite's unique data-handling features.
Next, we'll consider how to handle the actual generation and persistence of prediction images. We have only implemented a webhook stub so far, and created the predictions in a peculiar way in our PromptsController
. We will look into performing this step asynchronously with LiteJob.
Until then, happy coding!
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!
This post is part of Develop Single-Machine Rails Applications with LiteStack Series
- 1An Introduction to LiteStack for Ruby on Rails
- 2A Deep Dive Into LiteDB for Ruby on Rails
- 3Handle Incoming Webhooks with LiteJob for Ruby on Rails
- 4Stream Updates to Your Users with LiteCable for Ruby on Rails
- 5Speed Up Your Ruby on Rails Application with LiteCache
- 6Full-Text Search for Ruby on Rails with Litesearch