# Prisma db push vs migrate deploy: which commands can destroy data?

> **Quick answer:** All three can. `db push` drops columns and whole tables once
> you pass `--accept-data-loss`. `migrate dev` stops on drift and points you at
> `migrate reset`, which wipes the schema. `migrate deploy` is the right command
> for production, but it runs whatever SQL is in the migration files, including
> `DROP TABLE`. Every behaviour on this page was run against Prisma 7.10.0 and
> PostgreSQL 17.11 in containers.

## What is the problem?

Prisma has three commands that change a relational database, and they are for
different situations. None of them guarantees that data will be preserved.
Everything below is PostgreSQL; MongoDB uses `db push` and has no
`migrate deploy` path at all.

| command | what it is for | what it can destroy |
|---|---|---|
| `prisma db push` | prototyping, no migration files | columns and whole tables, with `--accept-data-loss`; everything in the schema, with `--force-reset` |
| `prisma migrate dev` | local development | the whole schema, through the `migrate reset` it points you at on drift |
| `prisma migrate deploy` | staging and production | whatever the pending migration files say to destroy |

The confusion is reasonable. `db push` is the command in most getting-started
guides, because at that point there is nothing to lose. And `migrate deploy` is
correctly described everywhere as the production command, which can be mistaken
for a safe command. Those are different claims.

## The one that matters: migrate deploy runs your SQL

`migrate deploy` does not generate migrations, prompt, or reset. It also does not
inspect what it is about to run. Committed to
`prisma/migrations/20260919000001_drop_order/migration.sql`:

<!-- sql-check: skip this is the content of a migration file, quoted; it was applied on a Prisma 7.10.0 and PostgreSQL 17.11 fixture -->
```sql
DROP TABLE "Order";
```

Measured on Prisma 7.10.0 against PostgreSQL 17.11, with two rows in that table:

```
Applying migration `20260919000001_drop_order`

The following migration(s) have been applied:

migrations/
  └─ 20260919000001_drop_order/
    └─ migration.sql

All migrations have been successfully applied.
```

The table and both rows were gone. The command exited 0 without a prompt, a
warning, or a flag.

The safety decision therefore happens before the migration is committed, not
when it is deployed. `migrate deploy` is the production command because it is
reproducible and non-interactive. Those properties do not make it
non-destructive. Review the SQL, rehearse it on a copy with real volume, and
have a backup you have actually restored from.

It refuses in two narrow situations: a non-empty schema with no migration
history, which is P3005, further down, and an earlier migration that failed,
which is P3009. Neither is a data-loss check.

## What each one actually does

### db push stops, then does it anyway when told

Run against PostgreSQL 17 with a `memo` column removed from the model and one
non-null value in the table:

```
⚠️  There might be data loss when applying the changes:

  • You are about to drop the column `memo` on the `Order` table, which still
    contains 1 non-null values.

Error: Use the --accept-data-loss flag to ignore the data loss warnings
```

Nothing changed. The warning names the column and counts the values.

Re-run with the flag, and the column and its values were gone. The rest of the
row survived. This is a dropped column, not a dropped table.

The problem is that `--accept-data-loss` is exactly what a CI script may acquire
the first time the pipeline fails on that error.

It is not only columns. `db push` makes the database match the schema, so a table
the schema does not mention is a table `db push` wants gone, whether Prisma
created it or not. Measured against a database holding an unrelated
`legacy_audit` table with one row:

```
⚠️  There might be data loss when applying the changes:

  • You are about to drop the `legacy_audit` table, which is not empty (1 rows).
```

The risky case is a schema file that does not mention a table, combined with a
flag that suppresses the warning.

There is a second flag, `--force-reset`, described by the CLI's own help as
"Force a reset of the database before push". On PostgreSQL the destructive scope
is everything in the target schema. It is useful while prototyping, but it
should not reach a committed script.

