Typing before correctly parsing

October 15th, 2022

Hopscotching with a view

Instead of Postgres serial data type for primary keys, I use random bigint’s for Postgres ids. (This is yet another idea courtesy of Numeracy.) Despite my initial reversion to the idea (what if you have a collision!1), random bigints offer some advantages over serial. At minimal cost (4 bytes more per row), they obscure an entity’s id while being simpler than uuid’s and they can be trivially shortened by base64 encoding them. For short, secret links like numeracy.co/q/8m0Kx are harder to guess than numeracy.co/q/4.

One small hiccup I noticed pretty quickly: you can serialize bigints into JSON, but JavaScript’s default JSON.parse can’t handle it because JavaScript’s default number type does not handle it well. My initial thought is that I should just marshal these id’s as strings. Golang’s json marshalling actually provides a ,string option to struct tags for precisely this. However, I use sqlc to generate Go code (including json struct tags) and this is not currently possible (the go_struct_tag override doesn’t work for this).

My next thought was creating a custom data type BigInt and using data_type overrides to force the generated models to use my BigInt types. This kind of works.

It wasn’t until I was revisting the code today that I realized I was thinking of it backwards. Both JSON and JavaScript can handle bigint’s. It’s only JSON.parse that can’t. Instead of mangling the data, clients can just parse with a library like json-bigint. It all returns to a lesson I learned long ago. Clean interfaces are more important than clean code.

  1. There’s a 50% chance of a collision after 2.5 billion rows, at which point you would have filled up your serial space anyways. Given you’re unlikely to have 100% uptime on your way to a billion rows of anything, so clients are usually able to handle failed operations anyways.