Aller au contenu

Identité des lignes

L’identité des lignes détermine comment merql associe les lignes d’un instantané à l’autre. Lorsqu’il compare la base au nôtre, il doit savoir que la ligne X de la base est la même ligne logique que la ligne Y du nôtre, même si certaines valeurs de colonnes ont changé. La règle d’identité détermine les colonnes qui composent la clé de ligne.

PHP
use Merql\Identity\PrimaryKeyIdentity;

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

Interface RowIdentity

Toutes les stratégies d’identité implémentent deux méthodes :

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

key() construit un identifiant unique sous forme de chaîne à partir des données d’une ligne. columns() renvoie les noms des colonnes utilisées pour définir l’identité.

PrimaryKeyIdentity

La stratégie par défaut. Associe les lignes en fonction de leurs colonnes de clé primaire.

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)

C’est la stratégie utilisée par défaut par merql lorsque le schéma de la table définit une clé primaire.

NaturalKeyIdentity

Pour les tables sans clé primaire à incrémentation automatique, associe les lignes en fonction des colonnes de contrainte unique. Utile lorsque la PK est un UUID ou lorsque la table utilise une contrainte d’unicité composite comme identité logique.

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

Solution de repli pour les tables sans clé primaire ni contrainte d’unicité. Calcule une empreinte SHA-256 de toutes les valeurs de colonnes. Deux lignes sont identiques si toutes leurs colonnes correspondent.

PHP
use Merql\Identity\ContentHashIdentity;

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

L’identité par empreinte de contenu a une limite : si le contenu d’une ligne change, son empreinte change aussi. La ligne apparaît alors comme une suppression de l’ancienne empreinte suivie de l’insertion de la nouvelle, plutôt que comme une mise à jour. C’est un compromis inhérent à l’absence de colonnes d’identité stables.

IdentityRule

IdentityRule est la configuration d’identité sérialisable utilisée lors de la capture d’instantanés et dans les plans de fusion.

PHP
use Merql\Identity\IdentityRule;

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

Types de règles :

Type Signification
primary Colonnes de clé primaire de la base de données
natural Colonnes métier uniques et stables
composite Colonnes d’identité logique composite
content Solution de repli par empreinte de contenu

IdentityRule::forSchema($schema) détermine la règle par défaut :

  1. Si la table a une clé primaire, utilise ses colonnes.
  2. Si la table a des clés uniques, utilise les colonnes de la première.
  3. Si elle n’a ni l’une ni l’autre, utilise toutes les colonnes (solution de repli par empreinte de contenu).

IdentityRuleSet

Utilisez IdentityRuleSet lorsqu’une application hôte connaît une règle d’identité plus adaptée que celle définie par défaut dans le schéma.

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

Le Snapshotter détermine un IdentityRule pour chaque table capturée. Les colonnes retenues sont enregistrées dans TableSnapshot::identityColumns et utilisées pour toutes les opérations ultérieures de comparaison, de fusion, de planification, de SQL et de restauration.

Merql::init() peut recevoir le même jeu de règles avec la façade statique :

PHP
use Merql\Merql;

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

Les jeux de règles ont une sérialisation stable sous forme de tableau :

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

Identité ambiguë

Si deux lignes capturées produisent la même clé d’identité, la capture échoue au lieu d’écraser une ligne par l’autre.

PHP
use Merql\Exceptions\SnapshotException;

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

Les applications hôtes peuvent vérifier les lignes brutes à l’avance avec IdentityRuleSet::conflictsFor() pour signaler les ambiguïtés avant la capture :

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

Pour les tables de métadonnées WordPress, post_id + meta_key est généralement utile pour le regroupement et l’affichage, mais ne convient pas comme identité de ligne, car les clés de métadonnées en double sont valides. Utilisez la clé primaire de la table comme identité de ligne, sauf si l’application hôte peut prouver que la clé naturelle est unique.

Encodage de la clé de ligne

Les clés de ligne sont des chaînes encodées qui combinent plusieurs valeurs de colonnes avec un séparateur d’unité (\x1F). Les valeurs contenant le séparateur ou le signe pour cent sont échappées pour éviter les collisions :

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

Règles d’échappement :

Valeur d’origine Valeur échappée
% %25
\x1F (séparateur d’unité) %1F

Cela garantit qu’une valeur contenant \x1F ne sera pas répartie en plusieurs parties de clé. L’encodage est déterministe et réversible.

Divergence des identifiants à incrémentation automatique

Lorsque les deux côtés insèrent de nouvelles lignes, les ID à incrémentation automatique diffèrent d’une branche à l’autre. La ligne #100 du nôtre peut être un enregistrement complètement différent de la ligne #100 du leur.

merql gère cela en comparant les clés primaires. Si les deux côtés insèrent une ligne avec le même ID à incrémentation automatique, un conflit insert_insert est signalé. Pour éviter cela, utilisez des clés naturelles (des colonnes uniques stables d’une branche à l’autre) dans les tables où des insertions simultanées sont prévues.

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

Identité dans les instantanés

Les colonnes d’identité choisies lors de la capture sont enregistrées avec chaque TableSnapshot :

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

SqlGenerator utilise ces colonnes pour construire les clauses WHERE des instructions UPDATE et DELETE. Les plans de fusion s’en servent également pour présenter les valeurs d’identité décodées afin de les examiner, de les sélectionner, de les appliquer sous contrôle et de les restaurer.