name: database-workflow description: How to read and change a project's database safely through TableStudio. Use for any task that touches a real database: querying or inspecting data, investigating a data bug, fixing or backfilling rows, seeding, adding or changing tables, columns, indexes or constraints, writing or reviewing a migration, or running SQL. Use it too whenever you'd otherwise reach for psql, mysql, sqlite3, wrangler d1 execute, a DATABASE_URL or a one-off script that opens its own connection.
Working with databases through TableStudio
TableStudio is the user's database app on their Mac. Its MCP tools reach their saved connections without credentials, and they come with safety nets a raw psql session doesn't have: an exact dry run of a write against the real rows, throwaway sandbox copies, and a review inbox where the user approves changes. Use them for database work instead of psql, mysql, sqlite3, or a script that opens its own connection.
The tool names below are TableStudio's. Your client may show them with a prefix (in Claude Code: mcp__plugin_tablestudio_tablestudio__list_connections). If they're deferred, load them by that name before the first call.
1. Find the right database before doing anything
The session's Database context note, from the TableStudio plugin, usually names the TableStudio connection behind each of the repo's database settings, with its id and environment. Use that id as connection_id. The note also says when TableStudio's tools aren't available and what the user should do about it.
Without the note, or for a database it doesn't name:
- Call
list_connections. Each connection may carry what the user told TableStudio about it:project: the project it belongs to. Prefer connections whose project matches the repo you're working in.environment:development,stagingorproduction. Absent means untagged, not development.read_only: TableStudio refuses writes to it. Staging still works.location:local(this Mac),remote, orsandbox(a throwaway copy, safe to experiment on).agent_writes:directmeans execute_query may write there (when writes are on);reviewmeans every change goes throughstage_migrationfor the user to apply. TableStudio decides this, not you: production, and any remote database the user hasn't tagged development or staging, arereview.notes: the user's notes on the database's conventions (soft deletes, money in cents, naming). Follow them.
- Match the project first, then confirm with
host,portanddatabaseagainst the project's own settings (.env, config). - If two connections could be the one the user means, or you can't tell whether one is production, ask. Don't guess.
Then:
- Call
describe_capabilitiesonce before planning anything beyond a read. It says whether direct writes, staging and sandboxes are allowed right now, and names the setting behind each. Plan around it rather than finding a limit by being refused. agent_writes: review(production, or remote and untagged) means never write to it directly: stage changes for review. TableStudio refuses a direct write there anyway, so plan for staging from the start.- Check
engine(postgres, mysql, sqlite, d1, clickhouse) before writing SQL, and write that dialect. - A database that isn't saved in TableStudio can't be reached through its tools. Ask the user to add it in TableStudio. Never ask for a password in chat.
- If a call fails because the database is unreachable, call
connection_status. When it says a local server is stopped, callstart_local_databasewith theinstance_idit gives you.
2. Reading and investigating
- Look things up instead of guessing names:
search_schemafinds tables and columns by name fragment, andget_table_ddlgives the full structure, indexes, foreign keys andreferenced_by. - Run reads with
execute_query. Select the columns you need, filter, aggregate, andLIMIT. Results are capped (max_rows, default 200), so atruncatedresult is not the whole answer. - Reads run in a read-only transaction with a 30-second limit, on production too, so a read can't change data even through a function. Keep them cheap anyway: on a big production table, filter on an indexed column or use
database_overviewrow estimates instead ofcount(*). - A data bug: reproduce it in the data first. Find the bad rows, count them, and find when they started (
min(created_at)). Then read the code that writes them. Show the user the query that proves it. Fix the code, and if rows need repairing, that's a data change (section 3). - Never paste secrets, passwords or connection strings into SQL, code, docs or chat. The one place a connection string belongs is the project's gitignored env file, for a local database (the local-databases skill covers
get_connection_url).
3. Changing data (INSERT, UPDATE, DELETE)
On PostgreSQL:
- Write the statement, then call
preflight_statementwith it. You get averdict(looks_fine,revieworwould_fail),dry_run.rows(exactly how many rows it would change, read-only), cascades into other tables, and what would make it fail. - Tell the user what it will do in plain words: "This updates 142 rows in
invoices; nothing cascades." If the verdict iswould_fail, fix the statement first. If it'sreview, explain why. - Then either:
agent_writes: direct, and direct writes are allowed:execute_querywithexpected_rowsset todry_run.rows. It commits only if exactly that many rows change, and rolls back otherwise. On a remote PostgreSQL database, TableStudio refuses a data change withoutexpected_rows.agent_writes: review, writes not allowed, or anything the user should see first:stage_migrationwith a cleartitle, the SQL, and arationalethat includes the dry-run count ("Voids the 142 duplicate invoices found by …"). It goes to the user's review queue in TableStudio's Agent Center and does not run.
On other engines there's no dry run: first run a SELECT count(*) with the same WHERE, report the count, then follow the same execute-or-stage rule.
Before anything destructive on a database that matters (a large DELETE, a DROP, a TRUNCATE), call create_restore_point when sandboxes are enabled, and tell the user you did.
4. Changing the schema
If the repo uses a migration tool (Prisma, Drizzle, Rails, Django, Alembic, Laravel, Supabase, Knex, Ecto, TypeORM, Wrangler D1, or plain numbered SQL files; the session context names it when the plugin detected one), the change belongs in that tool:
- Make the change the tool's way: edit the schema and generate a migration, or write the migration file.
- Apply it to the local development database with the tool's dev command.
- Check it against real data (below).
- Leave applying it anywhere else to the user's normal deploy process. Never run the tool's deploy command against production yourself.
Running ALTER TABLE directly when a migration tool owns the schema leaves the tool's history out of sync with the database, which breaks the next deploy.
If there's no migration tool, or the user asks for a one-off change to an existing database:
- Read the table first with
get_table_ddl. Checkreferenced_bybefore dropping or renaming anything. - Try it on a copy:
create_sandbox(data:nonefor schema only,samplefor a slice of real rows), run the DDL on the sandbox's connection withexecute_query, and check it does what you meant. - Show the difference with
compare_schemas(sandbox assource_connection_id, the real database astarget_connection_id). - Propose it with
stage_migrationon the real connection. The user applies it from TableStudio.
Check a migration against real data
A migration that passes on the local database can still fail, or lock the app, where the real rows are. Before you call a migration done, check it read-only against the database it will deploy to (usually staging or production, from the session note or list_connections):
-
get_table_ddlfor each table it alters: how many rows, its indexes, and what references it. -
For each constraint it adds,
execute_querythe SELECT that finds rows that would break it:NOT NULL:SELECT count(*) FROM t WHERE col IS NULLUNIQUE:SELECT col, count(*) FROM t GROUP BY col HAVING count(*) > 1 LIMIT 20- a foreign key: child rows whose parent doesn't exist (
LEFT JOIN … WHERE parent.id IS NULL) - a
CHECK, or a narrower type or length: rows outside the new range
Any rows means the deploy would fail. Add a backfill or cleanup step to the migration, and say so.
-
On PostgreSQL, flag steps that lock or rewrite a large table (see below) and switch to the safe pattern.
-
When the change is tricky, run it for real on a copy:
create_sandboxfrom that connection withdata: "sample",execute_querythe migration's SQL on the sandbox, thencompare_schemas.
Tell the user what you checked and what you found ("orders.status has 0 NULLs across 1.2M rows, so SET NOT NULL is safe"). All of this is read-only: never apply the migration anywhere but the local development database.
Locks on PostgreSQL
- Building an index on a big, busy table: use
CREATE INDEX CONCURRENTLY. It can't run inside a transaction, so don't wrap that statement inBEGIN … COMMIT. - Adding a
NOT NULLcolumn with a volatile default, changing a column type, or adding a foreign key can rewrite or lock the table. Say so in the rationale, and consider splitting it into safe steps (add the column nullable, backfill in batches, thenSET NOT NULL; add a foreign keyNOT VALID, thenVALIDATE CONSTRAINT). schema_issuesreturns fix SQL with alock_impactfor each issue. Read it before proposing a fix.
5. After you stage something
- Tell the user it's waiting for their review in TableStudio and has not run, and give them the
review_urlfrom the result as a link. It opens the review with the change's impact (rows it touches, locks, what would fail), a diagram of the schema after it, and an undo plan. Never say a staged change was applied. - If the user asks for changes,
list_staged_migrationsshowsstatus: changes_requestedwith their note inreview_feedback. Revise it withupdate_staged_migration(samemigration_id) rather than staging a second copy, then tell them it's ready to look at again. To withdraw it, useremove_staged_migration. list_staged_migrationsalso shows whether the user applied, ignored or failed it.
When the user asks what's waiting for them ("what did you stage?", "did my migration run?"), call list_staged_migrations for this repo's connections. Report the pending ones first, newest first: the title, the connection, when it was staged, its review_url, and one line on what it does and how risky it is (does it lock or rewrite a large table, delete data, or lack a WHERE?). Then any with changes requested, with the user's note, and any that failed, with the error. Say plainly that the user applies, sends back or ignores them in TableStudio's Agent Center; you can't apply them yourself.
6. When TableStudio says no
- A refusal that names a setting (for example, writes are off) is the user's choice. Tell them which setting would allow it, offer to stage the change instead, and stop. Don't route around it.
- If TableStudio's tools aren't available at all, the session note says why: the app is closed, its MCP server is off (Settings → MCP), or the client isn't connected to it yet. Pass on the fix it gives. In Claude Code, the user can reconnect with
/mcponce TableStudio is open. - A refusal with
blocked: production_review_requiredorreview_requiredmeans that connection only takes changes through review: stage it.dry_run_requiredmeans runpreflight_statementfirst and passexpected_rows.side_effect_functionmeans the statement calls something that acts outside a transaction; show the user the SQL instead. - The plugin's shell guard blocks shell commands that write to production. Treat a block as a signal to use the steps above, not as something to work around.
- Everything you do through TableStudio is shown to the user in its Agent Center, grouped by this conversation.