# \[ANN\] hpgsql, a pure Haskell PostgreSQL driver (no libpq)

**URL:** <https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101>\
**Category:** Announcements\
**Created:** [May 13, 2026, 7:54pm UTC](https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101 "2026-05-13T19:54:15Z")\
**Posts on this page:** 15\
**Page:** 1

<div class="post-metadata">

**Author:** ![mzabani](https://avatars.discourse-cdn.com/v4/letter/m/c68b51/32.png) [@mzabani](https://discourse.haskell.org/u/mzabani)\
**Post date:** [May 13, 2026, 7:54pm UTC](https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101/1 "2026-05-13T19:54:15Z")

</div>

It’s a pleasure to announce [hpgsql](https://hackage.haskell.org/package/hpgsql), a PostgreSQL driver written in pure Haskell (no libpq), with an API largely inspired by the great postgresql-simple library, but featuring:

- Usage of PostgreSQL’s binary protocol
- Query arguments passed via the protocol instead of being escaped into the query string
- Pipelining
- Prepared statements
- Ability to stream query results directly from the socket (not just with cursors)
- [Interruption safety](https://github.com/mzabani/hpgsql/blob/master/INTERRUPTION-SAFETY.md), except for very specific (and documented) edge cases
- Thread safety, unless specific (and documented) instructions say otherwise
- A SQL quasiquoter like the one in [postgresql-query](https://hackage.haskell.org/package/postgresql-query) and [hasql-interpolate](https://hackage-content.haskell.org/package/hasql-interpolate-1.0.1.0/docs/Hasql-Interpolate.html)

Here’s an example of a pipeline mixing streams, prepared and non prepared statements:

```haskell
f :: Int -> IO (Stream (Of Aeson.Value) IO ())
f val = do
  (updateTbl :: IO (), aggRes :: IO (Only Int), largeResults) <-
    runPipeline conn $
      (,,)
        <$> pipelineExec_ [sql|UPDATE tbl SET val=#{val}|]
        <*> pipeline1 [sql|SELECT SUM(val) FROM tbl|]
        <*> pipelineSWith
          (rowDecoder @(Vector Int, Vector Text))
          -- We use a prepared statement for the query below
          [sqlPrep|SELECT x, y FROM tbl|]
  updateTbl
  Only total <- aggRes
  Streaming.map Aeson.toJSON <$> largeResults

```

Also, I am maintaining [hpgsql-simple-compat](https://github.com/mzabani/hpgsql/blob/master/MIGRATING.md), a fork of postgresql-simple that preserves its API as much as I could make it, but with internals rewritten to use hpgsql. The idea is to provide a simpler migration path from postgresql-simple; one that allows a smaller initial changeset and then the possibility of migrating queries to hpgsql one at a time. I have even migrated a CLI tool of mine, [codd](https://github.com/mzabani/codd/pull/227), as an example.

hpgsql-simple-compat is not in Hackage as I wasn’t sure duplicating the search space with modules, types, and functions already in postgresql-simple would annoy users. It’s in the “hpgsql-simple-compat” folder in [the repository](https://github.com/mzabani/hpgsql).

[Some initial benchmarks](https://github.com/mzabani/hpgsql/blob/master/BENCHMARKS.md) show hpgsql can materialize rows from a query in ~38% the time postgresql-simple takes, and ~70% the time hasql takes (on my computer, Linux x64, GHC 9.10.3, compiled with -O1). Peak memory usage is trickier to analyze, and I therefore welcome people that know more to read the benchmarks page and help me better understand them. Also, please scrutinize these benchmarks as much as you can.

I want to encourage and welcome contributions, bug reports, questions, suggestions. Nothing’s off the table: what would you want or what do you need from such a library?

I really want to hear from the community, both those eager to switch and potential future users.

- Link to the repository: [GitHub - mzabani/hpgsql: A pure Haskell (no libpq) postgresql driver that draws inspiration from postgresql-simple, has streaming built into its core, speaks postgresql's binary protocol, has pipelining, interruption safety, and (reasonable) thread safety. · GitHub](https://github.com/mzabani/hpgsql) (contains both hpgsql and hpgsql-simple-compat)
- Link to hackage: [hpgsql: Pure Haskell PostgreSQL driver (no libpq)](https://hackage.haskell.org/package/hpgsql)

---

<div class="post-metadata">

**Author:** ![chrisdone](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/chrisdone/32/1408_2.png) [@chrisdone](https://discourse.haskell.org/u/chrisdone)\
**Post date:** [May 13, 2026, 8:55pm UTC](https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101/2 "2026-05-13T20:55:38Z")

</div>

Nice, I wrote a very naive pure Haskell implementation ([GitHub - chrisdone-archive/pgsql-simple: A mid-level client library for the PostgreSQL database, intended to be fast and easy to use. · GitHub](https://github.com/chrisdone-archive/pgsql-simple)) back in the day before I knew anything about writing performant Haskell. I’d been recently perusing the protocol documentation again, thinking I’d do a fresh ground up implementation for fun, try for zero-allocation message handling I’d possible, and really take advantage of the implementation being in Haskell.

Nice to see someone take a swing at it!

I’m curious, what’s the thread safety situation?

---

<div class="post-metadata">

**Author:** ![mzabani](https://avatars.discourse-cdn.com/v4/letter/m/c68b51/32.png) [@mzabani](https://discourse.haskell.org/u/mzabani)\
**Post date:** [May 13, 2026, 9:13pm UTC](https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101/3 "2026-05-13T21:13:20Z")

</div>

That’s neat, and thanks!

As for thread safety, multiple threads sharing a connection will block until all results from previous queries/pipelines are fully consumed (or until a query errs), but there’s a bit more to it than that, like [runPipeline](https://hackage-content.haskell.org/package/hpgsql-0.1.0.0/docs/Hpgsql-Pipeline.html#v:runPipeline) requiring the same thread that sends a pipeline to be the thread that consumes the results of all the statements, and the streaming query/pipeline functions require the same thread that send the query/pipeline to be the thread that consumes the Stream fully (or until an error).

I thought these might be reasonable constraints, although I did try a more relaxed version of runPipeline’s constraints to begin with, but was unable to get something that didn’t run into deadlocks in some situations. I can get into more detail if you’re interested!

---

<div class="post-metadata">

**Author:** ![Yuras](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/yuras/32/5296_2.png) [@Yuras](https://discourse.haskell.org/u/Yuras)\
**Post date:** [May 13, 2026, 9:16pm UTC](https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101/4 "2026-05-13T21:16:53Z")

</div>

This is cool, thank you for creating this!

I have a question about the semantics. Is it always safe to reuse connection unless I got `IrrecoverableHpgsqlError`? The docs are not 100% clear, e.g. this:

> It is possible Hpgsql throws a different kind of exception. File a bug report if that happens, and if you know it came from Hpgsql, treat it like a `IrrecoverableHpgsqlError`.

But most of the time I have no idea where the exception came from. And in `withTransactionMode` it looks like `IrrecoverableHpgsqlError` might get lost if async exception arrives between `tryAny` and `throw`, which (unless I’m misreading the code) will break the promise.

(Also [these TODOs](https://github.com/mzabani/hpgsql/blob/06fcce0405592112ab4a84dc3de66ca23c116854/hpgsql/src/Hpgsql/Internal.hs#L335) are suspicious)

---

<div class="post-metadata">

**Author:** ![mzabani](https://avatars.discourse-cdn.com/v4/letter/m/c68b51/32.png) [@mzabani](https://discourse.haskell.org/u/mzabani)\
**Post date:** [May 13, 2026, 9:29pm UTC](https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101/5 "2026-05-13T21:29:23Z")

</div>

Thank you, Yuras! I think you were in fact the person that taught me a lot about asynchronous exceptions in a postgresql-simple PR review. I learned a lot then, so thanks!

> I have a question about the semantics. Is it always safe to reuse connection unless I got `IrrecoverableHpgsqlError`? The docs are not 100% clear, e.g. this:

Oh, a different type of exception would be considered a bug in the library. I did try to wrap all potentially exception throwing code in a way that turns them into a `IrrecoverableHpgsqlError` precisely because otherwise it’s impossible to know what to do with the connection.

Since it’s the first release, though, I wouldn’t be surprised if something escaped me. Still, take this as a project goal commitment.

> And in `withTransactionMode` it looks like `IrrecoverableHpgsqlError` might get lost if async exception arrives between `tryAny` and `throw`

I think I see what you’re saying. Very nice catch! I created [a github issue](https://github.com/mzabani/hpgsql/issues/14) for this, and will address it as soon as I can.

> (Also [these TODOs](https://github.com/mzabani/hpgsql/blob/06fcce0405592112ab4a84dc3de66ca23c116854/hpgsql/src/Hpgsql/Internal.hs#L335) are suspicious)

Yes, there are a few warts I want to address in the codebase. This one is a potential problem “only” when opening new connections, so I thought I could do a first release before addressing them. I did take a lot more care once a connection is opened, and I have [stress tests](https://github.com/mzabani/hpgsql/blob/06fcce0405592112ab4a84dc3de66ca23c116854/hpgsql-tests/ThreadSafetySpec.hs#L250-L255) that have helped me catch and hone parts of the code.

---

<div class="post-metadata">

**Author:** ![Yuras](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/yuras/32/5296_2.png) [@Yuras](https://discourse.haskell.org/u/Yuras)\
**Post date:** [May 13, 2026, 9:43pm UTC](https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101/6 "2026-05-13T21:43:19Z")

</div>

> [@mzabani](#):
>
> I learned a lot then, so thanks!

Wow, I’m happy to hear this!

> a different type of exception would be considered a bug in the library.

I see. This is an interesting approach. Though I think you’ll fight an uphill battle maintaining this promise.

---

<div class="post-metadata">

**Author:** ![zlonast](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/zlonast/32/5134_2.png) [@zlonast](https://discourse.haskell.org/u/zlonast)\
**Post date:** [May 14, 2026, 9:47am UTC](https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101/7 "2026-05-14T09:47:13Z")

</div>

Do you think your project is not big enough to play with linear types and the new Pure Borrow?

---

<div class="post-metadata">

**Author:** ![mzabani](https://avatars.discourse-cdn.com/v4/letter/m/c68b51/32.png) [@mzabani](https://discourse.haskell.org/u/mzabani)\
**Post date:** [May 14, 2026, 10:36am UTC](https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101/8 "2026-05-14T10:36:55Z")

</div>

I was quite tempted to use linear streams in the public API, because the natural requirement that emerges from postgresql’s protocol is that query results must be consumed fully or until an error.

However, there is one use-case that I **think** prevents me from doing that: query cancellation.

```haskell
streamResults <- queryS conn "select ..."
-- Force Stream until some criteria are satisfied, then cancel the query
cancelActiveStatement conn False

```

IIUC (but I have very little practical experience with linear types!) the user would then have to force the stream to get an exception, or cancelActiveStatement would have to take the stream as an argument, or other similar API choices/changes? And something similar would happen to pipelines.

But you can tell I didn’t explore the space too much.

I also haven’t read about pure borrow yet, so I’ll owe you on that one.

---

<div class="post-metadata">

**Author:** ![zlonast](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/zlonast/32/5134_2.png) [@zlonast](https://discourse.haskell.org/u/zlonast)\
**Post date:** [May 14, 2026, 12:34pm UTC](https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101/9 "2026-05-14T12:34:17Z")

</div>

You can use more performance-optimized things with them, so I was interested in asking. I’ll also note that it’s not necessary to rework the high-level logic specifically for linear types; rewriting all the internals is enough, in my opinion.

---

<div class="post-metadata">

**Author:** ![mzabani](https://avatars.discourse-cdn.com/v4/letter/m/c68b51/32.png) [@mzabani](https://discourse.haskell.org/u/mzabani)\
**Post date:** [May 14, 2026, 3:07pm UTC](https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101/10 "2026-05-14T15:07:17Z")

</div>

Ah right, rethinking internals could be super nice. Do you have any ideas? I mean, even if you’re not familiar with the codebase, have you imagined some kind of recv buffer optimization, or anything else, for that matter? I think this would be a great learning experience for me, and I understand putting in effort might not be on your plate, so please feel free to suggest things even at a high level - I can read/learn and see if there’s something that can be done. Of course, if you wish to contribute more directly, I’m also very much open to that.

---

<div class="post-metadata">

**Author:** ![mkleczek](https://avatars.discourse-cdn.com/v4/letter/m/54ee81/32.png) [@mkleczek](https://discourse.haskell.org/u/mkleczek)\
**Post date:** [May 15, 2026, 8:24am UTC](https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101/11 "2026-05-15T08:24:40Z")

</div>

That is really great. I’ve started working on pure Haskell PostgreSQL driver myself but will happily contribute if needed instead.

PostgREST is considering moving away from `hasql` in the long run: [Vendor hasql · Issue #4823 · PostgREST/postgrest · GitHub](https://github.com/PostgREST/postgrest/issues/4823) - vendoring it is supposed to be the firs step. But I’ve just proposed to possibly skip it and give your library a try first.

---

<div class="post-metadata">

**Author:** ![mzabani](https://avatars.discourse-cdn.com/v4/letter/m/c68b51/32.png) [@mzabani](https://discourse.haskell.org/u/mzabani)\
**Post date:** [May 15, 2026, 10:11am UTC](https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101/12 "2026-05-15T10:11:07Z")

</div>

I would love to take contributions! I did my best to have a good CI pipeline, test suite and tooling (better summarized in the repo’s Readme), so please feel free to create issues and PRs.

I wrote in that thread about hpgsql’s current limitations too, mostly the lack of auth methods and TLS encryption. Thanks for pointing it out to me.

---

<div class="post-metadata">

**Author:** ![kakkun61](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/kakkun61/32/5364_2.png) [@kakkun61](https://discourse.haskell.org/u/kakkun61)\
**Post date:** [June 10, 2026, 10:28am UTC](https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101/13 "2026-06-10T10:28:43Z")

</div>

I once implemented a PostgreSQL client that didn’t use libpq either.

You might find it interesting to examine the differences in implementation approaches.

(I haven’t been updating it much recently, but

> **[postgresql-pure](https://hackage.haskell.org/package/postgresql-pure)**
>
> pure Haskell PostgreSQL driver

---

<div class="post-metadata">

**Author:** ![mzabani](https://avatars.discourse-cdn.com/v4/letter/m/c68b51/32.png) [@mzabani](https://discourse.haskell.org/u/mzabani)\
**Post date:** [June 11, 2026, 6:59pm UTC](https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101/14 "2026-06-11T18:59:05Z")

</div>

Very interesting!

From a brief look, I’d say yours exposes inner parts of the protocol more explicitly; I see functions like `flush`, `sync`, `bind`, and portals as well. Hpgsql exposes much higher-level parts (and I’ve had feedback that I should consider having lower-level bindings).

I find it interesting as well that `FromField` and `ToField` support both text and binary formats, which I guess is understandable for a lower-level binding. I started with that in hpgsql as well, but thought implementers would have too much choice, and then things like array and composite types encoding would become a bit more annoying in a higher-level API, because in some cases you can’t mix formats, e.g. for composite types, and in other cases… well, I’d have to have two implementations (e.g. for binary/text arrays).

In the end I learned that enums are encoded as text even with the binary format - and that is user friendly enough - and that made me settle on binary only.

The one thing I think will be annoying in hpgsql is implementing all the auth methods. I recently added SCRAM-SHA-256, but there are still many others. TLS-encrypted connections as well.

---

<div class="post-metadata">

**Author:** ![tdammers](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/tdammers/32/3319_2.png) [@tdammers](https://discourse.haskell.org/u/tdammers)\
**Post date:** [June 11, 2026, 8:11pm UTC](https://discourse.haskell.org/t/ann-hpgsql-a-pure-haskell-postgresql-driver-no-libpq/14101/15 "2026-06-11T20:11:14Z")

</div>

> [@mzabani](#):
>
> - Query arguments passed via the protocol instead of being escaped into the query string

I applaud you for this alone.

`postgresql-simple` does not do this, and that’s bad, especially considering how it seems to be the go-to postgresql library in the Haskell ecosystem.
