MIT licensed and maintained by Nikita Volkov
This version can be pinned in stack with:hasql-2.0.1.0@sha256:424f5ce423d132d2d883682cc7a3738b2608c4fc48da14d613386cbf8149a411,11026

Hasql

Hackage Continuous Haddock

PostgreSQL driver for Haskell, that prioritizes:

  • Reliability
  • Flexibility
  • Performance

Status

Hasql is production-ready, actively maintained and the API is moderately stable. It’s used by many companies and most notably by the Postgrest project.

Pluggable Transport

Hasql’s transport is pluggable via pqi. Hasql itself carries no C dependency. It programs against the pqi interface, and you pick the adapter that implements it. That means you depend on two packages, not one:

build-depends:
  hasql,
  pqi-ffi,  -- or pqi-native

Hasql.Connection.acquire then takes the adapter explicitly, as its first argument:

import Pqi.Ffi qualified    -- the C-backed libpq transport
import Pqi.Native qualified -- alpha: pure-Haskell, no C dependency, interchangeable with Pqi.Ffi

connection <- Hasql.Connection.acquire Pqi.Ffi.adapter settings
-- or
connection <- Hasql.Connection.acquire Pqi.Native.adapter settings

pqi-ffi is the stable, production-proven default. It binds the C libpq library, so it requires libpq of at least version 14 to be installed to compile - which typically just means having a recent PostgreSQL distro installed. Through it Hasql is tested against a wide range of PostgreSQL servers, starting from version 9.

pqi-native is a from-scratch, pure-Haskell implementation of the Postgres wire protocol, with no C dependency at all. It is thoroughly tested: pqi-conformance runs it side by side with libpq on the same inputs and checks that the results agree, and the test-suites of hasql, hasql-pool and hasql-transaction now run against both adapters, so the whole stack above the transport is exercised on pqi-native too. It’s still labelled alpha - not yet proven at production scale. The two adapters are fully interchangeable: swapping between them is a one-line change (a different Adapter value, nothing else), so you can try pqi-native today with no lock-in and no rewrite to fall back if needed.

Ecosystem

Hasql is not just a single library, it is a granular ecosystem of composable libraries, each isolated to perform its own task and stay simple.

Want to list your package or correct something here? Make a PR.

Transport adapters

Unlike the extension libraries above, which are optional, a transport adapter is mandatory: Hasql needs one to talk to the server at all. See Pluggable Transport for how to pick one.

  • “pqi” - the driver-agnostic interface that Hasql programs against. Pulled in automatically. You don’t depend on it directly.

  • “pqi-ffi” - the stable adapter, backed by the C libpq library.

  • “pqi-native” - an alpha pure-Haskell adapter speaking the PostgreSQL wire protocol directly, with no C dependency.

Why make it an ecosystem?

  • Focus. Each library in isolation provides a simple API, which is focused on a specific task or a few related tasks.

  • Flexibility. The user picks and chooses the features, thus precisely matching the level of abstraction that he needs for his task.

  • Much more stable and descriptive semantic versioning. E.g., a change in the API of the “hasql-transaction” library won’t affect any of the other libraries and it gives the user a more precise information about which part of his application he needs to update to conform.

  • Interchangeability and competition of the ecosystem components. E.g., not everyone will agree with the restrictive design decisions made in the “hasql-transaction” library. However those decisions are not imposed on the user, and instead of having endless debates about how to abstract over transactions, another extension library can simply be released, which will provide a different interpretation of what the abstraction over transactions should be.

  • Horizontal scalability of the ecosystem. Instead of posting feature- or pull-requests, the users are encouraged to release their own small extension-libraries, with themselves becoming the copyright owners and taking on the maintenance responsibilities. Compare this model to the classical one, where some core-team is responsible for everything. One is scalable, the other is not.

Documentation

  • Data-Access Architecture — a normative reference for organising database integration code built on Hasql. It specifies how to layer types, statements, transactions and sessions, where the application domain enters the picture, how the three error channels differ, and what to test at each level. Every rule carries its rationale and derives from the capability differences between Hasql’s four constructs.