**Prisma 7 has a gate on that flag aimed specifically at AI agents.** Measured on
7.10.0, `prisma db push --force-reset` refused to run at all:

```
Error: Prisma Migrate detected that it was invoked by an AI agent identifying
itself as "claude-code_2-1-271_agent".

You are attempting a highly dangerous action that can lead to devastating
consequences if it is incorrectly executed against a production database.

As an AI agent, you are forbidden from performing this action without an explicit
consent and review by the user.
```

It then requires an environment variable carrying the exact text of the user's
consent. This check has two limits: it is a Prisma-side check based on
environment variables that agents set, so a person or a CI job running the same
command is not gated, and it covers `--force-reset`, `--accept-data-loss` and
`migrate reset` rather than every destructive path. `migrate deploy` applying a
committed `DROP TABLE`, above, was not gated.

### migrate dev stops on drift, and tells you to reset

Run against a database that had been built with `db push`, so the schema exists
and the migration history does not:

```
Drift detected: Your database schema is not in sync with your migration history.

[+] Added tables
  - Order

We need to reset the "public" schema at "127.0.0.1:15432"

You may use prisma migrate reset to drop the development database.
All data will be lost.
```

It stopped there, with exit code 130, and it does the same with a terminal
attached: it does not offer to reset. The next command it suggests wipes the
schema, and running it at the end of a long afternoon is the risk.

This is not a bug. `migrate dev` keeps the database matching the migration files,
and a reset is the normal way back for a disposable development database. It is
a development command, and it stops rather than resetting on its own. For a
database with data you want to keep, the answer is baselining rather than a
reset.

### migrate deploy refuses when it has no history

A database with a schema in it and no migration history:

```
Error: P3005

The database schema is not empty. Read more about how to baseline an existing
production database: https://pris.ly/d/migrate-baseline
```

That refusal is about history, not about data. Prisma cannot tell which
migrations are already applied, so it stops rather than guess. "Not empty" means
objects rather than rows: measured on 7.10.0, one table containing zero rows was
enough to produce P3005.

It also does not tell you how the schema got there. `_prisma_migrations` being
absent means Prisma Migrate has no history here, which is equally consistent with
`db push`, another migration tool, a restored dump, or hand-written SQL. Find out
which before choosing what to do, because the answer changes it.

The fix for a database you want to keep is **baselining**: generate an initial
migration from the current schema, read it, and edit it where Prisma's model
cannot represent what is really there, such as triggers, stored procedures, or
partitioning. Then mark it applied without running it. New databases run that
same migration normally.

## One more thing that changed in Prisma 7

A `url` in the `datasource` block no longer validates:

```
Error code: P1012
error: The datasource property `url` is no longer supported in schema files.
```

Connection URLs for the Prisma CLI, which is `migrate` and `db` and the commands
on this page, moved to `prisma.config.ts`. Runtime connection configuration for
Prisma Client is a separate question in Prisma 7 and is not what this error is
about.

This matters here because Prisma 6 era tutorials and the `schema.prisma` files
generated from them still carry the old form, and the resulting error reads as a
syntax problem rather than a version change.

Measured on 2026-09-19, `npx prisma -v` reported 7.10.0 and the CLI's own update
notice named `8.0.0-rc.15` as the `latest` tag on npm. Pin an exact Prisma
version in your project rather than resolving `latest`, and check what your
tooling actually installed before trusting instructions about the command
surface, including this page.

## How do I confirm what a command will do?

Ask what is pending first. `migrate status` names the migrations that have not
run:

```
2 migrations found in prisma/migrations
Following migration have not yet been applied:
20260919000001_drop_order
```

Then ask for the SQL those migrations will execute by diffing the live database
against the migration directory. On Prisma 7 the flags are:

```
prisma migrate diff \
  --from-config-datasource \
  --to-migrations ./prisma/migrations \
  --script
```

Measured on 7.10.0 against the database above, that printed exactly the statement
`migrate deploy` was about to run:

