
# PHP API 참조 문서

merql의 모든 공개 클래스와 메서드에 대한 전체 참조 문서입니다.

## Merql (파사드)

`Merql\Merql`은 일반 작업을 위한 정적 진입점입니다. 하위 수준 클래스를 감싸고 스냅샷 저장을 자동으로 관리합니다.

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

데이터베이스 연결로 파사드를 초기화합니다. `snapshot()`, `apply()` 또는 `applyGuarded()`보다 먼저 호출해야 합니다. 데이터베이스를 전환하려면 다시 호출하세요. 예를 들어 서로 다른 두 복사본에서 ours와 theirs를 스냅샷으로 만들 때 사용할 수 있습니다. `$driver`이 없으면 `DriverFactory::create()`를 호출합니다. 드라이버가 등록되지 않은 경우 이 메서드는 MySQL 및 SQLite 이외의 엔진에서는 예외를 발생시킵니다. 자동 감지를 재정의하려면 드라이버를 전달하고, 스냅샷을 캡처할 때 테이블별 행 식별 규칙을 재정의하려면 식별 규칙 집합을 전달하세요.

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

현재 데이터베이스 상태를 캡처해 디스크에 저장합니다. 특정 테이블의 스냅샷을 만들려면 테이블 이름 배열을 전달하고, 모든 테이블을 대상으로 하려면 생략하세요.

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

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

이름이 지정된 스냅샷 두 개를 불러와 그 사이의 변경 집합을 계산합니다. 저장된 스냅샷을 사용하므로 `init()`이 필요하지 않습니다.

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

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

