Pular para o conteúdo

Identidade da linha

A identidade da linha determina como merql associa linhas entre snapshots. Ao comparar base com ours, é preciso saber que a linha X em base é a mesma linha lógica que a linha Y em ours, mesmo que alguns valores das colunas tenham mudado. A regra de identidade define quais colunas formam a chave da linha.

PHP
use Merql\Identity\PrimaryKeyIdentity;

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

A interface RowIdentity

Todas as estratégias de identidade implementam dois métodos:

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

key() cria um identificador de string único a partir dos dados de uma linha. columns() retorna os nomes das colunas usados para identificar a linha.

PrimaryKeyIdentity

A estratégia padrão. Associa linhas pelas colunas da chave primária.

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)

É isso que merql usa por padrão quando a tabela tem uma chave primária definida no esquema.

NaturalKeyIdentity

Em tabelas sem chave primária com incremento automático, associa linhas pelas colunas de restrição única. É útil quando a PK é uma UUID ou quando a tabela usa uma restrição única composta como identidade lógica.

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

Alternativa para tabelas sem chave primária nem restrições únicas. Calcula um hash SHA-256 de todos os valores das colunas. Duas linhas são iguais se todas as colunas coincidirem.

PHP
use Merql\Identity\ContentHashIdentity;

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

A identidade por hash de conteúdo tem uma limitação: se o conteúdo de uma linha mudar, seu hash também muda. Assim, a linha aparece como uma exclusão do hash antigo e uma inserção do novo, em vez de uma atualização. Essa é uma consequência inerente à ausência de colunas de identidade estáveis.

IdentityRule

IdentityRule é a configuração de identidade serializável usada pela captura de snapshots e pelos planos de mesclagem.

PHP
use Merql\Identity\IdentityRule;

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

Tipos de regra:

Tipo Significado
primary Colunas da chave primária do banco de dados
natural Colunas comerciais únicas e estáveis
composite Colunas de identidade lógica composta
content Alternativa de hash de conteúdo

IdentityRule::forSchema($schema) resolve a regra padrão:

  1. Se a tabela tiver uma chave primária, use as colunas dessa chave.
  2. Se a tabela tiver chaves únicas, use as colunas da primeira chave única.
  3. Se não tiver nenhuma das duas, use todas as colunas (alternativa de hash de conteúdo).

IdentityRuleSet

Use IdentityRuleSet quando uma aplicação hospedeira conhecer uma regra de identidade melhor que o padrão do esquema.

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 resolve um IdentityRule para cada tabela capturada. As colunas escolhidas são armazenadas em TableSnapshot::identityColumns e usadas em todas as operações subsequentes de diff, mesclagem, plano, SQL e reversão.

Merql::init() pode receber o mesmo conjunto de regras ao usar a fachada estática:

PHP
use Merql\Merql;

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

Os conjuntos de regras têm serialização de array estável:

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

Identidade ambígua

Se duas linhas capturadas produzirem a mesma chave de identidade, a captura falhará em vez de sobrescrever uma linha com outra.

PHP
use Merql\Exceptions\SnapshotException;

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

Aplicações hospedeiras podem fazer uma verificação prévia das linhas brutas com IdentityRuleSet::conflictsFor() para detectar a ambiguidade antes da captura:

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

Para tabelas meta de WordPress, post_id + meta_key costuma ser útil para agrupamento e exibição, mas não é seguro como identidade de linha, pois chaves meta duplicadas são válidas. Use a chave primária da tabela como identidade da linha, a menos que a aplicação hospedeira possa comprovar que a chave natural é única.

Codificação da chave da linha

As chaves de linha são strings codificadas que combinam vários valores de coluna usando um separador de unidade (\x1F). Valores que contenham o separador ou o sinal de porcentagem são escapados para evitar colisões:

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']

Regras de escape:

Original Escapado
% %25
\x1F (separador de unidade) %1F

Isso garante que um valor contendo \x1F não seja dividido em várias partes da chave. A codificação é determinística e reversível.

Divergência de incremento automático

Quando os dois lados inserem linhas novas, os IDs de incremento automático serão diferentes entre as ramificações. A linha #100 em ours pode ser um registro completamente diferente da linha #100 em theirs.

merql lida com isso associando linhas pela chave primária. Se os dois lados inserirem uma linha com o mesmo ID de incremento automático, ela será sinalizada como um conflito insert_insert. Para evitar isso, use chaves naturais (colunas únicas e estáveis entre ramificações) em tabelas nas quais se esperam inserções simultâneas.

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

Identidade em snapshots

As colunas de identidade escolhidas durante a captura são armazenadas com cada TableSnapshot:

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

Essas colunas são usadas por SqlGenerator para criar cláusulas WHERE para instruções UPDATE e DELETE e pelos planos de mesclagem para expor valores de identidade decodificados para análise, seleção, aplicação protegida e reversão.