External Publication
Visit Post

ANN: pGenie – a SQL-first code generator for PostgreSQL: no DSLs, no ORMs, no hand-rolled codecs

Haskell Community [Unofficial] April 2, 2026
Source

However in many codebases they use a single type to model a database row and a domain model that also happens to have a JSON instance that surfaces in the public API, which results in coupling of everything, leading to changes caused by one tiny aspect escalating to the entire codebase or worse, silently breaking APIs. I want to stress that this is not what pGenie does or ever will do.

That’s great to hear. Then again I’ve seen this much more in C#/Java than in Haskell where people generally care more about types.

The core problem here is that in Postgres all fields of composite types are nullable and there’s no control over that. Elements of all arrays are also nullable and there’s no control over that either. So this merely represents the truth

Exactly. I agree with the way it’s represented but only in the context that this is actually a row from in a Postgres database, and not a “real” track.

Not in this case. The generated type you mention is located under the *.Types.*

Well, yeah, but that means you need qualified imports to do something like fromRow :: Row.TrackInfo -> TrackInfo which I guess is a matter of preference.

In case of result sets you get complete control of issues like this via the signature files. They let you narrow down such types by stating that you expect only non-null values for both the array and its elements, as is done here, which leads to generation of a Vector Text without maybes. Doing such tweaks will lead to runtime decoding errors in case the database will present you with nulls, but that’s a tradeoff that you can choose to make in order to have a more ergonomic API. The default is safe.

I get that, but I don’t see how it’s better than just defining your domain types separately and having a conversion function. In that case any queries you do will return [*Row] and then it’s the responsibility of the caller to figure out how to handle each row individually, whether it’s defaulting the Maybe (Vector x) to mempty or something else.

Now regarding Set . Postgres doesn’t have a set type. So the starting argument is again about the truth.

I don’t argue with the truthfulness of what pGenie generated, in fact I agree completely with the Maybe (Vector (Maybe Text)) representation – I just think the naming itself makes it seem like we’re inferring a real domain model from a Postgres schema.


To summarize: I think what you’ve done is really cool, the only changes I would do is be more explicit about the naming (even if that means changing the .Types namespace to .Tables or something more obviously database-related).

Discussion in the ATmosphere

Loading comments...