· 8 min de lecture

Pourquoi une interface graphique de base de données ne devrait jamais écrire directement

Toute interface graphique de base de données a ce moment où un clic se transforme en écriture sur la production. Nous avons décidé que ce moment ne devait pas exister. Dans DynoTable, rien de ce que tu fais — ni une modification d’item, ni une suppression en masse, ni le changement d’un — ne touche DynamoDB directement. Chaque écriture atterrit dans une sous forme de diff vérifiable attribut par attribut, et ne part que lorsqu’un humain la valide.

Le modèle mental vers lequel nous avons construit, c’est git : une zone de staging pour la revue, et un commit qui se comporte comme git push --force-with-lease — il ne réussit que si le distant ressemble encore à ce que tu as vu la dernière fois. Cet article, c’est l’ingénierie en dessous : le bug inter-onglets qui a remodelé toute la conception, le refactor dont le principal produit était du code supprimé, et pourquoi l’arrivée des agents IA a transformé un raffinement d’UX en mur de sécurité porteur.

Les écritures sont des diffs

Les modifications s’accumulent dans un stage local adossé à SQLite et s’affichent sous forme de cartes de diff — ancienne valeur, nouvelle valeur, par attribut — dans un panneau latéral. Les lignes se teintent dans la grille, si bien que l’état staged est visible depuis les données, pas seulement depuis le panneau. Le commit expédie l’ensemble staged sous forme de DynamoDB, découpées selon les limites du service (100 items par transaction, avec des budgets d’octets par opération et par requête), séquentiellement, avec un timeout de 30 secondes par lot.

Le panneau de staging affichant les changements en attente sous forme de cartes de diff par attribut, avec commit par ligne et en masse.
Le panneau de staging affichant les changements en attente sous forme de cartes de diff par attribut, avec commit par ligne et en masse.

La doc du staging couvre le workflow quotidien. Ce qu’elle ne couvre pas, c’est ce qui sert de clé au stage — ce qui s’est avéré être la décision la plus lourde de conséquences du système.

Clé sur la table, pas sur l’onglet

La première version limitait chaque stage à l’onglet qui l’avait créé. Cette conception produisait un vrai bug : l’outil de staging de l’IA dérivait sa cible de l’onglet actif, et son garde ignorait les onglets qui ne sont pas des vues de table. Donc si tu demandais à l’assistant de corriger une ligne alors qu’un onglet SQL Workbench était au premier plan, la modification était staged dans le stage du Workbench — et la puce « révéler le changement staged » ouvrait le mauvais onglet.

Pire, le verrou de commit était lui aussi par onglet. Deux onglets affichant la même table détenaient deux verrous indépendants — ce qui signifiait que tous deux pouvaient valider les mêmes lignes staged vers le vrai DynamoDB de manière concurrente. Une course au double commit contre la production, intégrée au modèle de données.

Le correctif a consisté à re-cléer tout par identité de table{profile, region, tableName} — au lieu de par onglet :

  • Un stage par table, visible depuis chacune de ses vues, survivant à la fermeture et à la réouverture d’onglet. L’IA peut stager sans aucun onglet de table ouvert.
  • Un verrou de commit par table. La course au double commit n’est pas « gérée » — elle est irreprésentable.
  • La classe de bug du mauvais onglet a disparu par construction : il n’y a pas d’onglet dans la clé.

Et le re-cléage a supprimé plus de code qu’il n’en a ajouté. Le balayage au démarrage qui garbage-collectait les stages des onglets fermés ? Un relecteur l’a signalé comme destructeur de fonctionnalité sous le nouveau modèle — les stages ne meurent plus avec les onglets — donc il a été retiré purement et simplement. L’abandon à la fermeture d’onglet et sa boîte de dialogue de confirmation : retirés. Le seul véritable orphelin restant est un profil de connexion supprimé, qui se propage explicitement.

La migration elle-même a été la première du dépôt à muter des lignes : une reconstruction manuelle de table SQLite qui dédupliquait les anciennes lignes par onglet avec une fenêtre ROW_NUMBER() OVER (PARTITION BY …) (la modification la plus récente l’emporte) et remplissait la nouvelle clé avec des séparateurs char(0), octet-identiques au constructeur de clé du runtime. Les migrations qui mutent des lignes ont eu leur propre harnais de test seed-puis-migrate ce jour-là.

--force-with-lease, pour DynamoDB

Une interface de revue ne vaut rien si ce que tu as relu n’est pas ce qui est écrit. Entre le staging et le commit, quelqu’un d’autre a pu changer la ligne. Donc chaque opération de commit porte des qui épinglent l’écriture au snapshot exact que tu as relu — , attribut par attribut :

  • Une création affirme que l’item n’existe pas déjà.
  • Une mise à jour affirme que chaque attribut que tu modifies détient encore la valeur que tu as vue au moment du staging.
  • Même une suppression affirme que l’attribut est encore égal à son ancienne valeur — si un coéquipier a changé note de "old" en "new" pendant que ta suppression de note était staged, le commit ne doit pas supprimer silencieusement sa modification.

Quand une condition échoue, DynamoDB annule toute la transaction et nous indique quel item a dérivé. Cette ligne obtient une bannière de conflit — rebaser sur la valeur en direct ou abandonner — plutôt qu’un écrasement silencieux dans un sens ou dans l’autre.

