Detailed feature gap analysis and tiered implementation plan covering: - Core table browsing and schema exploration (databases, tables, schema, rows, get) - Write operations (upsert, update, delete, drop, create-table) - Output format improvements (CSV, TSV, table, NL-JSON) - Authentication UX (browser login, env vars, keyring, auth status) - Discoverability features (info, URL inference, shell completions) https://claude.ai/code/session_01DoiJf9Gbvbw2asN9yvP6dx
17 KiB
dclient Improvement Plan
Feature Gap Analysis: dclient vs Datasette
What dclient currently supports
| Feature | Status |
|---|---|
SQL query execution (query) |
Supported |
Bulk data insertion (insert) |
Supported (CSV, TSV, JSON, JSONL) |
Actor/token introspection (actor) |
Supported |
URL aliases (alias add/list/remove) |
Supported |
Token storage (auth add/list/remove) |
Supported |
| Bearer token authentication | Supported |
| Progress bar for inserts | Supported |
| Verbose/debug mode | Supported |
Datasette plugin (datasette dc) |
Supported |
What Datasette exposes that dclient does NOT cover
Read Operations (high value)
| Datasette Feature | API Endpoint | dclient Status |
|---|---|---|
| List databases | GET /-/databases.json |
Missing |
| List tables/views in a database | GET /{db}.json |
Missing |
| Browse/filter table rows | GET /{db}/{table}.json?filters... |
Missing |
| Get single row by PK | GET /{db}/{table}/{pk}.json |
Missing |
| Table schema inspection | GET /{db}/{table}/-/schema.json |
Missing |
| Database schema inspection | GET /{db}/-/schema.json |
Missing |
| Full-text search | ?_search=term |
Missing |
| Column filtering (exact, gt, lt, contains, etc.) | ?col__op=val |
Missing |
| Faceted browsing | ?_facet=col |
Missing |
| Sorting | ?_sort=col / ?_sort_desc=col |
Missing |
| Pagination | ?_next=cursor&_size=N |
Missing |
| Column selection | ?_col=a&_col=b / ?_nocol=x |
Missing |
| CSV/TSV export of table data | GET /{db}/{table}.csv |
Missing |
| Canned queries listing | metadata in GET /{db}.json |
Missing |
Write Operations (high value)
| Datasette Feature | API Endpoint | dclient Status |
|---|---|---|
| Row upsert (insert-or-update) | POST /{db}/{table}/-/upsert |
Missing |
| Update a single row | POST /{db}/{table}/{pk}/-/update |
Missing |
| Delete a single row | POST /{db}/{table}/{pk}/-/delete |
Missing |
| Drop a table | POST /{db}/{table}/-/drop |
Missing |
| Create empty table with schema | POST /{db}/-/create (columns only) |
Missing |
Metadata/Admin (medium value)
| Datasette Feature | API Endpoint | dclient Status |
|---|---|---|
| Instance version info | GET /-/versions.json |
Missing |
| Installed plugins | GET /-/plugins.json |
Missing |
| Instance settings | GET /-/settings.json |
Missing |
| Token creation | POST /-/create-token |
Missing |
| Permission checks | GET /-/allowed.json |
Missing |
Proposed Plan
The plan is organized into tiers by impact and complexity, with the most valuable additions first.
Tier 1 -- Core Table Browsing & Schema Exploration
These commands turn dclient from "a way to run raw SQL" into a genuine database exploration tool. They make up the biggest gap today: a user who wants to see what tables exist or browse rows must either open a browser or hand-craft SQL.
1.1 dclient databases -- List databases
dclient databases <instance_url_or_alias>
Hits GET {url}/-/databases.json. Prints a table of database names, sizes,
and whether they are mutable. Add --json for raw JSON output.
1.2 dclient tables -- List tables and views
dclient tables <db_url_or_alias> # e.g. https://latest.datasette.io/fixtures
dclient tables <db_url_or_alias> --views # include views
dclient tables <db_url_or_alias> --counts # include row counts
dclient tables <db_url_or_alias> --schema # include CREATE TABLE SQL
Hits GET {db}.json. Prints table names, row counts, and column lists.
--json for raw JSON.
1.3 dclient schema -- Show table/database schema
dclient schema <db_url_or_alias> # full database schema
dclient schema <db_url_or_alias>/<table> # single table schema
Hits GET {db}/-/schema.json or GET {db}/{table}/-/schema.json.
Outputs the CREATE TABLE / CREATE VIEW SQL.
1.4 dclient rows -- Browse table rows with filtering
This is the biggest single feature addition. It exposes Datasette's powerful table filtering API without requiring the user to write SQL.
dclient rows <table_url_or_alias> [OPTIONS]
Filter flags:
--where 'column = value' # maps to ?column__exact=value
--where 'column > 100' # maps to ?column__gt=100
--where 'column contains foo' # maps to ?column__contains=foo
--where 'column is null' # maps to ?column__isnull
--where-sql 'age > 30' # maps to ?_where=age > 30 (raw SQL)
Or a simpler column-value approach:
-w column=value # exact
-w column__gt=100 # pass Datasette filter operators directly
Other flags:
--sort column # ascending sort
--sort-desc column # descending sort
--search 'full text query' # FTS search (?_search=...)
--col name --col email # select specific columns
--nocol description # exclude columns
--facet column # facet counts
--size 50 # page size (default 100)
--all # auto-paginate through all pages
--limit N # stop after N total rows
--csv # output as CSV
--tsv # output as TSV
--nl # output as newline-delimited JSON
--json # output as JSON (default)
--table # human-readable table format
Pagination:
By default, print one page of results. --all follows next_url to
fetch every page. --limit N caps total rows returned.
1.5 dclient get -- Fetch a single row by primary key
dclient get <table_url_or_alias> <pk_value>
dclient get <table_url_or_alias> <pk1>,<pk2> # compound key
Hits GET {db}/{table}/{pk}.json. Prints the row as JSON.
Tier 2 -- Write Operations
dclient already supports insert and create. These additions round out
the full CRUD lifecycle.
2.1 dclient upsert -- Insert or update rows
dclient upsert <db_url_or_alias> <table> <filepath> [OPTIONS]
Same interface as insert but hits the /-/upsert endpoint. Requires
rows to contain primary key columns.
Shares most code with insert; the main difference is the endpoint URL
and the absence of --replace/--ignore flags.
2.2 dclient update -- Update a single row
dclient update <table_url_or_alias> <pk_value> <key=value> [<key=value> ...]
dclient update <table_url_or_alias> <pk_value> --input file.json
Hits POST {db}/{table}/{pk}/-/update with {"update": {...}}.
2.3 dclient delete -- Delete rows
dclient delete <table_url_or_alias> <pk_value>
dclient delete <table_url_or_alias> <pk_value> --yes # skip confirmation
Hits POST {db}/{table}/{pk}/-/delete. Prompts for confirmation unless
--yes is passed.
2.4 dclient drop -- Drop a table
dclient drop <table_url_or_alias>
dclient drop <table_url_or_alias> --yes # skip confirmation
Hits POST {db}/{table}/-/drop with {"confirm": true}. Shows row count
and asks for confirmation unless --yes.
2.5 dclient create-table -- Create an empty table with explicit schema
dclient create-table <db_url_or_alias> <table_name> \
--column id integer \
--column name text \
--column score float \
--pk id
Hits POST {db}/-/create with a columns array instead of rows.
The existing insert --create handles creation-with-data; this handles
the schema-only case.
Tier 3 -- Output & UX Improvements
3.1 Multiple output formats for query and rows
Currently query only outputs JSON. Add:
--csv # CSV output
--tsv # TSV output
--nl # newline-delimited JSON
--table # human-readable ASCII table (like sqlite-utils)
--yaml # YAML output (optional, low priority)
For --table output, use a simple column-aligned format or integrate
with tabulate / rich (consider keeping dependencies minimal).
A lightweight approach: sqlite-utils already has table-formatting
utilities that could be reused.
3.2 Pipe-friendly defaults
Detect whether stdout is a TTY:
- TTY: default to
--table(human-readable) output - Pipe: default to JSON (machine-readable) output
This matches the UX convention of tools like gh and jq.
3.3 --output / -o flag for writing to a file
dclient query ... -o results.csv --csv
dclient rows ... -o dump.json
3.4 Streaming output for large result sets
For --all pagination mode and --csv/--nl formats, stream rows as
they arrive rather than buffering everything in memory. Write each page
to stdout immediately.
Tier 4 -- Authentication Improvements
The current auth system works but has friction points. These changes make it smoother.
4.1 dclient auth login -- Browser-based authentication
dclient auth login <instance_url_or_alias>
Flow:
- Open the user's browser to
{instance}/-/create-token - Start a temporary local HTTP server to receive the token callback
- User creates a token in the Datasette UI and pastes it, OR:
- If the instance supports it, redirect back to the local server with the token
- Store the token automatically via the existing auth system
For instances that don't support redirect, fall back to:
- Open the browser to
/-/create-token - Prompt the user to paste the token into the terminal
This is similar to how gh auth login, gcloud auth login, and
heroku login work.
4.2 dclient auth token -- Create a token via the API
dclient auth token <instance_url_or_alias>
dclient auth token <instance_url_or_alias> --expires-after 3600
dclient auth token <instance_url_or_alias> --read-only
Hits POST /-/create-token. Requires an existing root token or session.
Prints the new token and optionally stores it.
4.3 dclient auth status -- Validate stored credentials
dclient auth status <instance_url_or_alias>
Hits /-/actor.json with the stored token and prints:
- Whether the token is valid
- The actor identity (id, permissions)
- Token expiry if available
This is just a friendlier wrapper around the existing actor command,
integrated into the auth subgroup.
4.4 Environment variable support
export DCLIENT_TOKEN=dstok_xxx
export DCLIENT_URL=https://my-datasette.example.com/mydb
Token resolution order (highest to lowest priority):
--tokenCLI flagDCLIENT_TOKENenvironment variable- Stored token in
auth.json(existing behavior)
The DCLIENT_URL variable provides a default instance so users don't
have to type it every time:
export DCLIENT_URL=https://my-datasette.example.com/mydb
dclient tables # uses DCLIENT_URL
dclient query 'select 1' # uses DCLIENT_URL
dclient rows mytable --limit 10 # uses DCLIENT_URL
4.5 Keyring integration (optional, lower priority)
Instead of storing tokens in plaintext auth.json, optionally use the
system keyring via the keyring Python package. This keeps tokens
encrypted at rest on macOS (Keychain), Windows (Credential Vault), and
Linux (Secret Service / KWallet).
Make this opt-in: dclient auth add --keyring <url>. Fall back to the
current file-based storage if keyring is not installed.
Tier 5 -- Discoverability & Convenience
5.1 dclient info -- Instance overview
dclient info <instance_url_or_alias>
Hits /-/versions.json, /-/plugins.json, /-/databases.json in
parallel. Prints a summary:
Datasette 1.0.1
Python 3.12.1
SQLite 3.45.0
Databases: 3 (fixtures, content, ephemeral)
Plugins: 12 installed
5.2 URL inference and shortcuts
Allow shorthand for common patterns:
# These should all work equivalently:
dclient rows https://latest.datasette.io/fixtures/facetable
dclient rows latest fixtures/facetable # if 'latest' is an alias
dclient rows latest facetable # if alias points to a database URL
The key insight: if an alias points to https://x.com/dbname, then
dclient rows <alias> <table> should construct
https://x.com/dbname/table.json.
Currently the alias is just a URL prefix that gets used verbatim. Making the alias system slightly smarter about joining paths would reduce typing significantly.
5.3 dclient config -- Manage configuration
dclient config show # print config directory and contents
dclient config path # print config directory path
dclient config edit # open config in $EDITOR
Useful for debugging when tokens or aliases aren't working as expected.
5.4 Shell completions
Use Click's built-in shell completion support to generate completions for bash, zsh, and fish:
dclient --install-completion bash
dclient --install-completion zsh
dclient --install-completion fish
Click 8.x supports this natively via shell_complete. This could also
dynamically complete alias names, database names, and table names by
querying the stored aliases.
Tier 6 -- Advanced Features (lower priority)
6.1 dclient export -- Bulk export table data
dclient export <table_url> -o data.csv --csv
dclient export <table_url> -o data.json --json
dclient export <table_url> -o data.db # export to local SQLite
This is essentially dclient rows --all but optimized for bulk export:
- Streams using
?_stream=1(CSV format) when available - For JSON, auto-paginates using
_nextcursor - For SQLite output, pipes into
sqlite-utils insert
6.2 Parallel insert for large files
When inserting very large files, optionally send batches in parallel:
dclient insert <url> <table> huge.csv --parallel 4
Uses a thread pool to send N batches concurrently. Would require
switching from synchronous httpx to httpx.AsyncClient or using
concurrent.futures.ThreadPoolExecutor.
6.3 dclient diff -- Compare local and remote data
dclient diff local.db remote_alias/tablename
Compare a local SQLite table with a remote Datasette table. Show added, removed, and modified rows. Useful for sync workflows.
Implementation Notes
Shared infrastructure to build first
Several of the proposed commands share common patterns. Before implementing individual commands, refactor these into shared utilities:
-
URL resolution helper: Centralize the alias-lookup + URL-construction logic that is currently duplicated in every command. A single function like
resolve_url(url_or_alias, suffix=None)that handles aliases, adds.jsonwhere appropriate, and joins path segments. -
Authenticated HTTP client helper: A function or context manager that creates an
httpx.Clientwith the rightAuthorizationheader and timeout, resolved from--token/ env var / auth.json. This eliminates the repeated token-resolution boilerplate in every command. -
Output formatter: A shared output module that handles
--json,--csv,--tsv,--nl,--tableflags consistently across all commands that return data. -
Pagination helper: A generator that follows
next_urllinks to yield all pages of results, used byrows --allandexport.
Dependency considerations
The current dependencies are minimal: click, httpx, sqlite-utils.
The proposed changes mostly stay within these. Potential additions:
tabulateorrichfor--tableoutput (or implement a minimal version to avoid new deps)keyringfor optional encrypted token storage (optional dependency)
Backward compatibility
All proposed changes are additive (new commands and new flags). No
existing command signatures or behaviors change. The only subtle change
would be if TTY-detection changes the default output format of query,
which should be gated behind a major version bump or opt-in flag.
Testing strategy
Each new command should follow the existing test patterns:
- Unit tests with
pytest-httpxfor HTTP mocking - Integration tests against a real in-memory Datasette instance
(as done in
test_insert.py) cogappfor keeping--helpoutput in docs in sync
Priority Summary
| Priority | Feature | Rationale |
|---|---|---|
| P0 | databases, tables, schema |
Basic exploration; most-requested gap |
| P0 | rows with filtering/sorting/pagination |
Replaces need for raw SQL for common tasks |
| P0 | Environment variable support (DCLIENT_TOKEN, DCLIENT_URL) |
Near-zero effort, big UX win |
| P1 | get, update, delete, drop |
Completes CRUD lifecycle |
| P1 | upsert |
Natural complement to existing insert |
| P1 | Multiple output formats (CSV, table, NL) | Makes query/rows output usable in pipelines |
| P1 | auth login (browser-based) |
Biggest auth UX improvement |
| P2 | create-table (schema only) |
Useful but insert --create covers most cases |
| P2 | TTY-aware default output | Polish |
| P2 | auth status, auth token |
Convenience wrappers |
| P2 | info |
Nice discoverability |
| P2 | URL inference / smarter aliases | Reduces typing |
| P3 | export (bulk) |
Covered by rows --all for most cases |
| P3 | Shell completions | Polish |
| P3 | Keyring integration | Security hardening |
| P3 | Parallel insert | Performance optimization |
| P3 | diff |
Advanced workflow |