
# Фільтри

Фільтри визначають, які дані потрапляють до знімка. Виключайте таблиці кешу, ігноруйте мінливі стовпці з часовими мітками або пропускайте тимчасові рядки. Фільтри застосовуються під час створення знімка й впливають на всі подальші операції (порівняння, об’єднання, застосування).

```php
use Merql\Filter\TableFilter;
use Merql\Filter\ColumnFilter;
use Merql\Snapshot\Snapshotter;

$snapshotter = new Snapshotter($connection);

$snapshot = $snapshotter->capture('baseline', filters: [
    TableFilter::exclude(['sessions', 'cache_*']),
    ColumnFilter::ignore(['updated_at', 'modified_date']),
]);
```

## TableFilter

Визначає, які таблиці потрапляють до знімка. Підтримує шаблони glob за допомогою `fnmatch()` від PHP.

### Виключення таблиць

```php
use Merql\Filter\TableFilter;

$filter = TableFilter::exclude(['sessions', 'cache_*', 'wp_*_log']);
```

Таблиці, що відповідають шаблону виключення, не потрапляють до знімка. Підстановний символ `*` відповідає будь-якій послідовності символів.

### Додавання таблиць

```php
$filter = TableFilter::include(['posts', 'comments', 'users']);
```

До знімка потрапляють лише таблиці, що відповідають шаблону включення. Усі інші виключаються.

### Як це працює

Метод `apply()` фільтра приймає список назв таблиць і повертає відфільтровану вибірку:

```php
$allTables = ['posts', 'comments', 'sessions', 'cache_pages', 'users'];

$filter = TableFilter::exclude(['sessions', 'cache_*']);
$filtered = $filter->apply($allTables);
// ['posts', 'comments', 'users']

$filter = TableFilter::include(['posts', 'users']);
$filtered = $filter->apply($allTables);
// ['posts', 'users']
```

Якщо задано шаблони включення й виключення (через окремі екземпляри фільтрів), спочатку перевіряються шаблони виключення. Щоб таблиця залишилася, вона не повинна відповідати жодному шаблону виключення й має відповідати принаймні одному шаблону включення.

## ColumnFilter

Вилучає певні стовпці з даних рядків під час створення знімка. Корисно для мінливих стовпців, значення яких змінюються під час кожного запису й спричиняють хибні відмінності.

```php
use Merql\Filter\ColumnFilter;

$filter = ColumnFilter::ignore(['updated_at', 'modified_date', 'last_login']);
```

### Типові випадки використання

- `updated_at` / `modified_at` часові позначки, які змінюються під час кожного збереження.
- `revision` лічильники, що автоматично збільшуються.
- `session_token` або інші тимчасові поля.

### Як це працює

Фільтр вилучає названі стовпці з кожного рядка після обчислення ключа рядка:

```php
$filter = ColumnFilter::ignore(['updated_at']);

$row = ['id' => 1, 'title' => 'Hello', 'updated_at' => '2026-01-15 10:30:00'];
$filtered = $filter->applyToRow($row);
// ['id' => 1, 'title' => 'Hello']
```

Ключ рядка (ідентичність) обчислюється на основі нефільтрованого рядка. Це означає, що фільтр стовпців ніколи не впливає на стовпці ідентичності. Фільтруються лише дані, що використовуються для обчислення відбитка та порівняння відмінностей.

## RowFilter

Фільтрує окремі рядки за допомогою замикання-предиката. Замикання отримує назву таблиці та повні дані рядка й повертає `true`, щоб включити рядок, або `false`, щоб виключити його.

```php
use Merql\Filter\RowFilter;

// Exclude draft posts.
$filter = RowFilter::create(function (string $table, array $row): bool {
    if ($table === 'posts' && ($row['status'] ?? '') === 'draft') {
        return false;
    }
    return true;
});

// Exclude rows with NULL email.
$filter = RowFilter::create(function (string $table, array $row): bool {
    if ($table === 'users') {
        return $row['email'] !== null;
    }
    return true;
});
```

### Як це працює

Метод `shouldInclude()` викликається для кожного рядка під час знімання даних:

```php
$include = $filter->shouldInclude('posts', ['id' => 1, 'status' => 'draft']);
// false
```

Виключені рядки не отримують відбитків, не зберігаються у знімку й не з’являються у відмінностях або злиттях.

## Поєднання фільтрів

Передайте кілька фільтрів у `capture()`. Використовується не більш ніж один фільтр кожного типу:

```php
$snapshot = $snapshotter->capture('baseline', filters: [
    TableFilter::exclude(['cache_*']),
    ColumnFilter::ignore(['updated_at']),
    RowFilter::create(fn($table, $row) => ($row['active'] ?? '1') === '1'),
]);
```

Порядок обробки:

1. Спочатку виконується **TableFilter**, який визначає, які таблиці знімати.
2. Для кожної таблиці з бази даних зчитуються рядки.
3. **RowFilter** виконується для кожного рядка й виключає ті, що не відповідають умовам.
4. **Ключ рядка** обчислюється на основі нефільтрованого рядка (зі стовпцями ідентичності).
5. **ColumnFilter** вилучає з даних рядка проігноровані стовпці.
6. **Відбиток** обчислюється на основі відфільтрованих даних рядка.

Такий порядок гарантує, що стовпці ідентичності завжди доступні для обчислення ключа, навіть якщо пізніше їх вилучить фільтр стовпців.

## Фільтри та узгодженість

Для всіх трьох знімків у злитті (base, ours, theirs) слід використовувати однакові фільтри. Якщо застосувати різні фільтри, може здатися, що рядки або стовпці додали чи вилучили, хоча насправді їх лише по-різному відфільтрували.

```php
// Use the same filter set for all snapshots.
$filters = [
    TableFilter::exclude(['sessions']),
    ColumnFilter::ignore(['updated_at']),
];

$base   = $snapshotter->capture('base', filters: $filters);
$ours   = $snapshotter->capture('ours', filters: $filters);
$theirs = $snapshotter->capture('theirs', filters: $filters);
```