<!-- sql-check: skip this is migrate diff output, quoted from the Prisma 7.10.0 run -->
```sql
-- DropTable
DROP TABLE "public"."Order";
```

Two things are required. `prisma/migrations/migration_lock.toml` has to be
present, or it cannot tell which connector the directory is for. And
`datasource.shadowDatabaseUrl` has to be set in `prisma.config.ts`, because
diffing a migrations directory means replaying it into a scratch database first.
Both failures produce explicit error messages rather than wrong answers.

The older `--from-schema-datasource` and `--to-schema-datamodel` flags are gone
in Prisma 7. They were also the wrong comparison: diffing the database against
the schema file tells you nothing about what the committed migrations will do.

Then read the SQL, not only for `DROP`. Anything that rewrites the table,
tightens a constraint, narrows a type, changes a collation, drops an index or an
enum value, or cascades through a foreign key is a decision rather than a step.
`ALTER COLUMN ... TYPE` is on that list because of the rewrite and the lock, not
because it is inherently destructive.

Check whether the database has a migration history at all, which decides
between the `deploy` path and the baselining path:

```sql
SELECT migration_name, finished_at, applied_steps_count
FROM _prisma_migrations
ORDER BY finished_at DESC
LIMIT 10;
```

If that table does not exist, Prisma Migrate has no history for this database and
`migrate deploy` will give you P3005. It does not tell you what built the schema.

Also confirm the current schema from the database rather than from the model
file:

```sql
SELECT table_name, column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position;
```

One detail that surprises people there: Prisma's `Decimal` maps to
`numeric(65,30)` on PostgreSQL, which is what the column will be unless you
override it.

## Why does AI-generated code cause this?

`db push` is the command in the quickstart, so it is the command a model
suggests, and it is correct for the situation the quickstart describes. Nothing
in "update the schema and apply it" says which environment this is.

The second pattern is the flag that makes an error go away. An agent that sees
`Use the --accept-data-loss flag` will add that flag, because the message is an
instruction and following it makes the command succeed. That is the correct
reading of the text and the wrong thing to do.

The third is `prisma migrate dev` in a deploy script, because it is the command
from the development loop and it works.

The fourth is the situation this page addresses: an agent told to use
`migrate deploy` because it is the production command writes a migration that
drops a column and commits it to the migrations directory. Each step follows
the documented workflow. The destructive SQL is in a file nobody read.

## How do I stop it coming back?

**Only `migrate deploy` in CI and production.** Make it the only Prisma command
the deploy role can run, if your tooling allows that. This limits which command
runs, not what the command does.

**Review the migration SQL, because that is where the safety is.** The file is
committed, so it goes through the same review as the code. A `DROP` in it is a
decision somebody made, and the deploy step will carry it out.

**Never let `--accept-data-loss` or `--force-reset` into a committed script.**
Grep for both in review; they are easy to spot and easy to overlook.

**Baseline properly once**, so that P3005 does not have to be worked around.

**Preview with `migrate status` then `migrate diff --to-migrations --script`**
for anything touching a table with data, and have a backup you have restored from
before you apply it. A reverse migration puts the column back; it does not put
the values back.

**Rename with `ALTER TABLE ... RENAME COLUMN`, in a migration you edited.** A
renamed field is otherwise inferred as a drop plus an add, which loses every
value in the column while looking like an ordinary schema change.

## How DBGorilla helps

DBGorilla connects read-only and gives your AI coding agent the schema as the
database actually holds it, its history over time, and the table sizes and row
counts behind it. That is what tells the difference between dropping a column
from an empty table and dropping one with forty million values in it, which is
the distinction the ORM's warning cannot make for you.
[Get started free →](https://app.dbgorilla.com/signup)

## Related

- [How to handle database migrations safely](https://www.dbgorilla.com/learn/postgres/how-to-handle-database-migrations-safely/)