
# Filter

Filter mengontrol data yang disertakan dalam snapshot. Kecualikan tabel cache, abaikan kolom cap waktu yang berubah-ubah, atau lewati baris sementara. Filter diterapkan saat pengambilan snapshot dan memengaruhi semua operasi selanjutnya (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

Mengontrol tabel yang disertakan dalam snapshot. Mendukung pola glob melalui PHP `fnmatch()`.

### Kecualikan tabel

```php
use Merql\Filter\TableFilter;

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

Tabel apa pun yang cocok dengan pola pengecualian tidak disertakan dalam snapshot. Wildcard `*` cocok dengan rangkaian karakter apa pun.

### Sertakan tabel

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

Hanya tabel yang cocok dengan pola penyertaan yang diambil. Semua tabel lainnya dikecualikan.

### Cara kerjanya

Metode `apply()` pada filter menerima daftar nama tabel dan mengembalikan subset yang telah difilter:

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

Jika pola penyertaan dan pengecualian sama-sama ada (melalui instance filter terpisah), pengecualian dievaluasi terlebih dahulu. Agar dipertahankan, tabel tidak boleh cocok dengan pola pengecualian mana pun dan harus cocok dengan setidaknya satu pola penyertaan.

## ColumnFilter

Menghapus kolom tertentu dari data baris saat pengambilan. Berguna untuk kolom yang berubah-ubah setiap kali ditulis dan dapat menimbulkan diff palsu.

```php
use Merql\Filter\ColumnFilter;

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

### Kasus penggunaan umum

- Cap waktu `updated_at` / `modified_at` yang berubah setiap kali disimpan.
- Penghitung `revision` yang bertambah otomatis.
- `session_token` atau kolom sementara lainnya.

### Cara kerjanya

Filter menghapus kolom bernama dari setiap baris setelah kunci baris dihitung:

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

Kunci baris (identitas) dihitung dari baris yang belum difilter. Artinya, kolom identitas tidak pernah terpengaruh oleh filter kolom. Hanya data yang digunakan untuk sidik jari dan diff yang difilter.

## RowFilter

Memfilter baris satu per satu menggunakan closure predikat. Closure menerima nama tabel dan seluruh data baris, lalu mengembalikan `true` untuk menyertakan baris atau `false` untuk mengecualikannya.

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

### Cara kerjanya

Metode `shouldInclude()` dipanggil untuk setiap baris selama pengambilan:

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

Baris yang dikecualikan tidak dibuatkan sidik jari, tidak disimpan dalam snapshot, dan tidak muncul dalam diff atau merge.

## Menggabungkan filter

Berikan beberapa filter ke `capture()`. Maksimal satu filter untuk setiap jenis yang digunakan:

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

Urutan pemrosesan:

1. **TableFilter** dijalankan terlebih dahulu untuk menentukan tabel yang akan diambil.
2. Untuk setiap tabel, baris dibaca dari database.
3. **RowFilter** dijalankan pada setiap baris untuk mengecualikan baris yang tidak cocok.
4. **Kunci baris** dihitung dari baris yang belum difilter (kolom identitas tetap utuh).
5. **ColumnFilter** menghapus kolom yang diabaikan dari data baris.
6. **Sidik jari** dihitung dari data baris yang telah difilter.

Urutan ini memastikan kolom identitas selalu tersedia untuk penghitungan kunci, meskipun kolom tersebut kemudian dihapus oleh filter kolom.

## Filter dan konsistensi

Filter yang sama sebaiknya digunakan untuk ketiga snapshot dalam sebuah merge (base, ours, theirs). Jika filter berbeda digunakan, baris atau kolom mungkin tampak ditambahkan atau dihapus padahal sebenarnya hanya difilter secara berbeda.

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