
# Filter

Filter steuern, welche Daten in einen Snapshot aufgenommen werden. Schließe Cache-Tabellen aus, ignoriere veränderliche Zeitstempelspalten oder überspringe temporäre Zeilen. Die Filter werden während der Snapshot-Erfassung angewendet und wirken sich auf alle nachgelagerten Vorgänge aus (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

Legt fest, welche Tabellen in einen Snapshot aufgenommen werden. Unterstützt Glob-Muster über PHPs `fnmatch()`.

### Tabellen ausschließen

```php
use Merql\Filter\TableFilter;

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

Tabellen, die einem Ausschlussmuster entsprechen, werden aus dem Snapshot ausgelassen. Der Platzhalter `*` steht für eine beliebige Zeichenfolge.

### Tabellen einschließen

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

Nur Tabellen, die einem Einschlussmuster entsprechen, werden erfasst. Alle anderen werden ausgeschlossen.

### Funktionsweise

Die Methode `apply()` des Filters nimmt eine Liste von Tabellennamen entgegen und gibt die gefilterte Teilmenge zurück:

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

Wenn Einschluss- und Ausschlussmuster vorhanden sind (über separate Filterinstanzen), wird der Ausschluss zuerst ausgewertet. Eine Tabelle bleibt erhalten, wenn sie keinem Ausschlussmuster und mindestens einem Einschlussmuster entspricht.

## ColumnFilter

Entfernt bei der Erfassung bestimmte Spalten aus den Zeilendaten. Nützlich für veränderliche Spalten, die sich bei jedem Schreibvorgang ändern und falsche Diffs verursachen würden.

```php
use Merql\Filter\ColumnFilter;

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

### Häufige Anwendungsfälle

- `updated_at` / `modified_at`-Zeitstempel, die sich bei jedem Speichern ändern.
- `revision`-Zähler, die automatisch hochgezählt werden.
- `session_token` oder andere temporäre Felder.

### Funktionsweise

Der Filter entfernt benannte Spalten aus jeder Zeile, nachdem der Zeilenschlüssel berechnet wurde:

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

Der Zeilenschlüssel (Identität) wird anhand der ungefilterten Zeile berechnet. Daher sind Identitätsspalten vom Spaltenfilter nicht betroffen. Gefiltert werden nur die Daten, die für Fingerprinting und Diff-Berechnungen verwendet werden.

## RowFilter

Filtert einzelne Zeilen mithilfe einer Prädikat-Closure. Die Closure erhält den Tabellennamen und die vollständigen Zeilendaten und gibt `true` zurück, um die Zeile einzuschließen, oder `false`, um sie auszuschließen.

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

### Funktionsweise

Die Methode `shouldInclude()` wird während der Erfassung für jede Zeile aufgerufen:

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

Ausgeschlossene Zeilen werden nicht mit einem Fingerprint versehen, nicht im Snapshot gespeichert und erscheinen weder in Diffs noch in Merges.

## Filter kombinieren

Übergib mehrere Filter an `capture()`. Es wird höchstens ein Filter jedes Typs verwendet:

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

Verarbeitungsreihenfolge:

1. **TableFilter** wird zuerst ausgeführt und bestimmt, welche Tabellen erfasst werden.
2. Für jede Tabelle werden die Zeilen aus der Datenbank gelesen.
3. **RowFilter** wird auf jede Zeile angewendet und schließt nicht passende Zeilen aus.
4. Der **Zeilenschlüssel** wird anhand der ungefilterten Zeile berechnet (Identitätsspalten bleiben erhalten).
5. **ColumnFilter** entfernt ignorierte Spalten aus den Zeilendaten.
6. Der **Fingerprint** wird anhand der gefilterten Zeilendaten berechnet.

Diese Reihenfolge stellt sicher, dass Identitätsspalten für die Berechnung des Schlüssels verfügbar bleiben, auch wenn der Spaltenfilter sie später entfernt.

## Filter und Konsistenz

Für alle drei Snapshots eines Merge-Vorgangs (Basis, ours, theirs) sollten dieselben Filter verwendet werden. Bei unterschiedlichen Filtern können Zeilen oder Spalten als hinzugefügt oder entfernt erscheinen, obwohl sie lediglich unterschiedlich gefiltert wurden.

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