Et à tout échec de lot, le committer s’arrête. Les lots validés restent validés, le lot échoué est annulé atomiquement, rien de plus n’est tenté. Nous avons délibérément rejeté la continuation au mieux : un outil de revue qui continue d’écrire au-delà d’un conflit applique un ensemble de changements que personne n’a relu.

human reviews diffsTransactWriteItems +per-attribute conditionscondition failedrebaseEdit / AI stageItemStaging areaper tableCommitDynamoDBConflict banner:rebase or abort

Le refactor qui a supprimé un sous-système

Les commits sont de longue durée : l’app enregistre chacun dans un replay registry pour qu’un rechargement du renderer — ou une seconde fenêtre — puisse se rattacher et le regarder se terminer. Au fil du temps, trois chemins de code différents rattachaient chacun deux observateurs par commit, et tout un mécanisme d’arbitrage a grandi autour d’une seule question : quel observateur est autorisé à libérer le verrou de commit ? Il avait son propre nom effrayant (ownsLockOnBeforeRegistration), un bloc de commentaire expliquant une course d’ordonnancement IPC, et un tracker de toasts de 230 lignes.

Le correctif a été un propriétaire unique : une Commit Session qui se rattache exactement une fois par commit, détient le verrou exclusivement, projette la progression dans le store, et garantit que son appel de démarrage se résout sur chaque chemin de sortie — ne bloque jamais, ne rejette jamais. La machinerie d’arbitrage et le tracker n’ont pas été déplacés ; ils ont été supprimés. Le benchmark mémoire qui malmène le panneau de staging a perdu environ un tiers de son tas. Le meilleur commentaire de revue de code que nous ayons reçu ce mois-là : « ce PR est surtout du rouge ».

Deux comportements découlent de ce modèle de propriété que nous considérons comme le minimum pour tout outil qui écrit sur la production : un commit en vol survit à un rechargement du renderer (la session se rattache et termine), et il survit même au basculement de ta licence en lecture seule en plein commit — de nouveaux commits sont bloqués, mais une écriture déjà en vol est observée jusqu’à son terme, jamais abandonnée à moitié appliquée.

Pourquoi le format filaire est délibérément moche

Les valeurs staged sont stockées sous forme d’enveloppes typées DynamoDB brutes — {N: "42"}, {S: "42"} — et non sous forme de JSON unmarshalled convivial. Deux raisons. Le diff doit être conscient du type : {N: "1"} et {S: "1"} sont des valeurs différentes, et un hash de clé primaire construit sur des valeurs unmarshalled les ferait entrer en collision. Et l’aller-retour doit être sans perte : les wrappers de nombres unmarshalled du SDK ne survivent pas à la sérialisation JSON vers SQLite et retour, et l’égalité profonde par rapport à la saisie de l’utilisateur casse. Le chemin de commit utilise le client brut pour la même raison — ce que tu as relu est octet pour octet ce qui est conditionné et écrit.

Puis les agents sont arrivés

Le staging précède nos fonctionnalités IA, mais c’est la raison pour laquelle elles ont pu être livrées tout court. L’outil d’écriture de l’assistant stage ; sa propre description dit au modèle — mot pour mot :

The user reviews + commits from the staging panel —
this tool never writes to DynamoDB directly.

Il n’y a pas d’outil de commit. Ni protégé par permission, ni audité — absent. Le serveur MCP expose la même frontière de façon structurelle : sa portée de consentement intermédiaire est littéralement nommée « read + stage ». Un agent à cette portée peut proposer n’importe quelle absurdité, et le rayon de souffle est une carte de diff qu’un humain lit. Et parce que les commits portent des verrous optimistes par attribut, même une modification d’agent périmée ne peut pas écraser silencieusement celle d’un humain concurrent — elle remonte sous forme de conflit comme tout le reste.

Nous avons écrit sur comment rendre les requêtes de l’agent fiables ; le staging est l’autre moitié — rendre ses écritures ennuyeuses.

Ce qu’il faut exiger de tout outil qui écrit dans ta base de données

  • Une étape de revue entre l’intention et l’écriture — des diffs, pas des boîtes de dialogue de confirmation.
  • De la concurrence optimiste sur le snapshot relu, y compris les suppressions et les retraits — jamais le dernier-écrivain-gagne.
  • Des lots atomiques avec arrêt à l’échec, pas de continuation au mieux au-delà d’un conflit.
  • Un chemin d’écriture qui survit à un crash ou à un rechargement sans laisser un ensemble à moitié appliqué.
  • Pour l’IA : le staging comme seule primitive d’écriture dont dispose un agent — une capacité absente vaut mieux qu’une capacité sous garde.

La doc du staging montre le workflow, et éditer les données DynamoDB couvre les bases qu’il protège. Ou télécharge DynoTable, stage quelques modifications avec ⌘S, et regarde un changement en masse devenir quelque chose que tu peux vraiment lire avant qu’il n’arrive.

Travaille avec DynamoDB sans la Console

DynoTable est un client de bureau rapide pour DynamoDB — parcours les tables, exécute des requêtes de style SQL et édite les items en local.