Back to articles

My First Rust Program

This is a basic CLI program that will clone an entire PostgreSQL schema from a source to target database

Share

For the longest time I was really only using JavaScript/TypeScript for my projects. A React frontend and a NodeJS backend using Express was my typical project setup. I will admit that I'm so done with React and the whole JavaScript ecosystem. Working with the frontend and ui/ux makes me want to puke at this point. I dread every second of it, I like working in the backend so much more. One of the things I love most about AI is how I never have to write react code again, it's actually quite good at this. In fact, I got gpt 5.5 to take this site (which used to be a simple static html/css site) and transform it into a dynamic site that can render articles using honojs web framework. It one-shotted this, and having to do this by hand would have taken weeks.

At first I chose Go as the new programming language I wanted to learn. I loved it so much. Super easy to read and write, and from now on I'm going to be using Go for my backend language instead of TypeScript. After using Go for a couple months and building small little personal projects with it (along with rewriting entire backends in it) I decided it was time to really step it up and learn one of the "hard" programming languages: C, C++, or Rust. I ended up choosing Rust as people online seem to really love it and I heard it's difficult to learn, so this was a perfect time to build a tool I've always wanted, a database schema clone CLI.

Dealing with database migrations

One of the problems I hate having to deal with at my job is getting my local database up-to-date with changes we make on our development and productions servers (which happens all the time). New tables, new columns, changes to names of table and columns, updated views that point to things I don't have locally, it is such a pain to deal with. I use DataGrip as my database tool to help with all this, and it works great, but I wanted a simpler way to deal with it, just a single command to run in the terminal and have everything just work. To be clear, my CLI program will basically delete everything you have locally (tables, columns, views) and clone what the target database has, it will not try to append and remove things to match while keeping all the current data in the tables. Think of it as a "clean start" and from there you can start adding data to tables again over time. Even though at my job we use MySQL as our main database, I wanted this tool to be specific to PostgreSQL cause I enjoy using postgres for my personal projects.

How it Works

Diagram of a source PostgreSQL database cloning tables, columns, primary keys, foreign keys, and extensions into a target database
Source database metadata is collected and applied to the target as a full schema clone

The basic building blocks of this app are a "source" and "target" database. The tool would scan the source database and collect all the tables, columns, and views that the target would need. This also includes the primary keys, foreign keys, custom enum column types, and extensions that would also be needed for the target database. All of these data points would be collected and prepared to be cloned to the target database. On top of just cloning all these things, my app can also import data from the source to target, ensuring all the rows of data are added after the initial clone happens. It all works so perfectly and I was surprised at how quickly I caught on to how Rust worked, although the compiler would get upset with me often along the way. I had a ton of places in my code where I had passed a variable into a function, and then attempted to use that variable again after the function had run, not knowing that value could not be used again as the ownership of this variable had been moved into the function. I also had places in my code where I would use multiple instances of a mutable reference, which Rust didn't like. Confusing for a little bit, but over time I feel like I really started to get the hang of it.

The order in which the logic in this program runs is also very important during the cloning process. It must go:

  1. Create all the tables
  2. Add the extensions (user must manually install on target db before running command)
  3. Add columns (including type, default value, allow nullable)
  4. Add primary keys
  5. Add foreign keys
  6. Add the views

One good reason this order is important for example is you cannot add foreign keys that point to columns that have not already been declared as a primary key (or having a unique constraint). Another good one is each view definition query must point to tables that actually exist, which is why views must be created after the tables and columns have.

My Thoughts on Rust

Rust is my first lower level language I've attempted to learn. JavaScript, C#, and Go are the only other programming languages I have any experience with, and with C# in particular I don't remember much from it. Go is awesome, although I'm still a beginner and have tons more to learn I would say it's my favorite language to use.

To learn Rust I started watching this amazing YouTube video that taught me the basics very quickly. Once you know the basics of a single programming language, you kinda know them all, it's just learning the syntax of that specific language. Rust's ownership thing was confusing to me at first. Here's a good example of an error I got that made me so mad because I just didn't understand what was wrong:

src/main.rsrust
let target_client: Client = match target_db_config.create_client() {
    Ok(client) => client,
    Err(e) => {
        println!("ERROR: {:#?}", e);
        return;
    }
};

// USE A TRANSACTION FOR ANYTHING THAT DEALS WITH THE TARGET DB
let mut target_transaction: postgres::Transaction<'_> = match target_client.transaction() {
    Ok(x) => x,
    Err(e) => {
        println!("ERROR: {:#?}", e);
        return;
    }
};

It's because the transaction() method borrows the Postgres client as mutable for the entire lifetime of the transaction, not just a regular reference. You cannot use this target_client variable while the target_transaction still exists.

Here's another example of where ownership gave me problems:

src/main.rsrust
// creating a function that takes in a DbConfig variable
fn create_target_schema(
    source_client: &mut Client,
    source_db_config: DbConfig,
    target_client: &mut Transaction,
) -> Result<(), Box<dyn Error>> {
    ...
}

// passing in my dbConfig variable
create_target_schema(
    &mut source_client,
    source_db_config,
    &mut target_transaction,
)?;

Later in the code I would attempt to use source_db_config again somewhere, but the ownership was moved to the function and I could not use it anymore.

Looping through an iterable with iter_mut() would also throw errors and I never understood why. Inside the loop I would constantly attempt to do something with the tables vector like pushing to it or modifying it in some way, and didn't understand that the loop itself mutably borrowed the items inside, and that adding to it would make the vector bigger and attempt to move all the values inside a new memory location.

src/main.rsrust
for t in tables.iter_mut() {
    ...
}

I Plan on Sticking with Rust

This is the very first program I've used the Rust programming language with. I will admit, right now I think the syntax is really hard to read but over time I might get more used to it. I believe Rust, C++, or C are good programming languages for me to learn and use going forward with at this point my career. I'm more interested in backend and CLI stuff or anything that doesn't have to do with a website or React app, and these three languages are difficult but offer very good knowledge and concepts. It makes you think harder about how to do something, and coming from web apps mostly, things like memory management, the heap and stack, pointers, and stuff like that is all new to me. I would start using only Go, and again I love it so much, but I'd only use it for backend APIs, Rust for me is going to be my main "lower level" language

Feel free to checkout the code