
# はじめに

このガイドでは、インストール、データベースへの接続、スナップショットの取得、差分の計算、三者マージの実行について説明します。

## インストール

```bash
composer require merql/merql
```

merqlには PHP 8.2以降と `ext-mbstring`、`ext-zlib` が必要です（どちらも `pitmaster/pitmaster` の依存関係で必須です）。使用するアダプターに対応した拡張機能も必要です。PDOアダプターの場合は、`ext-pdo` と `ext-pdo_sqlite` または `ext-pdo_mysql`、mysqliアダプターの場合は `ext-mysqli` が必要です。Composerはこれらを推奨するだけなので、必要なものがインストールされていることを確認してください。

このパッケージは CLI を `vendor/bin/merql` としてインストールします。CLI を使用するにはmerql 0.4.1以降が必要です。それ以前のリリースでは、merqlを依存関係としてインストールした場合に、バイナリがオートローダーを見つけられませんでした。

## データベースへの接続

merqlは小規模な接続アダプターインターフェースを介して動作します。PDOとmysqli用の組み込みアダプターに加え、MySQLとSQLite用の組み込み方言ドライバーを提供しています。

```php
use Merql\Connection;

// SQLite (in-memory or file-based, through PDO).
$connection = Connection::sqlite(':memory:');
$connection = Connection::sqlite('/path/to/database.sqlite');

// MySQL through PDO.
$connection = Connection::mysql('127.0.0.1', 'my_database', 'root', 'password');

// MySQL through mysqli.
$connection = Connection::mysqli('127.0.0.1', 'my_database', 'root', 'password');

// Any MySQL or SQLite PDO DSN.
$connection = Connection::fromDsn('mysql:host=127.0.0.1;dbname=my_database;charset=utf8mb4', 'root', 'password');
```

`fromDsn()` はPDOがサポートする任意のDSNを開きますが、merqlに付属する方言ドライバーはMySQLとSQLite用のみです。`pgsql:` など、別のエンジンを使う場合、接続は開けますが、そのエンジン用のドライバーを登録するまでは `Merql::init()` が `Unsupported database driver: pgsql` をスローします。[Database Drivers](https://merql.inline0.dev/ja/docs/apply/drivers)を参照してください。

## スナップショットの取得

スナップショットには、各テーブルのすべての行（または一部のテーブルの行）がフィンガープリントとデータとして記録されます。フィンガープリントによって変更をすばやく検出でき、差分が必要な場合は行データ全体を利用できます。

```php
use Merql\Merql;

Merql::init($connection);

// Snapshot all tables.
$snapshot = Merql::snapshot('baseline');

// Snapshot specific tables.
$snapshot = Merql::snapshot('baseline', ['posts', 'comments', 'users']);
```

スナップショットはデフォルトで `.merql/snapshots/` 内の JSON ファイルとして保存されます。保存場所は変更できます:

```php
use Merql\Snapshot\SnapshotStore;

SnapshotStore::setDirectory('/var/data/merql-snapshots');
```

## 差分の計算

差分は2つのスナップショットを比較し、挿入、更新、削除をまとめた変更セットを返します。両方のスナップショットがすでに存在している必要があります。ここでは `current` は、データベースに変更を加えた後に取得した2つ目のスナップショットです。

```php
$changeset = Merql::diff('baseline', 'current');

echo "Inserts: " . count($changeset->inserts()) . "\n";
echo "Updates: " . count($changeset->updates()) . "\n";
echo "Deletes: " . count($changeset->deletes()) . "\n";

// Inspect changes for a single table.
$postChanges = $changeset->forTable('posts');

// Check per-column diffs on updates.
foreach ($changeset->updates() as $update) {
    echo "Row {$update->rowKey} in {$update->table}:\n";
    foreach ($update->columnDiffs as $diff) {
        echo "  {$diff->column}: {$diff->oldValue} -> {$diff->newValue}\n";
    }
}
```

## 三者マージ

中心となる操作です。名前付きのスナップショットを3つ（base、ours、theirs）受け取り、2組の変更をマージします。

baseは、両方のコピーが出発点とした状態です。oursとtheirsは、それぞれ独立して変更した後の2つのコピーなので、各状態を保持するデータベースから取得する必要があります。たとえば、サンドボックスを本番データベースからコピーした場合:

```php
use Merql\Connection;
use Merql\Merql;

$live    = Connection::sqlite('/data/live.sqlite');
$sandbox = Connection::sqlite('/data/sandbox.sqlite');

Merql::init($live);
Merql::snapshot('base');      // before the sandbox is copied from live

// ... copy live.sqlite to sandbox.sqlite, then both databases change ...

Merql::init($sandbox);
Merql::snapshot('ours');      // the sandbox with our edits
Merql::init($live);
Merql::snapshot('theirs');    // live with its own changes

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

echo "Operations: {$result->operationCount()}\n";
echo "Conflicts:  {$result->conflictCount()}\n";
echo "Clean:      " . ($result->isClean() ? 'yes' : 'no') . "\n";
```

3つすべてのスナップショットを1つのデータベースから順番に取得しないでください。そうするとtheirsにはすでにoursの変更が含まれ、oursが挿入したすべての行が `insert_insert` の競合として表示されます。

`Merql::merge()` は列単位でマージします。TEXTと JSON の値の内部もマージするには、`CellMergeConfig` を指定して `ThreeWayMerge` を使用してください。[Cell-Level Merge](https://merql.inline0.dev/ja/docs/merge/cell-level)を参照してください。

### 競合の処理

両方の側が同じ列を異なる値に変更すると、merqlは競合を発生させます。競合は自動で解決することも、手動で確認することもできます。

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

if (!$result->isClean()) {
    // Auto-resolve: one side wins.
    $resolved = ConflictResolver::resolve($result, ConflictPolicy::TheirsWins);

    // Or inspect each conflict.
    foreach ($result->conflicts() as $conflict) {
        echo "Conflict on {$conflict->table()} row {$conflict->rowKey()}:\n";
        echo "  Type:   {$conflict->type()}\n";
        echo "  Column: {$conflict->column()}\n";
        echo "  Ours:   " . var_export($conflict->oursValue(), true) . "\n";
        echo "  Theirs: " . var_export($conflict->theirsValue(), true) . "\n";
    }
}
```

## マージの適用

競合が解決され、マージ結果がクリーンになったら、`Merql::init()` に渡したデータベースに適用します。

マージ結果の操作はbaseをマージ後の状態に変換し、両側の変更を含みます。`Merql::apply()` はそれらすべてを実行するため、baseの状態を保持するデータベースには問題なく適用できます。一方、すでに片側の変更が入っているデータベースに適用すると、その側が挿入した行がすでに存在するため失敗し、マージ全体がロールバックされます。たとえばサンドボックスの編集を本番環境へ反映する場合は、[Guarded Apply](https://merql.inline0.dev/ja/docs/apply/guarded)を使って、まだ適用されていない操作だけを適用してください。

```php
$applied = Merql::apply($result);

echo "Rows affected: {$applied->rowsAffected()}\n";

if ($applied->hasErrors()) {
    foreach ($applied->errors() as $error) {
        echo "Error: {$error}\n";
    }
}
```

適用処理はすべての操作をトランザクションで実行します。いずれかのステートメントが失敗すると、マージ全体がロールバックされ、エラーが `errors()` に返されます。その場合、ロールバックされていても、`rowsAffected()` には失敗前に実行されたステートメントが引き続きカウントされます（[#4](https://github.com/inline0/merql/issues/4)）。まず `hasErrors()` を確認してください。

## ドライラン

マージを実行せずに、マージによって生成される SQL をプレビューします。

```php
use Merql\Apply\DryRun;

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

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

## 二者パッチ

baseと変更セットだけがあり、同時に加えられた「ours」の変更がない場合は、`patch()` を使います。これはoursがbaseから変更されていない場合のマージと同じです。

```php
$result = Merql::patch('base', 'changes');
// Always clean: no concurrent edits means no conflicts.
Merql::apply($result);
```

## CLI の使い方

これらの操作はすべてコマンドラインからも利用できます。詳しくは[CLI Reference](https://merql.inline0.dev/ja/docs/cli)を参照してください。

```bash
export MERQL_DB_DSN="sqlite:mydb.sqlite"

vendor/bin/merql snapshot base
# ... change the database ...
vendor/bin/merql snapshot current
vendor/bin/merql diff base current
```

三者マージでは、各状態を保持するデータベースからスナップショットを取得してから、マージします:

```bash
MERQL_DB_DSN="sqlite:/data/live.sqlite" vendor/bin/merql snapshot base
MERQL_DB_DSN="sqlite:/data/sandbox.sqlite" vendor/bin/merql snapshot ours
MERQL_DB_DSN="sqlite:/data/live.sqlite" vendor/bin/merql snapshot theirs
vendor/bin/merql merge base ours theirs --dry-run
```

## テーブルと列のフィルタリング

スナップショットでは、テーブル（キャッシュテーブルやセッションテーブルなど）を除外したり、値が変わりやすい列（`updated_at` のタイムスタンプなど）を無視したりできます。

```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']),
]);
```

## 次のステップ

- 完全なコマンドラインインターフェースについては[CLI Reference](https://merql.inline0.dev/ja/docs/cli)を参照してください。
- すべてのクラスとメソッドの詳しいドキュメントについては[PHP API Reference](https://merql.inline0.dev/ja/docs/api)を参照してください。
