Skip to content

Decoding PG custom types efficiently without OID collisions across databases #310

Description

@jgaskins

I'm working with a database that uses Postgres ENUM types extensively. They've been there for years and too many things depend on them at this point1, so I can't replace them. Unfortunately, using non-core DB types (custom types and those provided by extensions) doesn't have first-class support yet in either db or this shard.

Part of it, for this shard at least, seems to be that we depend on the oid of the Postgres type to decode it, but the oid of non-core types (including via extensions that come with the core Postgres distribution) are usually different between databases. One way I've worked around this is to have the Crystal shard register a decoder with the correct oids after querying them from the specified DB. I don't know if I like that approach, though. Each oid must be unique across the entire application. If you connect to multiple databases (such as a primary and its replicas) and each one has a bunch of different extensions enabled, you may end up with collisions. That makes it impossible to use PG::Decoders::Decoder as the abstraction for decoding non-core types across those databases.

There have been several issues and PRs opened about using non-core types (many listed below, not exhaustive) and, when considering the above, I'm wondering if oid is the right abstraction for types with dynamic oids. The target Crystal type for the value is typically (always?) known at compile time, so we may simply be able to rely on ResultSet#read(TheType) to decode the value off the wire without considering the oid at all.

The main benefit of using PG::Decoders::Decoder is that you get direct access to the IO, which helps you avoid unnecessary heap allocations while decoding into the target type, but I think we can provide that directly to ResultSet#read, as well.

I have a proof of concept in this branch (which is a simplified version of the implementation I'm using in the app mentioned in the opening sentence), where I decode Postgres ENUM types into Crystal enum types directly from PG::ResultSet. The db shard already provides the ability to decode strings and integers from the DB into Crystal enum types but the approach in bbbcf16 (in that branch) is more efficient if the value is not stored as an integer2.

The main thing to note in that branch is that ResultSet#read delegates everything to ResultSet#read(&), which yields io, bytesize, similar to how Decoder#decode receives io, bytesize, oid. This makes read(&) the most fundamental abstraction, even though most things still delegate to read.

Related to #250
Related to #115
Related to #63
Related to #88
Related to #43
Related to #221
Related to crystal-lang/crystal-db#8

A bunch of the issues above are closed, but the problem isn't really solved. We have workarounds for specific situations (and many of the issues listed above are also about specific situations), but no full solutions for the general problem. For example, decoding Postgres citext as a Crystal String works for deserializing text-like types into String, but involves an intermediate heap allocation for each value that is only used to create the String.

Since most of the above issues are about specific use cases, I wanted to open a discussion of the underlying problem. All previously proposed solutions (including my own) fall short in some way and I think the one I'm proposing here (implementing PG::ResultSet#read(&) as the base abstraction) might work in an oid-agnostic way. If not, I'd still like to try to find a generalizable solution.

Footnotes

  1. I'd call them "load-bearing", but that's apparently become a signal that something was written by an LLM. The overuse of LLMs is ruining everything I like — including em dashes. Ugh.

  2. The branch also doesn't support decoding them from ints because it's intended for illustrative purposes rather than covering all possible bases.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions