
# Referensi PHP API

Referensi lengkap untuk semua kelas dan metode publik di merql.

## Merql (facade)

`Merql\Merql` adalah titik masuk statis untuk operasi umum. Komponen ini membungkus kelas tingkat rendah dan mengelola persistensi snapshot secara otomatis.

```php
use Merql\Connection;
use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;
use Merql\Merql;

$connection = Connection::sqlite(':memory:');
Merql::init($connection, identityRules: new IdentityRuleSet([
    'options' => IdentityRule::natural(['option_name']),
]));
```

### `init(DatabaseConnection $connection, ?Driver $driver = null, ?IdentityRuleSet $identityRules = null): void`

Inisialisasi facade dengan koneksi basis data. Harus dipanggil sebelum
`snapshot()`, `apply()`, atau `applyGuarded()`; panggil lagi untuk berpindah basis data,
misalnya untuk membuat snapshot sisi kita dan sisi mereka dari dua salinan berbeda. Tanpa
`$driver`, komponen ini memanggil `DriverFactory::create()`, yang menghasilkan exception untuk mesin selain
MySQL dan SQLite, kecuali driver telah didaftarkan. Berikan driver untuk mengganti deteksi otomatis, atau kumpulan aturan identitas untuk mengganti identitas baris per tabel saat pengambilan snapshot.

### `snapshot(string $name, array $tables = []): Snapshot`

Ambil keadaan basis data saat ini dan simpan ke disk. Berikan array nama tabel untuk membuat snapshot tabel tertentu, atau abaikan parameter ini untuk menyertakan semua tabel.

```php
$snapshot = Merql::snapshot('baseline');
$snapshot = Merql::snapshot('baseline', ['posts', 'users']);
```

### `diff(string $base, string $current): Changeset`

Muat dua snapshot bernama dan hitung set perubahan di antara keduanya. `init()` tidak diperlukan karena operasi ini menggunakan snapshot yang telah disimpan.

```php
$changeset = Merql::diff('before', 'after');
```

### `merge(string $base, string $ours, string $theirs): MergeResult`

Merge tiga arah atas tiga snapshot bernama. Mengembalikan hasil yang berisi operasi tanpa konflik dan konflik yang ditemukan. Merge dilakukan pada tingkat kolom: facade menyusun `ThreeWayMerge` tanpa `CellMergeConfig`, sehingga merge tingkat sel tidak diterapkan. Untuk itu, gunakan [`ThreeWayMerge`](#threewaymerge) secara langsung.

```php
$result = Merql::merge('base', 'ours', 'theirs');
```

### `patch(string $base, string $changes): MergeResult`

Merge dua arah: terapkan perubahan ke base. Setara dengan `merge($base, $base, $changes)`. Hasilnya selalu bersih karena tidak ada pengeditan bersamaan. Hanya pada tingkat kolom, seperti `merge()`.

```php
$result = Merql::patch('base', 'changes');
```

### `apply(MergeResult $result): ApplyResult`

Jalankan hasil merge tanpa konflik pada basis data yang diberikan ke `init()`. Memerlukan `init()`. Menghasilkan exception `ConflictException` jika masih ada konflik yang belum terselesaikan.

Setiap operasi dalam hasil akan dijalankan, sehingga seluruh hasil `merge()` hanya dapat diterapkan dengan bersih pada basis data yang masih berada dalam keadaan base. Untuk basis data yang sudah berisi salah satu sisi, terapkan pilihan operasi yang belum dimilikinya; lihat [Penerapan dengan Pengamanan](https://merql.inline0.dev/id/docs/apply/guarded).

```php
$applied = Merql::apply($result);
echo $applied->rowsAffected();
```

### `plan(string $id, string $base, string $ours, string $theirs, array $metadata = []): MergePlan`

Bangun rencana merge siap untuk UI dari tiga snapshot bernama. Rencana ini mencakup ID operasi yang stabil, ID grup perubahan, konteks operasi, konflik, ketidakcocokan skema, ringkasan, metadata, dan hash rencana. Merge dilakukan pada tingkat kolom, seperti di `merge()`; berikan hasil merge tingkat sel ke `MergePlanBuilder::build()` untuk merge tingkat sel.

```php
$plan = Merql::plan('plan-1', 'base', 'ours', 'theirs', [
    'source' => 'review-ui',
]);
```

### `applyGuarded(MergeResult $result, Snapshot $expectedLive): ApplyResult`

Terapkan hasil merge tanpa konflik dengan prasyarat optimistis pada baris aktif. Untuk pembaruan dan penghapusan, baris aktif saat ini harus cocok dengan `$expectedLive`; untuk penyisipan, identitas target tidak boleh sudah ada. `$expectedLive` adalah snapshot basis data target (sisi mereka, saat mempromosikan sisi kita ke basis data tempat snapshot sisi mereka dibuat). Pernyataan apa pun yang tidak mengubah baris akan me-rollback transaksi.

```php
use Merql\Snapshot\SnapshotStore;

$applied = Merql::applyGuarded($selectedResult, SnapshotStore::load('theirs'));
```

### `reset(): void`

Hapus koneksi internal dan instance Snapshotter. Utamanya digunakan dalam pengujian untuk mengatur ulang status di antara proses.

---

## Koneksi

`Merql\Connection` membangun adapter koneksi dengan default yang sesuai. Koneksi PDO menggunakan exception saat terjadi kesalahan, mode pengambilan asosiatif, prepared statement tanpa emulasi, dan hasil fetch dalam bentuk string. Adapter mysqli menormalkan nilai yang diambil ke bentuk string atau null yang sama.

### `mysql(string $host, string $database, string $username, string $password = '', int $port = 3306, string $charset = 'utf8mb4'): PdoDatabaseConnection`

```php
use Merql\Connection;

$connection = Connection::mysql('127.0.0.1', 'my_app', 'root', 'secret');
```

### `mysqli(string $host, string $database, string $username, string $password = '', int $port = 3306, string $charset = 'utf8mb4'): MysqliDatabaseConnection`

```php
$connection = Connection::mysqli('127.0.0.1', 'my_app', 'root', 'secret');
```

### `sqlite(string $path = ':memory:'): PdoDatabaseConnection`

Membuat koneksi SQLite. Mengaktifkan foreign key secara otomatis melalui `PRAGMA foreign_keys = ON`.

```php
$connection = Connection::sqlite('/var/data/app.sqlite');
$connection = Connection::sqlite(':memory:');
```

### `fromDsn(string $dsn, string $username = '', string $password = ''): PdoDatabaseConnection`

Hubungkan menggunakan string DSN PDO apa pun. merql menyediakan driver hanya untuk `mysql` dan `sqlite`; DSN lain dapat membuka koneksi, tetapi `Merql::init()`, `Snapshotter`, dan applier akan menghasilkan exception `Unsupported database driver` kecuali driver didaftarkan dengan `DriverFactory::register()`.

```php
$connection = Connection::fromDsn('mysql:host=localhost;dbname=app', 'root', 'pass');
```

### `fromPdo(PDO $pdo): PdoDatabaseConnection`

Bungkus instance PDO yang sudah ada.

### `fromMysqli(mysqli $mysqli, string $charset = 'utf8mb4'): MysqliDatabaseConnection`

Bungkus instance mysqli yang sudah ada.

### Kontrak adapter

Semua adapter mengimplementasikan `Merql\Database\DatabaseConnection`:

```php
interface DatabaseConnection
{
    public function driverName(): string;
    public function execute(string $sql, array $params = []): int;
    public function query(string $sql, array $params = []): array;
    public function scalar(string $sql, array $params = []): ?string;
    public function beginTransaction(): void;
    public function commit(): void;
    public function rollBack(): void;
    public function lastInsertId(): string;
}
```

`execute()`, `query()`, dan `scalar()` menerima parameter posisi `list<scalar|null>`
untuk placeholder `?`. `query()` mengembalikan
`list<array<string, scalar|null>>`; nilai dinormalisasi ke bentuk string atau null yang sama di seluruh adapter PDO dan mysqli.

---

## Snapshotter

`Merql\Snapshot\Snapshotter` mengambil keadaan basis data. Berbeda dari facade, komponen ini memberi Anda kendali penuh atas filter dan tidak menyimpan data secara otomatis.

### `__construct(DatabaseConnection $connection, ?Driver $driver = null, ?IdentityRuleSet $identityRules = null)`

Driver dideteksi otomatis dari identitas koneksi jika tidak diberikan. Berikan
`IdentityRuleSet` jika identitas baris khusus tabel perlu mengganti default skema.

### `capture(string $name, array $tables = [], array $filters = []): Snapshot`

Ambil keadaan saat ini. Berikan nama tabel untuk membatasi cakupan. Berikan objek filter untuk mengecualikan tabel, kolom, atau baris.

```php
use Merql\Snapshot\Snapshotter;
use Merql\Filter\TableFilter;
use Merql\Filter\ColumnFilter;
use Merql\Filter\RowFilter;
use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;

$snapshotter = new Snapshotter(
    $connection,
    identityRules: new IdentityRuleSet([
        'wp_options' => IdentityRule::natural(['option_name']),
    ]),
);

// All tables.
$snapshot = $snapshotter->capture('full');

// Specific tables with filters.
$snapshot = $snapshotter->capture('filtered', ['posts', 'users'], [
    TableFilter::exclude(['cache_*', 'sessions']),
    ColumnFilter::ignore(['updated_at', 'created_at']),
    RowFilter::create(fn(string $table, array $row) => $row['status'] !== 'trash'),
]);
```

### `captureAliased(string $name, array $physicalToCanonicalTableMap, array $filters = []): Snapshot`

Ambil tabel fisik menggunakan nama tabel kanonis. Gunakan ini ketika tabel logis yang sama memiliki nama fisik berbeda di berbagai lingkungan.

```php
$snapshot = $snapshotter->captureAliased('sandbox', [
    'wp_onumia_abcd_posts' => 'wp_posts',
    'wp_onumia_abcd_postmeta' => 'wp_postmeta',
]);
```

Snapshot yang dihasilkan menyimpan nama tabel kanonis sekaligus mempertahankan skema tabel fisik, kolom identitas yang dikonfigurasi, kunci baris, dan filter.

### `fromData(string $name, array $tableData): Snapshot` (statis)

Buat snapshot dari data mentah tanpa koneksi database. Berguna untuk pengujian atau saat mengimpor data dari sumber lain.

```php
use Merql\Snapshot\Snapshotter;
use Merql\Snapshot\TableSnapshotData;
use Merql\Schema\TableSchema;

$schema = new TableSchema('posts', ['id' => 'int', 'title' => 'varchar'], ['id']);

$snapshot = Snapshotter::fromData('test', [
    'posts' => new TableSnapshotData($schema, [
        ['id' => '1', 'title' => 'Hello'],
        ['id' => '2', 'title' => 'World'],
    ], ['id']),
]);
```

---

## Snapshot

`Merql\Snapshot\Snapshot` adalah objek nilai hanya-baca yang merepresentasikan seluruh keadaan database.

```php
$snapshot->name;                    // string: snapshot identifier
$snapshot->tables;                  // array<string, TableSnapshot>
$snapshot->tableNames();            // list<string>
$snapshot->hasTable('posts');       // bool
$snapshot->getTable('posts');       // ?TableSnapshot
```

---

## TableSnapshot

`Merql\Snapshot\TableSnapshot` menyimpan baris, sidik jari, dan skema dari satu tabel.

```php
$table = $snapshot->getTable('posts');

$table->schema;                    // TableSchema
$table->identityColumns;           // list<string>: columns used for row identity
$table->fingerprints;              // array<string, string>: row key to hash
$table->rows;                      // array<string, array<string, mixed>>

$table->rowKeys();                 // list<string>
$table->rowCount();                // int
$table->hasRow('42');              // bool
$table->getRow('42');              // ?array<string, mixed>
$table->getFingerprint('42');      // ?string
```

---

## TableSnapshotData

`Merql\Snapshot\TableSnapshotData` adalah data masukan untuk `Snapshotter::fromData()`.

```php
use Merql\Snapshot\TableSnapshotData;
use Merql\Schema\TableSchema;

$data = new TableSnapshotData(
    schema: new TableSchema('posts', ['id' => 'int', 'title' => 'varchar'], ['id']),
    rows: [
        ['id' => '1', 'title' => 'First Post'],
    ],
    identityColumns: ['id'],
);
```

---

## IdentityRule

`Merql\Identity\IdentityRule` menentukan kolom yang digunakan untuk membuat kunci baris.

```php
use Merql\Identity\IdentityRule;

$primary = IdentityRule::primary(['id']);
$natural = IdentityRule::natural(['option_name']);
$composite = IdentityRule::composite(['object_id', 'term_taxonomy_id']);
$content = IdentityRule::content(['name', 'value']);

$key = $natural->key(['option_name' => 'siteurl']);
// "siteurl"
```

### `forSchema(TableSchema $schema): IdentityRule` (statis)

Menentukan aturan identitas default dari skema: kunci utama, kunci unik pertama,
lalu fallback konten.

### `toArray(): array`

Menyerialkan aturan sebagai `['type' => string, 'columns' => list<string>]`.

### `fromArray(array $data): IdentityRule` (statis)

Memuat aturan dari data yang diserialkan.

---

## IdentityRuleSet

`Merql\Identity\IdentityRuleSet` menyimpan penggantian identitas khusus tabel.

```php
use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;

$rules = new IdentityRuleSet([
    'wp_options' => IdentityRule::natural(['option_name']),
]);

$rule = $rules->ruleFor($schema);
```

### `with(string $table, IdentityRule $rule): IdentityRuleSet`

Mengembalikan kumpulan aturan baru dengan satu penggantian tabel yang ditambahkan.

### `conflictsFor(string $table, IdentityRule $rule, array $rows): array`

Mengembalikan entri `IdentityConflict` ketika beberapa baris menghasilkan kunci yang sama untuk suatu aturan.

### `toArray(): array`

Menyerialkan aturan tabel sebagai array yang aman untuk JSON.

### `fromArray(array $data): IdentityRuleSet` (statis)

Memuat kumpulan aturan dari data yang diserialkan.

---

## SnapshotStore

`Merql\Snapshot\SnapshotStore` menyimpan dan memuat snapshot sebagai file JSON.

### `setDirectory(string $directory): void`

Atur direktori penyimpanan. Default-nya adalah `.merql/snapshots`.

### `save(Snapshot $snapshot): void`

Tulis snapshot ke disk.

### `load(string $name): Snapshot`

Muat snapshot berdasarkan nama. Melempar `SnapshotException` jika tidak ditemukan.

### `exists(string $name): bool`

Periksa apakah file snapshot ada.

### `delete(string $name): void`

Hapus file snapshot.

---

## Differ

`Merql\Diff\Differ` membandingkan dua snapshot dan menghasilkan kumpulan perubahan.

### `diff(Snapshot $base, Snapshot $current): Changeset`

```php
use Merql\Diff\Differ;

$differ = new Differ();
$changeset = $differ->diff($baseSnapshot, $currentSnapshot);
```

### `valuesEqual(mixed $a, mixed $b): bool` (statis)

Bandingkan dua nilai dengan mengubahnya menjadi string. NULL diperlakukan sebagai nilai tersendiri: `NULL === NULL` bernilai true, `NULL === ''` bernilai false.

---

## Changeset

`Merql\Diff\Changeset` adalah koleksi operasi tingkat baris yang hanya-baca.

```php
$changeset->inserts();             // list<RowInsert>
$changeset->updates();             // list<RowUpdate>
$changeset->deletes();             // list<RowDelete>
$changeset->isEmpty();             // bool
$changeset->count();               // int: total operations
$changeset->forTable('posts');     // Changeset: filtered to one table
```

---

## RowInsert, RowUpdate, RowDelete

Objek nilai hanya-baca yang merepresentasikan perubahan pada masing-masing baris.

### RowInsert

```php
$insert->table;                    // string
$insert->rowKey;                   // string
$insert->values;                   // array<string, mixed>
```

### RowUpdate

```php
$update->table;                    // string
$update->rowKey;                   // string
$update->columnDiffs;              // list<ColumnDiff>
$update->fullRow;                  // array<string, mixed>: complete current row
$update->columnDiffMap();          // array<string, ColumnDiff>: keyed by column name
```

### RowDelete

```php
$delete->table;                    // string
$delete->rowKey;                   // string
$delete->oldValues;                // array<string, mixed>: values before deletion
```

---

## ColumnDiff

`Merql\Diff\ColumnDiff` merepresentasikan satu perubahan kolom dalam pembaruan baris.

```php
$diff->column;                     // string
$diff->oldValue;                   // mixed
$diff->newValue;                   // mixed
```

---

## ThreeWayMerge

`Merql\Merge\ThreeWayMerge` adalah algoritme penggabungan inti.

### `__construct(?CellMergeConfig $cellMergeConfig = null)`

Teruskan `CellMergeConfig` untuk mengaktifkan penggabungan tingkat sel bagi kolom TEXT dan JSON.

```php
use Merql\Merge\ThreeWayMerge;
use Merql\CellMerge\CellMergeConfig;

// Default: column-level merge only.
$merge = new ThreeWayMerge();

// With cell-level merge for TEXT and JSON columns.
$merge = new ThreeWayMerge(CellMergeConfig::auto());
```

### `merge(Snapshot $base, Snapshot $ours, Snapshot $theirs): MergeResult`

Lakukan penggabungan tiga arah. Menghitung kumpulan perubahan secara internal (base ke milik kita, base ke milik mereka), lalu menggabungkannya.

```php
$result = $merge->merge($baseSnapshot, $oursSnapshot, $theirsSnapshot);
```

### `patch(Snapshot $base, Snapshot $changes): MergeResult`

Pintasan penggabungan dua arah. Setara dengan `merge($base, $base, $changes)`.

### `schemaMismatches(): array`

Mengembalikan `SchemaException[]` untuk setiap perbedaan skema yang terdeteksi selama penggabungan terakhir (kolom yang ditambahkan atau dihapus di antara snapshot).

---

## MergeResult

`Merql\Merge\MergeResult` adalah objek nilai hanya-baca yang berisi hasil penggabungan.

```php
$result->isClean();                // bool: no conflicts
$result->operations();             // list<MergeOperation>
$result->conflicts();              // list<Conflict>
$result->operationCount();         // int
$result->conflictCount();          // int
$result->baseSnapshot();           // ?Snapshot
$result->schemaMismatches();       // list<SchemaException>
$result->hasSchemaMismatches();    // bool
```

---

## MergePlan

`Merql\Plan\MergePlan` adalah model tinjauan penggabungan yang dapat diserialkan.

```php
$plan->id;                        // string
$plan->baseSnapshot;              // string
$plan->oursSnapshot;              // string
$plan->theirsSnapshot;            // string
$plan->summary;                   // MergePlanSummary
$plan->changeGroups;              // list<MergePlanChangeGroup>
$plan->operations;                // list<MergePlanOperation>
$plan->conflicts;                 // list<MergePlanConflict>
$plan->schemaMismatches;          // list<string>
$plan->hash;                      // string
$plan->metadata;                  // array<string, mixed>
```

Gunakan `MergePlanSerializer::toJson()` dan `::fromJson()` untuk roundtrip JSON yang stabil.

## Selections

`ChangeGroupSelection` menyimpan ID grup perubahan yang dipentaskan. Ubah menjadi `OperationSelection` saat membuat hasil penggabungan terpilih atau rencana rollback.

```php
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\SelectedMergeResultFactory;

$selection = ChangeGroupSelection::fromIds([$groupId]);
$operationSelection = $selection->toOperationSelection($plan);

$selected = (new SelectedMergeResultFactory())
    ->fromOperationSelection($plan, $operationSelection);
```

## RollbackPlan

`Merql\Rollback\RollbackPlan` menyimpan operasi invers dan ekspektasi penyimpangan untuk penerapan yang dipilih.

```php
use Merql\Rollback\RollbackPlanBuilder;
use Merql\Rollback\RollbackPlanSerializer;

$rollback = (new RollbackPlanBuilder())->build(
    'rollback-1',
    $plan,
    $operationSelection,
    $liveBeforeRows,
);

$json = RollbackPlanSerializer::toJson($rollback);
```

Gunakan `RollbackDrift` sebelum menerapkan rollback, lalu `RollbackPlanApplier` untuk menjalankan operasi invers.

---

## MergeOperation

`Merql\Merge\MergeOperation` adalah satu operasi hasil resolusi dari penggabungan.

```php
$op->type;                         // string: 'insert' | 'update' | 'delete'
$op->table;                        // string
$op->rowKey;                       // string
$op->values;                       // array<string, mixed>
$op->source;                       // string: 'ours' | 'theirs' | 'merged'
```

Konstanta tipe:

```php
MergeOperation::TYPE_INSERT;       // 'insert'
MergeOperation::TYPE_UPDATE;       // 'update'
MergeOperation::TYPE_DELETE;       // 'delete'
```

---

## Conflict

`Merql\Merge\Conflict` merepresentasikan konflik penggabungan yang belum terselesaikan.

```php
$conflict->table();                // string
$conflict->rowKey();               // string
$conflict->type();                 // string: 'update_update' | 'update_delete' | 'delete_update' | 'insert_insert'
$conflict->column();               // ?string (null for structural conflicts)
$conflict->oursValue();            // mixed
$conflict->theirsValue();          // mixed
$conflict->baseValue();            // mixed
$conflict->primaryKey(['id']);     // array<string, string>: decoded from rowKey
```

### Jenis konflik

| Jenis | Makna |
|---|---|
| `update_update` | Kedua sisi mengubah kolom yang sama menjadi nilai berbeda |
| `update_delete` | Sisi kita memperbarui baris yang dihapus oleh sisi mereka |
| `delete_update` | Sisi kita menghapus baris yang diperbarui oleh sisi mereka |
| `insert_insert` | Kedua sisi menyisipkan baris dengan kunci utama yang sama |

---

## ConflictResolver

`Merql\Merge\ConflictResolver` menerapkan kebijakan resolusi ke semua konflik dalam hasil penggabungan.

### `resolve(MergeResult $result, ConflictPolicy $policy): MergeResult` (statis)

Mengembalikan `MergeResult` baru dengan konflik yang diselesaikan sesuai kebijakan.

```php
use Merql\Merge\ConflictResolver;
use Merql\Merge\ConflictPolicy;

// Accept all "theirs" values when conflicts arise.
$resolved = ConflictResolver::resolve($result, ConflictPolicy::TheirsWins);

// Accept all "ours" values.
$resolved = ConflictResolver::resolve($result, ConflictPolicy::OursWins);

// Return unchanged (for manual inspection).
$resolved = ConflictResolver::resolve($result, ConflictPolicy::Manual);
```

---

## ConflictPolicy

`Merql\Merge\ConflictPolicy` adalah enum berbasis dengan tiga kasus.

```php
use Merql\Merge\ConflictPolicy;

ConflictPolicy::OursWins;          // Resolve all conflicts in favor of "ours"
ConflictPolicy::TheirsWins;        // Resolve all conflicts in favor of "theirs"
ConflictPolicy::Manual;            // Leave conflicts unresolved for manual handling
```

---

## ColumnMerge

`Merql\Merge\ColumnMerge` menjalankan logika penggabungan per kolom untuk satu baris. Digunakan secara internal oleh `ThreeWayMerge`.

### `merge(string $table, string $rowKey, array $base, array $ours, array $theirs, ?CellMergeConfig $cellMergeConfig = null, array $columnTypes = []): array` (statis)

Mengembalikan `['values' => array, 'conflicts' => list<Conflict>]`.

```php
use Merql\Merge\ColumnMerge;

$result = ColumnMerge::merge(
    'posts',
    '42',
    ['title' => 'Hello', 'status' => 'draft'],     // base
    ['title' => 'Hello', 'status' => 'published'],  // ours
    ['title' => 'New Title', 'status' => 'draft'],   // theirs
);

// $result['values'] = ['title' => 'New Title', 'status' => 'published']
// $result['conflicts'] = [] (clean merge)
```

---

## Applier

`Merql\Apply\Applier` menjalankan hasil penggabungan pada database.

### `__construct(DatabaseConnection $connection, ?Driver $driver = null)`

### `apply(MergeResult $result, ?Snapshot $base = null): ApplyResult`

Menjalankan semua operasi dalam satu transaksi. Melakukan rollback jika terjadi kesalahan dan melaporkannya di `ApplyResult::errors()` alih-alih melemparnya. Melempar `ConflictException` jika hasilnya memiliki konflik yang belum terselesaikan.

```php
use Merql\Apply\Applier;

$applier = new Applier($connection);
$applied = $applier->apply($result);

echo "Rows affected: {$applied->rowsAffected()}\n";
if ($applied->hasErrors()) {
    print_r($applied->errors());
}
```

Urutan kunci asing ditangani secara otomatis: penyisipan diurutkan berdasarkan dependensi, penghapusan diurutkan menurut urutan dependensi terbalik.

---

## ApplyResult

`Merql\Apply\ApplyResult` adalah objek nilai hanya-baca yang dikembalikan oleh applier.

```php
$applied->rowsAffected();         // int
$applied->errors();               // list<string>
$applied->hasErrors();            // bool
```

Setelah penerapan yang di-rollback, `rowsAffected()` tetap menghitung pernyataan yang dijalankan sebelum kegagalan ([#4](https://github.com/inline0/merql/issues/4)). Periksa `hasErrors()` sebelum mengandalkan hitungan tersebut.

---

## DryRun

`Merql\Apply\DryRun` menghasilkan SQL yang dapat dibaca manusia tanpa menjalankan apa pun.

### `generate(MergeResult $result, ?Snapshot $base = null, array $fkDependencies = [], ?Driver $driver = null): array` (statis)

Mengembalikan daftar string SQL dengan nilai yang disisipkan langsung (hanya untuk ditampilkan).

```php
use Merql\Apply\DryRun;

$statements = DryRun::generate($result);

foreach ($statements as $sql) {
    echo "{$sql};\n";
}

// Output:
// INSERT INTO `posts` (`id`, `title`) VALUES ('10', 'New Post');
// UPDATE `posts` SET `title` = 'Updated' WHERE `id` = '5';
// DELETE FROM `comments` WHERE `id` = '3';
```

---

## SqlGenerator

`Merql\Apply\SqlGenerator` menghasilkan pernyataan SQL berparameter. Digunakan secara internal oleh `Applier` dan `DryRun`.

### `generate(MergeResult $result, ?Snapshot $base = null, array $fkDependencies = [], ?Driver $driver = null): array` (statis)

Mengembalikan `list<array{sql: string, params: list<scalar|null>}>`.

```php
use Merql\Apply\SqlGenerator;

$statements = SqlGenerator::generate($result);

foreach ($statements as $stmt) {
    $connection->execute($stmt['sql'], $stmt['params']);
}
```

---

## CellMergeConfig

`Merql\CellMerge\CellMergeConfig` mengonfigurasi strategi penggabungan tingkat sel untuk kolom atau jenis kolom tertentu.

### `auto(): self` (statis)

Fungsi pembuat praktis yang menetapkan `TextCellMerger` untuk kolom TEXT/LONGTEXT dan `JsonCellMerger` untuk kolom JSON.

```php
use Merql\CellMerge\CellMergeConfig;

$config = CellMergeConfig::auto();
```

### `forColumn(string $column, CellMerger $merger): self`

Tetapkan merger ke kolom tertentu. Gunakan `table.column` untuk nama berkualifikasi atau cukup `column` untuk semua tabel.

```php
use Merql\CellMerge\CellMergeConfig;
use Merql\CellMerge\TextCellMerger;

$config = (new CellMergeConfig())
    ->forColumn('posts.content', new TextCellMerger())
    ->forColumn('description', new TextCellMerger());
```

### `forType(string $typePattern, CellMerger $merger): self`

Tetapkan merger ke semua kolom yang cocok dengan pola jenis (pencocokan substring tanpa membedakan huruf besar-kecil).

```php
use Merql\CellMerge\CellMergeConfig;
use Merql\CellMerge\JsonCellMerger;

$config = (new CellMergeConfig())
    ->forType('json', new JsonCellMerger());
```

### `getMerger(string $table, string $column, string $columnType = ''): CellMerger`

Cari merger untuk tabel, kolom, dan jenis tertentu. Prioritas: nama kolom berkualifikasi, nama kolom tanpa kualifikasi, pola jenis, default (opaque).

---

## CellMerger (antarmuka)

`Merql\CellMerge\CellMerger` adalah antarmuka untuk strategi penggabungan tingkat sel.

```php
use Merql\CellMerge\CellMerger;
use Merql\CellMerge\CellMergeResult;

interface CellMerger
{
    public function merge(mixed $base, mixed $ours, mixed $theirs): CellMergeResult;
}
```

Tersedia tiga implementasi bawaan.

### TextCellMerger

`Merql\CellMerge\TextCellMerger` menggabungkan teks multi-baris menggunakan diff tiga arah baris demi baris (algoritme yang sama dengan yang digunakan git untuk konten file). Jika kedua sisi mengedit baris yang berbeda, penggabungan berhasil tanpa konflik. Jika kedua sisi mengedit baris yang sama secara berbeda, konflik akan dilaporkan.

```php
use Merql\CellMerge\TextCellMerger;

$merger = new TextCellMerger();
$result = $merger->merge(
    "line 1\nline 2\nline 3",
    "line 1\nline 2 modified\nline 3",
    "line 1\nline 2\nline 3 modified",
);
// $result->clean === true
// $result->value === "line 1\nline 2 modified\nline 3 modified"
```

### JsonCellMerger

`Merql\CellMerge\JsonCellMerger` menggabungkan nilai JSON dengan membandingkan kunci tingkat atas secara terpisah. Setiap kunci digabungkan menggunakan logika tiga arah. Objek bertingkat dibandingkan sebagai string JSON opaque (tidak digabungkan secara rekursif).

```php
use Merql\CellMerge\JsonCellMerger;

$merger = new JsonCellMerger();
$result = $merger->merge(
    '{"a": 1, "b": 2}',
    '{"a": 10, "b": 2}',        // changed "a"
    '{"a": 1, "b": 20}',         // changed "b"
);
// $result->clean === true
// $result->value === '{"a":10,"b":20}'
```

### OpaqueCellMerger

`Merql\CellMerge\OpaqueCellMerger` adalah default. Nilai diperlakukan sebagai string opaque dan konflik selalu dilaporkan jika nilainya berbeda.

---

## CellMergeResult

`Merql\CellMerge\CellMergeResult` adalah jenis nilai kembalian semua merger sel.

```php
$result->clean;                    // bool: true if merge succeeded
$result->value;                    // mixed: merged value (or ours on conflict)
$result->conflicts;                // int: number of conflicts

// Factory methods.
CellMergeResult::resolved($value);           // clean merge
CellMergeResult::conflict($oursValue, $n);   // conflict with count
```

---

## TableSchema

`Merql\Schema\TableSchema` adalah representasi struktur tabel yang hanya-baca.

```php
use Merql\Schema\TableSchema;

$schema = new TableSchema(
    name: 'posts',
    columns: ['id' => 'int', 'title' => 'varchar(255)', 'content' => 'text'],
    primaryKey: ['id'],
    uniqueKeys: [['slug']],
);

$schema->name;                     // string
$schema->columns;                  // array<string, string>: name to type
$schema->primaryKey;               // list<string>
$schema->uniqueKeys;               // list<list<string>>
$schema->hasPrimaryKey();          // bool
$schema->hasUniqueKeys();          // bool
$schema->columnNames();            // list<string>
```

---

## Sistem driver

merql menggunakan sistem driver yang dapat dipasang untuk mengabstraksikan operasi khusus database. Driver dideteksi otomatis dari identitas driver koneksi.

### Driver (antarmuka)

`Merql\Driver\Driver` menentukan kontrak yang diterapkan semua driver database.

```php
interface Driver
{
    public function quoteIdentifier(string $name): string;
    public function listTables(DatabaseConnection $connection): array;
    public function readSchema(DatabaseConnection $connection, string $table): TableSchema;
    public function readForeignKeys(DatabaseConnection $connection): array;
    public function selectAll(string $table): string;
}
```

Implementasi bawaan: `MysqlDriver` dan `SqliteDriver`.

### DriverFactory

`Merql\Driver\DriverFactory` mendeteksi driver secara otomatis dari koneksi.

```php
use Merql\Driver\DriverFactory;

$driver = DriverFactory::create($connection);
```

Daftarkan driver khusus untuk database lain:

```php
DriverFactory::register('pgsql', MyPostgresDriver::class);
```

---

## Filter

### TableFilter

`Merql\Filter\TableFilter` menyertakan atau mengecualikan tabel menggunakan pola glob.

```php
use Merql\Filter\TableFilter;

$filter = TableFilter::exclude(['cache_*', 'sessions', 'temp_*']);
$filter = TableFilter::include(['posts', 'users', 'comments']);
```

### ColumnFilter

`Merql\Filter\ColumnFilter` menghapus kolom tertentu dari snapshot.

```php
use Merql\Filter\ColumnFilter;

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

### RowFilter

`Merql\Filter\RowFilter` memfilter baris menggunakan fungsi predikat.

```php
use Merql\Filter\RowFilter;

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

---

## Exception

### SnapshotException

`Merql\Exceptions\SnapshotException` dilempar jika snapshot tidak dapat ditemukan atau namanya tidak valid.

```php
SnapshotException::notFound('baseline', '/path/to/baseline.json');
```

### MergeException

`Merql\Exceptions\MergeException` dilempar jika snapshot yang diperlukan untuk penggabungan tidak tersedia.

```php
MergeException::snapshotRequired('base');
```

### ConflictException

`Merql\Exceptions\ConflictException` dilempar saat mencoba menerapkan hasil penggabungan yang memiliki konflik belum terselesaikan.

```php
ConflictException::unresolved(3);
// Message: "Cannot apply merge with 3 unresolved conflict(s)"
```

### SchemaException

`Merql\Exceptions\SchemaException` dilempar (atau dikumpulkan) jika skema berbeda di antara snapshot.

```php
SchemaException::mismatch('posts', 'column "tags" exists in current but not in base');
```

Ketidakcocokan skema tidak menghentikan penggabungan. Ketidakcocokan dikumpulkan dan dapat diperiksa melalui `$result->schemaMismatches()`.
