By Dmitriy · · 11 min read

Why a database GUI should stage writes by default

Every database GUI has a moment where a click turns into a write against production. In DynoTable, that write passes through a first, with three exceptions covered below.

An item edit, a row deleted from its context menu, a PartiQL INSERT, UPDATE or DELETE, and an 's change each land there as a per-attribute diff. A commit then ships the staged set as DynamoDB .

The model is git. The stage is where you review, and the commit behaves like git push --force-with-lease: it succeeds only if the remote still looks the way you last saw it.

Every item edit lands in the stage before DynamoDB sees it

Staged edits render as diff cards in a side panel, with the old and new value of each attribute. Rows tint in the grid as well, so staged state is visible from the data itself.

DynoTable's staging panel showing pending DynamoDB changes as per-attribute diff cards, with per-row and bulk commit.
DynoTable's staging panel showing pending DynamoDB changes as per-attribute diff cards, with per-row and bulk commit.

Reviewing is optional, and the defaults favor speed. The editor's default Save action stages and commits in one gesture. Press ⌘S instead and the edit stays staged for review.

Deleting one row from its context menu opens a picker. Out of the box it preselects Commit delete, which also stages and commits in one gesture. Stage delete leaves the delete for review.

Both save paths run through the same commit pipeline, so a one-gesture save gets the same conflict checks as a reviewed one.

Three writes skip the stage. The first is a batch delete of selected rows: choosing Commit N deletes, or pressing ⌘⇧⌫, writes with no conditions. The second is an import, which writes through its own path.

The third is a direct run, offered only when you select every item a query matches. Its two options, Write edits directly and Delete directly, write each item as it is read, with no stage and no item limit.

Recent committed changes are recorded in the staging panel's History tab, batch deletes included, and can be reverted from there. Imports and direct runs are not recorded, so neither can be reverted.

The agent gets no such shortcut. Its write tools, stageItem and bulkEdit (up to 10,000 items per call), only stage. No agent tool can start a direct run.

A human always makes the commit. The staging docs cover the daily workflow.

The stage keeps DynamoDB's typed values intact

Staged values keep DynamoDB's own typed form, such as {N: "42"} and {S: "42"}. We chose that over friendlier unmarshalled JSON for two properties the stage can't do without.

The diff has to know the type. {N: "1"} and {S: "1"} are different values, and a hash of an item's key built from unmarshalled values would make the two collide.

The round trip has to be lossless too. The SDK's unmarshalled number wrappers don't survive JSON serialization into the app's local store and back, and deep equality against user input then breaks.

The commit talks to DynamoDB through the low-level client for the same reason. The values a condition checks are exactly the values you reviewed.

A commit is a --force-with-lease on the attributes you changed

A review UI is worthless if the thing you reviewed isn't the thing that gets written. Between staging and committing, someone else may have changed the row.

So the commit attaches that pin each write to the snapshot taken when you staged it. This is , applied per attribute:

  • A create asserts that the item doesn't already exist.
  • An update asserts that every attribute you're changing still holds the value you saw, or is still absent if it was absent then.
  • An attribute removal asserts that the attribute still equals its old value. If a teammate changed note from "old" to "new" while your removal sat staged, the commit must not silently delete their edit.

An ordinary item delete is the weak spot. Its only condition is that the item still exists, so it commits even if someone edited the item after you staged the delete.

Treat a staged delete as removing whatever the item holds at commit time. The snapshot on the diff card shows what it held when you staged it.

History is the backstop. Revert stages the inverse change for review, so a deleted item comes back as it was when you staged the delete. A teammate's later edit is not in that snapshot.

The first failed chunk halts the commit

A commit ships the stage in chunks sized to DynamoDB's transaction limits: 100 items per transaction, plus byte budgets per operation and per request. Chunks go out one at a time, each with a 30-second timeout.

While a commit runs, the app holds a commit lock on the table. You can't stage into or discard from that table in the UI until the commit ends.

When a condition fails, DynamoDB cancels the whole transaction and reports which item failed its check. That row gets a conflict banner instead of a silent overwrite in either direction:

  • Drift. The item changed remotely. "Rebase onto remote" refreshes the snapshot to the live item so you can review the change again. Discard on the card drops it.
  • Remote item deleted. There is nothing to rebase onto, so Discard is the only way out.
  • Network or service failure. Retry sends that one change again, and Discard drops it.

On any chunk failure, the commit halts. Chunks that succeeded stay committed, and nothing after the failed chunk is attempted.

A cancelled transaction writes nothing. A timeout is less certain: the chunk may have landed after we stopped waiting, so its rows are marked unknown until you retry or discard them.

