Why I Let the Database Own Its Own Schema
How ditching the ORM translation layer and letting PostgreSQL own its schema solved the drift problem for good.


If you've read my previous article, you already know the root cause I ran into: treating the ORM as the owner of the database schema. If you haven't, I'd recommend starting there before continuing.
When you use an ORM, you define your tables in your project's language Python or TypeScript and the ORM translates that into SQL and creates the tables in the database. Naturally, you start treating the ORM as the source of truth.
But that translation process is lossy.
What you define in the ORM and what actually ends up in the database can be different things and the gap between them is easy to miss.
The Shift in Approach
The fix is simpler than you'd expect: let the database own its own schema by writing your tables in raw SQL.
No translation layer.
No silent omissions.
But before you close this tab, I hear you:
"Raw SQL? So now I have to manually track every single change forever?"
Not quite.
Enter pg_schemadiff
This is exactly where pg_schemadiff enters the picture.
It's the tool I now use to define and manage everything schemas, tables, indexes, relationships… the works.
What it does
pg_schemadiff computes the difference between two PostgreSQL schemas and generates the exact SQL needed to go from point A to point B.
It’s built with minimal downtime in mind, making it practical for both development and production deployments.
In short:
You define what you want the schema to look like → point the tool at your actual database → it tells you exactly what needs to change.
A Simple Example
Let’s make this concrete.
Say you created a users table with a plain UUID primary key, and later you want to switch to PostgreSQL’s built-in gen_random_uuid() for automatic ID generation.
With the SQL-as-source-of-truth approach, you simply update your schema file.
Before
CREATE TABLE IF NOT EXISTS auth.users (
id UUID PRIMARY KEY,
email VARCHAR(320) NOT NULL UNIQUE,
full_name VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
);
After
CREATE TABLE IF NOT EXISTS auth.users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(320) NOT NULL UNIQUE,
full_name VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
);
That one-line change is all you touch.
pg_schemadiff then:
-
Compares your updated schema file with the actual database
-
Detects the difference
-
Generates the exact
ALTER TABLEcommand needed
No inference.
No silent skips.
Just an explicit, reviewable migration.
Wrapping Up
By now, you should have a clear picture of what pg_schemadiff does—and why this approach works.
In the next article, we’ll go hands-on:
-
Real commands
-
Real workflow
-
A practical walkthrough in an actual project
Part of the Database migration tutorial series
View all partsRelated Posts
- 5 min read
Managing PostgreSQL Schema Changes with pg_schemadiff
A practical guide to building a simple database migration system using pg_schemadiff, explaining how to structure a project, write schema definitions, and use a migration script to plan, apply, and track database changes safely.
SQLAlchemyDatabase MigrationPostgreSQL - 4 min read
The Empty Migration File That Changed How I Manage Databases
How a silent empty migration file taught me that SQLAlchemy was never meant to own my database schema.
SQLAlchemyDatabase MigrationPostgreSQL