The reference is written to be consumed directly by coding agents as well as by people. Point an agent at the raw file and it has the whole system in context, with the rules numbered so they can be cited back in review.

Short Example

Following is a complete application, which performs some arithmetic in Postgres using Hasql.

{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}

import Data.Functor.Contravariant
import Data.Int
import Hasql.Session (Session)
import Prelude
import qualified Hasql.Connection as Connection
import qualified Hasql.Connection.Settings as Settings
import qualified Hasql.Decoders as Decoders
import qualified Hasql.Encoders as Encoders
import qualified Hasql.Session as Session
import qualified Hasql.Statement as Statement
import qualified Pqi.Ffi -- from "pqi-ffi" (stable). Swap for "Pqi.Native" from "pqi-native" (alpha, fully interchangeable) to try the pure-Haskell backend

main :: IO ()
main = do
  Right connection <- Connection.acquire Pqi.Ffi.adapter connectionSettings
  result <- Connection.use connection (sumAndDivModSession 3 8 3)
  print result
  where
    connectionSettings =
      mconcat
        [ Settings.hostAndPort "localhost" 5432,
          Settings.user "postgres",
          Settings.password "postgres",
          Settings.dbname "postgres"
          -- Prepared statements are enabled by default.
          -- To disable them (e.g., for pgbouncer compatibility):
          -- Settings.noPreparedStatements True
        ]

-- * Sessions

-- Session abstracts over the execution of operations on a database connection.
-- It is composable and has a Monad instance.
-------------------------

sumAndDivModSession :: Int64 -> Int64 -> Int64 -> Session (Int64, Int64)
sumAndDivModSession a b c = do
  -- Get the sum of a and b
  sumOfAAndB <- Session.statement (a, b) sumStatement
  -- Divide the sum by c and get the modulo as well
  Session.statement (sumOfAAndB, c) divModStatement

-- * Statements

-- Statement is a definition of an individual SQL-statement,
-- accompanied by a specification of how to encode its parameters and
-- decode its result.
-------------------------

-- | A statement with two integer parameters and an integer result.
sumStatement :: Statement.Statement (Int64, Int64) Int64
sumStatement = Statement.preparable sql encoder decoder
  where
    -- The SQL of the statement, with $1, $2, ... placeholders for parameters.
    sql =
      "select $1 + $2"
    -- Specification of how to encode the parameters of the statement
    -- where the association with placeholders is achieved by order.
    encoder =
      mconcat
        [ -- Encoder of the first parameter as a non-nullable int8.
          -- It extracts the first element of the tuple using the contravariant functor
          -- instance.
          fst >$< Encoders.param (Encoders.nonNullable Encoders.int8),
          -- Encoder of the second parameter,
          -- which extracts the second element of the tuple.
          snd >$< Encoders.param (Encoders.nonNullable Encoders.int8)
        ]
    -- Specification of how to decode the result of the statement.
    -- States that we expect a single row with a single non-nullable int8 column.
    decoder =
      Decoders.singleRow
        (Decoders.column (Decoders.nonNullable Decoders.int8))

divModStatement :: Statement.Statement (Int64, Int64) (Int64, Int64)
divModStatement = Statement.preparable sql encoder decoder
  where
    sql =
      "select $1 / $2, $1 % $2"
    encoder =
      mconcat
        [ fst >$< Encoders.param (Encoders.nonNullable Encoders.int8),
          snd >$< Encoders.param (Encoders.nonNullable Encoders.int8)
        ]
    -- Decoder that expects a single row with two non-nullable int8 columns,
    -- returning the result as a tuple.
    -- Uses the applicative functor instance to combine two column decoders.
    decoder =
      Decoders.singleRow
        ( (,)
            <$> Decoders.column (Decoders.nonNullable Decoders.int8)
            <*> Decoders.column (Decoders.nonNullable Decoders.int8)
        )

For the general use-case it is advised to prefer declaring statements using the “hasql-th” library, which validates the statements at compile-time and generates codecs automatically. So the above two statements could be implemented the following way:

import qualified Hasql.TH as TH -- from "hasql-th"

sumStatement :: Statement.Statement (Int64, Int64) Int64
sumStatement =
  [TH.singletonStatement|
    select ($1 :: int8 + $2 :: int8) :: int8
  |]