We rejected best-effort continuation on purpose. A review tool that keeps writing past a conflict is applying a change set nobody reviewed.

human reviews diffsTransactWriteItems +condition expressionscondition failedrebaseEdit / AI stageItem, bulkEditStaging areaper tableCommitDynamoDBConflict banner:rebase or discard

Keying the stage to the tab sent the agent's edits to the wrong tab

The first version scoped each stage to the tab that created it. That produced a real bug. The agent's staging tool picked its target from the active tab, and its table-mismatch check didn't run when the active tab wasn't a table view.

So if you asked the assistant to fix a row while a SQL Workbench tab had focus, the edit landed in the Workbench's stage. The chip that reveals a staged change then opened the wrong tab.

The commit lock was per tab too. Sharing one stage across tabs meant the lock had to move with it, or two tabs on the same table could commit the same rows at once.

The fix was to key everything by table: the connection profile, the region and the table name. The tab is no longer part of the key.

  • One stage per table, visible from every view of it. It survives closing and reopening a tab, and the agent can stage with no table tab open.
  • One commit lock per table, so two views of a table can't commit at once.
  • The agent's edits land in the table's stage, whatever tab has focus.

Re-keying also removed code. A startup sweep used to clean up stages for closed tabs. A plan reviewer flagged it as feature-destroying under the new model, where stages outlive tabs, and we deleted it.

Discard-on-close and its confirmation dialog went too. A deleted connection profile is now the only way to orphan a stage, and deleting a profile clears its stages explicitly.

Existing stages had to move to the new key. It was the app's first migration that rewrote rows: per-tab copies of the same item collapsed to the newest edit, and the migration got its own seed-then-migrate tests.

We deleted the code that decided who releases the lock

A commit can outlive the window that started it. The app tracks every in-flight commit, so a reloaded window can re-attach and watch it finish.

Over time, two of the three code paths that start or resume a commit attached two observers to it. An arbitration mechanism grew up around one question: which observer may release the commit lock?

It came with a long comment explaining an ordering race and a separate tracker for progress notifications.

The fix gave each commit exactly one owner. It attaches once, holds the lock alone and reports progress. Whoever starts a commit always gets an answer back, and the call never hangs.

We deleted the arbitration code outright, and progress reporting moved into the new owner.

We rewrote the staging panel's memory benchmark for the new flow, and its median heap came in roughly a third lower. The old and new scenarios differ, so treat that number as a rough signal.

Two behaviors fall out of single ownership, and we consider both table stakes for a tool that writes to production. First, an in-flight commit survives a window reload: the reloaded window re-attaches and sees it through.

Second, it survives your license flipping to read-only mid-commit. New commits are blocked, but a write already in flight is observed to its end and never abandoned.

The agent has no commit tool

Staging predates our AI features. The agent's write tools only stage, and the description of stageItem tells the model, verbatim:

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

No agent tool commits or starts a direct run, so there is no unstaged write path to permission-gate or audit.

The MCP server draws the same line in its consent scopes. The middle scope is labeled "Read & stage", and even the top scope's description ends with "Writes still go through staging."

A agent at either scope can propose garbage, but no agent tool can commit it to DynamoDB. The change waits until a human commits it from the stage or saves an editor buffer the agent filled.

The commit-time locks cover the agent too. An update or attribute removal it staged against a stale snapshot surfaces as a conflict instead of overwriting a newer human edit.

An ordinary delete the agent stages checks only that the item exists, the same as yours.

Staging guards the agent's writes. Making the agent's queries reliable covers its reads.

What to demand from any tool that writes to your database

  • A review step between intent and write that shows per-attribute diffs.
  • Optimistic locking on the reviewed snapshot for creates, updates and removals. Ask what a delete checks: a staged delete checks only that the item exists, and a one-gesture batch delete checks nothing.
  • A history of committed changes, batch deletes included, with a revert that goes back through review.
  • Atomic batches that halt at the first failure instead of writing past a conflict.
  • A commit that keeps running through a window reload and reports how far it got.
  • For AI agents, staging as the only write path, with no commit tool.

The staging docs show the workflow, and editing DynamoDB data covers the basics it protects. Or download DynoTable, stage a few edits with ⌘S, and read a bulk change before it happens.

Work with DynamoDB without the Console

A fast DynamoDB desktop client that runs the real SQL DynamoDB can’t — JOINs, GROUP BY, aggregates — with visual editing and an AI agent on your own Bedrock keys.

Free 30-day trial, no credit card — then the Free plan with no time limit.