# Миграции и база данных

Миграции описывают структуру таблиц в коде. Они идемпотентны: уже применённые пропускаются (учёт в таблице `cl_celena_migrations`), новые применяются автоматически — при включении плагина или на старте (для ядра).

## Файл миграции

Лежит в `plugins/<slug>/migrations/` (или `core/Database/migrations/`). Имя — по алфавиту, с числовым префиксом: `0001_create_reviews.php`. Файл возвращает замыкание:

```php
<?php
declare(strict_types=1);

use Celena\Core\Database\Blueprint;
use Celena\Core\Database\Connection;
use Celena\Core\Database\Schema;

return function (Schema $schema, Connection $conn): void {
    $schema->create('reviews', function (Blueprint $t) {
        $t->id();                                   // PK auto-increment
        $t->string('author', 191)->default('');
        $t->text('text')->nullable();
        $t->integer('rating')->default(5);
        $t->boolean('is_published')->default(false);
        $t->json('meta')->nullable();
        $t->datetime('created_at')->nullable();
        $t->index('is_published');
    });
};
```

Доступные типы `Blueprint`: `id`, `string`, `text`, `integer`, `bigInteger`, `boolean`, `decimal`, `json`, `datetime`, `timestamps`, плюс `default()`, `nullable()`, `unique()`, `index()`.

## Кросс-СУБД

Локальная разработка — на PostgreSQL, прод — на MySQL. Поэтому:

- Используйте только `Blueprint`-API и `QueryBuilder` — они компилируются под нужный драйвер.
- **Не пишите MySQL-only SQL** (или PG-only). Например, сортировку версий делайте в PHP (`version_compare`), а не SQL-функциями конкретной СУБД.
- MySQL неявно коммитит DDL: после `CREATE TABLE` транзакция уже закрыта. Не смешивайте DDL и DML в одной миграции, если рассчитываете на откат. `MigrationRunner` это учитывает.

Префикс таблиц (`cl_`) добавляется автоматически: в коде обращайтесь к таблице без префикса (`builder('reviews')` → `cl_reviews`).

## Запросы: QueryBuilder

```php
// чтение
$rows = $conn->builder('reviews')
    ->select('id', 'author', 'rating')
    ->where('is_published', '=', true)
    ->whereIn('rating', [4, 5])
    ->orderBy('created_at', 'desc')
    ->limit(10)->offset(0)
    ->get();

$one   = $conn->builder('reviews')->where('id', '=', 1)->first();  // ?array
$count = $conn->builder('reviews')->count();

// запись
$id = $conn->builder('reviews')->insert(['author' => 'Анна', 'rating' => 5]);
$conn->builder('reviews')->where('id', '=', 1)->update(['is_published' => true]);
$conn->builder('reviews')->where('id', '=', 1)->delete();
```

Сырой SQL — только через подготовленные выражения:

```php
$stmt = $conn->run('SELECT count(*) FROM ' . $conn->table('reviews') . ' WHERE rating >= ?', [4]);
```

**Никогда не конкатенируйте пользовательские данные в SQL** — только параметры. См. [Безопасность](/docs/code/security).

## Транзакции

```php
$conn->transaction(function () use ($conn) {
    $conn->builder('orders')->insert([...]);
    $conn->builder('order_items')->insert([...]);
});
```

`transaction()` корректно обрабатывает неявный commit DDL в MySQL.

## Запуск

- Плагин: миграции прогоняются при включении и идемпотентно — на старте (если плагин активен, как в примере `plugin.php`).
- Ядро: на старте из `core/Database/migrations/`.
- CLI: `php bin/celena` содержит команды для миграций и обслуживания.

> На проде перед `DELETE`/`ALTER` делайте бэкап и используйте узкие условия `WHERE` — без широких `LIKE '%...'`.
