# Міграції та база даних

Міграції описують структуру таблиць у коді. Вони ідемпотентні: вже застосовані пропускаються (облік у таблиці `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 '%...'`.
