Studio Power Tools

These surfaces sit on top of the read and write APIs. They’re optional — none of them change the CRUD contract — but they’re what makes Studio useful as an admin tool rather than just a row browser.

SQL preview

Renders the SQL Studio would run for an operation without touching the database. Lets you understand what the abstraction is doing before pulling the trigger, or copy it into a query tool to tweak by hand. Bound parameters are returned alongside the SQL text so you don’t have to read the placeholders.
API-backed targets return 501 UNSUPPORTED — Studio doesn’t render SQL it doesn’t run.

Query plans (explain=true)

Adding explain=true asks the driver to plan the rendered statement and returns the result in a plan field alongside the SQL:

The same request from the UI — tick Explain next to Show SQL.

This is the one part of the preview endpoint that reaches the database, which is why it’s opt-in rather than on by default: rendering SQL is pure and instant, planning it is a round trip. Two guarantees worth stating plainly:
  • Studio never runs EXPLAIN ANALYZE. There is no config knob, no query parameter, and no code path that emits it — so “plan this” can never become “execute this”.
  • Only reads are planned. create / update / delete return the preview plus a note instead of a plan. Beyond the defence-in-depth argument, the mutation previews bind placeholder sample values, so asking Postgres to plan them would fail on type inference rather than produce a plan worth reading.
Per-dialect specifics: op=get needs a pk= to bind against; without one you get a note saying so rather than a plan. A backend that can’t plan at all still returns the SQL preview you successfully asked for — a missing plan never costs you the preview.

Drift

Compares the schema’s declared columns to the live database (information_schema on Postgres, PRAGMA table_info on SQLite) and reports the result per model:
The UI renders a small ⚠ drift chip next to drifting models in the sidebar and ✕ table for missing tables.

CSV/JSON export

Pulls up to limit rows (capped at 10 000) through cursor pagination internally and returns one body. Sets Content-Disposition: attachment; filename="<target>-<table>.<ext>" so the browser downloads the file straight from the link. CSV uses RFC-4180-style escaping (quote-wrap on commas, quotes, or newlines; double up embedded quotes). JSON is a flat array of objects, field names as keys.
The export endpoint is bounded — it’s for “developer pulling a sample for a notebook,” not “ETL.” For multi-million-row dumps, hit the database directly.
Case-insensitive substring over models, fields (including the Model.field path and the field’s type), enums and their variants, mixins, types, and procedures. Empty query returns { "hits": [] }.
Hit kind is one of model, field, type, enum, mixin, procedure. The UI renders an inline dropdown directly under the search bar in the header.

Audit log

Studio holds every successful write (CREATE / UPDATE / DELETE) in an in-memory ring buffer, capped at 500 entries, FIFO when full. Entries are returned newest-first:
The UI’s “Audit” button in the header opens an overlay listing the most recent 100 entries:

The audit overlay after two edits to catalog/Post, newest first.

Persisting the log

By default the ring buffer lives in process memory only, so restarting the binary clears it. Set audit_file to also append every entry to a JSONL file, replayed on boot:
  • Opt-in. Unset, Studio writes nothing to your filesystem. Silently starting to drop a log file into someone’s repo is a behavioural change they didn’t ask for.
  • Studio-local, never your database. The sink is a file next to studio.toml. Studio has never created a table, and persisting audit rows into the target’s own schema would mean migrating the user’s database behind their back — wrong twice over for ro targets and for API-only targets that have no database at all.
  • Append-only, never rotated. That’s what makes it cheap to write under a lock and safe to tail -f. Not rotating is deliberate: an audit log that silently discards its own history is worse than no audit log. The in-memory ring the API serves stays capped however large the file grows.
  • Ids resume across restarts. Boot replays the file, keeps the newest entries up to the cap, and continues numbering above the highest id it saw — so resumed ids never collide with historical ones.
Failure handling is deliberately asymmetric:

Constraint errors → VALIDATION_ERROR

Studio used to surface driver constraint failures as DATABASE_ERROR (500). Phase 4 maps the well-known codes back to the same per-field VALIDATION_ERROR envelope as the in-process validators: Unrecognized errors still come back as DATABASE_ERROR. The wire envelope is identical to the in-process validator path (see the write API page), so the UI doesn’t care whether the rejection came from Studio’s pre-flight check or the database’s constraint engine.