
# Filters

Filters bepalen welke gegevens in een snapshot worden opgenomen. Sluit cachetabellen uit, negeer vluchtige tijdstempelkolommen of sla tijdelijke rijen over. Filters worden toegepast tijdens het vastleggen van een snapshot en zijn van invloed op alle daaropvolgende bewerkingen (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

Bepaalt welke tabellen in een snapshot worden opgenomen. Ondersteunt globpatronen via PHP's `fnmatch()`.

### Tabellen uitsluiten

```php
use Merql\Filter\TableFilter;

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

Tabellen die overeenkomen met een uitsluitingspatroon worden weggelaten uit de snapshot. Het jokerteken `*` komt overeen met elke tekenreeks.

### Tabellen opnemen

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

Alleen tabellen die overeenkomen met een opnamepatroon worden vastgelegd. Alle andere worden uitgesloten.

### Werking

De `apply()`-methode van het filter neemt een lijst met tabelnamen aan en retourneert de gefilterde subset:

```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']
```

Wanneer zowel opname- als uitsluitingspatronen aanwezig zijn (via afzonderlijke filterinstanties), wordt eerst uitgesloten. Een tabel wordt alleen behouden als deze met geen enkel uitsluitingspatroon overeenkomt en met minstens één opnamepatroon.

## ColumnFilter

Verwijdert specifieke kolommen uit rijgegevens tijdens het vastleggen. Handig voor vluchtige kolommen die bij elke schrijfbewerking veranderen en daardoor onjuiste diffs zouden veroorzaken.

```php
use Merql\Filter\ColumnFilter;

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

### Veelvoorkomende toepassingen

- Tijdstempels `updated_at` / `modified_at` die bij elke opslag veranderen.
- `revision` tellers die automatisch oplopen.
- `session_token` of andere tijdelijke velden.

### Werking

Het filter verwijdert benoemde kolommen uit elke rij nadat de rijsleutel is berekend:

```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']
```

De rijsleutel (identiteit) wordt berekend op basis van de ongefilterde rij. Dit betekent dat identiteitskolommen nooit door het kolomfilter worden beïnvloed. Alleen de gegevens die worden gebruikt voor fingerprinting en het vergelijken van verschillen, worden gefilterd.

## RowFilter

Filtert afzonderlijke rijen met een predicaatclosure. De closure ontvangt de tabelnaam en de volledige rijgegevens en retourneert `true` om de rij op te nemen of `false` om deze uit te sluiten.

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

### Werking

De `shouldInclude()`-methode wordt tijdens het vastleggen voor elke rij aangeroepen:

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

Uitgesloten rijen krijgen geen fingerprint, worden niet in de snapshot opgeslagen en verschijnen niet in diffs of merges.

## Filters combineren

Geef meerdere filters door aan `capture()`. Van elk filtertype wordt er hoogstens één gebruikt:

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

Verwerkingsvolgorde:

1. **TableFilter** wordt als eerste uitgevoerd en bepaalt welke tabellen worden vastgelegd.
2. Per tabel worden rijen uit de database gelezen.
3. **RowFilter** wordt op elke rij uitgevoerd en sluit niet-overeenkomende rijen uit.
4. De **rijsleutel** wordt berekend op basis van de ongefilterde rij (identiteitskolommen intact).
5. **ColumnFilter** verwijdert genegeerde kolommen uit de rijgegevens.
6. De **fingerprint** wordt berekend op basis van de gefilterde rijgegevens.

Dankzij deze volgorde zijn identiteitskolommen altijd beschikbaar voor het berekenen van de sleutel, zelfs als het kolomfilter ze daarna verwijdert.

## Filters en consistentie

Gebruik voor alle drie de snapshots in een merge (base, ours, theirs) dezelfde filters. Bij verschillende filters kunnen rijen of kolommen lijken te zijn toegevoegd of verwijderd, terwijl ze alleen anders zijn gefilterd.

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