How pgrust was built: four attempts to rewrite Postgres in Rust with AI Developers spent $100k and four attempts to rewrite PostgreSQL in Rust using AI, resulting in pgrust, a 1.8M-line idiomatic Rust codebase. The project aims to enable safer AI-driven feature additions to Postgres, with a fifth version already outperforming Postgres in transactional workloads and matching Clickhouse in analytical queries. tl;dr After four attempts and dropping $100k, we succeeded in using Opus to rewrite Postgres in Rust. We defined a number of skills and were able to build a repeatable process to rewrite Postgres files into Rust. After running up to 40 concurrent subagents for days, the end result is the 1.8M lines of idiomatic rust code that make up pgrust. Over the past three months, I’ve been working with my friend Jason Seibel on building pgrust. The idea behind pgrust is we want to show that by leveraging AI, it’s possible to can build a much better version of Postgres. Unfortunately Postgres is written in C and AI still makes a lot of mistakes. For this reason, we started by first rewriting Postgres in Rust. We believe that by having Postgres in Rust it will become a lot easier for AI to safely add new features to Postgres. This thesis is starting to be proven true. Currently in development we have a new, 5th version of pgrust that is faster than Postgres for transactional workloads and is as fast as Clickhouse when it comes to analytical style queries. For context, Clickhouse is hundreds of times faster than Postgres when it comes to analytical queries. Let us walk you through each of our attempts and what we learned from each approach. We figure for those interested in completely rewriting legacy systems, this post will serve as a useful guide as how to go about such a rewrite of your own. Attempt 1 – pgrust-og The first version of pgrust, pgrust-og was initially was going well. It got very close to completion, but ended up having fundamental issues that made it unsalvageable. What I consider to be the root issue is we approached pgrust-og by porting one feature at a time for contrast in later versions I worked on porting one file at a time . There were roughly three different phases to the pgrust-og lifecycle: - Foundation - Features - Last Mile Foundation Initially I worked on laying out the core foundation of the database. Postgres has a couple of key systems that everything else relies on. To name a few: - The query parser – The parser converts your SQL query into a representation Postgres can understand what data you’re asking for - The query planner – The query planner takes the output from the parser and produces a query plan. A specific way for Postgres to retrieve the data you’re looking for - The executor – Once Postgres has a plan for how to fetch your data, the executor is the one that actually fetches it Along with these three, there are maybe 15 other core subsystems of Postgres. Over a couple of days I built a new database in Rust that architecturally had the same subsystems as Postgres. This step seemingly went very smooth. For each component, I already had a pretty good sense of how it should work. I gave Codex 5.4 access to the Postgres codebase and would ask it to come up with a plan for rewriting X subsystem of Postgres into pgrust-og. I would then review the plan and make sure it covered everything I wanted and would give Codex feedback on the plan where it fell short. From there I had codex execute the plan. Given these were the core systems of Postgres, I read the code thoroughly to make sure I understood how they worked. This approach worked pretty well and after a couple of days I had knocked out pretty much all the core pieces. Features, Features, Features Once I had the core scaffolding in place, I started working on adding all the features that Postgres supports. The easiest way to do this was to look at the Postgres regression tests and look at all the features they were testing. Now the thing about Postgres is it has a surprising number of features. When it comes to features, Postgres looks almost like a programming language’s standard library. Postgres has: - Over 300 different types including two different json types - Nearly 500 different SQL keywords - More than 3000 different builtin functions That’s a lot to implement Initially I was following the same approach I did when working on the foundation phase. The “problem” I was running into was that Codex was too good at implementing these features. For any individual feature, Codex is usually able to one-shot it. If you want to try it yourself, ask Codex to write a JSON parser for you. The problem I was running into was I would ask Codex to come up with a plan and then wait hours for it to implement the plan. I spent most of my time “programming” being idle This is when I realized that it might be possible to run multiple agents at a time. I decided it was time for a new approach. Instead of working on one feature at a time, I would try multiple. The next day I installed Conductor a tool that let’s you manage multiple agent sessions simultaneously. You can see the moment I started using Conductor in the git history: Surprisingly, this approach worked Because each feature was relatively independent of each other, multiple Codex agents could work on them independently. I eventually scaled up this approach and started spinning up as many as 20 agent sessions each. Usually I would look at which test files were left to work on and then spin up a session for each file. From there I would see what the current failures were and have Codex work on completing that file. There were two issues with this approach. First I started burning through my Codex quota. I had to get multiple Codex accounts and switch between them in order to stay on the subscription plan. Second, I started maxing out CPU on my computer. Rust builds end up being somewhat cpu intensive and when you run enough builds, even my M4 Macbook Pro couldn’t handle them all. I ended up setting up GitHub actions, just so I could run tests off of my computer. Last Mile Over the course of about four weeks I was able to get 96% of the Postgres tests passing. That’s when progress started to slow down and things started to grind to a halt. The big obstacle came when I tried to get all the Postgres planner tests to work. The postgres planner is the most complicated part of Postgres. It’s 50k lines of very nuanced logic that figures out the optimal way to fetch the data your SQL query is asking for. In short, it evaluates every possible way to fetch your rows out of the table, every possible way to perform the joins, and applies a advanced cost model to figure out the best way to plan the query. While pgrust-og had a planner and had all the functionality that postgres does, the way the planner was integrated into the rest of the codebase was subtly different. This led to an “organ transplant failure”. To give an example of one of the failures, let me show you the differences in how pgrust-og and pgrust would represent a plan for the query: SELECT name FROM users WHERE age 30 . In Postgres this ends up being represented as a single plan node: SeqScan { .scan.scanrelid = 1, .plan.targetlist = name , .plan.qual = age 30 , } While in Rust, it ended up being represented as three plan nodes: Projection { .columns = name , .input = Filter { .predicate = age 30 , .input = SeqScan { .table = users, }, }, } On top of that in Postgres the tableis referred to by an index for the query while in Rust it’s referred to by a string. While these look like small differences, they end up being massive. In C lots of functions will take a sequential scan node with a filter. In rust, those functions would sometimes need to take a sequential scan, sometimes take a filter node, and other times need to take a projection node. Fix just this one issue would be difficult as it would require rewriting everywhere where a seqscan, filter, or projection node is used. In total, dozens of slight differences like this made it infeasible to get all the tests passing with pgrust-og. After struggling to get the planner to work I realized just how different the internals of pgrust-og were versus Postgres. If there were subtle differences in how the planner worked, who knew how many subtle differences there were in things like replication and backups. I realized it would be impossible to get pgrust-og into a production ready state. That’s when I decided it was time for a new approach. Enter c2rust. cost: 8x codex accounts for 1 month = $1600 Attempt 2 – c2rust For our next attempt we wanted to take a different approach. Instead of trying to go feature by feature, we wanted to see if there was some way we could transpile the Postgres code from C to Rust. That way each Rust code would match up line by line with equivalent Postgres code. It would be much easier to audit the code and show that I was doing things exactly the same way Postgres was doing. c2rust is a open source project that will transpile your C code to unsafe rust. While that technically would be “Postgres rewritten in Rust” it wouldn’t help me further my goal of making it easy to add new features to Postgres. The idea was that we would use c2rust to unsafe rust and from there gradually rewrite the unsafe rust code into safe rust. After toying with c2rust for two days, Jason managed to get it to completely transpile the Postgres C code to 5.3M lines of unsafe Rust. That resulting code completely passed the Postgres test suite and passed 100% of the Postgres regressions suite and had similar performance to Postgres. That’s not even the coolest part. The c2rust version of pgrust was ABI compatible with Postgres. That meant things like Postgres extensions would be compatible with Postgres. We didn’t even think that was possible At that point, we thought the hard part was over. Now all we had to do was have Codex refactor the code into idiomatic Rust. It turns out the hard part was not over. When we tried to refactor the unsafe rust code, we found it would take hours to convert a single function. To show you why let me show you what some of the output of c2rust looks like: pub unsafe extern "C" fn makeAlias mut aliasname: const ::core::ffi::c char, mut colnames: mut List, - mut Alias { let mut a: mut Alias = newNode ::core::mem::size of::