Hasql separates the driver from the transport it runs on
Reliable and fast PostgreSQL driver for Haskell
At a glance
- What is it?
- Hasql is a PostgreSQL driver for Haskell whose error types tell you whether a connection survived a failure and whose pqi transport layer lets you choose the adapter. It fits teams that want typed failures and composable sessions, and it costs you a second dependency and a major-version migration to get there.
- Who is it for?
- Hasql is worth adopting when a Haskell service needs error handling that distinguishes a dead connection from a failed statement, since the type system does that work instead of a runtime check, and when the team is comfortable owning a major version upgrade every year. It is a poor fit for a script that needs one query and a connection string, because acquiring a connection already requires choosing a transport package.
- 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 last received commits 9 days ago.
- What is it written in?
- Mainly Haskell, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Acquiring a connection takes a transport adapter as its first argument
Hasql is a PostgreSQL driver written in Haskell, and its defining structural decision is that it does not carry a C dependency of its own. Instead it programs against an interface called pqi, and the concrete adapter is something the application chooses. The consequence shows up immediately in the dependency list, because you no longer depend on one package but two:
build-depends:
hasql,
pqi-ffi, -- or pqi-nativeThe first argument to acquisition is that adapter:
connection <- Hasql.Connection.acquire Pqi.Ffi.adapter settingsThis is also the shape of the upgrade from the 1.x line, since the README notes that acquire now takes a transport adapter as its first argument, making the switch a one-argument change, although the error types were reshaped at the same time. The driver is therefore two decisions in one: what you talk to Postgres with, and how the bytes get there. Both the build-depends block and the acquire call are quoted from the project's own getting-started section, so a reader can copy them directly into a cabal file and a source file.
A session composes because it is a monad over one connection
The unit of work in Hasql is a session, described in the project's own example comment as abstracting over the execution of operations on a connection, and it carries a Monad instance so statements compose. The example program sums three numbers by running one statement twice inside a single session, with the second statement consuming the first result:
sumSession :: Int64 -> Int64 -> Int64 -> Session Int64
sumSession a b c = do
ab <- Session.statement (a, b) sumStatement
Session.statement (ab, c) sumStatementA statement is itself a pair of halves: the SQL text, an encoder for the parameters and a decoder for the result. The example builds the encoder from two parameter encoders combined with mconcat and maps them onto the tuple fields with the contravariant functor operator, then decodes with Decoders.singleRow over a non-nullable int8 column. The pieces are deliberately small and composable, which is the point of the design, since a decoder for a business record ends up as an assembly of column decoders rather than a bespoke parse function. Non-nullability is expressed in the types as well, using Decoders.nonNullable and Encoders.nonNullable, so a NULL arriving where a value was expected is a type-level statement about the schema rather than a runtime surprise.
Prepared statements are the default, and pgbouncer users have to opt out
Performance in Hasql is not something you switch on. According to the project documentation, statements are prepared by default, and the example carries a comment to that effect inside the settings block. That default matters for connection pools sitting behind pgbouncer, because the tool's own documentation gives you the escape hatch, Settings.noPreparedStatements True, described as being for pgbouncer compatibility. The tradeoff is visible in the shape of the API. Preparing a statement costs a round trip of its own the first time it is used, and it ties a server-side prepared plan to a session, so a pooler that multiplexes connections can break the assumption. The alternative transport choice can also affect throughput, since the driver exposes Hasql.Session.pipeline for sending several statements in a single round trip, which the documentation lists alongside prepared statements as a performance feature. If you run pgbouncer in transaction pooling mode, check the setting before you benchmark anything, because prepared statements and multiplexing are the two features that most often collide with a pooler.
The error type tells you whether the connection is still usable
Every operation in Hasql reports failure as a value, and the constructor you get tells you what state you are left in. That is the strongest idea in the project, and the documentation splits it clearly. Hasql.Connection.acquire returns an AcquireError organized by the stage that failed, namely connecting, checking the server version or initializing session settings. Hasql.Connection.use returns a UseError, which splits on the only distinction the caller can act on:
result <- Connection.use connection session
case result of
Right a -> pure a
-- The session failed, the connection is still live and reusable.
Left (Errors.SessionUseError err) -> ...
-- The connection is gone. Hasql has already closed it.
Left (Errors.ConnectionUseError reason) -> ...The consequence for a pool is spelled out. A ConnectionUseError means the handle is spent, Hasql has already closed it, so every later use on it reports the same error and release is a no-op, and pools must discard it rather than return it. A SessionUseError carries a SessionError and leaves the connection untouched. All three types implement IsError, which renders a message and details for logging and exposes the server SQLSTATE through toSqlState where one was reported. A pool that returns a dead connection to the pool is a bug this design makes hard to write.
Version 2 broke the error types and the acquire signature together
The release history in the repository shows three tagged versions inside a narrow window: v2.0.0.2 on 2026-08-06, v2.0.0.3 on 2026-08-10 and v2.0.1.0 on 2026-08-12. The documentation states that the API changes at major versions, and that each major release is supported for at least a year under a stated support policy, with every change recorded in a changelog at the root of the repository. That combination, a yearly support window for a major line and a changelog that lists every break, is what makes the driver adoptable in a long-lived service. A patch release such as v2.0.1.0 landing days after v2.0.0.3 tells you the 2.0 line is where the work is. The cost of that policy is that a 1.x to 2.x move is not free: the acquire signature gained an argument, the error types were reshaped, and CHANGELOG.md is the file that tells you what else changed. Nothing in the changelog itself is quoted here, so the specific break list is worth reading before you start the migration rather than after.
The long-form documentation is written to be pasted into an agent
Most of the long-form explanation does not live in the repository README. It sits in a separate hasql-docs repository, which holds three documents. The first, Your First Statement, is an annotated walkthrough of a complete Hasql program that explains the encoder and decoder vocabulary line by line, and it is offered as the slower introduction for anyone who found the example above moved too fast. The second, Data-Access Architecture, is described as a normative reference for organizing database integration code, specifying how to layer types, statements, transactions and sessions, where the application domain enters, how the three error channels differ, and what to test at each level. The third, Why Make It an Ecosystem?, gives the rationale for splitting Hasql into many small separately-versioned libraries instead of one large one. The unusual detail is that the architecture reference is described as written to be consumed directly by coding agents, with the raw file link offered so an agent can hold the whole system in context, and with the rules numbered so they can be cited in review.
pqi-ffi and pqi-native are not interchangeable in practice
The choice of transport adapter is the fork in the road, and the two documented options sit at different levels of maturity. pqi-ffi is described as the stable, production-proven default, and the repository topics list libpq, which is the C library that adapter is associated with. pqi-native is described as a pure-Haskell alpha with no C dependency at all. The practical reading is that the alpha status attaches to the native implementation, not to Hasql itself, so a team with a policy against C code on the build machine has a path but is taking on a less settled transport. That is a real trade-off rather than a nitpick, since the transport is what carries every byte to the server and is the layer where connection setup, version checking and session initialization failures originate. The README refers to a transport adapters section of its own documentation for how to choose, so the comparison in this repository is short and the long version lives elsewhere. Anyone evaluating pqi-native should treat the alpha label as the deciding fact and read the changelog for the current version before committing.
Where Hasql is the wrong size for the job
Hasql is built for services, and it pays for that in ways a small program will notice. The first cost is dependency shape. A one-off script that wants a single query now needs hasql plus a pqi adapter, an explicit acquire call that takes a transport, and a settings block before any SQL runs. The second cost is version churn. Because the API changes at major versions and the 1.x to 2.x move reshaped error types, an application on an older major line accumulates migration work. The third is the reliance on a quality server on the other side. The analysis step of acquiring a connection, checking the server version and initializing session settings is three separate stages that can each fail, which is exactly what you want in production and exactly what you do not want in a throwaway script. On the other hand, the error design is a poor reason to avoid the driver, because returning a dead connection to a pool is the kind of bug that only shows up under load, and Hasql pushes that failure into the type system where it is visible in review.
Editorial conclusion
Hasql is worth adopting when a Haskell service needs error handling that distinguishes a dead connection from a failed statement, since the type system does that work instead of a runtime check, and when the team is comfortable owning a major version upgrade every year. It is a poor fit for a script that needs one query and a connection string, because acquiring a connection already requires choosing a transport package. It is also a poor fit when a C dependency is not allowed on the target machine unless you accept pqi-native, which the documentation still calls alpha. Before committing, check the CHANGELOG.md for the break list of the version you are moving to, confirm that your pgbouncer setup tolerates the prepared statement behaviour that `Settings.noPreparedStatements True` turns off, and read hasql-docs/data-access-architecture.md, since the layering rules it defines are what the driver expects you to follow.
Frequently asked questions
What is Hasql and which transport adapter should I use?
Hasql is a PostgreSQL driver for Haskell that carries no C dependency of its own and programs against the pqi interface, so you pick the adapter. The documentation calls pqi-ffi the stable, production-proven default and pqi-native a pure-Haskell alpha with no C dependency at all.
How do I tell in Haskell whether a Postgres connection is still alive after an error?
Hasql.Connection.use returns a UseError that splits into SessionUseError, where the connection is untouched and reusable, and ConnectionUseError, where the connection is gone and Hasql has already closed it. A ConnectionUseError must be discarded by a pool rather than returned to it.
Does Hasql work with pgbouncer?
The documentation states that prepared statements are enabled by default and gives Settings.noPreparedStatements True as the way to disable them, for example for pgbouncer compatibility. It also notes that Hasql.Session.pipeline can send several statements in a single round trip.
Official sources
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.
[](https://hysenlabs.com/projects/nikita-volkov-hasql)Was this useful?