행 식별성
행 식별성은 merql이 스냅샷 간 행을 일치시키는 방식을 결정합니다. base와 ours를 비교할 때 일부 열 값이 바뀌었더라도 base의 행 X와 ours의 행 Y가 같은 논리적 행인지 알아야 합니다. 식별 규칙은 어떤 열로 행 키를 구성할지 정합니다.
use Merql\Identity\PrimaryKeyIdentity;
$identity = new PrimaryKeyIdentity(['id']);
$key = $identity->key(['id' => 42, 'title' => 'Hello']);
// "42"
RowIdentity 인터페이스
모든 식별 전략은 두 메서드를 구현합니다:
interface RowIdentity
{
public function key(array $row): string;
public function columns(): array;
}
key()은 행 데이터에서 고유한 문자열 식별자를 만듭니다. columns()은 식별에 사용하는 열 이름을 반환합니다.
PrimaryKeyIdentity
기본 전략입니다. 기본 키 열을 기준으로 행을 일치시킵니다.
use Merql\Identity\PrimaryKeyIdentity;
// Single-column PK.
$identity = new PrimaryKeyIdentity(['id']);
$key = $identity->key(['id' => 42, 'title' => 'Hello']);
// "42"
// Composite PK.
$identity = new PrimaryKeyIdentity(['tenant_id', 'user_id']);
$key = $identity->key(['tenant_id' => 1, 'user_id' => 99, 'name' => 'Alice']);
// "1\x1F99" (unit separator between parts)
스키마에 테이블의 기본 키가 정의되어 있으면 merql은 기본적으로 이 방식을 사용합니다.
NaturalKeyIdentity
자동 증가 기본 키가 없는 테이블에서는 고유 제약 조건 열을 기준으로 행을 일치시킵니다. PK가 UUID이거나 테이블이 복합 고유 제약 조건을 논리적 식별자로 사용하는 경우에 유용합니다.
use Merql\Identity\NaturalKeyIdentity;
$identity = new NaturalKeyIdentity(['email']);
$key = $identity->key(['id' => 1, 'email' => 'alice@example.com', 'name' => 'Alice']);
// "alice@example.com"
$identity = new NaturalKeyIdentity(['country', 'postal_code']);
$key = $identity->key(['country' => 'US', 'postal_code' => '90210', 'city' => 'Beverly Hills']);
// "US\x1F90210"
ContentHashIdentity
기본 키와 고유 제약 조건이 없는 테이블을 위한 대체 방식입니다. 모든 열 값의 SHA-256 해시를 계산합니다. 모든 열이 일치하면 두 행은 같은 행으로 간주합니다.
use Merql\Identity\ContentHashIdentity;
$identity = new ContentHashIdentity(['name', 'value']);
$key = $identity->key(['name' => 'color', 'value' => 'blue']);
// "a3f2..." (SHA-256 hash)
콘텐츠 해시 식별에는 한계가 있습니다. 행의 콘텐츠가 바뀌면 해시도 바뀌므로, 해당 행은 업데이트가 아니라 이전 해시의 삭제와 새 해시의 삽입으로 처리됩니다. 안정적인 식별 열이 없을 때 따르는 본질적인 절충입니다.
IdentityRule
IdentityRule은 스냅샷 캡처와 병합 계획에 사용하는 직렬화 가능한 식별 설정입니다.
use Merql\Identity\IdentityRule;
$rule = IdentityRule::natural(['option_name']);
$key = $rule->key(['option_name' => 'siteurl']);
// "siteurl"
규칙 유형:
| 유형 | 의미 |
|---|---|
primary |
데이터베이스 기본 키 열 |
natural |
안정적인 고유 비즈니스 열 |
composite |
복합 논리 식별 열 |
content |
콘텐츠 해시 대체 방식 |
IdentityRule::forSchema($schema)은 기본 규칙을 결정합니다:
- 테이블에 기본 키가 있으면 기본 키 열을 사용합니다.
- 고유 키가 있으면 첫 번째 고유 키의 열을 사용합니다.
- 둘 다 없으면 모든 열을 사용합니다(콘텐츠 해시 대체 방식).
IdentityRuleSet
호스트 애플리케이션에서 스키마 기본값보다 나은 식별 규칙을 알고 있다면 IdentityRuleSet을 사용합니다.
use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;
use Merql\Snapshot\Snapshotter;
$rules = new IdentityRuleSet([
'wp_options' => IdentityRule::natural(['option_name']),
'wp_term_relationships' => IdentityRule::composite(['object_id', 'term_taxonomy_id']),
]);
$snapshotter = new Snapshotter($connection, identityRules: $rules);
$snapshot = $snapshotter->capture('live', ['wp_options', 'wp_term_relationships']);
Snapshotter은 캡처한 각 테이블의 IdentityRule을 결정합니다. 선택된 열은 TableSnapshot::identityColumns에 저장되며 이후의 모든 diff, merge, plan, SQL 및 롤백 작업에 사용됩니다.
정적 파사드를 사용할 때도 Merql::init()에 같은 규칙 집합을 전달할 수 있습니다:
use Merql\Merql;
Merql::init($connection, identityRules: $rules);
Merql::snapshot('live', ['wp_options']);
규칙 집합은 배열로 직렬화할 때 안정적인 순서를 유지합니다:
$payload = $rules->toArray();
$rules = IdentityRuleSet::fromArray($payload);
모호한 식별성
캡처한 두 행의 식별 키가 같으면 한 행이 다른 행을 덮어쓰는 대신 캡처가 실패합니다.
use Merql\Exceptions\SnapshotException;
try {
$snapshotter->capture('live', ['wp_postmeta']);
} catch (SnapshotException $e) {
// The configured identity rule matched multiple rows.
}
호스트 애플리케이션은 캡처 전에 모호성을 알리고 싶을 때 IdentityRuleSet::conflictsFor()을 사용해 원시 행을 사전 검사할 수 있습니다:
$conflicts = $rules->conflictsFor(
'wp_postmeta',
IdentityRule::natural(['post_id', 'meta_key']),
$rows,
);
WordPress 메타 테이블에서 post_id + meta_key은 그룹화와 표시에 유용한 경우가 많지만, 중복 메타 키가 허용되므로 행 식별자로는 안전하지 않습니다. 호스트 애플리케이션에서 자연 키의 고유성을 증명할 수 없다면 테이블의 기본 키를 행 식별자로 사용하세요.
행 키 인코딩
행 키는 단위 구분자(\x1F)로 여러 열 값을 결합한 인코딩 문자열입니다. 충돌을 방지하기 위해 구분자나 퍼센트 기호가 포함된 값은 이스케이프 처리됩니다:
use Merql\Snapshot\Snapshotter;
// Encode.
$key = Snapshotter::buildRowKey(
['tenant_id' => '1', 'user_id' => '99'],
['tenant_id', 'user_id'],
);
// "1\x1F99"
// Decode.
$parts = Snapshotter::decodeRowKey($key);
// ['1', '99']
이스케이프 규칙:
| 원본 | 이스케이프된 값 |
|---|---|
% |
%25 |
\x1F (단위 구분자) |
%1F |
이렇게 하면 \x1F이 포함된 값이 여러 키 부분으로 나뉘지 않습니다. 인코딩은 결정적이며 되돌릴 수 있습니다.
자동 증가 값의 차이
두 쪽에서 새 행을 삽입하면 브랜치마다 자동 증가 ID가 달라집니다. ours의 행 #100은 theirs의 행 #100과 완전히 다른 레코드일 수 있습니다.
merql은 기본 키를 기준으로 일치시켜 이 문제를 처리합니다. 양쪽에서 같은 자동 증가 ID로 행을 삽입하면 insert_insert 충돌로 표시됩니다. 이를 피하려면 동시 삽입이 예상되는 테이블에 브랜치 간에 안정적으로 유지되는 고유 열인 자연 키를 사용하세요.
// A natural key on (slug) avoids auto-increment collisions.
$identity = new NaturalKeyIdentity(['slug']);
스냅샷의 식별성
캡처 중 선택한 식별 열은 각 TableSnapshot에 저장됩니다:
$tableSnapshot = $snapshot->getTable('posts');
$tableSnapshot->identityColumns;
// ['id']
SqlGenerator은 이 열을 사용해 UPDATE 및 DELETE 문에 필요한 WHERE 절을 구성합니다. 병합 계획은 디코딩된 식별 값을 제공해 검토, 선택, 보호된 적용, 롤백에 사용합니다.