The cheapest time to learn that an AI-written migration drops the wrong column is before you point it at anything that matters, and the cheapest place to learn that is a throwaway database on a free server. The point is not to make model-generated schema changes perfect; it is to catch the failure mode that a syntax check cannot see, the one where every statement executes cleanly and yet the wrong invariant disappears.
When you use MonkeyCode's free model access to turn a plain-language request into a migration, the output is best treated as a hypothesis rather than a finished change. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The dangerous part of DDL is that a successful apply does not prove a safe change. A migration can rename the email column to login_email, apply without error, and leave every downstream query pointing at a name that no longer exists. It can add a NOT NULL column with no default and instantly make a table uninsertable. It can drop a unique constraint that your application has been relying on for identity. If you only verify that psql -f exited zero, you are testing the database's ability to execute statements, not the migration's ability to preserve your system.
Instead of reviewing the migration by eye, you run it against a small, controlled copy of the schema and the few invariants that must survive. The script below starts a disposable PostgreSQL container, creates a tiny users table with two rows, applies a migration file, and then asserts two facts: the email column still exists and the table still contains two rows.
#!/usr/bin/env bash
set -euo pipefail
MIGRATION_FILE=${1:?usage: run_migration_check.sh migration.sql}
DB_NAME=migration_check
PG_IMAGE=postgres:16-alpine
CONTAINER=migration_check_pg
docker rm -f ${CONTAINER} >/dev/null 2>&1 || true
docker run -d --name ${CONTAINER} -e POSTGRES_PASSWORD=postgres -p 55432:5432 ${PG_IMAGE} >/dev/null
cleanup() {
docker rm -f ${CONTAINER} >/dev/null 2>&1 || true
}
trap cleanup EXIT
for i in {1..30}; do
if docker exec ${CONTAINER} pg_isready -U postgres >/dev/null 2>&1; then
break
fi
sleep 1
done
docker exec -i ${CONTAINER} psql -U postgres -v ON_ERROR_STOP=1 <<'SQL'
CREATE DATABASE migration_check;
SQL
docker exec -i ${CONTAINER} psql -U postgres -d ${DB_NAME} -v ON_ERROR_STOP=1 <<'SQL'
CREATE TABLE users (
id bigserial primary key,
email text not null unique,
created_at timestamptz not null default now()
);
INSERT INTO users(email) VALUES ('alice@example.com'), ('bob@example.com');
SQL
docker cp ${MIGRATION_FILE} ${CONTAINER}:/tmp/migration.sql
docker exec -i ${CONTAINER} psql -U postgres -d ${DB_NAME} -v ON_ERROR_STOP=1 -f /tmp/migration.sql
docker exec -i ${CONTAINER} psql -U postgres -d ${DB_NAME} -tAc <<'SQL' | grep -q '^1$'
SELECT count(*) FROM information_schema.columns WHERE table_name='users' AND column_name='email';
SQL
docker exec -i ${CONTAINER} psql -U postgres -d ${DB_NAME} -tAc <<'SQL' | grep -q '^2$'
SELECT count(*) FROM users;
SQL
echo 'Migration preserved the required invariants.'
The script is deliberately narrow. It does not try to understand the migration; it only checks the facts you refuse to give up. A migration that renames the email column will apply cleanly but the first assertion will fail, and the command will exit nonzero. A migration that accidentally truncates the table will fail the second assertion. That is enough to turn a silent surprise into a visible build failure.
When the script fails, you do not have to reason about the whole migration at once. Dump the resulting schema with docker exec ${CONTAINER} pg_dump -U postgres -d ${DB_NAME} --schema-only > after_schema.sql and compare it with the baseline you meant to keep. The diff usually shows the exact clause that stole an invariant. You can then go back to the model, add the missing constraint as an explicit requirement, and run the same check again.
The container is stateless because the script never mounts a volume, so any Linux host with Docker works. If you have a free server option available through MonkeyCode, this is a sensible place to run the check; the same command also works on a local daemon when you are offline. The important part is not where the container runs, but that it disappears after the check.
The harness has real limits. It only verifies what you explicitly assert. If you forget to assert that a unique constraint survives, a migration that silently drops it will pass. It also does not exercise concurrent write paths, query plans, or data transformations; it proves that the schema change leaves a minimal fixture in the expected shape, not that production traffic will be safe. Do not use this as your only gate for a large or sensitive dataset. If your migration rewrites millions of rows, uses extensions, or depends on triggers, extend the check with a sanitized copy and a representative workload.
This workflow is not for everyone. If you do not have a clear invariant to protect, or you cannot run a small copy of the relevant schema, stop. The script will give you false confidence if you feed it a schema that is only vaguely like production. It is most useful when the migration is small, the invariants are explicit, and the failure mode is the kind of silent change that a syntax check cannot catch.
A free server is enough here because the database disappears when the container exits; if you have such an option available, this is a better use for it than keeping an idle demo running.
Top comments (0)