divModStatement :: Statement.Statement (Int64, Int64) (Int64, Int64)
divModStatement =
  [TH.singletonStatement|
    select
      (($1 :: int8) / ($2 :: int8)) :: int8,
      (($1 :: int8) % ($2 :: int8)) :: int8
  |]

Discussions

Join GitHub Discussions to ask questions, provide feedback, suggest and vote on features, and help shape the future of Hasql.

Support Policy

This policy is intended to balance stability for users with the ability to evolve the library.

Each major release of Hasql is supported for at least one year from the date of its first release. During this period, fixes are backported to the latest minor version of that major release.

After the support period ends, the release may continue to work but is no longer guaranteed to receive fixes.

You’re welcome to post requests to change the policy or issues if you believe something is not being addressed.

Changes

v2.0.1.0

New Features

  • IsError gained a toSqlState method, exposing the SQLSTATE the server reported for an error, or Nothing where the error carries no server code. It saves consumers from pattern-matching their way down to the nested ServerError — a dig that has to be rewritten every time the error types gain a constructor.

    case Errors.toSqlState err of
      Just "23505" -> handleUniqueViolation
      _ -> rethrow err
    

    The method has a default implementation returning Nothing, so existing instances keep compiling. Instances for error types that wrap another error type must override it and delegate to the wrapped value, otherwise they silently report Nothing for codes they do carry.

v2.0.0.3

Fixes

  • Work around the bug in Cabal due to which documentation does not get generated for definitions reexported from sublibs.

v2.0.0.2

Work around the bugs in Cabal/Haddock that cause missing documentation for two-hop reexported internal modules.

v2.0.0.1

Support for pqi-1.1.

v2.0.0.0

New era: the transport layer is now pluggable via pqi, and an alpha pure-Haskell backend, pqi-native, is available for early adopters. Goal: a reliable, performant, no-C-dependency replacement for libpq.

Breaking

  • Hasql.Connection.acquire now takes an explicit adapter as its first argument, ahead of Settings. To keep prior behaviour, depend on pqi-ffi and pass Pqi.Ffi.adapter. To try the native backend, depend on pqi-native and pass Pqi.Native.adapter.

1.10

Major revision happened.

New Features

  • OID by name resolution.

    Encoders and decoders now support resolving PostgreSQL type OIDs by their names at runtime. This enables working with custom types (enums, composite types, domains) without hardcoding OID values. The system includes an OID cache to optimize repeated lookups and automatically queries pg_type and related system catalogs when needed. This change affects array, composite, and value encoders/decoders throughout the codec system.

  • Decoder compatibility checks.

    Previously decoders were silently accepting values of different types, if binary decoding did not fail. Now decoders check if the actual type of the column matches the expected type of the decoder and report UnexpectedColumnTypeStatementError error if they do not match. They also match the amount of columns in the result with the amount of columns expected by the decoder and report an error if they do not match.

  • No resets on errors.

    Previously when an async exception was raised during the execution of a session, the connection would get reestablished to recover from any possible half-finished states. That led to a loss of the connection-local state on the server side. Now the connection recovers without resetting.

  • Redesigned connection configuration API.

    The connection settings API has been completely redesigned to be more composable and user-friendly. Settings are now represented as a monoid, allowing easy combination of multiple configuration options. The API now supports both URI and key-value connection string formats, with individual setters for common parameters like host, port, user, password, etc.

  • Custom codec API.

    Added Hasql.Encoders.custom and Hasql.Decoders.custom functions providing a low-level API for defining custom value encoders and decoders. These functions offer fine-grained control over OID resolution, allowing you to:

    • Specify static OIDs when known at compile time
    • Automatically resolve OIDs at runtime by type name
    • Declare dependencies on other types needed for serialization/deserialization (e.g., field types in composite types)
    • Implement custom binary encoding/decoding logic with access to resolved OIDs

    This is particularly useful for advanced use cases like custom composite types with field validation or specialized binary formats.

