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.
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 :
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.
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.
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.
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.
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 :
- Si la table a une clé primaire, utilise ses colonnes.
- Si la table a des clés uniques, utilise les colonnes de la première.
- 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.
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 :
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 :
$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.
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 :
$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 :
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.
// 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 :
$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.