# Plugin migrations

A plugin that needs its own tables declares migrations. It never creates tables by executing DDL on a public page load.

## Declaring

Migrations are declared in `plugin.json` and shipped as `.sql` files inside the plugin directory. Order is lexicographic, exactly like core migrations:

```
plugins/my-plugin/
├── plugin.json
└── migrations/
    ├── 001_create_my_plugin_items.sql
    └── 002_add_my_plugin_index.sql
```

## Rules

1. **Forward-only.** Never edit a migration that has already run anywhere. Add a new one.
2. **Idempotent where possible.** Prefer `CREATE TABLE IF NOT EXISTS`. Running the set twice must not fail.
3. **Namespaced table names.** Prefix with the plugin slug so two plugins cannot collide, and so an operator reading the schema knows what owns a table.
4. **No subqueries on the table being modified.** MySQL rejects them, and it is the classic way a migration passes on SQLite and fails in production.
5. **InnoDB and `utf8mb4`.** Match the core convention; a `latin1` table breaks Arabic content silently.
6. **Foreign keys reference core tables carefully.** Use `ON DELETE CASCADE` for owned rows and `ON DELETE SET NULL` for references, so deleting a user does not fail on your table.

## Running

Plugin migrations run with the core migration step — the admin **Upgrade database** action, or `php bin/migrate.php`. Activating a plugin that has pending migrations surfaces the pending state rather than silently running DDL during a request.

## What migrations must not do

- Delete or rewrite core rows
- Alter core tables' existing columns
- Assume a specific `AUTO_INCREMENT` value or row id
- Depend on data that only exists on the developer's machine

## Testing

Test against an empty database **and** against a database that already has the plugin's earlier version. A migration set that only works on a fresh install is a migration set that breaks every existing user.

See [Plugins](PLUGINS.md) and [Database schema](../07-internals/DATABASE-SCHEMA.md).
