AI-Generated Migrations: The Data Loss Pattern
AI-generated Alembic and Django migrations have a specific failure pattern: column type mismatches and missing nullable defaults that corrupt data silently.
Database migrations are where AI-generated code fails quietly and expensively. An AI assistant will produce a migration file that passes linting and applies cleanly on an empty development database. Then it ships to production — where it either fails at apply time or silently truncates values that don’t fit the new column type. The linter saw nothing wrong. Neither did the code review. The problem was structural, not syntactic.
Why AI Assistants Generate Plausible but Wrong Migrations
AI coding assistants generate migrations by pattern-matching against training data — they produce syntactically valid Alembic or Django migration files, but they don’t inspect the live database schema, existing data distribution, or production engine differences that make a migration safe. BrassCoders scans migration files for security patterns (hardcoded connection strings, raw SQL with format strings) but does not evaluate whether the schema change itself is structurally safe.
The gap is inherent to how the tools work. An AI assistant sees the model definition you’ve described in this conversation, plus some ORM boilerplate from your imports. It doesn’t know what data already sits in the users table, and it has no view into the production PostgreSQL version or the downstream foreign key constraints on the column you’re changing. Alembic’s autogenerate mode closes part of this gap by inspecting the live database to generate diff-based migrations, but AI assistants frequently bypass autogenerate entirely and write op.add_column() calls from scratch. That’s where the structural errors enter.
The Three Migration Patterns That Corrupt Data
BrassCoders flags the Python code that produces migrations, but the data-loss risk lives in three structural patterns: adding a NOT NULL column to a populated table without a server_default, changing a column type that truncates existing values, and generating a migration that runs correctly on SQLite test data but fails on PostgreSQL production schema.
The NOT NULL pattern is the most common. An AI assistant sees a model field added with nullable=False and generates op.add_column(table, Column('status', String, nullable=False)). On an empty table, this applies fine. On a table with 50,000 rows, PostgreSQL rejects it at apply time: existing rows have no value for the new column, and the constraint can’t be satisfied. The correct migration takes two steps: add the column as nullable first, then run a separate data migration to backfill existing rows before converting it to NOT NULL. AI assistants generate the column definition and miss the backfill.
Column type changes are subtler. Narrowing a String(500) to String(100) in SQLAlchemy generates an ALTER TABLE ... ALTER COLUMN under PostgreSQL, which silently truncates values over 100 characters. Some databases throw an error; PostgreSQL, depending on version and configuration, may just truncate. The AI assistant generated code that is syntactically correct and semantically destructive.
The SQLite-to-PostgreSQL Gap AI Misses
SQLite accepts almost any column type and coerces data silently; PostgreSQL enforces types strictly and rejects operations that would lose data — an AI assistant running tests against SQLite sees a passing migration that will corrupt or reject data when deployed against a PostgreSQL production database. BrassCoders’s security scanner flags hardcoded SQLite paths in production configuration, but the type-coercion gap is a runtime behavior difference, not a source pattern.
A concrete example: SQLite has no native boolean type. It stores True as 1 and False as 0 in an integer column. If an AI assistant writes a migration that changes a boolean column to a string, SQLite coerces the integer to a string silently. PostgreSQL’s type system is stricter. The same migration may error or produce unexpected casts. The test suite passes on SQLite; production fails on PostgreSQL. This isn’t a static-analysis problem. Static analysis can flag the column type change, but whether it’s safe depends on what data is already in the column. Only a live database can answer that.
The pattern repeats with JSON columns, ARRAY types, and timestamp precision. SQLite’s loose typing absorbs mismatches that PostgreSQL surfaces as errors or data changes.
Adding a Migration Review Gate to Your CI Pipeline
BrassCoders scans the Python migration files for security patterns (hardcoded connection strings, raw SQL with string formatting), but the structural safety check belongs in a separate step: run migrations against a production-schema clone in CI before they touch production.
The concrete setup: spin up a PostgreSQL instance in CI (most CI providers offer native PostgreSQL services) and seed it from a production dump with production-representative row counts but anonymized PII. Then run your migration suite against that database on every pull request. For Django, this is python manage.py migrate --database=ci. For Alembic, alembic upgrade head against a connection string pointing to your CI database. If the migration fails or produces unexpected results on realistic data, you find out before the deploy.
Django’s migrations documentation covers the test database connection patterns for CI, and Alembic’s documentation on autogenerate covers how the tool inspects live schemas to generate diffs. Both approaches catch the structural errors that AI assistants miss — but they require a real database with real data.
Run BrassCoders on the migration files in the same CI step. It catches the security issues a structural test won’t: op.execute(f"UPDATE users SET role = '{role}'") in a data migration is a SQL injection vector even when the schema change is structurally sound. Both checks together cover what neither handles alone.
Install the OSS core with pip install brasscoders and run brasscoders scan . — it picks up migration files alongside the rest of your Python codebase and flags the security patterns without sending any code offsite. The structural database review requires a live CI database; the security review runs locally, zero outbound network calls.
Frequently Asked Questions
Can BrassCoders catch bad database migrations?
BrassCoders catches security patterns in migration files — raw SQL string formatting (SQL injection risk), hardcoded database credentials, and unsafe eval() in data migration functions. It does not analyze the structural safety of schema changes like adding NOT NULL columns or changing column types.
What's the most common AI migration bug?
Adding a NOT NULL column to an existing table without a server_default or a data migration to populate existing rows. This fails at apply time on populated tables. AI assistants generate the column definition correctly but miss the backfill step.
Does this apply to Django migrations and Alembic?
Yes to both. Django's ORM generates migrations through makemigrations but AI assistants often hand-write or modify them. Alembic's autogenerate is helpful but AI assistants frequently bypass it and write migrations directly, introducing the same structural errors.
How do I test migrations safely in CI?
Run migrations against a database seeded with production-representative data (anonymized if necessary) in CI before every deploy. Use a PostgreSQL instance in CI even if you develop locally on SQLite. Alembic and Django both support running against a test database connection.
Will AI get better at this?
The core problem is that migration safety is context-dependent — it depends on the live data distribution, the production engine version, and the existing schema state. These aren't available in the training context or the AI's conversational context unless explicitly supplied.