
# Филтри

Филтрите определят кои данни се включват в моментната снимка. Изключвайте таблици с кеш, пренебрегвайте колони с променливи времеви отметки или пропускайте временни редове. Филтрите се прилагат при заснемането на моментната снимка и влияят на всички последващи операции (diff, merge, apply).

```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 шаблони чрез PHP и неговия `fnmatch()`.

### Изключване на таблици

```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);
```
