Persistance SQL
ausus/persistence-sql (couche L3) est le driver de persistance de la v0.1.0. Il est
basé sur SQLite, dérive son schéma du graphe de métadonnées,
et applique l'isolation des tenants et la concurrence optimiste.
Dériver le schéma
SchemaDeriver transforme un graphe compilé en instructions CREATE TABLE — une
table par entité, plus la table du journal d'audit :
use Ausus\Persistence\Sql\SchemaDeriver;
foreach (SchemaDeriver::deriveAll($graph) as $stmt) {
$pdo->exec($stmt);
}
- Le nom de la table est le FQN de l'entité avec les points remplacés par des underscores —
billing.invoice→billing_invoice. - Les types de colonnes correspondent aux types de champs :
string/datetime→TEXT,integer→INTEGER,money→NUMERIC,identity/version→TEXT NOT NULL. idest la clé primaire. Les champs non nullables reçoiventNOT NULL; les valeurs par défaut des champs deviennent lesDEFAULTdes colonnes.
Le driver
SqlitePersistenceDriver implémente le contrat PersistenceDriver du kernel :
use Ausus\Persistence\Sql\SqlitePersistenceDriver;
$driver = new SqlitePersistenceDriver($pdo, $graph);
$tx = $driver->beginTransaction($tenant);
$ctx = $driver->context($tenant, $tx);
$repo = $ctx->repository('billing.invoice');
// ... use the repository ...
$driver->commit($tx);
Un PersistenceContext est toujours lié à un Tenant ; demander un contexte avec
un tenant qui ne correspond pas au handle de transaction lève
TenantBoundaryViolation.
Le repository
SqliteRepository est l'API de données par entité. La v0.1.0 a trois opérations :
| Méthode | Comportement |
|---|---|
find(Reference $ref): ?Entity | lit une ligne par id, limitée au tenant |
create(array $payload, ?string $identity = null): Entity | insère une ligne, en générant un id ULID et _version |
update(Reference $ref, array $patch, Version $expected): Entity | met à jour une ligne si $expected correspond à la _version courante |
$entity = $repo->find($ref);
$entity = $repo->create(['number' => 'INV-1', /* ... */]);
$entity = $repo->update($ref, ['customer_name' => 'New'], $entity->version);
Isolation des tenants
Chaque table possède une colonne tenant_id. Chaque requête est filtrée par elle, et une
Reference dont le tenantId ne correspond pas au tenant actif est rejetée avec
TenantBoundaryViolation avant l'exécution de tout SQL. Le cantonnement par tenant est
appliqué dans le driver, et non laissé à l'appelant.
Concurrence optimiste
Chaque ligne porte une colonne _version — un ULID régénéré à chaque écriture.
update() inclut _version = :expected dans sa clause WHERE :
- Si la ligne est mise à jour, la version correspondait.
- Si zéro ligne est affectée, le driver vérifie si la ligne existe :
absente →
NotFound; présente mais avec une version différente →ConcurrencyConflict.
C'est ainsi qu'une écriture obsolète est détectée — il n'y a pas de verrouillage de ligne.
Le journal d'audit
SchemaDeriver émet aussi une table kernel_audit_log. DatabaseAuditSink
implémente le contrat AuditSink du kernel et écrit une ligne par action réussie —
acteur, tenant, FQN de l'action, sujet, entrées, sorties, horodatage,
identifiant de corrélation et numéro de séquence. L'écriture a lieu à l'intérieur de la
transaction de l'action, de sorte que l'entrée d'audit et le changement de données sont
validés ou annulés ensemble. Voir Le Runtime.
Limites actuelles de la v0.1.0
- SQLite uniquement. Le driver cible SQLite via PDO. MySQL et PostgreSQL sont un objectif de conception mais ne sont ni implémentés ni validés dans la v0.1.0.
- Le repository n'a ni
findMany, ni API de requête/filtre, nidelete. Le rendu de liste lit les lignes via un chemin de requête direct (voir Projections). - Il n'y a pas de migrations —
SchemaDeriverutiliseCREATE TABLE IF NOT EXISTS. Changer les champs d'une entité ne modifie pas une table existante. _versionest régénéré comme un ULID ; c'est un jeton de changement, pas un compteur.
Voir aussi
- Le Runtime — écrit via ce driver.
- Le graphe de métadonnées — la source du schéma.
- Référence des erreurs —
NotFound,ConcurrencyConflict.