How to use PostgreSQL with Haskell. Elephantine library review 2023

Zelenya - Oct 3 '23 - - Dev Community

As of October 2023, there are around a dozen mature PostgreSQL libraries, all levels of abstractions, from low-level queries to the type level. More than enough for anybody. (Shout out to everyone who says Haskell has no libraries.)

Which one to use? Let’s see. Foreach library, we’ll talk about features and basics like writing queries, complexity, pitfalls, (everyone’s favorite topic) errors, and so on.

We assume you are familiar with the fundamentals of SQL and PostgreSQL.


📹 Hate reading articles? Check out the complementary video, which covers the same content.

Mise en place

Before integrating with the database, let’s discuss the data and the setup.

💡 If you want to follow allow at home, the repository contains all the imports and data types — we omit most of them from the tutorial for simplicity.

Data

Let’s imagine that we’re building a (tiny) warehouse management system:

  • Warehouse has multiple products, some quantity of each.
  • Product has a label and a description.
  • Product can belong to categories (many-to-many).
  • Category has a label.

The scripts/create_tables.sql contains all the definitions:

CREATE TABLE product (
    id SERIAL PRIMARY KEY,
    label TEXT NOT NULL,
    description TEXT,
    UNIQUE (label)
);

CREATE TABLE category (
    id SERIAL PRIMARY KEY,
    label TEXT NOT NULL,
    UNIQUE (label)
);

CREATE TABLE product_category (
    category_id INT NOT NULL,
    product_id INT NOT NULL,
    PRIMARY KEY (category_id, product_id),
    FOREIGN KEY (product_id) REFERENCES product(id),
    FOREIGN KEY (category_id) REFERENCES category(id)
);

CREATE TABLE warehouse (
    id SERIAL PRIMARY KEY, 
    product_id INT NOT NULL,
    quantity INT NOT NULL,
    created TIMESTAMP,
    modified TIMESTAMP,
    FOREIGN KEY (product_id) REFERENCES product(id)
);
Enter fullscreen mode Exit fullscreen mode

Schema

Postgres server

Here are a few things you need to know to play along at home.

🐘 If you don’t care, don’t like docker, or already have an easily-accessible postgres server, feel free to skip this section.

First, install docker.

docker compose up (docker-compose up on older versions) starts PostgreSQL (see docker-compose.yml) and initializes the databases and tables (using the scripts/create_tables.sql). It mounts the data to the postgres-data/ (in case you need to wipe it or something).

Some of the hardcoded things that the code relies on:

environment:
  - POSTGRES_DB=warehouse
  - POSTGRES_USER=postgres
  - POSTGRES_PASSWORD=password
ports:
  - 5432:5432
Enter fullscreen mode Exit fullscreen mode

💡 These hardcoded values are also hardcoded in the code in Hardcoded.hs (for laziness reasons)

You can connect to the container and run arbitrary queries using psql:

docker exec -it elephants-postgres-1 psql -U postgres -d warehouse
Enter fullscreen mode Exit fullscreen mode

Note: elephants-postgres-1 is a container name, which might be different for you; check with docker ps to get the correct container id (or name). We also pass a couple of flags: -U postgres for the user name and -d warehouse for the database name.

docker compose down to stop and remove the containers.

Project Overview

If you have stack installed, stack build to build and stack run to run.

💡 We use lts-21.7 (ghc-9.4.5), published on 2023-08-14.

🤷 To build the backend, you might need the libpq development libraries installed (e.g., libpq-dev on Debian-based distributions).

Extensions overview

Note that most of the libraries rely on using various extensions. Here is a quick overview of the most important ones.


OverloadedStrings

Used to simplify the construction of query values — we can use literal strings, like "SELECT * FROM user", instead of manually constructing the whole type; for example, Query . toByteString . stringUtf8 $ "SELECT * FROM user" (see Query in postgresql-simple).


TemplateHaskell

Template Haskell (TH) is fairly useful for generating boilerplate code. Some libraries provide the TH machinery to derive typeclass instances and/or generate custom type-safe data types at the compile time.


DeriveAnyClass and DeriveGeneric enable alternative ways to get free typeclass instances.

We’ll use DerivingStrategies to make the derivation explicit, for example:

data Category = Category {label :: Text}
  deriving (Show, Generic)           -- Derive Generic instance,
  deriving anyclass (ToRow, FromRow) -- used for these instances
Enter fullscreen mode Exit fullscreen mode

QuasiQuotes

Some libraries (e.g., postgresql-simple) provide quasi quoters for less tedious sql construction in Haskell code. For example:

[sql| select label from product |]
Enter fullscreen mode Exit fullscreen mode

DuplicateRecordFields

Only one postgres library requires it. But it’s a stylistic choice — we use this extension because we have multiple data types with a label field.


OverloadedRecordDot

To access record fields as well as specific columns of the tables (for example, product.label)


Note that the project uses GHC2021, which includes DeriveGeneric, TypeApplications, and many other extensions required for some libraries to work. Also, for the tutorial's sake, we’ll use the included ScopedTypeVariables to demonstrate some intermediate types.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player