# a2db v1 Design Spec

**Date:** 2026-03-28
**Status:** Draft
**Repo:** https://github.com/agentic-eng/a2db

## Overview

a2db is an agent-to-database tool with two frontends (CLI and MCP server) sharing a single core. It lets AI agents connect to databases, explore schemas, and run queries. The core differentiator from dbhub: multi-connection support with structured naming, named batch queries, and TSV output for token efficiency.

## Architecture

```
CLI (click)  ──┐
               ├──→  a2db core  ──→  SQLGlot (dialect transpilation)
MCP (FastMCP) ─┘                ──→  DBAPI 2.0 drivers (user-installed)
                                ──→  Connection store (~/.config/a2db/connections/)
```

Both frontends are thin wrappers around the same core module. No business logic lives in CLI or MCP layers.

## Connections

### Naming

Every connection is identified by three required segments:

| Flag | Long | Description | Example |
|------|------|-------------|---------|
| `-p` | `--project` | Project or product name | `myapp` |
| `-e` | `--env` | Environment | `prod`, `staging`, `dev` |
| `-d` | `--db` | Database name | `users`, `analytics` |

The triple `(project, env, db)` is the connection's unique key.

### Storage

Each connection is stored as an individual TOML file:

```
~/.config/a2db/connections/{project}-{env}-{db}.toml
```

File contents:

```toml
project = "myapp"
env = "prod"
db = "users"
dsn = "postgresql://admin:secret@db.example.com:5432/users"
```

Credentials are stored as plaintext. Users are responsible for file permissions.

### CLI: `a2db login`

```bash
a2db login -p myapp -e prod -d users "postgresql://admin:secret@db.example.com:5432/users"
```

Creates (or overwrites) the connection file. Validates that the DSN scheme maps to a known DBAPI driver.

### CLI: `a2db connections`

```bash
a2db connections          # list all
a2db connections -p myapp # filter by project
```

### MCP: `login` tool

Same parameters: `project`, `env`, `db`, `dsn`. Creates the connection file.

### MCP: `list_connections` tool

Returns all saved connections (without DSN secrets — only project/env/db and DB type).

## Query Execution

### Named Batch Queries

The primary query interface accepts a **dictionary of named queries**, where each query can target a different connection.

#### MCP: `execute` tool

Parameters:

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `queries` | `dict[str, object]` | yes | — | Named queries (see below) |
| `format` | `string` | no | `tsv` | Output format: `tsv` or `json` |
| `limit` | `int` | no | `100` | Max rows per query |
| `offset` | `int` | no | `0` | Row offset per query |

Each entry in `queries`:

```json
{
  "queries": {
    "active_users": {
      "connection": {"project": "myapp", "env": "prod", "db": "users"},
      "sql": "SELECT id, name, email FROM users WHERE active = true"
    },
    "recent_orders": {
      "connection": {"project": "myapp", "env": "prod", "db": "orders"},
      "sql": "SELECT id, user_id, total FROM orders WHERE created_at > '2026-01-01'"
    }
  }
}
```

`limit` and `offset` apply to all queries uniformly. Per-query overrides are not supported in v1.

#### CLI: `a2db query`

```bash
# Single query
a2db query -p myapp -e prod -d users "SELECT * FROM users LIMIT 10"

# Multiple named queries from a JSON file
a2db query --batch queries.json
```

For single CLI queries, the connection flags specify the target. For batch mode, a JSON file matching the MCP format is used.

### Pagination via SQLGlot

User queries are wrapped with LIMIT/OFFSET using SQLGlot's dialect-aware transpilation:

1. Parse the user's SQL with SQLGlot (auto-detect or use known dialect from DSN scheme).
2. Wrap as: `SELECT * FROM ({user_sql}) AS _q LIMIT {limit} OFFSET {offset}`.
3. Transpile the wrapped query to the target dialect (e.g., `TOP N` for SQL Server, `FETCH FIRST N ROWS` for Oracle).

This ensures the database only fetches the requested page, not the full result set.

### Safety Cap

A hard maximum of **10,000 rows** per query regardless of the `limit` parameter. Queries requesting more are capped silently.

### Response Format