Breaking changes

  • Text instead of ByteString for textual data.

    • The public API now uses Text instead of ByteString for SQL statements and error messages.
  • Custom type mappings (enums and composite types) now require specifying names for the types being mapped.

    • This will automatically identify the types with the DB and do deep compatibility checks.
  • Decoder checks are now more strict and report UnexpectedColumnTypeStatementError when the actual type of a column does not match the expected type of the decoder. Previously such mismatches were silently ignored and could lead to either autocasts or runtime errors in later stages.

    • E.g., int4 column decoded with int8 decoder will now report UnexpectedColumnTypeStatementError instead of silently accepting the value.
  • Session now has exclusive access to the connection for its entire duration. Previously it was releasing and reacquiring the lock on the connection between statements.

    • If you need the old behaviour, you can use ReaderT Connection (ExceptT SessionError IO).
  • Dropped MonadReader Connection instance for Session.

  • Dropped Monad and MonadFail instances for the Row decoder. Applicative is enough for all practical purposes.

  • Errors model completely overhauled.

    • ConnectionError restructured and moved from the Hasql.Connection module to Hasql.Errors.
    • SessionError restructured and moved from the Hasql.Session module to Hasql.Errors.
  • usePreparedStatements setting dropped. Use disablePreparedStatements instead.

  • Hasql.Session.sql renamed to Hasql.Session.script to better reflect its purpose.

  • Connection configuration API overhaul to improve UX.

    • Hasql.Connection.acquire now takes a single Settings value instead of a list of Setting values.
    • The Hasql.Connection.Setting module has been replaced with Hasql.Connection.Settings.
    • Settings are now constructed using flat monoid composition instead of hierarchical lists requiring multiple imports.
    • Removed Hasql.Connection.Setting.Connection and related submodules.
  • Custom value decoder signature changed.

    The Hasql.Decoders.custom function signature has been extended to support more explicit control over type resolution. It now requires:

    • Optional static OIDs parameter (previously implicit)
    • List of additional type dependencies needed for decoding
    • The decoder function now receives an OID lookup function as its first parameter

    This change enables more robust custom type handling but requires updating existing custom decoder implementations.

  • Exception instances on error types removed. The error types here were never thrown as exceptions. Wrap them in your own exception type if you need to throw them.

1.9

  • Revised the settings construction exposing a tree of modules
  • Added a global prepared statements setting

Why the changes?

To introduce the new global prepared statements setting and to make the settings API ready for extension without backward compatibility breakage.

Instructions on upgrading the 1.8 code

When explicit connection string is used

Replace

Hasql.Connection.acquire connectionString

with

Hasql.Connection.acquire 
  [ Hasql.Connection.Setting.connection (Hasql.Connection.Setting.Connection.string connectionString)
  ]

When parameteric connection string is used

Replace

Hasql.Connection.acquire (Hasql.Connection.settings host port user password dbname)

with

Hasql.Connection.acquire
  [ Hasql.Connection.Setting.connection
    ( Hasql.Connection.Setting.Connection.params
      [ Hasql.Connection.Setting.Connection.Param.host host,
        Hasql.Connection.Setting.Connection.Param.port port,
        Hasql.Connection.Setting.Connection.Param.user user,
        Hasql.Connection.Setting.Connection.Param.password password,
        Hasql.Connection.Setting.Connection.Param.dbname dbname
      ]
    )
  ]

1.8.1

  • In case of exceptions thrown by user from inside of Session, the connection status gets checked to be out of transaction and unless it is the connection gets reset.

1.8

  • Move to “iproute” from “network-ip” for the “inet” datatype (#163).

1.7

  • Decidable instance on Encoders.Params removed. It was useless and limited the design.
  • QueryError type renamed to SessionError.
  • PipelineError constructor added to the SessionError type.

1.6.3.1

  • Moved to “postgresql-libpq-0.10”

1.6.3

  • Added unknownEnum encoder

1.6.2

  • Added composite encoder
  • Added oid and name encoders

1.6.1

  • Added jsonLazyBytes and jsonbLazyBytes

1.6

  • Added position to ServerError (breaking change).
  • Disabled failure on empty query.

1.5

  • Added column number to RowError (breaking change).
  • Added MonadReader Connection instance for Session.