
# SQL 미러

`SqlMirror`는 storh 컬렉션을 SQLite 또는 MySQL 테이블로 보내므로 파일을 표준 상태로 유지하면서 SQL에서 조인, 테이블 간 정렬, 부분 문자열 검색 및 보고를 수행할 수 있습니다. 데이터베이스이기도 한 보조 인덱스라고 생각하면 됩니다. 미러는 파생된 데이터이며 언제든 폐기하고 다시 만들 수 있습니다.
`sqlite` 또는 `mysql` 드라이버를 사용하는 경우에만 `ext-pdo`가 필요하며, 핵심 패키지에는 종속성이 없습니다.

## 설정

```php
use Storh\DocStore;
use Storh\Schema;
use Storh\SqlMirror;

$schema = Schema::collection('pages')
    ->string('slug')->unique()
    ->string('kind')->index()
    ->int('publishedAt')->range();

$pages  = new DocStore($root, 'pages', schema: $schema);
$events = new Storh\SegmentedLog($root, 'events');

$mirror = new SqlMirror(new PDO('sqlite:' . $root . '/mirror.db'), 'app_');
$mirror->collection($pages, 'pages', $schema);
$mirror->collection($events, 'events');

$mirror->install();
$mirror->push();
```

두 엔진 모두 미러링할 수 있습니다. `DocStore` 컬렉션과 `SegmentedLog` 스트림(라이브 레코드만 전송)입니다. `Schema` 전달은 선택 사항이며 저장소와 별개입니다. 선언된 `string`/`int`/`float`/`bool` 필드는 형식 지정된 인덱스 열이 되고, 나머지는 모두 `data` JSON 열에서 쿼리할 수 있습니다. `mixed` 필드에는 인덱스 플래그를 설정할 수 없습니다.

## 연결

생성자는 `PDO` 핸들(`sqlite` 또는 `mysql` 드라이버로 자동 감지), `mysqli` 핸들 또는 모든 `SqlMirrorConnection` 구현을 허용합니다:

```php
$mirror = new SqlMirror(new PDO('sqlite:' . $root . '/mirror.db'));
$mirror = new SqlMirror(new PDO('mysql:host=127.0.0.1;dbname=app;charset=utf8mb4', $user, $password));
$mirror = new SqlMirror(new mysqli('127.0.0.1', $user, $password, 'app'), 'wp_storh_');
```

mysqli 경로는 두 가지 mysqli 오류 보고 모드 모두에서 작동하므로 호스트 애플리케이션의 기존 핸들(예: WordPress의 `$wpdb->dbh`)을 그대로 재사용할 수 있습니다. 드라이버 간 동작은 동일하며, 전송에 실패하면 모든 드라이버에서 해당 컬렉션의 트랜잭션을 롤백합니다.

각 테이블에는 `id`(기본 키), `hash`(증분 전송 처리), 형식 지정 열 및 `data`(전체 레코드 JSON)가 있습니다.

## 동기화

```php
$mirror->push();                    // reconcile: insert, update, delete as needed
$mirror->push('pages');             // reconcile a single collection
$mirror->flush('pages', $ids);      // push or remove just these ids
$mirror->pull();                    // write mirror rows back into the store
$mirror->verify();                  // report missing, stale, and orphaned rows
$mirror->rebuild();                 // clear all rows and push from scratch
$mirror->uninstall();               // drop the mirror tables
```

`push()`는 레코드 해시를 미러와 비교하고 각 컬렉션을 하나의 트랜잭션으로 기록하므로 전송 도중 충돌이나 실패가 발생해도 배치가 일부만 적용되지 않습니다. 변경되지 않은 레코드에는 레코드 순회와 함께 `SELECT` 한 번의 비용이 듭니다. 주기적인 `push()`(예: cron 틱에서 실행)는 미러를 최신 상태로 유지합니다. 요청 중 기록한 ID에 대해 미러에서 읽기 직후 쓰기 결과를 확인해야 한다면 `flush()`를 호출하세요.

## 쿼리

일반 SQL로 미러를 쿼리합니다. `table()`는 미러링된 테이블 이름을 반환합니다:

```php
$pdo->query(
    'SELECT p.slug, COUNT(*) AS sales
     FROM ' . $mirror->table('purchases') . ' o
     INNER JOIN ' . $mirror->table('products') . ' p ON p.id = o.product_id
     GROUP BY p.slug
     ORDER BY sales DESC'
);
```

## 파일로 다시 가져오기

`pull()`는 미러 행을 ID 순서로 읽어 등록된 저장소에 기록합니다. 두 가지 흐름을 지원합니다:

- 복원: 파일은 사라졌지만 미러는 남아 있습니다. 같은 컬렉션 이름으로 새 저장소를 등록한 뒤 가져오세요.
- 초기 데이터 채우기: 외부 도구로 미러 테이블에 불러온 행은 storh 레코드가 됩니다. 행에는 유효한 UUIDv7 ID와 `data` 열의 객체가 있어야 합니다. `data`가 전체 레코드이므로 가져올 때 형식 지정 열은 무시됩니다.

```php
$restored = new Storh\DocStore($root, 'pages');
$mirror   = new SqlMirror($pdo);
$mirror->collection($restored, 'pages');
$mirror->pull();          // ['written' => n, 'unchanged' => m]
```

가져온 데이터도 일반 저장소 처리 과정을 거치므로 스키마 검증과 고유 인덱스가 적용되고 모든 레코드가 내구성 있게 기록됩니다. `pull()`는 로컬 레코드를 삭제하지 않으며, 데이터가 미러 행과 이미 일치하는 레코드는 건너뜁니다. 직접 삽입한 행을 초기 데이터로 사용한 뒤 `push()`를 한 번 실행하세요. 해시를 일치시키기 위해 해당 행이 표준 형식으로 다시 기록됩니다.

## 계약 및 한도

- 파일이 표준 데이터입니다. 미러는 읽기 전용 출력이며, 직접 기록한 내용은 다음 `push()`에서 덮어쓰거나 삭제합니다.
- 미러의 고유 열은 보조 안전장치이며 쓰기 시 적용되지 않습니다. 파일 저장소에서 허용한 중복 값도 전송 실패로 나타나며 전체 롤백됩니다. 쓰기 시 적용하려면 storh 고유 인덱스를 사용하세요.
- 미러는 최대 한 번의 전송 간격만큼 파일보다 늦습니다. 단, `flush()`를 명시적으로 전송한 ID는 예외입니다.
- 미러링을 사용해도 storh 자체에 트랜잭션이 추가되지는 않습니다. 여러 레코드에 걸친 원자적 쓰기는 기준 시스템인 실제 데이터베이스에서 처리해야 합니다.
- 스키마를 변경하려면 미러를 삭제하고 다시 설치하세요: `uninstall()`, `install()`, `push()`.
