←back to thread

169 points adityaathalye | 4 comments | | HN request time: 0.001s | source
Show context
whalesalad ◴[] No.45119238[source]
I've been absolutely waist deep in a bitemporal system on top of PostgreSQL using tstzrange fields. We manage an enormous portfolio of hundreds of thousands of domain names. Every time our core db is modified, before/after states are emitted to a change table. We've been doing this since 2022. Those changes get lightly transformed via trigger into a time travel record, with the valid from/to range and a gist index to make asking questions about the state of the world at a particular point in time easy. For perspective our change table has 90M rows.

All of it works quite well and is decently performant. We can ask questions like, how many domains did we own on March 13th, 2024? Or look at the entire lifecycle of a domains ownership (owned, released, re-acquired, transfered, etc).

The big challenge and core issue we discovered though is that our data sucks. QAing this new capability has been a moving target. Tons of mistakes over time that were partially undone or manually undone without proper audit trail. Ghost records. Rapid changes by our bulk editor tool a->b->a->b that need to get squashed into just a->b. The schema of our database has evolved over time, too, which has made this tough to view a consistent representation of things even if the fields storing that data were renamed. When the system was first introduced, we had ~5 columns to track. Now we have over 30.

Suffice to say if I were to do things over again, I would implement a much better change tracking system that bakes in tools to clean/erase/undo/soft-delete/hard-delete mistakes so that future me (now) wouldn't have so many edge cases to deal with in this time traveling system. I'd also like to just make the change tracking capable of time travel itself, versus building that as a bolt-on side table that tracks and works from the change table. Transitioning to an EAV (entity-attr-value) approach is on my spike list, too. Makes it easier to just reduce (key,val) tuples down into an up to date representation versus looking at diffs of before/after.

Really interesting stuff. I learned a lot about this from Clojure/Datomic and think its quite neat that so many Clojurists are interested in and tackling this problem. As the author notes in this post, XTDB is another one.

replies(4): >>45119379 #>>45119468 #>>45120038 #>>45123316 #
adityaathalye ◴[] No.45119379[source]
For my little system (blog author here) I've decided that all tables will be append-only logs of facts. In the post, I tried keeping traditional schema alongside a "main" facts table. Of course, the problem of audit logs comes up for any change made to traditional "current-database-view" tables. And then who audit logs the audit log?

I feel like "two systems" is the problem.

Writes should either be traditional schema -> standard log of all changes, OR, should be "everything is a log", and the system keeps the "current" view updated, which is just a special case of the "as of" query, where "as of" is always "now".

How badly my schema will behave (in my SQLite-based architecture) is to be discovered. I will hopefully be in a position to run a reasonable benchmark next week.

A follow-up blog post is likely :sweat-smile:

replies(2): >>45119774 #>>45122749 #
1. bbkane ◴[] No.45122749[source]
Looking forward to the second blog!

As a side project, I'm writing a CLI to tie environment variables to project directories ( https://github.com/bbkane/enventory ) and share env vars between projects with a "reference" system. Similar to direnv but in a central SQLite DB.

See https://github.com/bbkane/enventory/tree/master/dbdoc for how the schema looks, but it's a few tables with foreign keys to each other to support the "reference" system.

Unfortunately those foreign keys mean I don't have a way to "undo" changes easily. So instead I added a confirmation prompt, but I'd still like that undo button.

Hopefully in subsequent blog posts you can follow up on your "Model an example domain of sufficient complexity" TODO and make this a bit less abstract (and hopefully more understandable) to me.

replies(1): >>45123138 #
2. bbkane ◴[] No.45123138[source]
Maybe I could convert every table into a "log" table (i.e., append only similar to your blog) and add a timestamp column to each table (or UUID). Then I also store all of these UUIDs in another table to keep track of the global order. To "undo" I look up the previous UUID, find entries with that UUID, and append to the log table the values that make the state the same as the previous UUID....

This isn't as general as your setup, but I think it would work ok with my foreign key issue.

replies(1): >>45124823 #
3. adityaathalye ◴[] No.45124823[source]
I believe UUIDv7 does not (cannot) guarantee total order. I am using it as a method to manage multiple timelines---each "fact" lives on its own virtual timeline (identified by a "valid_id", which is a UUIDv7). My DB system does not automatically disambiguate conflicting facts (because it cannot --- it simply does not have the meta-information). The users of the system can manually audit and fix inconsistencies by making correcting (append-only) entries.

For your case, I feel like your best bet would be to create the global order in the first place... i.e. force all writes through a single (and single-threaded) writer process, and fetch reads off one or more independent reader processes (I'm doing this with WAL-mode SQLite).

But I could be totally wrong --- this is my first foray into the freaky world of temporal data systems :D

replies(1): >>45130657 #
4. bbkane ◴[] No.45130657{3}[source]
As this is a single-user CLI tool I don't need any concurrency. So I can probably use the timestamp directly instead of a UUID and not worry about multiple timelines.

On thinking more this morning I think I can keep my original tables and add append-only "log" tables with triggers to keep them up to date when the main tables change. That doesn't slow down my main tables performance (they only contain the current data) and also allows me to incrementally change my db (adding new tables + triggers is easier than changing existing ones).

I've made https://github.com/bbkane/enventory/issues/122 to track this idea, though I'm not sure when (or even if) I'll have time to really play with the idea (much less "productionalize" it- add support for all enventory events, tab completion on the CLI, tests, UI, etc...).

But I'm at least tracking it for when I get time and motivation! Thanks for writing the post and replying to my comment.