본문으로 건너뛰기

행 식별성

행 식별성은 merql이 스냅샷 간 행을 일치시키는 방식을 결정합니다. base와 ours를 비교할 때 일부 열 값이 바뀌었더라도 base의 행 X와 ours의 행 Y가 같은 논리적 행인지 알아야 합니다. 식별 규칙은 어떤 열로 행 키를 구성할지 정합니다.

PHP
use Merql\Identity\PrimaryKeyIdentity;

$identity = new PrimaryKeyIdentity(['id']);
$key = $identity->key(['id' => 42, 'title' => 'Hello']);
// "42"

RowIdentity 인터페이스

모든 식별 전략은 두 메서드를 구현합니다:

PHP
interface RowIdentity
{
    public function key(array $row): string;
    public function columns(): array;
}

key()은 행 데이터에서 고유한 문자열 식별자를 만듭니다. columns()은 식별에 사용하는 열 이름을 반환합니다.

PrimaryKeyIdentity

기본 전략입니다. 기본 키 열을 기준으로 행을 일치시킵니다.

PHP
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이거나 테이블이 복합 고유 제약 조건을 논리적 식별자로 사용하는 경우에 유용합니다.

PHP
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 해시를 계산합니다. 모든 열이 일치하면 두 행은 같은 행으로 간주합니다.

PHP
use Merql\Identity\ContentHashIdentity;

$identity = new ContentHashIdentity(['name', 'value']);
$key = $identity->key(['name' => 'color', 'value' => 'blue']);
// "a3f2..." (SHA-256 hash)

콘텐츠 해시 식별에는 한계가 있습니다. 행의 콘텐츠가 바뀌면 해시도 바뀌므로, 해당 행은 업데이트가 아니라 이전 해시의 삭제와 새 해시의 삽입으로 처리됩니다. 안정적인 식별 열이 없을 때 따르는 본질적인 절충입니다.

IdentityRule

IdentityRule은 스냅샷 캡처와 병합 계획에 사용하는 직렬화 가능한 식별 설정입니다.

PHP
use Merql\Identity\IdentityRule;

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

규칙 유형:

유형 의미
primary 데이터베이스 기본 키 열
natural 안정적인 고유 비즈니스 열
composite 복합 논리 식별 열
content 콘텐츠 해시 대체 방식

IdentityRule::forSchema($schema)은 기본 규칙을 결정합니다:

  1. 테이블에 기본 키가 있으면 기본 키 열을 사용합니다.
  2. 고유 키가 있으면 첫 번째 고유 키의 열을 사용합니다.
  3. 둘 다 없으면 모든 열을 사용합니다(콘텐츠 해시 대체 방식).

IdentityRuleSet

호스트 애플리케이션에서 스키마 기본값보다 나은 식별 규칙을 알고 있다면 IdentityRuleSet을 사용합니다.

PHP
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()에 같은 규칙 집합을 전달할 수 있습니다:

PHP
use Merql\Merql;

Merql::init($connection, identityRules: $rules);
Merql::snapshot('live', ['wp_options']);

규칙 집합은 배열로 직렬화할 때 안정적인 순서를 유지합니다:

PHP
$payload = $rules->toArray();
$rules = IdentityRuleSet::fromArray($payload);

모호한 식별성

캡처한 두 행의 식별 키가 같으면 한 행이 다른 행을 덮어쓰는 대신 캡처가 실패합니다.

PHP
use Merql\Exceptions\SnapshotException;

try {
    $snapshotter->capture('live', ['wp_postmeta']);
} catch (SnapshotException $e) {
    // The configured identity rule matched multiple rows.
}

호스트 애플리케이션은 캡처 전에 모호성을 알리고 싶을 때 IdentityRuleSet::conflictsFor()을 사용해 원시 행을 사전 검사할 수 있습니다:

PHP
$conflicts = $rules->conflictsFor(
    'wp_postmeta',
    IdentityRule::natural(['post_id', 'meta_key']),
    $rows,
);

WordPress 메타 테이블에서 post_id + meta_key은 그룹화와 표시에 유용한 경우가 많지만, 중복 메타 키가 허용되므로 행 식별자로는 안전하지 않습니다. 호스트 애플리케이션에서 자연 키의 고유성을 증명할 수 없다면 테이블의 기본 키를 행 식별자로 사용하세요.

행 키 인코딩

행 키는 단위 구분자(\x1F)로 여러 열 값을 결합한 인코딩 문자열입니다. 충돌을 방지하기 위해 구분자나 퍼센트 기호가 포함된 값은 이스케이프 처리됩니다:

PHP
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 충돌로 표시됩니다. 이를 피하려면 동시 삽입이 예상되는 테이블에 브랜치 간에 안정적으로 유지되는 고유 열인 자연 키를 사용하세요.

PHP
// A natural key on (slug) avoids auto-increment collisions.
$identity = new NaturalKeyIdentity(['slug']);

스냅샷의 식별성

캡처 중 선택한 식별 열은 각 TableSnapshot에 저장됩니다:

PHP
$tableSnapshot = $snapshot->getTable('posts');
$tableSnapshot->identityColumns;
// ['id']

SqlGenerator은 이 열을 사용해 UPDATE 및 DELETE 문에 필요한 WHERE 절을 구성합니다. 병합 계획은 디코딩된 식별 값을 제공해 검토, 선택, 보호된 적용, 롤백에 사용합니다.