**TSV (default):**

```
query: active_users
id	name	email
1	Alice	alice@example.com
2	Bob	bob@example.com
rows: 2, truncated: false

query: recent_orders
id	user_id	total
101	1	49.99
102	2	129.00
rows: 2, truncated: false
```

**JSON:**

```json
{
  "active_users": {
    "columns": ["id", "name", "email"],
    "rows": [
      {"id": 1, "name": "Alice", "email": "alice@example.com"},
      {"id": 2, "name": "Bob", "email": "bob@example.com"}
    ],
    "count": 2,
    "truncated": false
  },
  "recent_orders": {
    "columns": ["id", "user_id", "total"],
    "rows": [
      {"id": 101, "user_id": 1, "total": 49.99},
      {"id": 102, "user_id": 2, "total": 129.00}
    ],
    "count": 2,
    "truncated": false
  }
}
```

## Schema Exploration

### MCP: `search_objects` tool

Progressive schema explorer modeled after dbhub.

Parameters:

| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `connection` | `object` | yes | — | `{project, env, db}` |
| `object_type` | `string` | yes | — | `schema`, `table`, `column`, `index`, `procedure`, `function` |
| `pattern` | `string` | no | `%` | SQL LIKE pattern for filtering |
| `schema` | `string` | no | — | Filter by schema (required for columns/indexes) |
| `table` | `string` | no | — | Filter by table (required for columns) |
| `detail_level` | `string` | no | `names` | `names`, `summary`, `full` |
| `limit` | `int` | no | `100` | Max results |

Detail levels:
- **names** — just object names (minimal tokens)
- **summary** — names + metadata (row count estimates, column count, types)
- **full** — complete structure (all columns with types, indexes, constraints, definitions)

Implementation: each detail level maps to specific DBAPI `information_schema` queries, built per-dialect via SQLGlot.

### CLI: `a2db schema`

```bash
a2db schema -p myapp -e prod -d users tables           # list tables
a2db schema -p myapp -e prod -d users columns -t users  # describe table
a2db schema -p myapp -e prod -d users full              # full schema dump
```

## Driver Management

a2db ships with zero database drivers. Users install what they need:

| DSN scheme | DBAPI driver | Install |
|------------|-------------|---------|
| `postgresql://` | `psycopg2` or `psycopg` | `pip install psycopg2-binary` |
| `mysql://` | `mysql-connector-python` | `pip install mysql-connector-python` |
| `mariadb://` | `mariadb` | `pip install mariadb` |
| `sqlite://` | `sqlite3` (stdlib) | built-in |
| `oracle://` | `oracledb` | `pip install oracledb` |
| `mssql://` | `pymssql` | `pip install pymssql` |

a2db detects the DSN scheme, imports the corresponding driver, and raises a clear error if it's not installed: `"Driver 'psycopg2' not found. Install it: pip install psycopg2-binary"`.

### Driver Registry

A simple mapping of DSN scheme → module name → DBAPI connect function. Adding a new database is adding one entry to the registry.

## Dependencies

### Runtime

| Package | Purpose |
|---------|---------|
| `click` | CLI framework |
| `mcp[cli]` | MCP server (FastMCP) |
| `sqlglot` | SQL dialect transpilation |
| `tomllib` (stdlib, 3.11+) | TOML parsing for connection files (no extra dep needed on 3.12+) |

### Dev

Inherited from current pyproject.toml: pytest, pytest-cov, ruff, ty, pytest-env, diff-cover.

## Out of Scope (v1)

- Server-side cursors
- CEL expression filtering
- Env var references in connection files
- OAuth / auth gateway
- SSH tunneling
- Connection pooling
- Write operations (INSERT/UPDATE/DELETE) — read-only in v1
- Custom tools (dbhub's TOML-defined tool templates)

## Security

- Connection files are plaintext. Docs recommend `chmod 600` on the connections directory.
- Read-only by default: a2db rejects DML statements (INSERT, UPDATE, DELETE, DROP, TRUNCATE, ALTER). Enforced at the core layer before SQL reaches the driver.
- Field truncation at 2000 chars for text fields to prevent context window flooding.
- Hard row cap at 10,000.