이름이 지정된 스냅샷 세 개를 3방향 병합합니다. 충돌 없이 적용 가능한 작업과 충돌을 포함한 결과를 반환합니다. 열 수준에서 병합합니다. 파사드는 `CellMergeConfig` 없이 `ThreeWayMerge`을 구성하므로 셀 수준 병합은 적용되지 않습니다. 셀 수준 병합에는 [`ThreeWayMerge`](#threewaymerge)를 직접 사용하세요.

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

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

2방향 병합: base에 변경 사항을 적용합니다. `merge($base, $base, $changes)`과 동일합니다. 동시 편집이 없으므로 항상 충돌 없는 결과를 생성합니다. `merge()`과 마찬가지로 열 수준에서만 병합합니다.

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

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

`init()`에 전달된 데이터베이스에 충돌 없이 적용 가능한 병합 결과를 실행합니다. `init()`이 필요합니다. 해결되지 않은 충돌이 남아 있으면 `ConflictException`를 발생시킵니다.

결과의 모든 작업이 실행되므로 `merge()`의 전체 결과는 여전히 base 상태인 데이터베이스에만 충돌 없이 적용됩니다. 데이터베이스에 이미 한쪽 변경 사항이 적용된 경우, 아직 적용되지 않은 작업을 선택해 적용하세요. [Guarded Apply](https://merql.inline0.dev/ko/docs/apply/guarded)를 참조하세요.

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

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

이름이 지정된 스냅샷 세 개에서 UI에 사용할 병합 계획을 만듭니다. 계획에는 안정적인 작업 ID, 변경 그룹 ID, 작업 컨텍스트, 충돌, 스키마 불일치, 요약, 메타데이터, 계획 해시가 포함됩니다. `merge()`과 마찬가지로 열 수준 병합을 수행합니다. 셀 수준 병합을 하려면 셀 병합 결과를 `MergePlanBuilder::build()`에 전달하세요.

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

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

라이브 행에 대한 낙관적 사전 조건을 적용해 충돌 없이 병합 결과를 적용합니다. 업데이트와 삭제는 현재 라이브 행이 `$expectedLive`과 일치해야 합니다. 삽입은 대상 식별자가 아직 존재하지 않아야 합니다. `$expectedLive`은 대상 데이터베이스의 스냅샷입니다(ours를 데이터베이스에 반영할 때는 theirs를 가져온 데이터베이스). 영향을 받은 행이 0개인 문이 하나라도 있으면 트랜잭션을 롤백합니다.

```php
use Merql\Snapshot\SnapshotStore;

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

### `reset(): void`

내부 연결 및 Snapshotter 인스턴스를 초기화합니다. 주로 테스트 사이에 상태를 재설정할 때 사용합니다.

---

## 연결

`Merql\Connection`은 적절한 기본값으로 연결 어댑터를 구성합니다. PDO 연결은 오류 시 예외를 사용하고, 연관 배열 가져오기 모드를 설정하며, 에뮬레이션된 프리페어를 비활성화하고, 가져온 값을 문자열로 변환합니다. mysqli 어댑터는 가져온 값을 같은 문자열 또는 null 형식으로 정규화합니다.

### `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`

SQLite 연결을 생성합니다. `PRAGMA foreign_keys = ON`을 통해 외래 키를 자동으로 활성화합니다.

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

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

모든 PDO DSN 문자열로 연결합니다. merql은 `mysql` 및 `sqlite` 드라이버만 제공합니다. 다른 DSN으로도 연결은 열리지만, 드라이버를 `DriverFactory::register()`에 등록하지 않으면 `Merql::init()`, `Snapshotter` 및 적용기가 `Unsupported database driver`를 발생시킵니다.

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

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

기존 PDO 인스턴스를 감쌉니다.

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

기존 mysqli 인스턴스를 감쌉니다.

### 어댑터 계약

모든 어댑터는 `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()` 및 `scalar()`는 `?` 플레이스홀더에 위치 기반 `list<scalar|null>` 매개변수를 받습니다. `query()`는 `list<array<string, scalar|null>>`을 반환합니다. 값은 PDO와 mysqli 어댑터에서 동일한 문자열 또는 null 형식으로 정규화됩니다.

---

## 스냅샷 생성기

`Merql\Snapshot\Snapshotter`은 데이터베이스 상태를 캡처합니다. 파사드와 달리 필터를 완전히 제어할 수 있으며 자동으로 저장하지 않습니다.

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

드라이버를 지정하지 않으면 연결 식별 정보에서 자동으로 감지합니다. 테이블별 행 식별 규칙이 스키마 기본값을 재정의해야 한다면 `IdentityRuleSet`을 전달하세요.

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

현재 상태를 캡처합니다. 범위를 제한하려면 테이블 이름을 전달하세요. 테이블, 열 또는 행을 제외하려면 필터 객체를 전달하세요.

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

실제 테이블을 정규 테이블 이름으로 캡처합니다. 환경마다 동일한 논리 테이블의 실제 이름이 다를 때 사용하세요.

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

결과 스냅샷에는 정규 테이블 이름이 저장되며, 실제 테이블 스키마, 설정된 식별 열, 행 키, 필터는 유지됩니다.

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

데이터베이스 연결 없이 원시 데이터에서 스냅샷을 만듭니다. 테스트나 다른 소스의 데이터를 가져올 때 유용합니다.

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

---

## 스냅샷

`Merql\Snapshot\Snapshot`은 전체 데이터베이스 상태를 나타내는 읽기 전용 값 객체입니다.

```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`은 한 테이블의 행, 지문, 스키마를 보유합니다.

```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`은 `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`은 행 키를 만드는 데 사용할 열을 정의합니다.

```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` (정적)

스키마에서 기본 식별 규칙을 확인합니다: 기본 키, 첫 번째 고유 키, 그다음 콘텐츠 대체 규칙 순입니다.

### `toArray(): array`

규칙을 `['type' => string, 'columns' => list<string>]`로 직렬화합니다.

### `fromArray(array $data): IdentityRule` (정적)

직렬화된 데이터에서 규칙을 복원합니다.

---

## IdentityRuleSet

`Merql\Identity\IdentityRuleSet`은 테이블별 식별 재정의 설정을 저장합니다.

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

테이블 재정의 항목 하나가 추가된 새 규칙 집합을 반환합니다.

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

규칙에 따라 여러 행에서 같은 키가 생성될 때 `IdentityConflict` 항목을 반환합니다.

### `toArray(): array`

테이블 규칙을 JSON에 안전한 배열로 직렬화합니다.

### `fromArray(array $data): IdentityRuleSet` (정적)

직렬화된 데이터에서 규칙 집합을 복원합니다.

---

## SnapshotStore

`Merql\Snapshot\SnapshotStore`은 스냅샷을 JSON 파일로 저장하고 불러옵니다.

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

저장 디렉터리를 설정합니다. 기본값은 `.merql/snapshots`입니다.

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

스냅샷을 디스크에 기록합니다.

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

이름으로 스냅샷을 불러옵니다. 찾을 수 없으면 `SnapshotException`을 발생시킵니다.

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

스냅샷 파일이 있는지 확인합니다.

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

스냅샷 파일을 제거합니다.

---

## 비교기

`Merql\Diff\Differ`은 두 스냅샷을 비교해 변경 집합을 생성합니다.

### `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` (정적)

문자열로 변환해 두 값을 비교합니다. NULL은 별개의 값으로 취급합니다: `NULL === NULL`은 true이고 `NULL === ''`은 false입니다.

---

## 변경 집합

`Merql\Diff\Changeset`은 행 수준 작업의 읽기 전용 컬렉션입니다.

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

개별 행 변경을 나타내는 읽기 전용 값 객체입니다.

### 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`은 행 업데이트 내의 단일 열 변경을 나타냅니다.

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

---

## ThreeWayMerge

`Merql\Merge\ThreeWayMerge`은 핵심 병합 알고리즘입니다.

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

TEXT 및 JSON 열에 셀 수준 병합을 사용하려면 `CellMergeConfig`을 전달하세요.

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

3방향 병합을 수행합니다. 변경 집합(base와 ours, base와 theirs)을 내부적으로 계산하고 병합합니다.

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

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

2방향 병합 단축 경로입니다. `merge($base, $base, $changes)`과 동일합니다.

### `schemaMismatches(): array`

마지막 병합에서 감지된 모든 스키마 차이(스냅샷 간에 추가되거나 제거된 열)에 대해 `SchemaException[]`을 반환합니다.

---

## MergeResult

`Merql\Merge\MergeResult`은 병합 결과를 담은 읽기 전용 값 객체입니다.

```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`은 병합을 검토하기 위한 직렬화 가능한 모델입니다.

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

안정적인 JSON 왕복 처리를 위해 `MergePlanSerializer::toJson()` 및 `::fromJson()`을 사용하세요.

## 선택 항목

`ChangeGroupSelection`은 스테이징된 변경 그룹 ID를 저장합니다. 선택한 병합 결과나 롤백 계획을 만들 때 이를 `OperationSelection`로 변환하세요.

```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`은 선택 적용을 위한 역연산과 드리프트 예상값을 저장합니다.

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

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

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

롤백을 적용하기 전에 `RollbackDrift`을 사용한 다음, 역연산을 실행하려면 `RollbackPlanApplier`을 사용하세요.

---

## MergeOperation

`Merql\Merge\MergeOperation`은 병합에서 해결된 단일 작업입니다.

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

유형 상수:

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

---

## 충돌

`Merql\Merge\Conflict`은 해결되지 않은 병합 충돌을 나타냅니다.

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

### 충돌 유형

| 유형 | 의미 |
|---|---|
| `update_update` | 양쪽에서 같은 열을 서로 다른 값으로 변경함 |
| `update_delete` | ours에서 업데이트한 행을 theirs에서 삭제함 |
| `delete_update` | ours에서 삭제한 행을 theirs에서 업데이트함 |
| `insert_insert` | 양쪽에서 같은 기본 키를 가진 행을 삽입함 |

---

## ConflictResolver

`Merql\Merge\ConflictResolver`은 병합 결과의 모든 충돌에 해결 정책을 적용합니다.

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

정책에 따라 충돌을 해결한 새 `MergeResult`을 반환합니다.

```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`은 세 가지 케이스가 있는 백드 enum입니다.

```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`은 단일 행에 대해 열별 병합 로직을 수행합니다. 내부적으로 `ThreeWayMerge`에서 사용합니다.

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

`['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)
```

---

## 적용기

`Merql\Apply\Applier`은 데이터베이스에 병합 결과를 실행합니다.

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

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

모든 작업을 단일 트랜잭션에서 실행합니다. 오류가 발생하면 롤백하고 예외를 발생시키는 대신 `ApplyResult::errors()`에 오류를 보고합니다. 결과에 해결되지 않은 충돌이 있으면 `ConflictException`을 발생시킵니다.

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

외래 키 순서는 자동으로 처리합니다. 삽입은 종속성 순서대로 정렬하고 삭제는 종속성의 역순으로 정렬합니다.

---

## ApplyResult

`Merql\Apply\ApplyResult`은 적용기가 반환하는 읽기 전용 값 객체입니다.

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

롤백된 적용 후에도 `rowsAffected()`에는 실패 전에 실행된 문이 포함됩니다([#4](https://github.com/inline0/merql/issues/4)). 이 횟수를 신뢰하기 전에 `hasErrors()`을 확인하세요.

---

## DryRun

`Merql\Apply\DryRun`은 아무것도 실행하지 않고 사람이 읽을 수 있는 SQL을 생성합니다.

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

표시 목적으로 값이 인라인 삽입된 SQL 문자열 목록을 반환합니다.

```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`은 매개변수화된 SQL 문을 생성합니다. `Applier`와 `DryRun` 모두 내부적으로 사용합니다.

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

`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`은 특정 열 또는 열 유형의 셀 수준 병합 전략을 설정합니다.

### `auto(): self` (정적)

편의 팩토리로, TEXT/LONGTEXT 열에는 `TextCellMerger`을, JSON 열에는 `JsonCellMerger`을 할당합니다.

```php
use Merql\CellMerge\CellMergeConfig;

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

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

특정 열에 병합기를 할당합니다. 정규화된 이름에는 `table.column`을, 모든 테이블에는 `column`만 사용하세요.

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

유형 패턴과 일치하는 모든 열에 병합기를 할당합니다(대소문자를 구분하지 않는 부분 문자열 일치).

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

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

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

테이블, 열, 유형에 해당하는 병합기를 조회합니다. 우선순위: 정규화된 열 이름, 정규화되지 않은 열 이름, 유형 패턴, 기본값(불투명).

---

## CellMerger (인터페이스)

`Merql\CellMerge\CellMerger`은 셀 수준 병합 전략의 인터페이스입니다.

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

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

기본 제공 구현은 세 가지입니다.

### TextCellMerger

`Merql\CellMerge\TextCellMerger`은 3방향 줄 단위 diff(파일 콘텐츠에 git이 사용하는 알고리즘)로 여러 줄의 텍스트를 병합합니다. 양쪽에서 서로 다른 줄을 수정하면 깔끔하게 병합됩니다. 같은 줄을 서로 다르게 수정하면 충돌을 보고합니다.

```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`은 최상위 키를 각각 비교해 JSON 값을 병합합니다. 각 키는 3방향 로직으로 병합합니다. 중첩 객체는 불투명한 JSON 문자열로 비교하며(재귀적으로 병합하지 않음),

```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`은 기본값입니다. 값을 불투명한 문자열로 취급하며 값이 다르면 항상 충돌을 보고합니다.

---

## CellMergeResult

`Merql\CellMerge\CellMergeResult`은 모든 셀 병합기의 반환 유형입니다.

```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`은 테이블 구조를 읽기 전용으로 표현합니다.

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

---

## 드라이버 시스템

merql은 데이터베이스별 작업을 추상화하기 위해 플러그형 드라이버 시스템을 사용합니다. 드라이버는 연결 드라이버 식별자를 바탕으로 자동 감지됩니다.

### Driver (인터페이스)

`Merql\Driver\Driver`은 모든 데이터베이스 드라이버가 구현하는 규약을 정의합니다.

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

기본 제공 구현: `MysqlDriver` 및 `SqliteDriver`.

### DriverFactory

`Merql\Driver\DriverFactory`은 연결에서 드라이버를 자동 감지합니다.

```php
use Merql\Driver\DriverFactory;

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

다른 데이터베이스용 사용자 지정 드라이버 등록:

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

---

## 필터

### TableFilter

`Merql\Filter\TableFilter`은 glob 패턴을 사용해 테이블을 포함하거나 제외합니다.

```php
use Merql\Filter\TableFilter;

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

### ColumnFilter

`Merql\Filter\ColumnFilter`은 스냅샷에서 특정 열을 제거합니다.

```php
use Merql\Filter\ColumnFilter;

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

### RowFilter

`Merql\Filter\RowFilter`은 조건자 함수로 행을 필터링합니다.

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

---

## 예외

### SnapshotException

스냅샷을 찾을 수 없거나 이름이 잘못된 경우 `Merql\Exceptions\SnapshotException`이 발생합니다.

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

### MergeException

병합에 필요한 스냅샷이 없는 경우 `Merql\Exceptions\MergeException`이 발생합니다.

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

### ConflictException

해결되지 않은 충돌이 있는 병합 결과를 적용하려고 하면 `Merql\Exceptions\ConflictException`이 발생합니다.

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

### SchemaException

스냅샷 간 스키마가 다르면 `Merql\Exceptions\SchemaException`이 발생하거나 수집됩니다.

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

스키마 불일치가 병합을 중단시키지는 않습니다. 불일치는 수집되며 검사할 수 있도록 `$result->schemaMismatches()`을 통해 제공됩니다.
